diff --git a/src/Cake/Commands/DefaultCommandSettings.cs b/src/Cake/Commands/DefaultCommandSettings.cs
index 88d0c43270..5e7fce5691 100644
--- a/src/Cake/Commands/DefaultCommandSettings.cs
+++ b/src/Cake/Commands/DefaultCommandSettings.cs
@@ -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; }
}
}
diff --git a/src/Cake/Infrastructure/CakeConfigurationExtensions.cs b/src/Cake/Infrastructure/CakeConfigurationExtensions.cs
new file mode 100644
index 0000000000..eebc9e9b64
--- /dev/null
+++ b/src/Cake/Infrastructure/CakeConfigurationExtensions.cs
@@ -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
+{
+ ///
+ /// Contains extension methods for .
+ ///
+ internal static class CakeConfigurationExtensions
+ {
+ ///
+ /// Gets the script cache directory path.
+ ///
+ /// The Cake configuration.
+ /// The default root path.
+ /// The environment.
+ /// The script cache directory path.
+ 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();
+ }
+ }
+}
diff --git a/src/Cake/Infrastructure/Constants.cs b/src/Cake/Infrastructure/Constants.cs
new file mode 100644
index 0000000000..f3af4a73db
--- /dev/null
+++ b/src/Cake/Infrastructure/Constants.cs
@@ -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";
+ }
+ }
+}
diff --git a/src/Cake/Infrastructure/IScriptHostSettings.cs b/src/Cake/Infrastructure/IScriptHostSettings.cs
index c9d34d39f5..3755facfa8 100644
--- a/src/Cake/Infrastructure/IScriptHostSettings.cs
+++ b/src/Cake/Infrastructure/IScriptHostSettings.cs
@@ -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; }
}
}
diff --git a/src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs b/src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs
index a610344540..6153104195 100644
--- a/src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs
+++ b/src/Cake/Infrastructure/Scripting/RoslynScriptSession.cs
@@ -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;
@@ -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 ReferencePaths { get; }
public HashSet References { get; }
@@ -40,6 +49,7 @@ public RoslynScriptSession(
IScriptHostSettings settings)
{
_host = host;
+ _fileSystem = host.Context.FileSystem;
_loader = loader;
_log = log;
_configuration = configuration;
@@ -48,6 +58,11 @@ public RoslynScriptSession(
ReferencePaths = new HashSet(PathComparer.Default);
References = new HashSet();
Namespaces = new HashSet(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)
@@ -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);
@@ -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("", new[] { typeof(object[]) });
+ var task = (Task