Skip to content

Commit

Permalink
cake-buildGH-2099: Cache compiled script on disk
Browse files Browse the repository at this point in the history
* Enable
    * Config: Settings.EnableScriptCache=true
    * Arg: --settings_enablescriptcache=true
    * Env: CAKE_SETTINGS_ENABLESCRIPTCACHE=true
* Cache Location
    * Config: Paths.Cache = /path/to/cache
    * Arg: --paths_cache=/path/to/cache
    * Env: CAKE_PATHS_CACHE=/path/to/cache
* fixes cake-build#2099

Co-authored-by: devlead@users.noreply.github.com
  • Loading branch information
tstewart65 authored and devlead committed Apr 3, 2022
1 parent 549ae60 commit f4c9ac7
Show file tree
Hide file tree
Showing 9 changed files with 350 additions and 1 deletion.
4 changes: 4 additions & 0 deletions src/Cake/Commands/DefaultCommandSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,9 @@ public sealed class DefaultCommandSettings : CommandSettings
[CommandOption("--info")]
[Description("Displays additional information about Cake.")]
public bool ShowInfo { get; set; }

[CommandOption("--" + Infrastructure.Constants.Cache.InvalidateScriptCache)]
[Description("Forces the script to be recompiled if caching is enabled.")]
public bool Recompile { get; set; }
}
}
34 changes: 34 additions & 0 deletions src/Cake/Infrastructure/CakeConfigurationExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Cake.Core;
using Cake.Core.Configuration;
using Cake.Core.IO;

namespace Cake.Infrastructure
{
/// <summary>
/// Contains extension methods for <see cref="ICakeConfiguration"/>.
/// </summary>
internal static class CakeConfigurationExtensions
{
/// <summary>
/// Gets the script cache directory path.
/// </summary>
/// <param name="configuration">The Cake configuration.</param>
/// <param name="defaultRoot">The default root path.</param>
/// <param name="environment">The environment.</param>
/// <returns>The script cache directory path.</returns>
public static DirectoryPath GetScriptCachePath(this ICakeConfiguration configuration, DirectoryPath defaultRoot, ICakeEnvironment environment)
{
var cachePath = configuration.GetValue(Constants.Paths.Cache);
if (!string.IsNullOrWhiteSpace(cachePath))
{
return new DirectoryPath(cachePath).MakeAbsolute(environment);
}
var toolPath = configuration.GetToolPath(defaultRoot, environment);
return toolPath.Combine("cache").Collapse();
}
}
}
24 changes: 24 additions & 0 deletions src/Cake/Infrastructure/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Cake.Infrastructure
{
internal static class Constants
{
public static class Settings
{
public const string EnableScriptCache = "Settings_EnableScriptCache";
}

public static class Paths
{
public const string Cache = "Paths_Cache";
}

public static class Cache
{
public const string InvalidateScriptCache = "invalidate-script-cache";
}
}
}
4 changes: 4 additions & 0 deletions src/Cake/Infrastructure/IScriptHostSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Cake.Core.IO;

namespace Cake.Infrastructure
{
public interface IScriptHostSettings
{
bool Debug { get; }

FilePath Script { get; }
}
}
76 changes: 75 additions & 1 deletion src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Cake.Core;
using Cake.Core.Configuration;
using Cake.Core.Diagnostics;
using Cake.Core.IO;
using Cake.Core.Reflection;
using Cake.Core.Scripting;
using Cake.Infrastructure.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Scripting;

