Skip to content

Commit

Permalink
Ensure Gradle home is completely empty for build, but sync download c…
Browse files Browse the repository at this point in the history
…ache for performance
  • Loading branch information
hach-que committed Dec 5, 2024
1 parent f1ad23a commit f462829
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 24 deletions.
4 changes: 4 additions & 0 deletions UET/Redpoint.IO/DirectoryAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ await Task.Run(() =>
// Now try to delete again.
Directory.Delete(path, recursive);
}
catch (DirectoryNotFoundException)
{
// Directory already doesn't exist; ignore.
}
}).ConfigureAwait(false);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Redpoint.IO;
using Redpoint.ProcessExecution;
using Redpoint.Uet.BuildPipeline.BuildGraph.Export;
using Redpoint.Uet.BuildPipeline.BuildGraph.Gradle;
using Redpoint.Uet.BuildPipeline.BuildGraph.MobileProvisioning;
using Redpoint.Uet.BuildPipeline.BuildGraph.Patching;
using Redpoint.Uet.Configuration.Engine;
Expand All @@ -24,21 +25,24 @@ internal class DefaultBuildGraphExecutor : IBuildGraphExecutor
private readonly IBuildGraphPatcher _buildGraphPatcher;
private readonly IDynamicWorkspaceProvider _dynamicWorkspaceProvider;
private readonly IMobileProvisioning _mobileProvisioning;
private readonly IGradleWorkspace _gradleWorkspace;

public DefaultBuildGraphExecutor(
ILogger<DefaultBuildGraphExecutor> logger,
IUATExecutor uatExecutor,
IBuildGraphArgumentGenerator buildGraphArgumentGenerator,
IBuildGraphPatcher buildGraphPatcher,
IDynamicWorkspaceProvider dynamicWorkspaceProvider,
IMobileProvisioning mobileProvisioning)
IMobileProvisioning mobileProvisioning,
IGradleWorkspace gradleWorkspace)
{
_logger = logger;
_uatExecutor = uatExecutor;
_buildGraphArgumentGenerator = buildGraphArgumentGenerator;
_buildGraphPatcher = buildGraphPatcher;
_dynamicWorkspaceProvider = dynamicWorkspaceProvider;
_mobileProvisioning = mobileProvisioning;
_gradleWorkspace = gradleWorkspace;
}

