diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 31e896e989..b6cadaf67b 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"cake.tool": {
- "version": "2.0.0",
+ "version": "2.1.0",
"commands": [
"dotnet-cake"
]
diff --git a/GitReleaseManager.yaml b/GitReleaseManager.yaml
index 6b71b019b1..76c66f5ad9 100644
--- a/GitReleaseManager.yaml
+++ b/GitReleaseManager.yaml
@@ -22,8 +22,6 @@ close:
The release is available on:
- [GitHub Release](https://github.com/{owner}/{repository}/releases/tag/{milestone})
- - [NuGet Package](https://www.nuget.org/packages/Cake/{milestone})
- - [Chocolatey Package](https://chocolatey.org/packages/cake.portable/{milestone})
- - [.Net Global Tool](https://www.nuget.org/packages/Cake.Tool/{milestone})
-
+ - [.NET Tool](https://www.nuget.org/packages/Cake.Tool/{milestone})
+
Your **[GitReleaseManager](https://github.com/GitTools/GitReleaseManager)** bot :package::rocket:
\ No newline at end of file
diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index cafe213900..3695d197c3 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -1,3 +1,22 @@
+### New in 2.2.0 (Released 2022/04/15)
+
+* 3821 PostAction is not setable on DotNetSettings.
+* 3485 Add alias for dotnet workload search command.
+* 2099 Cache compiled script on disk.
+* 3866 Update Microsoft.NETCore.Platforms to 6.0.3.
+* 3854 Update Spectre.Console to 0.44.0.
+* 3851 Update System.Reflection.Metadata to 6.0.1.
+* 3846 Update Microsoft.CodeAnalysis.CSharp.Scripting to 4.1.0.
+* 3844 Update Microsoft.NETCore.Platforms to 6.0.2.
+* 3843 Update NuGet.* to 6.1.0.
+* 2763 Provide property to return parent directory on DirectoryPath.
+* 2431 UploadFile should support option of username/password.
+* 3819 Update Git Release Manager Comment template to remove Cake NuGet package and Chocolatey portable.
+* 3859 PathCollapser.Collapse breaks UNC paths.
+* 3858 PathCollapser.Collapse shows wrong output for if .. is the second segment in the path.
+* 3823 Executing a cake script leads to System.IO.FileNotFoundException for several System.(...) assemblies.
+* 3735 Incorrect warnings in diagnostic logs.
+
### New in 2.1.0 (Released 2022/02/19)
* 2524 XmlTransform support for xsl arguments
diff --git a/global.json b/global.json
index c0d1b00472..65c27ed7be 100644
--- a/global.json
+++ b/global.json
@@ -3,7 +3,7 @@
"src"
],
"sdk": {
- "version": "6.0.102",
+ "version": "6.0.202",
"rollForward": "latestFeature"
}
}
\ No newline at end of file
diff --git a/src/Cake.Cli/Cake.Cli.csproj b/src/Cake.Cli/Cake.Cli.csproj
index 28ee02e248..f517071b83 100644
--- a/src/Cake.Cli/Cake.Cli.csproj
+++ b/src/Cake.Cli/Cake.Cli.csproj
@@ -19,6 +19,6 @@
-
+
\ No newline at end of file
diff --git a/src/Cake.Common.Tests/Cake.Common.Tests.csproj b/src/Cake.Common.Tests/Cake.Common.Tests.csproj
index edde2300af..60e0e0a249 100644
--- a/src/Cake.Common.Tests/Cake.Common.Tests.csproj
+++ b/src/Cake.Common.Tests/Cake.Common.Tests.csproj
@@ -16,7 +16,7 @@
-
+
all
diff --git a/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/Search/DotNetWorkloadSearcherFixture.cs b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/Search/DotNetWorkloadSearcherFixture.cs
new file mode 100644
index 0000000000..66cd3fbe82
--- /dev/null
+++ b/src/Cake.Common.Tests/Fixtures/Tools/DotNet/Workload/Search/DotNetWorkloadSearcherFixture.cs
@@ -0,0 +1,33 @@
+// 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.Collections.Generic;
+using Cake.Common.Tools.DotNet.Workload.Search;
+
+namespace Cake.Common.Tests.Fixtures.Tools.DotNet.Workload.Search
+{
+ internal sealed class DotNetWorkloadSearcherFixture : DotNetFixture
+ {
+ public string SearchString { get; set; }
+ public IEnumerable Workloads { get; set; }
+
+ public void GivenAvailableWorkloadsResult()
+ {
+ ProcessRunner.Process.SetStandardOutput(new string[]
+ {
+ "Workload ID Description",
+ "-----------------------------------------------------",
+ "maui .NET MAUI SDK for all platforms",
+ "maui-desktop .NET MAUI SDK for Desktop",
+ "maui-mobile .NET MAUI SDK for Mobile"
+ });
+ }
+
+ protected override void RunTool()
+ {
+ var tool = new DotNetWorkloadSearcher(FileSystem, Environment, ProcessRunner, Tools);
+ Workloads = tool.Search(SearchString, Settings);
+ }
+ }
+}
diff --git a/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/Search/DotNetWorkloadSearchTests.cs b/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/Search/DotNetWorkloadSearchTests.cs
new file mode 100644
index 0000000000..d6413d83f7
--- /dev/null
+++ b/src/Cake.Common.Tests/Unit/Tools/DotNet/Workload/Search/DotNetWorkloadSearchTests.cs
@@ -0,0 +1,88 @@
+// 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.Common.Tests.Fixtures.Tools.DotNet.Workload.Search;
+using Cake.Testing;
+using Xunit;
+
+namespace Cake.Common.Tests.Unit.Tools.DotNet.Workload.Search
+{
+ public sealed class DotNetWorkloadSearchTests
+ {
+ public sealed class TheWorkloadSearchMethod
+ {
+ [Fact]
+ public void Should_Throw_If_Process_Was_Not_Started()
+ {
+ // Given
+ var fixture = new DotNetWorkloadSearcherFixture();
+ fixture.GivenProcessCannotStart();
+
+ // When
+ var result = Record.Exception(() => fixture.Run());
+
+ // Then
+ AssertEx.IsCakeException(result, ".NET CLI: Process was not started.");
+ }
+
+ [Fact]
+ public void Should_Throw_If_Process_Has_A_Non_Zero_Exit_Code()
+ {
+ // Given
+ var fixture = new DotNetWorkloadSearcherFixture();
+ fixture.GivenProcessExitsWithCode(1);
+
+ // When
+ var result = Record.Exception(() => fixture.Run());
+
+ // Then
+ AssertEx.IsCakeException(result, ".NET CLI: Process returned an error (exit code 1).");
+ }
+
+ [Fact]
+ public void Should_Add_SearchString_Argument()
+ {
+ // Given
+ var fixture = new DotNetWorkloadSearcherFixture();
+ fixture.SearchString = "maui";
+
+ // When
+ var result = fixture.Run();
+
+ // Then
+ Assert.Equal("workload search maui", result.Args);
+ }
+
+ [Fact]
+ public void Should_Return_Correct_List_Of_Workloads()
+ {
+ // Given
+ var fixture = new DotNetWorkloadSearcherFixture();
+ fixture.SearchString = "maui";
+ fixture.GivenAvailableWorkloadsResult();
+
+ // When
+ var result = fixture.Run();
+
+ // Then
+ Assert.Collection(fixture.Workloads,
+ item =>
+ {
+ Assert.Equal(item.Id, "maui");
+ Assert.Equal(item.Description, ".NET MAUI SDK for all platforms");
+ },
+ item =>
+ {
+ Assert.Equal(item.Id, "maui-desktop");
+ Assert.Equal(item.Description, ".NET MAUI SDK for Desktop");
+ },
+ item =>
+ {
+ Assert.Equal(item.Id, "maui-mobile");
+ Assert.Equal(item.Description, ".NET MAUI SDK for Mobile");
+ });
+ }
+ }
+ }
+}
diff --git a/src/Cake.Common/Net/HttpAliases.cs b/src/Cake.Common/Net/HttpAliases.cs
index 2a0b4a1288..20b346861e 100644
--- a/src/Cake.Common/Net/HttpAliases.cs
+++ b/src/Cake.Common/Net/HttpAliases.cs
@@ -235,15 +235,20 @@ public static void DownloadFile(this ICakeContext context, Uri address, FilePath
///
///
/// var address = new Uri("http://www.example.org/upload");
- /// UploadFile(address, @"path/to/file.txt");
+ /// UploadFile(address, @"path/to/file.txt", new UploadFileSettings()
+ /// {
+ /// Username = "bob",
+ /// Password = "builder"
+ /// }
///
///
/// The context.
/// The URL of the upload resource.
/// The file to upload.
+ /// The settings.
[CakeMethodAlias]
[CakeAliasCategory("Upload")]
- public static void UploadFile(this ICakeContext context, Uri address, FilePath filePath)
+ public static void UploadFile(this ICakeContext context, Uri address, FilePath filePath, UploadFileSettings settings)
{
if (context == null)
{
@@ -259,8 +264,17 @@ public static void UploadFile(this ICakeContext context, Uri address, FilePath f
}
context.Log.Verbose("Uploading file: {0}", address);
- using (var client = GetHttpClient(context, false))
+ using (var client = GetHttpClient(context, settings.UseDefaultCredentials))
{
+ if (!settings.UseDefaultCredentials)
+ {
+ if (!string.IsNullOrWhiteSpace(settings.Username) && !string.IsNullOrWhiteSpace(settings.Password))
+ {
+ var byteArray = Encoding.ASCII.GetBytes(string.Concat(settings.Username, ":", settings.Password));
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
+ }
+ }
+
client.UploadFileAsync(address, filePath.FullPath).Wait();
}
context.Log.Verbose("File upload complete");
@@ -282,7 +296,7 @@ public static void UploadFile(this ICakeContext context, Uri address, FilePath f
[CakeAliasCategory("Upload")]
public static void UploadFile(this ICakeContext context, string address, FilePath filePath)
{
- UploadFile(context, new Uri(address), filePath);
+ UploadFile(context, new Uri(address), filePath, new UploadFileSettings());
}
///
@@ -291,16 +305,20 @@ public static void UploadFile(this ICakeContext context, string address, FilePat
///
///
/// var address = new Uri("http://www.example.org/upload");
- /// UploadFile(address, @"path/to/file.txt");
+ /// UploadFile(address, @"path/to/file.txt", new UploadFileSettings() {
+ /// Username = "bob",
+ /// Password = "builder"
+ /// });
///
///
/// The context.
/// The URL of the upload resource.
/// The data to upload.
/// The filename to give the uploaded data.
+ /// The settings.
[CakeMethodAlias]
[CakeAliasCategory("Upload")]
- public static void UploadFile(this ICakeContext context, Uri address, byte[] data, string fileName)
+ public static void UploadFile(this ICakeContext context, Uri address, byte[] data, string fileName, UploadFileSettings settings)
{
if (context == null)
{
@@ -316,8 +334,17 @@ public static void UploadFile(this ICakeContext context, Uri address, byte[] dat
}
context.Log.Verbose("Uploading file: {0}", address);
- using (var client = GetHttpClient(context, false))
+ using (var client = GetHttpClient(context, settings.UseDefaultCredentials))
{
+ if (!settings.UseDefaultCredentials)
+ {
+ if (!string.IsNullOrWhiteSpace(settings.Username) && !string.IsNullOrWhiteSpace(settings.Password))
+ {
+ var byteArray = Encoding.ASCII.GetBytes(string.Concat(settings.Username, ":", settings.Password));
+ client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
+ }
+ }
+
client.UploadFileAsync(address, data, fileName).Wait();
}
context.Log.Verbose("File upload complete");
@@ -340,7 +367,7 @@ public static void UploadFile(this ICakeContext context, Uri address, byte[] dat
[CakeAliasCategory("Upload")]
public static void UploadFile(this ICakeContext context, string address, byte[] data, string fileName)
{
- UploadFile(context, new Uri(address), data, fileName);
+ UploadFile(context, new Uri(address), data, fileName, new UploadFileSettings());
}
///
diff --git a/src/Cake.Common/Net/UploadFileSettings.cs b/src/Cake.Common/Net/UploadFileSettings.cs
new file mode 100644
index 0000000000..4c6f7d66af
--- /dev/null
+++ b/src/Cake.Common/Net/UploadFileSettings.cs
@@ -0,0 +1,30 @@
+// 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.Common.Net
+{
+ ///
+ /// Contains settings for .
+ ///
+ public sealed class UploadFileSettings
+ {
+ ///
+ /// Gets or sets the username to use when uploadingthe file.
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// Gets or sets the password to use when uploading the file.
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether default credentials are sent when uploading the file.
+ ///
+ ///
+ /// If set to true, any username and password that has been specified will be ignored.
+ ///
+ public bool UseDefaultCredentials { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs
index 311f0b6a67..0a84cb7bca 100644
--- a/src/Cake.Common/Tools/DotNet/DotNetAliases.cs
+++ b/src/Cake.Common/Tools/DotNet/DotNetAliases.cs
@@ -22,6 +22,7 @@
using Cake.Common.Tools.DotNet.Test;
using Cake.Common.Tools.DotNet.Tool;
using Cake.Common.Tools.DotNet.VSTest;
+using Cake.Common.Tools.DotNet.Workload.Search;
using Cake.Common.Tools.DotNetCore.Build;
using Cake.Common.Tools.DotNetCore.BuildServer;
using Cake.Common.Tools.DotNetCore.Clean;
@@ -1845,5 +1846,93 @@ public static void DotNetSDKCheck(this ICakeContext context)
var checker = new DotNetSDKChecker(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools);
checker.Check();
}
+
+ ///
+ /// Lists available workloads.
+ ///
+ /// The context.
+ /// The list of available workloads.
+ ///
+ ///
+ /// var workloads = DotNetWorkloadSearch();
+ ///
+ /// foreach (var workload in workloads)
+ /// {
+ /// Information($"Id: {workload.Id}, Description: {workload.Description}");
+ /// }
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory("Workload")]
+ [CakeNamespaceImport("Cake.Common.Tools.DotNet.Workload.Search")]
+ public static IEnumerable DotNetWorkloadSearch(this ICakeContext context)
+ {
+ return context.DotNetWorkloadSearch(null);
+ }
+
+ ///
+ /// Lists available workloads by specifying all or part of the workload ID.
+ ///
+ /// The context.
+ /// The workload ID to search for, or part of it.
+ /// The list of available workloads.
+ ///
+ ///
+ /// var workloads = DotNetWorkloadSearch("maui");
+ ///
+ /// foreach (var workload in workloads)
+ /// {
+ /// Information($"Id: {workload.Id}, Description: {workload.Description}");
+ /// }
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory("Workload")]
+ [CakeNamespaceImport("Cake.Common.Tools.DotNet.Workload.Search")]
+ public static IEnumerable DotNetWorkloadSearch(this ICakeContext context, string searchString)
+ {
+ return context.DotNetWorkloadSearch(searchString, null);
+ }
+
+ ///
+ /// Lists available workloads by specifying all or part of the workload ID.
+ ///
+ /// The context.
+ /// The workload ID to search for, or part of it.
+ /// The settings.
+ /// The list of available workloads.
+ ///
+ ///
+ /// var settings = new DotNetWorkloadSearchSettings
+ /// {
+ /// Verbosity = Detailed
+ /// };
+ ///
+ /// var workloads = DotNetWorkloadSearch("maui", settings);
+ ///
+ /// foreach (var workload in workloads)
+ /// {
+ /// Information($"Id: {workload.Id}, Description: {workload.Description}");
+ /// }
+ ///
+ ///
+ [CakeMethodAlias]
+ [CakeAliasCategory("Workload")]
+ [CakeNamespaceImport("Cake.Common.Tools.DotNet.Workload.Search")]
+ public static IEnumerable DotNetWorkloadSearch(this ICakeContext context, string searchString, DotNetWorkloadSearchSettings settings)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (settings == null)
+ {
+ settings = new DotNetWorkloadSearchSettings();
+ }
+
+ var searcher = new DotNetWorkloadSearcher(context.FileSystem, context.Environment, context.ProcessRunner, context.Tools);
+ return searcher.Search(searchString, settings);
+ }
}
}
diff --git a/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkload.cs b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkload.cs
new file mode 100644
index 0000000000..4ebd1583b1
--- /dev/null
+++ b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkload.cs
@@ -0,0 +1,33 @@
+// 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.Common.Tools.DotNet.Workload.Search
+{
+ ///
+ /// Workload information.
+ ///
+ public class DotNetWorkload
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The workload Id.
+ /// The workload description.
+ public DotNetWorkload(string id, string description)
+ {
+ Id = id;
+ Description = description;
+ }
+
+ ///
+ /// Gets the workload Id.
+ ///
+ public string Id { get; }
+
+ ///
+ /// Gets the workload description.
+ ///
+ public string Description { get; }
+ }
+}
diff --git a/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearchSettings.cs b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearchSettings.cs
new file mode 100644
index 0000000000..d94c6ee90d
--- /dev/null
+++ b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearchSettings.cs
@@ -0,0 +1,13 @@
+// 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.Common.Tools.DotNet.Workload.Search
+{
+ ///
+ /// Contains settings used by .
+ ///
+ public sealed class DotNetWorkloadSearchSettings : DotNetSettings
+ {
+ }
+}
diff --git a/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearcher.cs b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearcher.cs
new file mode 100644
index 0000000000..e24df64f52
--- /dev/null
+++ b/src/Cake.Common/Tools/DotNet/Workload/Search/DotNetWorkloadSearcher.cs
@@ -0,0 +1,107 @@
+// 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.Linq;
+using Cake.Core;
+using Cake.Core.IO;
+using Cake.Core.Tooling;
+
+namespace Cake.Common.Tools.DotNet.Workload.Search
+{
+ ///
+ /// .NET workloads searcher.
+ ///
+ public sealed class DotNetWorkloadSearcher : DotNetTool
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The file system.
+ /// The environment.
+ /// The process runner.
+ /// The tool locator.
+ public DotNetWorkloadSearcher(
+ IFileSystem fileSystem,
+ ICakeEnvironment environment,
+ IProcessRunner processRunner,
+ IToolLocator tools) : base(fileSystem, environment, processRunner, tools)
+ {
+ }
+
+ ///
+ /// Lists the latest available version of the .NET SDK and .NET Runtime, for each feature band.
+ ///
+ /// The workload ID to search for, or part of it.
+ /// The settings.
+ /// The list of available workloads.
+ public IEnumerable Search(string searchString, DotNetWorkloadSearchSettings settings)
+ {
+ if (settings == null)
+ {
+ throw new ArgumentNullException(nameof(settings));
+ }
+
+ var processSettings = new ProcessSettings
+ {
+ RedirectStandardOutput = true
+ };
+
+ IEnumerable result = null;
+ RunCommand(settings, GetArguments(searchString, settings), processSettings,
+ process => result = process.GetStandardOutput());
+
+ return ParseResult(result).ToList();
+ }
+
+ private ProcessArgumentBuilder GetArguments(string searchString, DotNetWorkloadSearchSettings settings)
+ {
+ var builder = CreateArgumentBuilder(settings);
+
+ builder.Append("workload search");
+
+ if (!string.IsNullOrEmpty(searchString))
+ {
+ builder.Append(searchString);
+ }
+
+ return builder;
+ }
+
+ private static IEnumerable ParseResult(IEnumerable result)
+ {
+ bool first = true;
+ int descriptionIndex = -1;
+ foreach (var line in result)
+ {
+ if (first)
+ {
+ if (line?.StartsWith("Workload ID") == true
+ && (descriptionIndex = line?.IndexOf("Description") ?? -1) > 11)
+ {
+ first = false;
+ }
+ continue;
+ }
+
+ if (string.IsNullOrWhiteSpace(line))
+ {
+ continue;
+ }
+
+ var trimmedLine = line.Trim();
+
+ if (trimmedLine.Trim().All(c => c == '-'))
+ {
+ continue;
+ }
+
+ yield return new DotNetWorkload(
+ string.Concat(trimmedLine.Take(descriptionIndex)).TrimEnd(),
+ string.Concat(trimmedLine.Skip(descriptionIndex)));
+ }
+ }
+ }
+}
diff --git a/src/Cake.Core.Tests/Cake.Core.Tests.csproj b/src/Cake.Core.Tests/Cake.Core.Tests.csproj
index 8cf5514fbf..c5977c536c 100644
--- a/src/Cake.Core.Tests/Cake.Core.Tests.csproj
+++ b/src/Cake.Core.Tests/Cake.Core.Tests.csproj
@@ -14,7 +14,7 @@
-
+
all
diff --git a/src/Cake.Core.Tests/Unit/IO/DirectoryPathTests.cs b/src/Cake.Core.Tests/Unit/IO/DirectoryPathTests.cs
index fb972ff85f..8ba9a9ea81 100644
--- a/src/Cake.Core.Tests/Unit/IO/DirectoryPathTests.cs
+++ b/src/Cake.Core.Tests/Unit/IO/DirectoryPathTests.cs
@@ -255,6 +255,139 @@ public void Can_Not_Combine_Directory_Path_With_Absolute_Windows_Directory_Path(
}
}
+ public sealed class TheGetParentMethod
+ {
+ public sealed class InUncFormat
+ {
+ [Theory]
+ [InlineData(@"\\server\share\folder", @"\\server\share")]
+ public void Should_Return_Parent_Directory(string directoryPath, string parentPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(parentPath, result.FullPath);
+ }
+
+ [Theory]
+ [InlineData(@"\\Server\")]
+ [InlineData(@"\\Server")]
+ [InlineData(@"\\Server\Share")]
+ public void Should_Return_Null_If_No_Parent(string directoryPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(null, result);
+ }
+ }
+ public sealed class InRelativeFormat
+ {
+ [Theory]
+ [InlineData("foo\\bar", "foo")]
+ [InlineData("foo\\bar\\baz\\..\\..\\Work", "foo")]
+ [InlineData("foo/bar/baz/../../Work", "foo")]
+ [InlineData("foo/bar", "foo")]
+ [InlineData("Data\\Work\\..\\foo", "Data")]
+ [InlineData("Data/Work/../foo", "Data")]
+ [InlineData("someFolder", ".")]
+ [InlineData("..", ".")] // a bit unexpected, but due to the way "Collapse" works.
+ [InlineData("./", ".")] // a bit unexpected, but due to the way "Collapse" works.
+ public void Should_Return_Parent_Directory(string directoryPath, string parentPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(parentPath, result.FullPath);
+ }
+ }
+
+ public sealed class InWindowsFormat
+ {
+ [WindowsTheory]
+ [InlineData("C:/Data", "C:/")]
+ [InlineData("C:/Data/Work", "C:/Data")]
+ [InlineData("C:/Data/Work/file.txt", "C:/Data/Work")]
+ [InlineData("C:\\folder\\foo\\..", "C:/")]
+ public void Should_Return_Parent_Directory(string directoryPath, string parentPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(parentPath, result.FullPath);
+ }
+
+ [WindowsTheory]
+ [InlineData("C:/")]
+ [InlineData("C:")]
+ [InlineData("C:/..")]
+ public void Should_Return_Null_If_No_Parent(string directoryPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(null, result);
+ }
+ }
+
+ public sealed class InUnixFormat
+ {
+ [NonWindowsTheory]
+ [InlineData("/C", "/")]
+ [InlineData("/C/", "/")]
+ [InlineData("/C/Data", "/C")]
+ [InlineData("/C/Data/Work", "/C/Data")]
+ [InlineData("/C/Data/Work/file.txt", "/C/Data/Work")]
+ [InlineData("/folder/foo/..", "/")]
+ public void Should_Return_Parent_Directory(string directoryPath, string parentPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(parentPath, result.FullPath);
+ }
+
+ [NonWindowsTheory]
+ [InlineData("/")]
+ [InlineData("/..")]
+ public void Should_Return_Null_If_No_Parent(string directoryPath)
+ {
+ // Given
+ var path = new DirectoryPath(directoryPath);
+
+ // When
+ var result = path.GetParent();
+
+ // Then
+ Assert.Equal(null, result);
+ }
+ }
+ }
+
public sealed class TheMakeAbsoluteMethod
{
public sealed class ThatTakesAnEnvironment
diff --git a/src/Cake.Core.Tests/Unit/IO/PathCollapserTests.cs b/src/Cake.Core.Tests/Unit/IO/PathCollapserTests.cs
index 8287931256..f5ef71dd09 100644
--- a/src/Cake.Core.Tests/Unit/IO/PathCollapserTests.cs
+++ b/src/Cake.Core.Tests/Unit/IO/PathCollapserTests.cs
@@ -22,103 +22,177 @@ public void Should_Throw_If_Path_Is_Null()
AssertEx.IsArgumentNullException(result, "path");
}
- [Fact]
- public void Should_Collapse_Relative_Path()
- {
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("hello/temp/test/../../world"));
-
- // Then
- Assert.Equal("hello/world", path);
- }
-
- [Fact]
- public void Should_Collapse_Path_With_Separated_Ellipsis()
+ public sealed class WithPathsInRelativeFormat
{
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("hello/temp/../temp2/../world"));
-
- // Then
- Assert.Equal("hello/world", path);
+ [Fact]
+ public void Should_Collapse_Relative_Path()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("hello/temp/test/../../world"));
+
+ // Then
+ Assert.Equal("hello/world", path);
+ }
+
+ [Fact]
+ public void Should_Collapse_Path_With_Separated_Ellipsis()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("hello/temp/../temp2/../world"));
+
+ // Then
+ Assert.Equal("hello/world", path);
+ }
+
+ [Theory]
+ [InlineData("./foo/..", ".")]
+ [InlineData("foo/..", ".")]
+ public void Should_Collapse_To_Dot_When_Only_One_Folder_Is_Followed_By_Ellipsis(string input,
+ string expected)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(input));
+
+ // Then
+ Assert.Equal(expected, path);
+ }
+
+ [Theory]
+ [InlineData(".")]
+ [InlineData("./")]
+ [InlineData("")]
+ public void Should_Collapse_Single_Dot_To_Single_Dot(string uncollapsedPath)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(uncollapsedPath));
+
+ // Then
+ Assert.Equal(".", path);
+ }
+
+ [Fact]
+ public void Should_Collapse_Single_Dot_With_Ellipsis()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("./.."));
+
+ // Then
+ Assert.Equal(".", path);
+ }
+
+ [Theory]
+ [InlineData("./a", "a")]
+ [InlineData("a/./b", "a/b")]
+ [InlineData("a/b/.", "a/b")]
+ public void Should_Collapse_Single_Dot(string uncollapsedPath, string collapsedPath)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(uncollapsedPath));
+
+ // Then
+ Assert.Equal(collapsedPath, path);
+ }
}
- [WindowsFact]
- public void Should_Collapse_Path_With_Windows_Root()
+ public sealed class WithPathsInUncFormat
{
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("c:/hello/temp/test/../../world"));
-
- // Then
- Assert.Equal("c:/hello/world", path);
+ [Theory]
+ [InlineData(@"\\server\share\folder\..", @"\\server\share")]
+ [InlineData(@"\\server\share\folder\..\..\..\..", @"\\server")]
+ [InlineData(@"\\server\share\folder\..\..\..\..\foo", @"\\server\foo")]
+ public void Should_Collapse_Ellipsis(string input,
+ string expected)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(input));
+
+ // Then
+ Assert.Equal(expected, path);
+ }
}
- [Fact]
- public void Should_Collapse_Path_With_Non_Windows_Root()
+ public sealed class WithPathsInNonWindowsFormat
{
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("/hello/temp/test/../../world"));
-
- // Then
- Assert.Equal("/hello/world", path);
+ [Fact]
+ public void Should_Collapse_Path_With_Non_Windows_Root()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("/hello/temp/test/../../world"));
+
+ // Then
+ Assert.Equal("/hello/world", path);
+ }
+
+ [NonWindowsFact]
+ public void Should_Stop_Collapsing_When_Root_Is_Reached()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("/hello/../../../../../../temp"));
+
+ // Then
+ Assert.Equal("/temp", path);
+ }
+
+ [NonWindowsTheory]
+ [InlineData("/foo/..", "/")]
+ [InlineData("/..", "/")]
+ public void Should_Collapse_To_Root_When_Only_One_Folder_Is_Followed_By_Ellipsis(string input,
+ string expected)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(input));
+
+ // Then
+ Assert.Equal(expected, path);
+ }
+
+ [Theory]
+ [InlineData("/a/./b", "/a/b")]
+ [InlineData("/a/b/.", "/a/b")]
+ [InlineData("/./a/b", "/a/b")]
+ public void Should_Collapse_Single_Dot(string uncollapsedPath, string collapsedPath)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(uncollapsedPath));
+
+ // Then
+ Assert.Equal(collapsedPath, path);
+ }
}
- [WindowsFact]
- public void Should_Stop_Collapsing_When_Windows_Root_Is_Reached()
+ public sealed class WithPathsInWindowsFormat
{
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("c:/../../../../../../temp"));
-
- // Then
- Assert.Equal("c:/temp", path);
- }
-
- [Fact]
- public void Should_Stop_Collapsing_When_Root_Is_Reached()
- {
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("/hello/../../../../../../temp"));
-
- // Then
- Assert.Equal("/temp", path);
- }
-
- [Theory]
- [InlineData(".")]
- [InlineData("./")]
- [InlineData("/.")]
- public void Should_Collapse_Single_Dot_To_Single_Dot(string uncollapsedPath)
- {
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath(uncollapsedPath));
-
- // Then
- Assert.Equal(".", path);
- }
-
- [Fact]
- public void Should_Collapse_Single_Dot_With_Ellipsis()
- {
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath("./.."));
-
- // Then
- Assert.Equal(".", path);
- }
-
- [Theory]
- [InlineData("./a", "a")]
- [InlineData("a/./b", "a/b")]
- [InlineData("/a/./b", "/a/b")]
- [InlineData("a/b/.", "a/b")]
- [InlineData("/a/b/.", "/a/b")]
- [InlineData("/./a/b", "/a/b")]
- public void Should_Collapse_Single_Dot(string uncollapsedPath, string collapsedPath)
- {
- // Given, When
- var path = PathCollapser.Collapse(new DirectoryPath(uncollapsedPath));
-
- // Then
- Assert.Equal(collapsedPath, path);
+ [Fact]
+ public void Should_Collapse_Path_With_Windows_Root()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("c:/hello/temp/test/../../world"));
+
+ // Then
+ Assert.Equal("c:/hello/world", path);
+ }
+
+ [WindowsFact]
+ public void Should_Stop_Collapsing_When_Windows_Root_Is_Reached()
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath("c:/../../../../../../temp"));
+
+ // Then
+ Assert.Equal("c:/temp", path);
+ }
+
+ [Theory]
+ [InlineData("C:/foo/..", "C:")]
+ public void Should_Collapse_To_Root_When_Only_One_Folder_Is_Followed_By_Ellipsis(string input,
+ string expected)
+ {
+ // Given, When
+ var path = PathCollapser.Collapse(new DirectoryPath(input));
+
+ // Then
+ Assert.Equal(expected, path);
+ }
}
}
}
diff --git a/src/Cake.Core.Tests/Unit/Tooling/ToolTests.cs b/src/Cake.Core.Tests/Unit/Tooling/ToolTests.cs
index 072f9b3488..efc111c990 100644
--- a/src/Cake.Core.Tests/Unit/Tooling/ToolTests.cs
+++ b/src/Cake.Core.Tests/Unit/Tooling/ToolTests.cs
@@ -159,6 +159,42 @@ public void Should_Not_Throw_On_Invalid_ExitCode_When_HandleExitCode_Returns_Tru
// Then
Assert.IsNotType(result);
}
+
+ [Fact]
+ public void Executes_PostAction()
+ {
+ var wasExecuted = false;
+
+ // Given
+ var fixture = new DummyToolFixture();
+ fixture.Settings.PostAction = (p) => wasExecuted = true;
+
+ fixture.GivenProcessExitsWithCode(0);
+
+ // When
+ _ = fixture.Run();
+
+ // Then
+ Assert.True(wasExecuted);
+ }
+
+ [Fact]
+ public void Executes_SetupProcessSettings()
+ {
+ var wasExecuted = false;
+
+ // Given
+ var fixture = new DummyToolFixture();
+ fixture.Settings.SetupProcessSettings = (p) => wasExecuted = true;
+
+ fixture.GivenProcessExitsWithCode(0);
+
+ // When
+ _ = fixture.Run();
+
+ // Then
+ Assert.True(wasExecuted);
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Cake.Core/IO/DirectoryPath.cs b/src/Cake.Core/IO/DirectoryPath.cs
index 884661d97d..5be5f29a7b 100644
--- a/src/Cake.Core/IO/DirectoryPath.cs
+++ b/src/Cake.Core/IO/DirectoryPath.cs
@@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Cake.Core.Polyfill;
namespace Cake.Core.IO
{
@@ -55,6 +56,55 @@ public FilePath GetFilePath(FilePath path)
return new FilePath(PathHelper.Combine(FullPath, path.GetFilename().FullPath));
}
+ ///
+ /// Gets the directory path of a .
+ ///
+ /// A to the parent directory of the given .
+ public DirectoryPath GetParent()
+ {
+ var collapsed = this.Collapse();
+
+ if (collapsed.Segments.Length == 0)
+ {
+ return null;
+ }
+
+ if (collapsed.IsUNC && collapsed.Segments.Length < 4)
+ {
+ // UNC is special: \\server\share makes 3 (!) Segments
+ // Also, \\server\share simply has no parent
+ return null;
+ }
+
+ if (collapsed.Segments.Length == 1)
+ {
+ if (collapsed.IsRelative)
+ {
+ // something like "relativeFolder/", whose parent is simply "."
+ return new DirectoryPath(".");
+ }
+
+ // one segment on Windows is e.g. "C:/"
+ // on all other systems one segment is e.g "/home"
+ if (EnvironmentHelper.GetPlatformFamily() == PlatformFamily.Windows)
+ {
+ // no more parents
+ return null;
+ }
+
+ // root ("/") is not really a segment for Cake,
+ // so we return that directly.
+ return new DirectoryPath("/");
+ }
+
+ var segments = collapsed.Segments.Take(collapsed.Segments.Length - 1);
+ var fullPath = collapsed.IsUNC
+ ? @"\\" + string.Join(Separator.ToString(), segments.Skip(1))
+ : string.Join(Separator.ToString(), segments);
+
+ return new DirectoryPath(fullPath);
+ }
+
///
/// Combines the current path with a .
/// The provided must be relative.
@@ -186,7 +236,7 @@ public FilePath GetRelativePath(FilePath to)
}
///
- /// Determines wheter two instances are equal.
+ /// Determines whether two instances are equal.
///
/// the to compare.
/// True if other is equal to current object, False otherwise.
@@ -194,7 +244,7 @@ public bool Equals(DirectoryPath other)
=> PathComparer.Default.Equals(this, other);
///
- /// Determines wheter two instances are equal.
+ /// Determines whether two instances are equal.
///
/// the to compare.
/// True if other is equal to current object, False otherwise.
@@ -202,7 +252,7 @@ public override bool Equals(object other)
=> Equals(other as DirectoryPath);
///
- /// Determines wheter two instances are equal.
+ /// Determines whether two instances are equal.
///
/// left side .
/// right side .
@@ -212,7 +262,7 @@ public override bool Equals(object other)
|| directoryPath?.Equals(otherDirectoryPath) == true;
///
- /// Determines wheter two instances are different.
+ /// Determines whether two instances are different.
///
/// left side .
/// right side .
diff --git a/src/Cake.Core/IO/PathCollapser.cs b/src/Cake.Core/IO/PathCollapser.cs
index b4c3e5e311..5e86198729 100644
--- a/src/Cake.Core/IO/PathCollapser.cs
+++ b/src/Cake.Core/IO/PathCollapser.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Cake.Core.Polyfill;
namespace Cake.Core.IO
{
@@ -16,8 +17,31 @@ public static string Collapse(Path path)
{
throw new ArgumentNullException(nameof(path));
}
+
+ var isUncPath = path.IsUNC;
+ var isWindowsPlatform = EnvironmentHelper.IsWindows(EnvironmentHelper.GetPlatformFamily());
+ var minStackHeight = 0;
var stack = new Stack();
var segments = path.FullPath.Split('/', '\\');
+ if (!path.IsRelative)
+ {
+ if (isUncPath)
+ {
+ // first two segments are string.Empty, followed by server and share
+ minStackHeight = 3;
+ }
+ else if (isWindowsPlatform)
+ {
+ // first segment is c:
+ minStackHeight = 1;
+ }
+ else
+ {
+ // first segment is string.Empty
+ minStackHeight = 1;
+ }
+ }
+
foreach (var segment in segments)
{
if (segment == ".")
@@ -26,7 +50,7 @@ public static string Collapse(Path path)
}
if (segment == "..")
{
- if (stack.Count > 1)
+ if (stack.Count > minStackHeight)
{
stack.Pop();
}
@@ -34,8 +58,23 @@ public static string Collapse(Path path)
}
stack.Push(segment);
}
- string collapsed = string.Join("/", stack.Reverse());
- return collapsed == string.Empty ? "." : collapsed;
+ var collapsed = string.Join(path.Separator.ToString(), stack.Reverse());
+ if (collapsed != string.Empty)
+ {
+ return collapsed;
+ }
+
+ if (path.IsRelative)
+ {
+ return ".";
+ }
+
+ if (isUncPath)
+ {
+ return @"\\";
+ }
+
+ return isWindowsPlatform ? path.Segments[0] : "/";
}
}
}
\ No newline at end of file
diff --git a/src/Cake.Core/Tooling/Tool.cs b/src/Cake.Core/Tooling/Tool.cs
index d41ad7101c..1b26013600 100644
--- a/src/Cake.Core/Tooling/Tool.cs
+++ b/src/Cake.Core/Tooling/Tool.cs
@@ -95,7 +95,7 @@ protected void Run(
}
// Post action specified?
- postAction?.Invoke(process);
+ (postAction ?? settings.PostAction)?.Invoke(process);
var exitCode = process.GetExitCode();
if (!settings.HandleExitCode?.Invoke(exitCode) ?? true)
@@ -190,6 +190,9 @@ protected IProcess RunProcess(
// Want to opt out of using a working directory?
info.NoWorkingDirectory = settings.NoWorkingDirectory;
+ // Configure process settings
+ settings.SetupProcessSettings?.Invoke(info);
+
// Run the process.
var process = _processRunner.Start(toolPath, info);
if (process == null)
diff --git a/src/Cake.Core/Tooling/ToolSettings.cs b/src/Cake.Core/Tooling/ToolSettings.cs
index 49158610d3..84b30f4f68 100644
--- a/src/Cake.Core/Tooling/ToolSettings.cs
+++ b/src/Cake.Core/Tooling/ToolSettings.cs
@@ -109,5 +109,15 @@ public class ToolSettings
///
///
public Func HandleExitCode { get; set; }
+
+ ///
+ /// Gets or sets a delegate which is executed after the process was started.
+ ///
+ public Action PostAction { get; set; }
+
+ ///
+ /// Gets or sets a delegate to configure the process settings.
+ ///
+ public Action SetupProcessSettings { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj b/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj
index 6ce529474d..96cae6ced5 100644
--- a/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj
+++ b/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj
@@ -16,7 +16,7 @@
-
+
all
diff --git a/src/Cake.Frosting.Tests/Cake.Frosting.Tests.csproj b/src/Cake.Frosting.Tests/Cake.Frosting.Tests.csproj
index d7d8625724..e2db5e902e 100644
--- a/src/Cake.Frosting.Tests/Cake.Frosting.Tests.csproj
+++ b/src/Cake.Frosting.Tests/Cake.Frosting.Tests.csproj
@@ -8,8 +8,8 @@
-
-
+
+
all
diff --git a/src/Cake.NuGet.Tests/Cake.NuGet.Tests.csproj b/src/Cake.NuGet.Tests/Cake.NuGet.Tests.csproj
index be0e14d723..0bdfc651a5 100644
--- a/src/Cake.NuGet.Tests/Cake.NuGet.Tests.csproj
+++ b/src/Cake.NuGet.Tests/Cake.NuGet.Tests.csproj
@@ -15,7 +15,7 @@
-
+
all
diff --git a/src/Cake.NuGet/Cake.NuGet.csproj b/src/Cake.NuGet/Cake.NuGet.csproj
index 840767db90..863c0d1c02 100644
--- a/src/Cake.NuGet/Cake.NuGet.csproj
+++ b/src/Cake.NuGet/Cake.NuGet.csproj
@@ -18,18 +18,18 @@
-
-
-
-
-
-
+
+
+
+
+
+
-
+
All
diff --git a/src/Cake.Testing.Xunit/NonWindowsFactAttribute.cs b/src/Cake.Testing.Xunit/NonWindowsFactAttribute.cs
new file mode 100644
index 0000000000..64d1c7269f
--- /dev/null
+++ b/src/Cake.Testing.Xunit/NonWindowsFactAttribute.cs
@@ -0,0 +1,18 @@
+// 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 Cake.Core;
+
+namespace Cake.Testing.Xunit
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class NonWindowsFactAttribute : PlatformRestrictedFactAttribute
+ {
+ public NonWindowsFactAttribute(string reason = null)
+ : base(PlatformFamily.Windows, true, reason)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Testing.Xunit/NonWindowsTheoryAttribute.cs b/src/Cake.Testing.Xunit/NonWindowsTheoryAttribute.cs
new file mode 100644
index 0000000000..ce2cc692bb
--- /dev/null
+++ b/src/Cake.Testing.Xunit/NonWindowsTheoryAttribute.cs
@@ -0,0 +1,18 @@
+// 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 Cake.Core;
+
+namespace Cake.Testing.Xunit
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class NonWindowsTheoryAttribute : PlatformRestrictedTheoryAttribute
+ {
+ public NonWindowsTheoryAttribute(string reason = null)
+ : base(PlatformFamily.Windows, true, reason)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Testing.Xunit/PlatformRestrictedFactAttribute.cs b/src/Cake.Testing.Xunit/PlatformRestrictedFactAttribute.cs
new file mode 100644
index 0000000000..43ebfffa27
--- /dev/null
+++ b/src/Cake.Testing.Xunit/PlatformRestrictedFactAttribute.cs
@@ -0,0 +1,52 @@
+// 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 Cake.Core;
+using Cake.Core.Polyfill;
+using Xunit;
+
+namespace Cake.Testing.Xunit
+{
+ public abstract class PlatformRestrictedFactAttribute : FactAttribute
+ {
+ private static readonly PlatformFamily _family;
+ private string _skip;
+
+ static PlatformRestrictedFactAttribute()
+ {
+ _family = EnvironmentHelper.GetPlatformFamily();
+ }
+
+ protected PlatformRestrictedFactAttribute(
+ PlatformFamily requiredFamily,
+ bool invert,
+ string reason = null)
+ {
+ if ((requiredFamily != _family) ^ invert)
+ {
+ if (string.IsNullOrEmpty(reason))
+ {
+ var platformName = Enum.GetName(typeof(PlatformFamily), requiredFamily);
+ if (invert)
+ {
+ platformName = $"Non-{platformName}";
+ }
+
+ reason = $"{platformName} test.";
+ }
+
+ Reason = reason;
+ }
+ }
+
+ private string Reason { get; }
+
+ public override string Skip
+ {
+ get => _skip ?? Reason;
+ set => _skip = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Testing.Xunit/PlatformRestrictedTheoryAttribute.cs b/src/Cake.Testing.Xunit/PlatformRestrictedTheoryAttribute.cs
new file mode 100644
index 0000000000..fd5bdab5e1
--- /dev/null
+++ b/src/Cake.Testing.Xunit/PlatformRestrictedTheoryAttribute.cs
@@ -0,0 +1,52 @@
+// 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 Cake.Core;
+using Cake.Core.Polyfill;
+using Xunit;
+
+namespace Cake.Testing.Xunit
+{
+ public abstract class PlatformRestrictedTheoryAttribute : TheoryAttribute
+ {
+ private static readonly PlatformFamily _family;
+ private string _skip;
+
+ static PlatformRestrictedTheoryAttribute()
+ {
+ _family = EnvironmentHelper.GetPlatformFamily();
+ }
+
+ protected PlatformRestrictedTheoryAttribute(
+ PlatformFamily requiredFamily,
+ bool invert,
+ string reason = null)
+ {
+ if ((requiredFamily != _family) ^ invert)
+ {
+ if (string.IsNullOrEmpty(reason))
+ {
+ var platformName = Enum.GetName(typeof(PlatformFamily), requiredFamily);
+ if (invert)
+ {
+ platformName = $"Non-{platformName}";
+ }
+
+ reason = $"{platformName} test.";
+ }
+
+ Reason = reason;
+ }
+ }
+
+ private string Reason { get; }
+
+ public override string Skip
+ {
+ get => _skip ?? Reason;
+ set => _skip = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Testing.Xunit/WindowsTheory.cs b/src/Cake.Testing.Xunit/WindowsTheory.cs
deleted file mode 100644
index 8f1091fcab..0000000000
--- a/src/Cake.Testing.Xunit/WindowsTheory.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.Polyfill;
-using Xunit;
-
-namespace Cake.Testing.Xunit
-{
- public sealed class WindowsTheory : TheoryAttribute
- {
- private static readonly PlatformFamily _family;
-
- static WindowsTheory()
- {
- _family = EnvironmentHelper.GetPlatformFamily();
- }
-
- // ReSharper disable once UnusedParameter.Local
- public WindowsTheory(string reason = null)
- {
- if (_family != PlatformFamily.Windows)
- {
- Skip = reason ?? "Windows test.";
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Cake.Testing.Xunit/WindowsTheoryAttribute.cs b/src/Cake.Testing.Xunit/WindowsTheoryAttribute.cs
new file mode 100644
index 0000000000..4baf2082b0
--- /dev/null
+++ b/src/Cake.Testing.Xunit/WindowsTheoryAttribute.cs
@@ -0,0 +1,18 @@
+// 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 Cake.Core;
+
+namespace Cake.Testing.Xunit
+{
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
+ public sealed class WindowsTheoryAttribute : PlatformRestrictedTheoryAttribute
+ {
+ public WindowsTheoryAttribute(string reason = null)
+ : base(PlatformFamily.Windows, false, reason)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Cake.Tests/Cake.Tests.csproj b/src/Cake.Tests/Cake.Tests.csproj
index 24aeb05565..3129807382 100644
--- a/src/Cake.Tests/Cake.Tests.csproj
+++ b/src/Cake.Tests/Cake.Tests.csproj
@@ -10,7 +10,7 @@
-
+
all
diff --git a/src/Cake/Cake.csproj b/src/Cake/Cake.csproj
index 6525339ef5..5cf4a6061f 100644
--- a/src/Cake/Cake.csproj
+++ b/src/Cake/Cake.csproj
@@ -26,9 +26,9 @@
-
+
-
+
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/ReferenceAssemblyResolver.cs b/src/Cake/Infrastructure/Scripting/ReferenceAssemblyResolver.cs
index d420818f87..843316378d 100644
--- a/src/Cake/Infrastructure/Scripting/ReferenceAssemblyResolver.cs
+++ b/src/Cake/Infrastructure/Scripting/ReferenceAssemblyResolver.cs
@@ -9,6 +9,7 @@ namespace Cake.Infrastructure.Scripting
{
public sealed class ReferenceAssemblyResolver : IReferenceAssemblyResolver
{
+ private static readonly Version VersionZero = new Version(0, 0, 0, 0);
private readonly ICakeLog _log;
public ReferenceAssemblyResolver(ICakeLog log)
@@ -36,7 +37,7 @@ IEnumerable TryGetReferenceAssemblies()
}
catch (Exception ex)
{
- _log.Debug(log => log("Failed to load {0}\r\n{1}", reference.FilePath, ex));
+ _log.Debug(log => log("Failed to load {0}\r\nException: {1}", reference.FilePath, ex));
continue;
}
@@ -49,6 +50,12 @@ IEnumerable TryGetReferenceAssemblies()
foreach (var assemblyRefName in assembly.GetReferencedAssemblies())
{
+ if (assemblyRefName == null ||
+ assemblyRefName.Version == VersionZero)
+ {
+ continue;
+ }
+
Assembly assemblyRef;
try
{
@@ -56,7 +63,7 @@ IEnumerable TryGetReferenceAssemblies()
}
catch (Exception ex)
{
- _log.Debug(log => log("Failed to load {0}\r\n{1}", reference.FilePath, ex));
+ _log.Debug(log => log("Failed to load {0}\r\nReference: {1}\r\n Exception: {2}", assemblyRefName, assembly, ex));
continue;
}
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