Skip to content

Commit

Permalink
cake-buildGH-2099: Also takes care of RFC-02.
Browse files Browse the repository at this point in the history
This is the same as pull request 2650 but has been updated to match the change made in Cake version 1.0.0.
  • Loading branch information
tstewart65 authored and devlead committed Mar 22, 2022
1 parent d9db99b commit 8bad918
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/Cake.Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,11 @@ public static class Paths
public const string Addins = "Paths_Addins";
public const string Modules = "Paths_Modules";
}

public static class Cache
{
public const string Enabled = "Cache_Enabled";
public const string Path = "Cache_Path";
}
}
}
18 changes: 18 additions & 0 deletions src/Cake.Core/Extensions/CakeConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,23 @@ public static DirectoryPath GetModulePath(this ICakeConfiguration configuration,
var toolPath = configuration.GetToolPath(defaultRoot, environment);
return toolPath.Combine("Modules").Collapse();
}

/// <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.Cache.Path);
if (!string.IsNullOrWhiteSpace(cachePath))
{
return new DirectoryPath(cachePath).MakeAbsolute(environment);
}
var toolPath = configuration.GetToolPath(defaultRoot, environment);
return toolPath.Combine("cache").Collapse();
}
}
}
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("--regeneratecache")]
[Description("Forces the script cache to be regenerated if caching is enabled.")]
public bool Recompile { get; set; }
}
}
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; }
}
}
85 changes: 84 additions & 1 deletion src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,38 @@
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;
using Cake.Utilities;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using RoslynScript = Microsoft.CodeAnalysis.Scripting.Script<object>;

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

var cacheEnabled = configuration.GetValue(Constants.Cache.Enabled) ?? "false";
_scriptCacheEnabled = cacheEnabled.Equals("true", StringComparison.OrdinalIgnoreCase);
_regenerateCache = host.Context.Arguments.HasArgument("regeneratecache");
_scriptCachePath = configuration.GetScriptCachePath(settings.Script.GetDirectory(), host.Context.Environment);
}

public void AddReference(Assembly assembly)
Expand Down Expand Up @@ -82,6 +99,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.InvariantCultureIgnoreCase))
{
_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 +200,49 @@ 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).Wait();
}
}

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[]) });
try
{
var task = (Task<object>)factoryMethod.Invoke(null, new object[] { new object[] { _host, null } });
task.Wait();
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
}
}
65 changes: 65 additions & 0 deletions src/Cake/Utilities/FastHash.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// 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.Utilities
{
/// <summary>
/// Optimized hash generator. Using SHA1CryptoServiceProvider 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 sha1 = new SHA1CryptoServiceProvider())
{
sha1.TransformBlock(input, 0, input.Length, input, 0);

// Just finalize with empty bytes so we don't have to iterate over the enumerable multiple times
sha1.TransformFinalBlock(Encoding.UTF8.GetBytes(string.Empty), 0, 0);
// Convert to hex string; This method is supposedly faster than the usual StringBuilder approach
return ConvertBits(sha1.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 sha1 = new SHA1CryptoServiceProvider())
{
foreach (var input in inputs)
{
sha1.TransformBlock(input, 0, input.Length, input, 0);
}

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

private static string ConvertBits(byte[] hash)
{
return BitConverter.ToString(hash)
// without dashes
.Replace("-", string.Empty)
// make lowercase
.ToLower();
}
}
}

0 comments on commit 8bad918

Please sign in to comment.