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)factoryMethod.Invoke(null, new object[] { new object[] { _host, null } }); + task.GetAwaiter().GetResult(); } } } \ No newline at end of file diff --git a/src/Cake/Infrastructure/Scripting/ScriptAssemblyResolver.cs b/src/Cake/Infrastructure/Scripting/ScriptAssemblyResolver.cs index e9406473d9..44ea0e886a 100644 --- a/src/Cake/Infrastructure/Scripting/ScriptAssemblyResolver.cs +++ b/src/Cake/Infrastructure/Scripting/ScriptAssemblyResolver.cs @@ -15,6 +15,7 @@ namespace Cake.Infrastructure.Scripting public sealed class ScriptAssemblyResolver : IDisposable { private const string AssemblyResourcesExtension = ".resources"; + private static readonly Version VersionZero = new Version(0, 0, 0, 0); private readonly ICakeEnvironment _environment; private readonly ICakeLog _log; @@ -49,6 +50,11 @@ private Assembly AssemblyResolve(object sender, ResolveEventArgs args) var shortName = assemblyName.Name; var version = assemblyName.Version; + if (version == VersionZero) + { + return null; + } + // Preventing indirect recursive calls via Assembly.Load() if (!_resolvedNames.Add(shortName + version)) { @@ -65,6 +71,11 @@ private Assembly AssemblyResolve(AssemblyName assemblyName) var shortName = assemblyName.Name; var version = assemblyName.Version; + if (version == VersionZero) + { + return null; + } + Assembly assembly = null; try { diff --git a/src/Cake/Infrastructure/Utilities/FastHash.cs b/src/Cake/Infrastructure/Utilities/FastHash.cs new file mode 100644 index 0000000000..6fec2a5439 --- /dev/null +++ b/src/Cake/Infrastructure/Utilities/FastHash.cs @@ -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 +{ + /// + /// Optimized hash generator. Using SHA512 since it is FIPS compliant. + /// + internal static class FastHash + { + /// + /// Generates a hash of the passed byte arrays. + /// + /// The binary data to hash. + /// The hash value. + 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); + } + } + + /// + /// Generates a hash of the passed byte arrays. + /// + /// The binary data to hash. + /// The hash value. + public static string GenerateHash(IEnumerable 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 + } + } +} diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index d022b6a603..9ebd693775 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -10,7 +10,7 @@ using System.Reflection; [assembly: AssemblyProduct("Cake")] -[assembly: AssemblyVersion("2.1.0.0")] -[assembly: AssemblyFileVersion("2.1.0.0")] -[assembly: AssemblyInformationalVersion("2.1.0-beta.1+0.Branch.release-2.1.0.Sha.d21c59a7546b49e37c063982b85b9281dec0183a")] +[assembly: AssemblyVersion("2.2.0.0")] +[assembly: AssemblyFileVersion("2.2.0.0")] +[assembly: AssemblyInformationalVersion("2.2.0-beta.1+0.Branch.release-2.2.0.Sha.8861b463a4eb651f9e5149ee7f7ada47d03ae1ad")] [assembly: AssemblyCopyright("Copyright (c) .NET Foundation and Contributors")] diff --git a/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake b/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake index a43f983bc5..4afd636041 100644 --- a/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake +++ b/tests/integration/Cake.Common/Tools/DotNet/DotNetAliases.cake @@ -246,7 +246,7 @@ Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetFormat") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.Setup") .Does(() => { - // Given + // Given var path = Paths.Temp.Combine("./Cake.Common/Tools/DotNet"); var project = path.CombineWithFilePath("hwapp/hwapp.csproj"); @@ -259,7 +259,25 @@ Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetSDKCheck") .Does(() => { // When - DotNetSDKCheck(); + DotNetSDKCheck(); +}); + +Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetWorkloadSearch") + .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.Setup") + .Does(() => +{ + // Given + var searchString = "maui"; + + // When + var workloads = DotNetWorkloadSearch(searchString); + + // Then + foreach(var workload in workloads) + { + Assert.Contains("maui", workload.Id); + Assert.Contains(".NET MAUI SDK", workload.Description); + } }); Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetBuildServerShutdown") @@ -279,6 +297,7 @@ Task("Cake.Common.Tools.DotNet.DotNetAliases.DotNetBuildServerShutdown") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetTest.Fail") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetFormat") .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetSDKCheck") + .IsDependentOn("Cake.Common.Tools.DotNet.DotNetAliases.DotNetWorkloadSearch") .Does(() => { // When diff --git a/tests/integration/Cake/ScriptCache.cake b/tests/integration/Cake/ScriptCache.cake new file mode 100644 index 0000000000..f97268f064 --- /dev/null +++ b/tests/integration/Cake/ScriptCache.cake @@ -0,0 +1,161 @@ +#load "./../../../utilities/xunit.cake" +#load "./../../../utilities/paths.cake" +using System.Diagnostics; + +public class ScriptCacheData +{ + public FilePath ScriptPath { get; } + public FilePath ScriptCacheAssemblyPath { get; } + public FilePath ScriptCacheHashPath { get; } + public FilePath ConfigScriptPath { get; } + public DirectoryPath ConfigScriptCachePath { get; } + public FilePath ConfigScriptCacheAssemblyPath { get; } + public FilePath ConfigScriptCacheHashPath { get; } + public (TimeSpan Elapsed, string Hash) CompileResult { get; set; } + public (TimeSpan Elapsed, string Hash) ExecuteResult { get; set; } + public (TimeSpan Elapsed, string Hash) ReCompileResult { get; set; } + public (TimeSpan Elapsed, string Hash) ConfigCompileResult { get; set; } + public CakeSettings Settings { get; } + private Action CakeExecuteScript { get; } + private Func CalculateFileHash { get; } + + public TimeSpan Time(Action action) + { + var stopwatch = Stopwatch.StartNew(); + try + { + action(); + } + finally + { + stopwatch.Stop(); + } + return stopwatch.Elapsed; + } + + public (TimeSpan Elapsed, string Hash) TimeCakeExecuteScript(FilePath scriptPath = null) + => TimeCakeExecuteScript(args => args, scriptPath); + + public (TimeSpan Elapsed, string Hash) TimeCakeExecuteScript(Func argumentCustomization, FilePath scriptPath = null) => + ( + Time( + () => { + Settings.ArgumentCustomization = argumentCustomization; + CakeExecuteScript( + scriptPath ?? ScriptPath, + Settings); + }), + CalculateFileHash(ScriptCacheAssemblyPath).ToHex() + ); + + public ScriptCacheData( + DirectoryPath scriptDirectoryPath, + Action cakeExecuteScript, + Func calculateFileHash + ) + { + ScriptPath = scriptDirectoryPath.CombineWithFilePath("build.cake"); + var cacheDirectoryPath = scriptDirectoryPath.Combine("tools").Combine("cache"); + ScriptCacheAssemblyPath = cacheDirectoryPath.CombineWithFilePath("build.cake.dll"); + ScriptCacheHashPath = cacheDirectoryPath.CombineWithFilePath("build.cake.hash"); + var configScriptDirectoryPath = scriptDirectoryPath.Combine("Config"); + ConfigScriptPath = configScriptDirectoryPath.CombineWithFilePath("build.cake"); + var configCacheRootPath = configScriptDirectoryPath.Combine("CacheRootPath"); + ConfigScriptCachePath = configCacheRootPath.Combine("cake-build").Combine("CacheLeafPath"); + ConfigScriptCacheAssemblyPath = ConfigScriptCachePath.CombineWithFilePath("build.cake.dll"); + ConfigScriptCacheHashPath = ConfigScriptCachePath.CombineWithFilePath("build.cake.hash"); + Settings = new CakeSettings { + EnvironmentVariables = new Dictionary { + { "CAKE_SETTINGS_ENABLESCRIPTCACHE", "true" }, + { "TEST_ROOT_PATH", configCacheRootPath.FullPath }, + { "TEST_LEAF_PATH", "CacheLeafPath" } + }, + Verbosity = Verbosity.Quiet + }; + CakeExecuteScript = cakeExecuteScript; + CalculateFileHash = calculateFileHash; + } +} + +Setup(context => + new ScriptCacheData( + Paths.Temp + .Combine("./Cake/ScriptCache"), + context.CakeExecuteScript, + context.CalculateFileHash + )); + +Task("Cake.ScriptCache.Setup") + .Does(() => +{ + var sourcePath = Paths.Resources.Combine("./Cake/ScriptCache"); + var targetPath = Paths.Temp.Combine("./Cake/ScriptCache"); + EnsureDirectoryExists(targetPath.Combine("../").Collapse()); + if (DirectoryExists(targetPath)) + { + DeleteDirectory( + targetPath, + new DeleteDirectorySettings { + Recursive = true, + Force = true + }); + } + CopyDirectory(sourcePath, targetPath); +}); + +Task("Cake.ScriptCache.Compile") + .IsDependentOn("Cake.ScriptCache.Setup") + .Does((context, data) => +{ + // Given / When + data.CompileResult = data.TimeCakeExecuteScript(); + + // Then + Assert.True(FileExists(data.ScriptCacheAssemblyPath), $"Script Cache Assembly Path {data.ScriptCacheAssemblyPath} missing."); + Assert.True(FileExists(data.ScriptCacheHashPath), $"Script Cache Hash Path {data.ScriptCacheHashPath} missing."); +}); + +var scriptCacheExecute = Task("Cake.ScriptCache.Execute"); +for(var index = 1; index <= 5; index++) +{ + scriptCacheExecute.IsDependentOn( + Task($"Cake.ScriptCache.Execute.{index}") + .Does((context, data) => + { + // Given / When + data.ExecuteResult = data.TimeCakeExecuteScript(); + + // Then + Assert.True(data.CompileResult.Elapsed > data.ExecuteResult.Elapsed, $"Compile time {data.CompileResult.Elapsed} should be greater than execute time {data.ExecuteResult.Elapsed}."); + Assert.Equal(data.CompileResult.Hash, data.ExecuteResult.Hash); + }) + ); +} + +Task("Cake.ScriptCache.ReCompile") + .IsDependentOn("Cake.ScriptCache.Execute") + .Does((context, data) => { + // Given / When + data.ReCompileResult = data.TimeCakeExecuteScript(args => args.Append("--invalidate-script-cache")); + + // Then + Assert.True(data.ReCompileResult.Elapsed> data.ExecuteResult.Elapsed, $"ReCompileTime time {data.ReCompileResult.Elapsed} should be greater than execute time {data.ExecuteResult.Elapsed}."); + Assert.NotEqual(data.CompileResult.Hash , data.ReCompileResult.Hash); + }); + +Task("Cake.ScriptCache.Config") + .Does((context, data) => { + // Given / When + data.ConfigCompileResult = data.TimeCakeExecuteScript(data.ConfigScriptPath); + + // Then + Assert.True(FileExists(data.ConfigScriptCacheAssemblyPath), $"Script Cache Assembly Path {data.ConfigScriptCacheAssemblyPath} missing."); + Assert.True(FileExists(data.ConfigScriptCacheHashPath), $"Script Cache Hash Path {data.ConfigScriptCacheHashPath} missing."); + }); + +Task("Cake.ScriptCache") + .IsDependentOn("Cake.ScriptCache.Setup") + .IsDependentOn("Cake.ScriptCache.Compile") + .IsDependentOn("Cake.ScriptCache.Execute") + .IsDependentOn("Cake.ScriptCache.ReCompile") + .IsDependentOn("Cake.ScriptCache.Config"); \ No newline at end of file diff --git a/tests/integration/build.cake b/tests/integration/build.cake index 143f685e50..3f7a2c536a 100644 --- a/tests/integration/build.cake +++ b/tests/integration/build.cake @@ -6,6 +6,7 @@ // Tests #load "setup.cake" #load "teardown.cake" +#load "./Cake/ScriptCache.cake" #load "./Cake.Common/ArgumentAliases.cake" #load "./Cake.Common/Build/BuildSystemAliases.cake" #load "./Cake.Common/EnvironmentAliases.cake" @@ -54,6 +55,9 @@ var target = Argument("target", "Run-All-Tests"); // TARGETS ////////////////////////////////////////////////// +Task("Cake") + .IsDependentOn("Cake.ScriptCache"); + Task("Cake.Core") .IsDependentOn("Cake.Core.Diagnostics") .IsDependentOn("Cake.Core.IO.Path") @@ -100,6 +104,7 @@ Task("Cake.Chocolatey") Task("Run-All-Tests") .IsDependentOn("Setup-Tests") + .IsDependentOn("Cake") .IsDependentOn("Cake.Core") .IsDependentOn("Cake.Common") .IsDependentOn("Cake.DotNetTool.Module") diff --git a/tests/integration/resources/Cake/ScriptCache/Config/build.cake b/tests/integration/resources/Cake/ScriptCache/Config/build.cake new file mode 100644 index 0000000000..e98d44e8e9 --- /dev/null +++ b/tests/integration/resources/Cake/ScriptCache/Config/build.cake @@ -0,0 +1 @@ +Information("Hello from compiled script!"); diff --git a/tests/integration/resources/Cake/ScriptCache/Config/cake.config b/tests/integration/resources/Cake/ScriptCache/Config/cake.config new file mode 100644 index 0000000000..af28129718 --- /dev/null +++ b/tests/integration/resources/Cake/ScriptCache/Config/cake.config @@ -0,0 +1,2 @@ +[Paths] +Cache=%TEST_ROOT_PATH%/cake-build/%TEST_LEAF_PATH% \ No newline at end of file diff --git a/tests/integration/resources/Cake/ScriptCache/build.cake b/tests/integration/resources/Cake/ScriptCache/build.cake new file mode 100644 index 0000000000..e98d44e8e9 --- /dev/null +++ b/tests/integration/resources/Cake/ScriptCache/build.cake @@ -0,0 +1 @@ +Information("Hello from compiled script!");