diff --git a/Directory.Build.props b/Directory.Build.props
index 97a615453..4a8c44efd 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -57,6 +57,7 @@ tools\TestAssetsDependencies\TestAssetsDependencies.csproj
+
diff --git a/build/BundledDotnetTools.proj b/build/BundledDotnetTools.proj
new file mode 100644
index 000000000..b94bac722
--- /dev/null
+++ b/build/BundledDotnetTools.proj
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ /p:TargetFramework=$(CliTargetFramework)
+ $(DotnetToolsRestoreAdditionalParameters) /p:TemplateFillInPackageName=$(TemplateFillInPackageName)
+ $(DotnetToolsRestoreAdditionalParameters) /p:TemplateFillInPackageVersion=$(TemplateFillInPackageVersion)
+ $(DotnetToolsRestoreAdditionalParameters) /p:RestorePackagesPath=$(DotnetToolsLayoutDirectory)
+ $(DotnetToolsRestoreAdditionalParameters) /p:RestoreProjectStyle=$(DotnetToolsRestoreProjectStyle)
+
+
+
+
+
diff --git a/build/BundledDotnetTools.props b/build/BundledDotnetTools.props
new file mode 100644
index 000000000..acd2156a5
--- /dev/null
+++ b/build/BundledDotnetTools.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/build/BundledTemplates.proj b/build/BundledTemplates.proj
index dfc66e111..4d215f874 100644
--- a/build/BundledTemplates.proj
+++ b/build/BundledTemplates.proj
@@ -11,26 +11,26 @@
-
-
+
+ Condition="!Exists('$(TemplateNuPkgPath)/$(TemplateFillInPackageName.ToLower()).nuspec')">
+ AdditionalParameters="/p:TargetFramework=netcoreapp1.0 /p:TemplateFillInPackageName=$(TemplateFillInPackageName) /p:TemplateFillInPackageVersion=$(TemplateFillInPackageVersion)" />
- $(NuGetPackagesDir)/$(TemplatePackageName.ToLower())/$(TemplatePackageVersion.ToLower())
+ $(NuGetPackagesDir)/$(TemplateFillInPackageName.ToLower())/$(TemplateFillInPackageVersion.ToLower())
diff --git a/build/templates/templates.csproj b/build/templates/templates.csproj
index 099a67e3f..2b8443fc6 100644
--- a/build/templates/templates.csproj
+++ b/build/templates/templates.csproj
@@ -3,12 +3,11 @@
Library
- netcoreapp1.0
false
-
+
diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs
index da52bf31b..d2b81f483 100644
--- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs
+++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs
@@ -14,6 +14,9 @@ namespace Microsoft.DotNet.Cli.Utils
// command loaded from project tools nuget package
ProjectToolsPackage,
+ // command loaded from bundled DotnetTools nuget package
+ DotnetToolsPackage,
+
// command loaded from the same directory as the executing assembly
BaseDirectory,
diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs
index d8a2c464b..711d63112 100644
--- a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs
+++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs
@@ -44,6 +44,7 @@ namespace Microsoft.DotNet.Cli.Utils
var compositeCommandResolver = new CompositeCommandResolver();
compositeCommandResolver.AddCommandResolver(new MuxerCommandResolver());
+ compositeCommandResolver.AddCommandResolver(new DotnetToolsCommandResolver());
compositeCommandResolver.AddCommandResolver(new RootedCommandResolver());
compositeCommandResolver.AddCommandResolver(
new ProjectToolsCommandResolver(packagedCommandSpecFactory, environment));
diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DotnetToolsCommandResolver.cs b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DotnetToolsCommandResolver.cs
new file mode 100644
index 000000000..64ba55118
--- /dev/null
+++ b/src/Microsoft.DotNet.Cli.Utils/CommandResolution/DotnetToolsCommandResolver.cs
@@ -0,0 +1,89 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Collections.Generic;
+using Microsoft.DotNet.PlatformAbstractions;
+
+namespace Microsoft.DotNet.Cli.Utils
+{
+ public class DotnetToolsCommandResolver : ICommandResolver
+ {
+ private string _dotnetToolPath;
+
+ public DotnetToolsCommandResolver(string dotnetToolPath = null)
+ {
+ if (dotnetToolPath == null)
+ {
+ _dotnetToolPath = Path.Combine(ApplicationEnvironment.ApplicationBasePath,
+ "DotnetTools");
+ }
+ else
+ {
+ _dotnetToolPath = dotnetToolPath;
+ }
+ }
+
+ public CommandSpec Resolve(CommandResolverArguments arguments)
+ {
+ if (string.IsNullOrEmpty(arguments.CommandName))
+ {
+ return null;
+ }
+
+ var packageId = new DirectoryInfo(Path.Combine(_dotnetToolPath, arguments.CommandName));
+ if (!packageId.Exists)
+ {
+ return null;
+ }
+
+ var version = packageId.GetDirectories()[0];
+ var dll = version.GetDirectories("tools")[0]
+ .GetDirectories()[0] // TFM
+ .GetDirectories()[0] // RID
+ .GetFiles($"{arguments.CommandName}.dll")[0];
+
+ return CreatePackageCommandSpecUsingMuxer(
+ dll.FullName,
+ arguments.CommandArguments,
+ CommandResolutionStrategy.DotnetToolsPackage);
+ }
+
+ private CommandSpec CreatePackageCommandSpecUsingMuxer(
+ string commandPath,
+ IEnumerable commandArguments,
+ CommandResolutionStrategy commandResolutionStrategy)
+ {
+ var arguments = new List();
+
+ var muxer = new Muxer();
+
+ var host = muxer.MuxerPath;
+ if (host == null)
+ {
+ throw new Exception(LocalizableStrings.UnableToLocateDotnetMultiplexer);
+ }
+
+ arguments.Add(commandPath);
+
+ if (commandArguments != null)
+ {
+ arguments.AddRange(commandArguments);
+ }
+
+ return CreateCommandSpec(host, arguments, commandResolutionStrategy);
+ }
+
+ private CommandSpec CreateCommandSpec(
+ string commandPath,
+ IEnumerable commandArguments,
+ CommandResolutionStrategy commandResolutionStrategy)
+ {
+ var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments);
+
+ return new CommandSpec(commandPath, escapedArgs, commandResolutionStrategy);
+ }
+ }
+}
diff --git a/src/redist/redist.csproj b/src/redist/redist.csproj
index 493683cb8..b86cde0d6 100644
--- a/src/redist/redist.csproj
+++ b/src/redist/redist.csproj
@@ -179,8 +179,8 @@
TemplateLayoutDirectory=$(SdkOutputDirectory)/Templates;
- TemplatePackageName=%(BundledTemplate.Identity);
- TemplatePackageVersion=%(BundledTemplate.Version);
+ TemplateFillInPackageName=%(BundledTemplate.Identity);
+ TemplateFillInPackageVersion=%(BundledTemplate.Version);
PreviousStageDirectory=$(PreviousStageDirectory)
@@ -192,6 +192,25 @@
+
+
+
+
+ DotnetToolsLayoutDirectory=$(SdkOutputDirectory)/DotnetTools;
+ TemplateFillInPackageName=%(BundledDotnetTools.Identity);
+ TemplateFillInPackageVersion=%(BundledDotnetTools.Version);
+ PreviousStageDirectory=$(PreviousStageDirectory)
+
+
+
+
+
+
+
+
+ Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestDotnetToolsLayoutDirectory");
+ private IEnumerable GetDotnetToolDirectory() =>
+ new DirectoryInfo(GetDotnetToolPath()).GetDirectories().Where(d => d.Name.StartsWith("dotnet-"));
+
+ [Fact]
+ public void Then_there_is_DotnetTools()
+ {
+ new DirectoryInfo(GetDotnetToolPath()).GetDirectories().Should().Contain(d => d.Name.StartsWith("dotnet-"));
+ }
+
+ [Fact]
+ public void Then_there_is_only_1_version()
+ {
+ foreach (var packageFolder in GetDotnetToolDirectory())
+ {
+ packageFolder.GetDirectories().Should().HaveCount(1);
+ }
+ }
+
+ [Fact]
+ public void Then_there_is_only_1_tfm()
+ {
+ foreach (var packageFolder in GetDotnetToolDirectory())
+ {
+ packageFolder.GetDirectories()[0]
+ .GetDirectories("tools")[0]
+ .GetDirectories().Should().HaveCount(1);
+ }
+ }
+
+ [Fact]
+ public void Then_there_is_only_1_rid()
+ {
+ foreach (var packageFolder in GetDotnetToolDirectory())
+ {
+ packageFolder.GetDirectories()[0]
+ .GetDirectories("tools")[0]
+ .GetDirectories()[0]
+ .GetDirectories().Should().HaveCount(1);
+ }
+ }
+
+ [Fact]
+ public void Then_packageName_is_the_same_as_dll()
+ {
+ foreach (var packageFolder in GetDotnetToolDirectory())
+ {
+ var packageId = packageFolder.Name;
+ packageFolder.GetDirectories()[0].GetDirectories("tools")[0].GetDirectories()[0].GetDirectories()[0]
+ .GetFiles()
+ .Should().Contain(f => string.Equals(f.Name, $"{packageId}.dll", StringComparison.Ordinal));
+ }
+ }
+ }
+}
diff --git a/test/InsertionTests/InsertionTests.csproj b/test/InsertionTests/InsertionTests.csproj
new file mode 100644
index 000000000..e4ac93433
--- /dev/null
+++ b/test/InsertionTests/InsertionTests.csproj
@@ -0,0 +1,40 @@
+
+
+ $(CliTargetFramework)
+ $(MicrosoftNETCoreAppPackageVersion)
+ true
+ msbuild.IntegrationTests
+ $(AssetTargetFallback);dotnet5.4;portable-net451+win8
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(OutputPath)/TestDotnetToolsLayoutDirectory
+
+
+
+
+
+ DotnetToolsLayoutDirectory=$(SdkOutputDirectory)/DotnetTools;
+ TemplateFillInPackageName=%(BundledDotnetTools.Identity);
+ TemplateFillInPackageVersion=%(BundledDotnetTools.Version);
+ PreviousStageDirectory=$(PreviousStageDirectory);
+ DotnetToolsLayoutDirectory=$(testAssetSourceRoot);
+ DotnetToolsRestoreProjectStyle=DotnetToolReference
+
+
+
+
+
+
diff --git a/test/Microsoft.DotNet.Cli.Tests.sln b/test/Microsoft.DotNet.Cli.Tests.sln
index 9e41297be..c1ce1757e 100644
--- a/test/Microsoft.DotNet.Cli.Tests.sln
+++ b/test/Microsoft.DotNet.Cli.Tests.sln
@@ -1,6 +1,7 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+
+Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.27004.2008
+VisualStudioVersion = 15.0.27130.2024
MinimumVisualStudioVersion = 10.0.40219.1
Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "dotnet-add-reference.Tests", "dotnet-add-reference.Tests\dotnet-add-reference.Tests.csproj", "{AB63A3E5-76A3-4EE9-A380-8E0C7B7644DC}"
EndProject
@@ -90,6 +91,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.Tools.Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-install-tool.Tests", "dotnet-install-tool.Tests\dotnet-install-tool.Tests.csproj", "{E2F5F115-0DE4-4CC0-920C-EF6F89D15EAB}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InsertionTests", "InsertionTests\InsertionTests.csproj", "{A9713391-3D44-4664-9C41-75765218FD6C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -592,6 +595,18 @@ Global
{E2F5F115-0DE4-4CC0-920C-EF6F89D15EAB}.Release|x64.Build.0 = Release|Any CPU
{E2F5F115-0DE4-4CC0-920C-EF6F89D15EAB}.Release|x86.ActiveCfg = Release|Any CPU
{E2F5F115-0DE4-4CC0-920C-EF6F89D15EAB}.Release|x86.Build.0 = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|x64.Build.0 = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Debug|x86.Build.0 = Debug|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|x64.ActiveCfg = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|x64.Build.0 = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|x86.ActiveCfg = Release|Any CPU
+ {A9713391-3D44-4664-9C41-75765218FD6C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs
index b2e354e30..5537218f7 100644
--- a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs
+++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADefaultCommandResolver.cs
@@ -17,13 +17,14 @@ namespace Microsoft.DotNet.Cli.Utils.Tests
var resolvers = defaultCommandResolver.OrderedCommandResolvers;
- resolvers.Should().HaveCount(7);
+ resolvers.Should().HaveCount(8);
resolvers.Select(r => r.GetType())
.Should()
.ContainInOrder(
new []{
typeof(MuxerCommandResolver),
+ typeof(DotnetToolsCommandResolver),
typeof(RootedCommandResolver),
typeof(ProjectToolsCommandResolver),
typeof(AppBaseDllCommandResolver),
diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADotnetToolsCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADotnetToolsCommandResolver.cs
new file mode 100644
index 000000000..d18bc8b2c
--- /dev/null
+++ b/test/Microsoft.DotNet.Cli.Utils.Tests/GivenADotnetToolsCommandResolver.cs
@@ -0,0 +1,68 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.IO;
+using FluentAssertions;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Tools.Test.Utilities;
+using Xunit;
+using System.Reflection;
+
+namespace Microsoft.DotNet.Tests
+{
+
+ public class GivenADotnetToolsCommandResolver : TestBase
+ {
+ private readonly DotnetToolsCommandResolver _dotnetToolsCommandResolver;
+
+ // Assets are placed during build of this project
+ private static string GetDotnetToolPath() => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestDotnetToolsLayoutDirectory");
+
+ public GivenADotnetToolsCommandResolver()
+ {
+ _dotnetToolsCommandResolver = new DotnetToolsCommandResolver(GetDotnetToolPath());
+ }
+
+ [Fact]
+ public void ItReturnsNullWhenCommandNameIsNull()
+ {
+ var commandResolverArguments = new CommandResolverArguments()
+ {
+ CommandName = null,
+ };
+
+ var result = _dotnetToolsCommandResolver.Resolve(commandResolverArguments);
+
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void ItReturnsNullWhenCommandNameDoesNotExistInProjectTools()
+ {
+ var commandResolverArguments = new CommandResolverArguments()
+ {
+ CommandName = "nonexistent-command",
+ };
+
+ var result = _dotnetToolsCommandResolver.Resolve(commandResolverArguments);
+
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public void ItReturnsACommandSpec()
+ {
+ var commandResolverArguments = new CommandResolverArguments()
+ {
+ CommandName = "dotnet-watch",
+ };
+
+ var result = _dotnetToolsCommandResolver.Resolve(commandResolverArguments);
+
+ result.Should().NotBeNull();
+
+ var commandPath = result.Args.Trim('"');
+ commandPath.Should().Contain("dotnet-watch.dll");
+ }
+ }
+}
diff --git a/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj b/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj
index d03b8c253..222c14025 100644
--- a/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj
+++ b/test/Microsoft.DotNet.Cli.Utils.Tests/Microsoft.DotNet.Cli.Utils.Tests.csproj
@@ -40,4 +40,26 @@
+
+
+
+
+ $(OutputPath)/TestDotnetToolsLayoutDirectory
+
+
+
+
+
+ DotnetToolsLayoutDirectory=$(SdkOutputDirectory)/DotnetTools;
+ TemplateFillInPackageName=dotnet-watch;
+ TemplateFillInPackageVersion=$(AspNetCoreVersion);
+ PreviousStageDirectory=$(PreviousStageDirectory);
+ DotnetToolsLayoutDirectory=$(testAssetSourceRoot);
+ DotnetToolsRestoreProjectStyle=DotnetToolReference
+
+
+
+
+
+