Expand All @@ -21,11 +25,16 @@ namespace Cake.Infrastructure.Scripting
public sealed class RoslynScriptSession : IScriptSession
{
private readonly IScriptHost _host;
private readonly IFileSystem _fileSystem;
private readonly IAssemblyLoader _loader;
private readonly ICakeLog _log;
private readonly ICakeConfiguration _configuration;
private readonly IScriptHostSettings _settings;

private readonly bool _scriptCacheEnabled;
private readonly bool _regenerateCache;
private readonly DirectoryPath _scriptCachePath;

public HashSet<FilePath> ReferencePaths { get; }

public HashSet<Assembly> References { get; }
Expand All @@ -40,6 +49,7 @@ public RoslynScriptSession(
IScriptHostSettings settings)
{
_host = host;
_fileSystem = host.Context.FileSystem;
_loader = loader;
_log = log;
_configuration = configuration;
Expand All @@ -48,6 +58,11 @@ public RoslynScriptSession(
ReferencePaths = new HashSet<FilePath>(PathComparer.Default);
References = new HashSet<Assembly>();
Namespaces = new HashSet<string>(StringComparer.Ordinal);

var cacheEnabled = configuration.GetValue(Constants.Settings.EnableScriptCache) ?? bool.FalseString;
_scriptCacheEnabled = cacheEnabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
_regenerateCache = host.Context.Arguments.HasArgument(Constants.Cache.InvalidateScriptCache);
_scriptCachePath = configuration.GetScriptCachePath(settings.Script.GetDirectory(), host.Context.Environment);
}

public void AddReference(Assembly assembly)
Expand Down Expand Up @@ -82,6 +97,30 @@ public void ImportNamespace(string @namespace)

public void Execute(Script script)
{
var scriptName = _settings.Script.GetFilename();
var cacheDLLFileName = $"{scriptName}.dll";
var cacheHashFileName = $"{scriptName}.hash";
var cachedAssembly = _scriptCachePath.CombineWithFilePath(cacheDLLFileName);
var hashFile = _scriptCachePath.CombineWithFilePath(cacheHashFileName);
string scriptHash = default;
if (_scriptCacheEnabled && _fileSystem.Exist(cachedAssembly) && !_regenerateCache)
{
_log.Verbose($"Cache enabled: Checking cache build script ({cacheDLLFileName})");
scriptHash = FastHash.GenerateHash(Encoding.UTF8.GetBytes(string.Concat(script.Lines)));
var cachedHash = _fileSystem.Exist(hashFile)
? _fileSystem.GetFile(hashFile).ReadLines(Encoding.UTF8).FirstOrDefault()
: string.Empty;
if (scriptHash.Equals(cachedHash, StringComparison.Ordinal))
{
_log.Verbose("Running cached build script...");
RunScriptAssembly(cachedAssembly.FullPath);
return;
}
else
{
_log.Verbose("Cache check failed.");
}
}
// Generate the script code.
var generator = new RoslynCodeGenerator();
var code = generator.Generate(script);
Expand Down Expand Up @@ -159,7 +198,42 @@ public void Execute(Script script)
throw new CakeException(message);
}

roslynScript.RunAsync(_host).Wait();
if (_scriptCacheEnabled)
{
// Verify cache directory exists
if (!_fileSystem.GetDirectory(_scriptCachePath).Exists)
{
_fileSystem.GetDirectory(_scriptCachePath).Create();
}
if (string.IsNullOrWhiteSpace(scriptHash))
{
scriptHash = FastHash.GenerateHash(Encoding.UTF8.GetBytes(string.Concat(script.Lines)));
}
var emitResult = compilation.Emit(cachedAssembly.FullPath);

if (emitResult.Success)
{
using (var stream = _fileSystem.GetFile(hashFile).OpenWrite())
using (var writer = new StreamWriter(stream, Encoding.UTF8))
{
writer.Write(scriptHash);
}
RunScriptAssembly(cachedAssembly.FullPath);
}
}
else
{
roslynScript.RunAsync(_host).GetAwaiter().GetResult();
}
}

private void RunScriptAssembly(string assemblyPath)
{
var assembly = _loader.Load(assemblyPath, false);
var type = assembly.GetType("Submission#0");
var factoryMethod = type.GetMethod("<Factory>", new[] { typeof(object[]) });
var task = (Task<object>)factoryMethod.Invoke(null, new object[] { new object[] { _host, null } });
task.GetAwaiter().GetResult();
}
}
}
67 changes: 67 additions & 0 deletions src/Cake/Infrastructure/Utilities/FastHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;

namespace Cake.Infrastructure.Utilities
{
/// <summary>
/// Optimized hash generator. Using SHA512 since it is FIPS compliant.
/// </summary>
internal static class FastHash
{
/// <summary>
/// Generates a hash of the passed byte arrays.
/// </summary>
/// <param name="input">The binary data to hash.</param>
/// <returns>The hash value.</returns>
public static string GenerateHash(byte[] input)
{
using (var sha512 = SHA512.Create())
{
sha512.TransformBlock(input, 0, input.Length, input, 0);

// Just finalize with empty bytes so we don't have to iterate over the enumerable multiple times
sha512.TransformFinalBlock(Encoding.UTF8.GetBytes(string.Empty), 0, 0);
// Convert to hex string; This method is supposedly faster than the usual StringBuilder approach
return ConvertBits(sha512.Hash);
}
}

/// <summary>
/// Generates a hash of the passed byte arrays.
/// </summary>
/// <param name="inputs">The binary data to hash.</param>
/// <returns>The hash value.</returns>
public static string GenerateHash(IEnumerable<byte[]> inputs)
{
using (var sha512 = SHA512.Create())
{
foreach (var input in inputs)
{
sha512.TransformBlock(input, 0, input.Length, input, 0);
}

// Just finalize with empty bytes so we don't have to iterate over the enumerable multiple times
sha512.TransformFinalBlock(Encoding.UTF8.GetBytes(string.Empty), 0, 0);
// Convert to hex string; This method is supposedly faster than the usual StringBuilder approach
return ConvertBits(sha512.Hash);
}
}

private static string ConvertBits(byte[] hash)
{
#if NETCOREAPP3_1
return BitConverter.ToString(hash)
// without dashes
.Replace("-", string.Empty);
#else
return Convert.ToHexString(hash);
#endif
}
}
}
Loading

0 comments on commit f4c9ac7

Please sign in to comment.