diff --git a/build/parameters.cake b/build/parameters.cake index 9978f56d58..5f1d598dc4 100644 --- a/build/parameters.cake +++ b/build/parameters.cake @@ -115,7 +115,8 @@ public class BuildParameters "Cake.Tool", "Cake.Frosting", "Cake.Frosting.Template", - "Cake.Cli" + "Cake.Cli", + "Cake.DotNetTool.Module" }, new [] { "cake.portable" }); diff --git a/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj b/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj new file mode 100644 index 0000000000..3a3f410b82 --- /dev/null +++ b/src/Cake.DotNetTool.Module.Tests/Cake.DotNetTool.Module.Tests.csproj @@ -0,0 +1,29 @@ + + + + Cake.DotNetTool.Module.Tests + net461;netcoreapp2.1;netcoreapp3.1;net5.0 + true + true + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + diff --git a/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerFixture.cs b/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerFixture.cs new file mode 100644 index 0000000000..8cc4065495 --- /dev/null +++ b/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerFixture.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Cake.Core; +using Cake.Core.Configuration; +using Cake.Core.Diagnostics; +using Cake.Core.IO; +using Cake.Core.Packaging; +using Cake.Testing; +using NSubstitute; + +namespace Cake.DotNetTool.Module.Tests +{ + /// + /// Fixture used for testing DotNetToolPackageInstaller + /// + internal sealed class DotNetToolPackageInstallerFixture + { + public ICakeEnvironment Environment { get; set; } + public IFileSystem FileSystem { get; set; } + public IProcessRunner ProcessRunner { get; set; } + public IDotNetToolContentResolver ContentResolver { get; set; } + public ICakeLog Log { get; set; } + + public PackageReference Package { get; set; } + public PackageType PackageType { get; set; } + public DirectoryPath InstallPath { get; set; } + + public ICakeConfiguration Config { get; set; } + + /// + /// Initializes a new instance of the class. + /// + internal DotNetToolPackageInstallerFixture() + { + Environment = FakeEnvironment.CreateUnixEnvironment(); + FileSystem = new FakeFileSystem(Environment); + ProcessRunner = Substitute.For(); + ContentResolver = Substitute.For(); + Log = new FakeLog(); + Config = Substitute.For(); + Package = new PackageReference("dotnet:?package=windirstat"); + PackageType = PackageType.Addin; + InstallPath = new DirectoryPath("./dotnet"); + } + + /// + /// Create the installer. + /// + /// The dotnet Tool package installer. + internal DotNetToolPackageInstaller CreateInstaller() + { + return new DotNetToolPackageInstaller(Environment, ProcessRunner, Log, ContentResolver, Config, FileSystem); + } + + /// DotNetPackageInstallerFixture + /// Installs the specified resource at the given location. + /// + /// The installed files. + internal IReadOnlyCollection Install() + { + var installer = CreateInstaller(); + return installer.Install(Package, PackageType, InstallPath); + } + + /// + /// Determines whether this instance can install the specified resource. + /// + /// true if this installer can install the specified resource; otherwise false. + internal bool CanInstall() + { + var installer = CreateInstaller(); + return installer.CanInstall(Package, PackageType); + } + } +} diff --git a/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerTests.cs b/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerTests.cs new file mode 100644 index 0000000000..3bc0f70e78 --- /dev/null +++ b/src/Cake.DotNetTool.Module.Tests/DotNetToolPackageInstallerTests.cs @@ -0,0 +1,154 @@ +using System; +using Cake.Core.Packaging; +using Xunit; + +namespace Cake.DotNetTool.Module.Tests +{ + /// + /// DotNetToolPackageInstaller unit tests + /// + public sealed class DotNetToolPackageInstallerTests + { + public sealed class TheConstructor + { + [Fact] + public void Should_Throw_If_Environment_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Environment = null; + + // When + var result = Record.Exception(() => fixture.CreateInstaller()); + + // Then + Assert.IsType(result); + Assert.Equal("environment", ((ArgumentNullException)result).ParamName); + } + + [Fact] + public void Should_Throw_If_Process_Runner_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.ProcessRunner = null; + + // When + var result = Record.Exception(() => fixture.CreateInstaller()); + + // Then + Assert.IsType(result); + Assert.Equal("processRunner", ((ArgumentNullException)result).ParamName); + } + + [Fact] + public void Should_Throw_If_Content_Resolver_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.ContentResolver = null; + + // When + var result = Record.Exception(() => fixture.CreateInstaller()); + + // Then + Assert.IsType(result); + Assert.Equal("contentResolver", ((ArgumentNullException)result).ParamName); + } + + [Fact] + public void Should_Throw_If_Log_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Log = null; + + // When + var result = Record.Exception(() => fixture.CreateInstaller()); + + // Then + Assert.IsType(result); + Assert.Equal("log", ((ArgumentNullException)result).ParamName); + } + } + + public sealed class TheCanInstallMethod + { + [Fact] + public void Should_Throw_If_URI_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Package = null; + + // When + var result = Record.Exception(() => fixture.CanInstall()); + + // Then + Assert.IsType(result); + Assert.Equal("package", ((ArgumentNullException)result).ParamName); + } + + [Fact] + public void Should_Be_Able_To_Install_If_Scheme_Is_Correct() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Package = new PackageReference("dotnet:?package=Octopus.DotNet.Cli"); + + // When + var result = fixture.CanInstall(); + + // Then + Assert.True(result); + } + + [Fact] + public void Should_Not_Be_Able_To_Install_If_Scheme_Is_Incorrect() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Package = new PackageReference("homebrew:?package=windirstat"); + + // When + var result = fixture.CanInstall(); + + // Then + Assert.False(result); + } + } + + public sealed class TheInstallMethod + { + [Fact] + public void Should_Throw_If_Uri_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.Package = null; + + // When + var result = Record.Exception(() => fixture.Install()); + + // Then + Assert.IsType(result); + Assert.Equal("package", ((ArgumentNullException)result).ParamName); + } + + [Fact] + public void Should_Throw_If_Install_Path_Is_Null() + { + // Given + var fixture = new DotNetToolPackageInstallerFixture(); + fixture.InstallPath = null; + + // When + var result = Record.Exception(() => fixture.Install()); + + // Then + Assert.IsType(result); + Assert.Equal("path", ((ArgumentNullException)result).ParamName); + } + } + } +} diff --git a/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.csproj b/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.csproj new file mode 100644 index 0000000000..6a2ee10821 --- /dev/null +++ b/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.csproj @@ -0,0 +1,27 @@ + + + Cake.DotNetTool.Module + net461;netstandard2.0;net5.0 + true + true + snupkg + + + Cake.DotNetTool.Module.ruleset + + + TRACE;DEBUG;NETSTANDARD + + + + Cake Module that extends Cake with ability to install tools using dotnet cli. + + + + + + + + + + diff --git a/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.ruleset b/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.ruleset new file mode 100644 index 0000000000..be6bff4573 --- /dev/null +++ b/src/Cake.DotNetTool.Module/Cake.DotNetTool.Module.ruleset @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Cake.DotNetTool.Module/DotNetToolContentResolver.cs b/src/Cake.DotNetTool.Module/DotNetToolContentResolver.cs new file mode 100644 index 0000000000..df8b0b8537 --- /dev/null +++ b/src/Cake.DotNetTool.Module/DotNetToolContentResolver.cs @@ -0,0 +1,141 @@ +// 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.Configuration; +using Cake.Core.Diagnostics; +using Cake.Core.IO; +using Cake.Core.Packaging; + +namespace Cake.DotNetTool.Module +{ + /// + /// Locates and lists contents of dotnet Tool Packages. + /// + public class DotNetToolContentResolver : IDotNetToolContentResolver + { + private readonly IFileSystem _fileSystem; + private readonly ICakeEnvironment _environment; + private readonly IGlobber _globber; + private readonly ICakeLog _log; + + private readonly ICakeConfiguration _config; + + /// + /// Initializes a new instance of the class. + /// + /// The file system. + /// The environment. + /// The Globber. + /// The Log. + /// the configuration. + public DotNetToolContentResolver( + IFileSystem fileSystem, + ICakeEnvironment environment, + IGlobber globber, + ICakeLog log, + ICakeConfiguration config) + { + _fileSystem = fileSystem; + _environment = environment; + _globber = globber; + _log = log; + _config = config; + } + + /// + /// Collects all the files for the given dotnet Tool Package. + /// + /// The dotnet Tool Package. + /// The type of dotnet Tool Package. + /// All the files for the Package. + public IReadOnlyCollection GetFiles(PackageReference package, PackageType type) + { + if (type == PackageType.Addin) + { + throw new InvalidOperationException("DotNetTool Module does not support Addins'"); + } + + if (type == PackageType.Tool) + { + if (package.Parameters.ContainsKey("global")) + { + if (_environment.Platform.IsUnix()) + { + return GetToolFiles(new DirectoryPath(_environment.GetEnvironmentVariable("HOME")).Combine(".dotnet/tools"), package); + } + else + { + return GetToolFiles(new DirectoryPath(_environment.GetEnvironmentVariable("USERPROFILE")).Combine(".dotnet/tools"), package); + } + } + else + { + return GetToolFiles(_config.GetToolPath(_environment.WorkingDirectory, _environment), package); + } + } + + throw new InvalidOperationException("Unknown resource type."); + } + + private IReadOnlyCollection GetToolFiles(DirectoryPath installationLocation, PackageReference package) + { + var result = new List(); + var toolFolder = installationLocation.Combine(".store/" + package.Package.ToLowerInvariant()); + + _log.Debug("Tool Folder: {0}", toolFolder); + var toolDirectory = _fileSystem.GetDirectory(toolFolder); + + if (toolDirectory.Exists) + { + result.AddRange(GetFiles(toolFolder, package)); + } + else + { + _log.Debug("Tool folder does not exist: {0}.", toolFolder); + } + + _log.Debug("Found {0} files in tool folder", result.Count); + return result; + } + + private IEnumerable GetFiles(DirectoryPath path, PackageReference package, string[] patterns = null) + { + var collection = new FilePathCollection(new PathComparer(_environment)); + + // Get default files (dll). + patterns = patterns ?? new[] { path.FullPath + "/**/*.dll" }; + foreach (var pattern in patterns) + { + collection.Add(_globber.GetFiles(pattern)); + } + + // Include files. + if (package.Parameters.ContainsKey("include")) + { + foreach (var include in package.Parameters["include"]) + { + var includePath = string.Concat(path.FullPath, "/", include.TrimStart('/')); + collection.Add(_globber.GetFiles(includePath)); + } + } + + // Exclude files. + if (package.Parameters.ContainsKey("exclude")) + { + foreach (var exclude in package.Parameters["exclude"]) + { + var excludePath = string.Concat(path.FullPath, "/", exclude.TrimStart('/')); + collection.Remove(_globber.GetFiles(excludePath)); + } + } + + // Return the files. + return collection.Select(p => _fileSystem.GetFile(p)).ToArray(); + } + } +} diff --git a/src/Cake.DotNetTool.Module/DotNetToolModule.cs b/src/Cake.DotNetTool.Module/DotNetToolModule.cs new file mode 100644 index 0000000000..7851d8d602 --- /dev/null +++ b/src/Cake.DotNetTool.Module/DotNetToolModule.cs @@ -0,0 +1,35 @@ +// 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.Annotations; +using Cake.Core.Composition; +using Cake.Core.Packaging; +using Cake.DotNetTool.Module; + +[assembly: CakeModule(typeof(DotNetToolModule))] +namespace Cake.DotNetTool.Module +{ + /// + /// The module responsible for registering + /// default types in the Cake.DotNetTool.Module assembly. + /// + public sealed class DotNetToolModule : ICakeModule + { + /// + /// Performs custom registrations in the provided registrar. + /// + /// The container registrar. + public void Register(ICakeContainerRegistrar registrar) + { + if (registrar == null) + { + throw new ArgumentNullException(nameof(registrar)); + } + + registrar.RegisterType().As().Singleton(); + registrar.RegisterType().As().Singleton(); + } + } +} diff --git a/src/Cake.DotNetTool.Module/DotNetToolOperation.cs b/src/Cake.DotNetTool.Module/DotNetToolOperation.cs new file mode 100644 index 0000000000..1311e04994 --- /dev/null +++ b/src/Cake.DotNetTool.Module/DotNetToolOperation.cs @@ -0,0 +1,27 @@ +// 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.DotNetTool.Module +{ + /// + /// Represents dotnet tool operation. + /// + public enum DotNetToolOperation + { + /// + /// Install operation. + /// + Install, + + /// + /// Uninstall operation. + /// + Uninstall, + + /// + /// Update operation. + /// + Update + } +} \ No newline at end of file diff --git a/src/Cake.DotNetTool.Module/DotNetToolPackage.cs b/src/Cake.DotNetTool.Module/DotNetToolPackage.cs new file mode 100644 index 0000000000..dcaf4ac75c --- /dev/null +++ b/src/Cake.DotNetTool.Module/DotNetToolPackage.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.DotNetTool.Module +{ + /// + /// Represents a dotnet tool package. + /// + public sealed class DotNetToolPackage + { + /// + /// Gets or sets the tool package ID. + /// + /// The tool package ID. + public string Id { get; set; } + + /// + /// Gets or sets the tool package version. + /// + /// The tool package version. + public string Version { get; set; } + + /// + /// Gets or sets the tool package short code. + /// + /// The tool package short code. + public string ShortCode { get; set; } + } +} \ No newline at end of file diff --git a/src/Cake.DotNetTool.Module/DotNetToolPackageInstaller.cs b/src/Cake.DotNetTool.Module/DotNetToolPackageInstaller.cs new file mode 100644 index 0000000000..f07a586ec0 --- /dev/null +++ b/src/Cake.DotNetTool.Module/DotNetToolPackageInstaller.cs @@ -0,0 +1,333 @@ +// 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 System.Text.RegularExpressions; +using Cake.Core; +using Cake.Core.Configuration; +using Cake.Core.Diagnostics; +using Cake.Core.IO; +using Cake.Core.Packaging; + +namespace Cake.DotNetTool.Module +{ + /// + /// Installer for dotnet Tool Packages. + /// + public sealed class DotNetToolPackageInstaller : IPackageInstaller + { + private readonly ICakeEnvironment _environment; + private readonly IProcessRunner _processRunner; + private readonly ICakeLog _log; + private readonly IDotNetToolContentResolver _contentResolver; + private readonly ICakeConfiguration _config; + private readonly IFileSystem _fileSystem; + + /// + /// Initializes a new instance of the class. + /// + /// The environment. + /// The process runner. + /// The log. + /// The DotNetTool Package Content Resolver. + /// the configuration. + /// The file system. + public DotNetToolPackageInstaller(ICakeEnvironment environment, IProcessRunner processRunner, ICakeLog log, IDotNetToolContentResolver contentResolver, ICakeConfiguration config, IFileSystem fileSystem) + { + if (environment == null) + { + throw new ArgumentNullException(nameof(environment)); + } + + if (processRunner == null) + { + throw new ArgumentNullException(nameof(processRunner)); + } + + if (log == null) + { + throw new ArgumentNullException(nameof(log)); + } + + if (contentResolver == null) + { + throw new ArgumentNullException(nameof(contentResolver)); + } + + _environment = environment; + _processRunner = processRunner; + _log = log; + _contentResolver = contentResolver; + _config = config; + _fileSystem = fileSystem; + } + + /// + /// Determines whether this instance can install the specified resource. + /// + /// The package reference. + /// The package type. + /// true if this installer can install the specified resource; otherwise false. + public bool CanInstall(PackageReference package, PackageType type) + { + if (package == null) + { + throw new ArgumentNullException(nameof(package)); + } + + return package.Scheme.Equals("dotnet", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Installs the specified resource at the given location. + /// + /// The package reference. + /// The package type. + /// The location where to install the package. + /// The installed files. + public IReadOnlyCollection Install(PackageReference package, PackageType type, DirectoryPath path) + { + if (package == null) + { + throw new ArgumentNullException(nameof(package)); + } + + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // We are going to assume that the default install location is the + // currently configured location for the Cake Tools Folder + var toolsFolderDirectoryPath = _config.GetToolPath(_environment.WorkingDirectory, _environment); + _log.Debug("Configured Tools Folder: {0}", toolsFolderDirectoryPath); + + var toolLocation = toolsFolderDirectoryPath.FullPath; + if (package.Parameters.ContainsKey("global")) + { + toolLocation = "global"; + } + + // First we need to check if the Tool is already installed + var installedTools = GetInstalledTools(toolLocation); + + _log.Debug("Checking for tool: {0}", package.Package.ToLowerInvariant()); + + var installedTool = installedTools.FirstOrDefault(t => string.Equals(t.Id, package.Package, StringComparison.InvariantCultureIgnoreCase)); + + if (installedTool != null) + { + // The tool is already installed, so need to check if requested version is the same as + // what is already installed + string requestedVersion = null; + + if (package.Parameters.ContainsKey("version")) + { + requestedVersion = package.Parameters["version"].First(); + } + + if (requestedVersion == null) + { + _log.Warning("Tool {0} is already installed, and no specific version has been requested via pre-processor directive, so leaving current version installed.", package.Package); + } + else if (requestedVersion.ToLowerInvariant() != installedTool.Version.ToLowerInvariant()) + { + _log.Warning("Tool {0} is already installed, but a different version has been requested. Uninstall/install will now be performed...", package.Package); + RunDotNetTool(package, toolsFolderDirectoryPath, DotNetToolOperation.Uninstall); + RunDotNetTool(package, toolsFolderDirectoryPath, DotNetToolOperation.Install); + } + else + { + _log.Information("Tool {0} is already installed, with required version.", package.Package); + } + } + else + { + // The tool isn't already installed, go ahead and install it + RunDotNetTool(package, toolsFolderDirectoryPath, DotNetToolOperation.Install); + } + + var result = _contentResolver.GetFiles(package, type); + if (result.Count == 0) + { + const string format = "Could not find any relevant files for tool '{0}'. Perhaps you need an include parameter?"; + _log.Warning(format, package.Package); + } + + return result; + } + + private List GetInstalledTools(string toolLocation) + { + var toolLocationArgument = string.Empty; + if (toolLocation != "global") + { + toolLocationArgument = string.Format("--tool-path \"{0}\"", toolLocation); + var toolLocationDirectoryPath = new DirectoryPath(toolLocation).MakeAbsolute(_environment); + var toolLocationDirectory = _fileSystem.GetDirectory(toolLocationDirectoryPath); + + // If the requested tools path doesn't exist, then there can't be any tools + // installed there, so simply return an empty list. + if (!toolLocationDirectory.Exists) + { + _log.Debug("Specified installation location doesn't currently exist."); + return new List(); + } + } + else + { + toolLocationArgument = "--global"; + } + + var isInstalledProcess = _processRunner.Start( + "dotnet", + new ProcessSettings + { + Arguments = string.Concat("tool list ", toolLocationArgument), + RedirectStandardOutput = true, + Silent = _log.Verbosity < Verbosity.Diagnostic + }); + + isInstalledProcess.WaitForExit(); + + var installedTools = isInstalledProcess.GetStandardOutput().ToList(); + var installedToolNames = new List(); + + const string pattern = @"(?[^\s]+)\s+(?[^\s]+)\s+(?[^`s])"; + + foreach (var installedTool in installedTools.Skip(2)) + { + foreach (Match match in Regex.Matches(installedTool, pattern, RegexOptions.IgnoreCase)) + { + _log.Debug("Adding tool {0}", match.Groups["packageName"].Value); + installedToolNames.Add(new DotNetToolPackage + { + Id = match.Groups["packageName"].Value, + Version = match.Groups["packageVersion"].Value, + ShortCode = match.Groups["packageShortCode"].Value + }); + } + } + + _log.Debug("There are {0} dotnet tools installed", installedToolNames.Count); + return installedToolNames; + } + + private void RunDotNetTool(PackageReference package, DirectoryPath toolsFolderDirectoryPath, DotNetToolOperation operation) + { + // Install the tool.... + _log.Debug("Running dotnet tool with operation {0}: {1}...", operation, package.Package); + var process = _processRunner.Start( + "dotnet", + new ProcessSettings + { + Arguments = GetArguments(package, operation, _log, toolsFolderDirectoryPath), + RedirectStandardOutput = true, + Silent = _log.Verbosity < Verbosity.Diagnostic, + NoWorkingDirectory = true + }); + + process.WaitForExit(); + + var exitCode = process.GetExitCode(); + if (exitCode != 0) + { + _log.Warning("dotnet exited with {0}", exitCode); + var output = string.Join(Environment.NewLine, process.GetStandardError()); + _log.Verbose(Verbosity.Diagnostic, "Output:\r\n{0}", output); + } + } + + private static ProcessArgumentBuilder GetArguments( + PackageReference definition, + DotNetToolOperation operation, + ICakeLog log, + DirectoryPath toolDirectoryPath) + { + var arguments = new ProcessArgumentBuilder(); + + arguments.Append("tool"); + arguments.Append(Enum.GetName(typeof(DotNetToolOperation), operation).ToLowerInvariant()); + arguments.AppendQuoted(definition.Package); + + if (definition.Parameters.ContainsKey("global")) + { + arguments.Append("--global"); + } + else + { + arguments.Append("--tool-path"); + arguments.AppendQuoted(toolDirectoryPath.FullPath); + } + + if (operation != DotNetToolOperation.Uninstall) + { + if (definition.Address != null) + { + arguments.Append("--add-source"); + arguments.AppendQuoted(definition.Address.AbsoluteUri); + } + + // Version + if (definition.Parameters.ContainsKey("version")) + { + arguments.Append("--version"); + arguments.Append(definition.Parameters["version"].First()); + } + + // Config File + if (definition.Parameters.ContainsKey("configfile")) + { + arguments.Append("--configfile"); + arguments.AppendQuoted(definition.Parameters["configfile"].First()); + } + + // Whether to ignore failed sources + if (definition.Parameters.ContainsKey("ignore-failed-sources")) + { + arguments.Append("--ignore-failed-sources"); + } + + // Framework + if (definition.Parameters.ContainsKey("framework")) + { + arguments.Append("--framework"); + arguments.Append(definition.Parameters["framework"].First()); + } + + switch (log.Verbosity) + { + case Verbosity.Quiet: + arguments.Append("--verbosity"); + arguments.Append("quiet"); + break; + case Verbosity.Minimal: + arguments.Append("--verbosity"); + arguments.Append("minimal"); + break; + case Verbosity.Normal: + arguments.Append("--verbosity"); + arguments.Append("normal"); + break; + case Verbosity.Verbose: + arguments.Append("--verbosity"); + arguments.Append("detailed"); + break; + case Verbosity.Diagnostic: + arguments.Append("--verbosity"); + arguments.Append("diagnostic"); + break; + default: + arguments.Append("--verbosity"); + arguments.Append("normal"); + break; + } + } + + return arguments; + } + } +} diff --git a/src/Cake.DotNetTool.Module/IDotNetToolContentResolver.cs b/src/Cake.DotNetTool.Module/IDotNetToolContentResolver.cs new file mode 100644 index 0000000000..1141443fae --- /dev/null +++ b/src/Cake.DotNetTool.Module/IDotNetToolContentResolver.cs @@ -0,0 +1,26 @@ +// 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.Core.IO; +using Cake.Core.Packaging; + +namespace Cake.DotNetTool.Module +{ + /// + /// Represents a file locator for dotnet Tool packages that returns relevant + /// files given the resource type. + /// + public interface IDotNetToolContentResolver + { + /// + /// Gets the relevant files for a dotnet Tool package + /// given a resource type. + /// + /// The package. + /// The resource type. + /// A collection of files. + IReadOnlyCollection GetFiles(PackageReference package, PackageType type); + } +} diff --git a/src/Cake.DotNetTool.Module/icon.png b/src/Cake.DotNetTool.Module/icon.png new file mode 100644 index 0000000000..9881edc4e9 Binary files /dev/null and b/src/Cake.DotNetTool.Module/icon.png differ diff --git a/src/Cake.Frosting/Cake.Frosting.csproj b/src/Cake.Frosting/Cake.Frosting.csproj index 8df60ac128..6f504c1384 100644 --- a/src/Cake.Frosting/Cake.Frosting.csproj +++ b/src/Cake.Frosting/Cake.Frosting.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Cake.Frosting/CakeHost.cs b/src/Cake.Frosting/CakeHost.cs index a97f40e7c4..2591b97475 100644 --- a/src/Cake.Frosting/CakeHost.cs +++ b/src/Cake.Frosting/CakeHost.cs @@ -11,6 +11,7 @@ using Cake.Core.Configuration; using Cake.Core.Diagnostics; using Cake.Core.Modules; +using Cake.DotNetTool.Module; using Cake.Frosting.Internal; using Cake.NuGet; using Microsoft.Extensions.DependencyInjection; @@ -125,6 +126,7 @@ private ServiceCollection CreateServiceCollection() services.UseModule(); services.UseModule(); services.UseModule(); + services.UseModule(); services.AddSingleton(); services.AddSingleton(f => f.GetService()); diff --git a/src/Cake.sln b/src/Cake.sln index acb0d1d5a5..aaee2c8742 100644 --- a/src/Cake.sln +++ b/src/Cake.sln @@ -68,6 +68,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{83848400 ..\build\version.cake = ..\build\version.cake EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.DotNetTool.Module", "Cake.DotNetTool.Module\Cake.DotNetTool.Module.csproj", "{4BA8DD2B-9AB6-482D-BBDE-FC038C5A44B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cake.DotNetTool.Module.Tests", "Cake.DotNetTool.Module.Tests\Cake.DotNetTool.Module.Tests.csproj", "{CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +138,14 @@ Global {B155E9B1-5F3E-4AA2-B0E1-A9F43601F5CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {B155E9B1-5F3E-4AA2-B0E1-A9F43601F5CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {B155E9B1-5F3E-4AA2-B0E1-A9F43601F5CE}.Release|Any CPU.Build.0 = Release|Any CPU + {4BA8DD2B-9AB6-482D-BBDE-FC038C5A44B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BA8DD2B-9AB6-482D-BBDE-FC038C5A44B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BA8DD2B-9AB6-482D-BBDE-FC038C5A44B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BA8DD2B-9AB6-482D-BBDE-FC038C5A44B3}.Release|Any CPU.Build.0 = Release|Any CPU + {CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -148,6 +160,7 @@ Global {15A49638-46DE-4361-B86E-4873BA5EF136} = {8615F41E-65C7-4BE5-AFD4-C9C78912AF80} {26F4E738-122C-428D-A014-62A357F39023} = {8615F41E-65C7-4BE5-AFD4-C9C78912AF80} {83848400-2E27-4B57-B733-FAF357ED1AE6} = {A01118B7-C6FC-4B0B-8B5C-F580F31FE57D} + {CE4433AD-A5F2-44DA-A0DA-E51D141FFFDC} = {8615F41E-65C7-4BE5-AFD4-C9C78912AF80} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {35585E1D-D23E-40C8-A01E-2E5FF5B41083} diff --git a/src/Cake/Cake.csproj b/src/Cake/Cake.csproj index 12bf281e0f..e773a945ce 100644 --- a/src/Cake/Cake.csproj +++ b/src/Cake/Cake.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Cake/Infrastructure/Composition/ModuleSearcher.cs b/src/Cake/Infrastructure/Composition/ModuleSearcher.cs index f17c1955cf..f47110b5a5 100644 --- a/src/Cake/Infrastructure/Composition/ModuleSearcher.cs +++ b/src/Cake/Infrastructure/Composition/ModuleSearcher.cs @@ -23,6 +23,11 @@ public interface IModuleSearcher public sealed class ModuleSearcher : IModuleSearcher { + private static readonly Dictionary _excludedModules = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "Cake.DotNetTool.Module", "Cake.DotNetTool.Module is now included with Cake, so should no longer be installed separately to module directory or using #module directive" } + }; + private readonly IFileSystem _fileSystem; private readonly ICakeEnvironment _environment; private readonly ICakeLog _log; @@ -64,6 +69,12 @@ private Type LoadModule(FilePath path, ICakeConfiguration configuration) { try { + if (_excludedModules.TryGetValue(path.GetFilenameWithoutExtension().FullPath, out var message)) + { + _log.Warning("{0}, Assembly {1} is excluded from loading.", message, path); + return null; + } + var loader = new AssemblyLoader(_environment, _fileSystem, new AssemblyVerifier(configuration, _log)); var assembly = loader.Load(path, true); diff --git a/src/Cake/Infrastructure/ContainerConfigurator.cs b/src/Cake/Infrastructure/ContainerConfigurator.cs index 9f8d08f09c..236e5eae8a 100644 --- a/src/Cake/Infrastructure/ContainerConfigurator.cs +++ b/src/Cake/Infrastructure/ContainerConfigurator.cs @@ -10,6 +10,7 @@ using Cake.Core.Diagnostics; using Cake.Core.Modules; using Cake.Core.Scripting; +using Cake.DotNetTool.Module; using Cake.Infrastructure.Scripting; using Cake.NuGet; using Spectre.Console.Cli; @@ -41,6 +42,7 @@ public void Configure( new CoreModule().Register(registrar); new CommonModule().Register(registrar); new NuGetModule().Register(registrar); + new DotNetToolModule().Register(registrar); // Misc registrations. registrar.RegisterType().As().Singleton(); diff --git a/tests/integration/Cake.DotNetTool.Module/Cake.DotNetTool.Module.cake b/tests/integration/Cake.DotNetTool.Module/Cake.DotNetTool.Module.cake new file mode 100644 index 0000000000..275aa31791 --- /dev/null +++ b/tests/integration/Cake.DotNetTool.Module/Cake.DotNetTool.Module.cake @@ -0,0 +1,39 @@ +#load "./../utilities/paths.cake" + +Task("Cake.DotNetTool.Module.Setup") + .Does(() => +{ + var path = Paths.Temp.Combine("./Cake.DotNetTool.Module"); + CleanDirectory(path); +}); + +Task("Cake.DotNetTool.Module.Install") + .IsDependentOn("Cake.DotNetTool.Module.Setup") + .Does(() => +{ + // Given + var scriptPath = Paths.Temp.Combine("./Cake.DotNetTool.Module/").CombineWithFilePath("build.cake"); + var script = "#tool \"dotnet:?package=Octopus.DotNet.Cli&version=7.4.6\""; + System.IO.File.WriteAllText(scriptPath.FullPath, script); + + // When + CakeExecuteScript(scriptPath); +}); + +Task("Cake.DotNetTool.Module.Update") + .IsDependentOn("Cake.DotNetTool.Module.Install") + .Does(() => +{ + // Given + var scriptPath = Paths.Temp.Combine("./Cake.DotNetTool.Module/").CombineWithFilePath("build.cake"); + var script = "#tool \"dotnet:?package=Octopus.DotNet.Cli&version=7.4.3121\""; + System.IO.File.WriteAllText(scriptPath.FullPath, script); + + // When + CakeExecuteScript(scriptPath); +}); + +Task("Cake.DotNetTool.Module") + .IsDependentOn("Cake.DotNetTool.Module.Setup") + .IsDependentOn("Cake.DotNetTool.Module.Install") + .IsDependentOn("Cake.DotNetTool.Module.Update"); diff --git a/tests/integration/build.cake b/tests/integration/build.cake index adc69ff49a..f512a155e2 100644 --- a/tests/integration/build.cake +++ b/tests/integration/build.cake @@ -36,6 +36,7 @@ #load "./Cake.Core/Scripting/UsingDirective.cake" #load "./Cake.Core/Tooling/ToolLocator.cake" #load "./Cake.Core/CakeAliases.cake" +#load "./Cake.DotNetTool.Module/Cake.DotNetTool.Module.cake" ////////////////////////////////////////////////// // ARGUMENTS @@ -84,7 +85,8 @@ Task("Cake.Common") Task("Run-All-Tests") .IsDependentOn("Setup-Tests") .IsDependentOn("Cake.Core") - .IsDependentOn("Cake.Common"); + .IsDependentOn("Cake.Common") + .IsDependentOn("Cake.DotNetTool.Module"); //////////////////////////////////////////////////