Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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