public async Task ListGraphAsync(
Expand Down Expand Up @@ -93,28 +97,8 @@ public async Task<int> ExecuteGraphNodeAsync(
Name = "NuGetPackages"
}, cancellationToken).ConfigureAwait(false)).AsAsyncDisposable(out var nugetPackages).ConfigureAwait(false))
{
await using ((await _dynamicWorkspaceProvider.GetWorkspaceAsync(new TemporaryWorkspaceDescriptor
await using ((await _gradleWorkspace.GetGradleWorkspaceInstance(cancellationToken).ConfigureAwait(false)).AsAsyncDisposable(out var gradleInstance).ConfigureAwait(false))
{
Name = "GradleUserHome"
}, cancellationToken).ConfigureAwait(false)).AsAsyncDisposable(out var gradleUserHome).ConfigureAwait(false))
{
// Delete the Gradle 'tarnsforms-4' cache folder, since it can become corrupt and then prevent
// any further Android build jobs from working on this machine.
var transformsFolder = Path.Combine(gradleUserHome.Path, "caches", "transforms-4");
if (Directory.Exists(transformsFolder))
{
_logger.LogInformation("Deleting Gradle 'transforms-4' cache...");
try
{
await DirectoryAsync.DeleteAsync(transformsFolder, true);
_logger.LogInformation("Successfully deleted Gradle 'transforms-4' cache.");
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to delete Gradle 'transforms-4' cache: {ex}");
}
}

var environmentVariables = new Dictionary<string, string>
{
{ "IsBuildMachine", "1" },
Expand All @@ -131,7 +115,7 @@ public async Task<int> ExecuteGraphNodeAsync(
// the same time.
{ "NUGET_PACKAGES", nugetPackages.Path },
// Adjust Gradle cache path so that Android packaging works under SYSTEM.
{ "GRADLE_USER_HOME", gradleUserHome.Path },
{ "GRADLE_USER_HOME", gradleInstance.GradleHomePath },
};
if (!string.IsNullOrWhiteSpace(buildGraphRepositoryRootPath))
{
Expand Down Expand Up @@ -171,7 +155,7 @@ public async Task<int> ExecuteGraphNodeAsync(
}
try
{
return await InternalRunAsync(
var exitCode = await InternalRunAsync(
enginePath,
buildGraphRepositoryRootPath,
uetPath,
Expand All @@ -191,6 +175,11 @@ public async Task<int> ExecuteGraphNodeAsync(
mobileProvisions,
captureSpecification,
cancellationToken).ConfigureAwait(false);
if (exitCode == 0)
{
gradleInstance.MarkBuildAsSuccessful();
}
return exitCode;
}
finally
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
namespace Redpoint.Uet.BuildPipeline.BuildGraph.Gradle
{
using Microsoft.Extensions.Logging;
using Redpoint.IO;
using Redpoint.Uet.Workspace;
using Redpoint.Uet.Workspace.Descriptors;
using System;
using System.Threading.Tasks;

internal class DefaultGradleWorkspace : IGradleWorkspace
{
private readonly IDynamicWorkspaceProvider _dynamicWorkspaceProvider;
private readonly ILogger<DefaultGradleWorkspace> _logger;

public DefaultGradleWorkspace(
IDynamicWorkspaceProvider dynamicWorkspaceProvider,
ILogger<DefaultGradleWorkspace> logger)
{
_dynamicWorkspaceProvider = dynamicWorkspaceProvider;
_logger = logger;
}

public async Task<GradleWorkspaceInstance> GetGradleWorkspaceInstance(CancellationToken cancellationToken)
{
IWorkspace? gradleDownloadCache = null;
IWorkspace? gradleTemporaryWorkspace = null;

var ok = false;
try
{
gradleDownloadCache = await _dynamicWorkspaceProvider.GetWorkspaceAsync(new TemporaryWorkspaceDescriptor
{
Name = "GradleDownloadCache",
}, cancellationToken).ConfigureAwait(false);

var gradleTemporaryWorkspaceAttempt = 0;
do
{
gradleTemporaryWorkspace = await _dynamicWorkspaceProvider.GetWorkspaceAsync(new TemporaryWorkspaceDescriptor
{
Name = $"GradleHome_{Environment.ProcessId}_{gradleTemporaryWorkspaceAttempt}",
}, cancellationToken).ConfigureAwait(false);

_logger.LogInformation($"Checking the following Gradle home: {gradleTemporaryWorkspace.Path}");

// If the gradle home has any files in it, delete them. The gradle home must be wiped out before/after each
// build.
try
{
foreach (var entry in new DirectoryInfo(gradleTemporaryWorkspace.Path).GetFileSystemInfos())
{
_logger.LogInformation($"Deleting file/directory from previous Gradle home: {entry.FullName}");
if (entry is DirectoryInfo directory)
{
directory.Delete(true);
}
else
{
entry.Delete();
}

cancellationToken.ThrowIfCancellationRequested();
}
}
catch
{
_logger.LogInformation($"Unable to use that Gradle home as one or more existing files/directories could not be removed.");
gradleTemporaryWorkspaceAttempt++;
await gradleTemporaryWorkspace.DisposeAsync().ConfigureAwait(false);
gradleTemporaryWorkspace = null;
cancellationToken.ThrowIfCancellationRequested();
continue;
}

// We have a Gradle home we can use.
break;
} while (true);

_logger.LogInformation($"Using the following Gradle home: {gradleTemporaryWorkspace.Path}");

cancellationToken.ThrowIfCancellationRequested();

var centralCacheSafeToUse = Path.Combine(gradleDownloadCache.Path, "safe-to-use");
var centralCacheModules2 = Path.Combine(gradleDownloadCache.Path, "modules-2");
var centralCacheWrapper = Path.Combine(gradleDownloadCache.Path, "wrapper");
if (File.Exists(centralCacheSafeToUse) && Directory.Exists(centralCacheModules2))
{
var homeCacheModules2 = Path.Combine(gradleTemporaryWorkspace.Path, "caches", "modules-2");
var homeCacheWrapper = Path.Combine(gradleTemporaryWorkspace.Path, "wrapper");

_logger.LogInformation($"Copying the existing Gradle download cache from '{centralCacheModules2}' to '{homeCacheModules2}': {gradleTemporaryWorkspace.Path}");
await DirectoryAsync.CopyAsync(centralCacheModules2, homeCacheModules2, true);

_logger.LogInformation($"Copying the existing Gradle wrapper from '{centralCacheWrapper}' to '{homeCacheWrapper}': {gradleTemporaryWorkspace.Path}");
await DirectoryAsync.CopyAsync(centralCacheWrapper, homeCacheWrapper, true);
}

cancellationToken.ThrowIfCancellationRequested();

ok = true;
return new GradleWorkspaceInstance(_logger, gradleDownloadCache, gradleTemporaryWorkspace);
}
finally
{
if (!ok)
{
if (gradleDownloadCache != null)
{
try
{
await gradleDownloadCache.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to release Gradle download cache: {ex}");
}
}

if (gradleTemporaryWorkspace != null)
{
try
{
await gradleTemporaryWorkspace.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to release Gradle home: {ex}");
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
namespace Redpoint.Uet.BuildPipeline.BuildGraph.Gradle
{
using Microsoft.Extensions.Logging;
using Redpoint.IO;
using Redpoint.Uet.Workspace;
using System;
using System.Threading.Tasks;

internal class GradleWorkspaceInstance : IAsyncDisposable
{
private readonly ILogger _logger;
private readonly IWorkspace _gradleDownloadCache;
private readonly IWorkspace _gradleTemporaryWorkspace;
private bool _buildSuccessful;

public GradleWorkspaceInstance(
ILogger logger,
IWorkspace gradleDownloadCache,
IWorkspace gradleTemporaryWorkspace)
{
_logger = logger;
_gradleDownloadCache = gradleDownloadCache;
_gradleTemporaryWorkspace = gradleTemporaryWorkspace;
_buildSuccessful = false;
}

public string GradleHomePath => _gradleTemporaryWorkspace.Path;

public void MarkBuildAsSuccessful()
{
_buildSuccessful = true;
}

public async ValueTask DisposeAsync()
{
if (_buildSuccessful)
{
// If the build was successful, copy the Gradle download cache back.
var centralCacheSafeToUse = Path.Combine(_gradleDownloadCache.Path, "safe-to-use");
var centralCacheModules2 = Path.Combine(_gradleDownloadCache.Path, "modules-2");
var centralCacheWrapper = Path.Combine(_gradleDownloadCache.Path, "wrapper");

var homeCacheModules2 = Path.Combine(_gradleTemporaryWorkspace.Path, "caches", "modules-2");
var homeCacheWrapper = Path.Combine(_gradleTemporaryWorkspace.Path, "wrapper");

try
{
_logger.LogInformation($"Removing existing download cache so we can copy across the new version from the build: {_gradleDownloadCache.Path}");
if (File.Exists(centralCacheSafeToUse))
{
File.Delete(centralCacheSafeToUse);
}

do
{
try
{
if (Directory.Exists(homeCacheModules2))
{
await DirectoryAsync.DeleteAsync(centralCacheModules2, true);
}
if (Directory.Exists(homeCacheWrapper))
{
await DirectoryAsync.DeleteAsync(centralCacheWrapper, true);
}
break;
}
catch (IOException ex) when (ex.Message.Contains("The directory is not empty.", StringComparison.Ordinal))
{
_logger.LogInformation("Can't remove existing central cache yet, trying again in 1 second...");
await Task.Delay(1000);
continue;
}
}
while (true);

if (Directory.Exists(homeCacheModules2))
{
_logger.LogInformation($"Copying home Gradle download cache from '{homeCacheModules2}' to central download cache at '{centralCacheModules2}'.");
await DirectoryAsync.CopyAsync(homeCacheModules2, centralCacheModules2, true);
}

if (Directory.Exists(homeCacheWrapper))
{
_logger.LogInformation($"Copying home Gradle wrapper from '{homeCacheWrapper}' to central download cache at '{centralCacheWrapper}'.");
await DirectoryAsync.CopyAsync(homeCacheWrapper, centralCacheWrapper, true);
}

_logger.LogInformation($"Marking central download cache as safe to use.");
File.WriteAllText(centralCacheSafeToUse, "ok");
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to copy download cache back to central download cache; it will not be used until a successful copy occurs again: {ex}");
}
}
else
{
_logger.LogInformation($"Not syncing download cache to central download cache as the build was not successful.");
}

try
{
await _gradleDownloadCache.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to release Gradle download cache: {ex}");
}

try
{
await _gradleTemporaryWorkspace.DisposeAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning($"Failed to release Gradle home: {ex}");
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Redpoint.Uet.BuildPipeline.BuildGraph.Gradle
{
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

internal interface IGradleWorkspace
{
Task<GradleWorkspaceInstance> GetGradleWorkspaceInstance(CancellationToken cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Redpoint.Uet.BuildPipeline
using Microsoft.Extensions.DependencyInjection;
using Redpoint.Uet.BuildPipeline.BuildGraph;
using Redpoint.Uet.BuildPipeline.BuildGraph.Dynamic;
using Redpoint.Uet.BuildPipeline.BuildGraph.Gradle;
using Redpoint.Uet.BuildPipeline.BuildGraph.MobileProvisioning;
using Redpoint.Uet.BuildPipeline.BuildGraph.Patching;
using Redpoint.Uet.BuildPipeline.BuildGraph.PreBuild;
Expand All @@ -24,6 +25,7 @@ public static void AddUETBuildPipeline(this IServiceCollection services)
services.AddSingleton<ISdkSetupForBuildExecutor, DefaultSdkSetupForBuildExecutor>();
services.AddSingleton<IDynamicBuildGraphIncludeWriter, DefaultDynamicBuildGraphIncludeWriter>();
services.AddSingleton<IPreBuild, DefaultPreBuild>();
services.AddSingleton<IGradleWorkspace, DefaultGradleWorkspace>();
if (OperatingSystem.IsMacOS())
{
services.AddSingleton<IMobileProvisioning, MacMobileProvisioning>();
Expand Down

0 comments on commit f462829

Please sign in to comment.