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");
//////////////////////////////////////////////////