From 08f050c012cb80bb9433cd401d9be866307e4687 Mon Sep 17 00:00:00 2001 From: William Lee Date: Fri, 16 Feb 2018 13:32:29 -0800 Subject: [PATCH] bundled DotnetTool (#8606) Extract packages to DotnetTools folder under sdk/{version} Add new resolver to discover it Add test to enforce package structure. It will fail when the structure changed --- Directory.Build.props | 1 + build/BundledDotnetTools.proj | 27 ++++++ build/BundledDotnetTools.props | 6 ++ build/BundledTemplates.proj | 10 +-- build/templates/templates.csproj | 3 +- .../CommandResolutionStrategy.cs | 3 + .../DefaultCommandResolverPolicy.cs | 1 + .../DotnetToolsCommandResolver.cs | 89 +++++++++++++++++++ src/redist/redist.csproj | 23 ++++- test/EndToEnd/GivenDotnetUsesDotnetTools.cs | 17 ++++ .../GivenDotnetInvokesMSBuild.cs | 73 +++++++++++++++ test/InsertionTests/InsertionTests.csproj | 40 +++++++++ test/Microsoft.DotNet.Cli.Tests.sln | 19 +++- .../GivenADefaultCommandResolver.cs | 3 +- .../GivenADotnetToolsCommandResolver.cs | 68 ++++++++++++++ .../Microsoft.DotNet.Cli.Utils.Tests.csproj | 22 +++++ 16 files changed, 393 insertions(+), 12 deletions(-) create mode 100644 build/BundledDotnetTools.proj create mode 100644 build/BundledDotnetTools.props create mode 100644 src/Microsoft.DotNet.Cli.Utils/CommandResolution/DotnetToolsCommandResolver.cs create mode 100644 test/EndToEnd/GivenDotnetUsesDotnetTools.cs create mode 100644 test/InsertionTests/GivenDotnetInvokesMSBuild.cs create mode 100644 test/InsertionTests/InsertionTests.csproj create mode 100644 test/Microsoft.DotNet.Cli.Utils.Tests/GivenADotnetToolsCommandResolver.cs 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 + + + + + +