From 584d3f05021af25c737136275fc1c77429b12db7 Mon Sep 17 00:00:00 2001 From: William Lee Date: Tue, 21 Nov 2017 20:10:06 -0800 Subject: [PATCH] Global tools package obtain (#8035) Add ExeutablePackageObtainer Given a tools package id, it can create a fake project and restore to correct folder - DI, aka no circular dependency of commands - Parser of config XML - I try to create test nupkg at build time, so I can run test and debug easily with VSCode. The code is in test csproj. --- Microsoft.DotNet.Cli.sln | 29 ++- .../DirectoryPath.cs | 47 ++++ .../FilePath.cs | 32 +++ src/dotnet/BundledTargetFramework.cs | 21 ++ src/dotnet/Properties/AssemblyInfo.cs | 1 + .../IPackageToProjectFileAdder.cs | 12 + .../ToolPackageObtainer/IProjectRestorer.cs | 15 ++ .../PackageObtainException.cs | 22 ++ .../ToolPackageObtainer/PackageVersion.cs | 28 +++ .../ToolPackageObtainer/ToolConfiguration.cs | 46 ++++ ...ToolConfigurationAndExecutableDirectory.cs | 21 ++ .../DotNetCliTool.cs | 13 ++ .../DotNetCliToolCommand.cs | 21 ++ .../ToolConfigurationDeserializer.cs | 61 +++++ .../ToolConfigurationException.cs | 14 ++ .../ToolPackageObtainer.cs | 190 ++++++++++++++++ .../PackageToProjectFileAdder.cs | 49 +++++ .../dotnet-install/ProjectRestorer.cs | 55 +++++ test/Microsoft.DotNet.Cli.Tests.sln | 73 +++--- .../DotnetToolsConfigGolden.xml | 6 + .../DotnetToolsConfigMalformed.xml | 6 + .../DotnetToolsConfigMissing.xml | 6 + ...ft.DotNet.ToolPackageObtainer.Tests.csproj | 56 +++++ .../SampleGlobalTool/DotnetToolsConfig.xml | 6 + .../SampleGlobalTool/Program.cs | 14 ++ .../SampleGlobalTool/consoledemo.csproj | 7 + .../SampleGlobalTool/includepublish.nuspec | 13 ++ .../ToolConfigurationDeserializerTests.cs | 52 +++++ .../ToolPackageObtainerTests.cs | 208 ++++++++++++++++++ .../NuGetConfig.cs | 18 ++ .../BundledTargetFramworkTests.cs | 39 ++++ test/dotnet.Tests/dotnet.Tests.csproj | 17 ++ 32 files changed, 1169 insertions(+), 29 deletions(-) create mode 100644 src/Microsoft.DotNet.InternalAbstractions/DirectoryPath.cs create mode 100644 src/Microsoft.DotNet.InternalAbstractions/FilePath.cs create mode 100644 src/dotnet/BundledTargetFramework.cs create mode 100644 src/dotnet/ToolPackageObtainer/IPackageToProjectFileAdder.cs create mode 100644 src/dotnet/ToolPackageObtainer/IProjectRestorer.cs create mode 100644 src/dotnet/ToolPackageObtainer/PackageObtainException.cs create mode 100644 src/dotnet/ToolPackageObtainer/PackageVersion.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfiguration.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfigurationAndExecutableDirectory.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliTool.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliToolCommand.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfigurationDeserializer.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolConfigurationException.cs create mode 100644 src/dotnet/ToolPackageObtainer/ToolPackageObtainer.cs create mode 100644 src/dotnet/commands/dotnet-install/PackageToProjectFileAdder.cs create mode 100644 src/dotnet/commands/dotnet-install/ProjectRestorer.cs create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigGolden.xml create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMalformed.xml create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMissing.xml create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/Microsoft.DotNet.ToolPackageObtainer.Tests.csproj create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/DotnetToolsConfig.xml create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/Program.cs create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/consoledemo.csproj create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/includepublish.nuspec create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolConfigurationDeserializerTests.cs create mode 100644 test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolPackageObtainerTests.cs create mode 100644 test/dotnet.Tests/BundledTargetFramworkTests.cs diff --git a/Microsoft.DotNet.Cli.sln b/Microsoft.DotNet.Cli.sln index 9c70a2236..c850fb43d 100644 --- a/Microsoft.DotNet.Cli.sln +++ b/Microsoft.DotNet.Cli.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.0 +VisualStudioVersion = 15.0.27004.2008 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{ED2FE3E2-F7E7-4389-8231-B65123F2076F}" EndProject @@ -230,6 +230,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.TestFramew EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Msbuild.Tests.Utilities", "test\Msbuild.Tests.Utilities\Msbuild.Tests.Utilities.csproj", "{E7C72EF2-8480-48B4-AAE8-A596F1A6048E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ToolPackageObtainer.Tests", "test\Microsoft.DotNet.ToolPackageObtainer.Tests\Microsoft.DotNet.ToolPackageObtainer.Tests.csproj", "{F0D50831-9468-4ACB-8FD8-E9883DD553FB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1614,6 +1616,30 @@ Global {E7C72EF2-8480-48B4-AAE8-A596F1A6048E}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {E7C72EF2-8480-48B4-AAE8-A596F1A6048E}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {E7C72EF2-8480-48B4-AAE8-A596F1A6048E}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|x64.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Debug|x86.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|x86.ActiveCfg = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.MinSizeRel|x86.Build.0 = Debug|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|Any CPU.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|x64.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|x64.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|x86.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.Release|x86.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {F0D50831-9468-4ACB-8FD8-E9883DD553FB}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1686,6 +1712,7 @@ Global {B4EE3671-C103-4A37-8DEB-C74E0134104E} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} {44759218-B558-4AF0-8991-515F1100DCF5} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} {E7C72EF2-8480-48B4-AAE8-A596F1A6048E} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} + {F0D50831-9468-4ACB-8FD8-E9883DD553FB} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B526D2CE-EE2D-4AD4-93EF-1867D90FF1F5} diff --git a/src/Microsoft.DotNet.InternalAbstractions/DirectoryPath.cs b/src/Microsoft.DotNet.InternalAbstractions/DirectoryPath.cs new file mode 100644 index 000000000..40e898d3e --- /dev/null +++ b/src/Microsoft.DotNet.InternalAbstractions/DirectoryPath.cs @@ -0,0 +1,47 @@ +// 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; + +namespace Microsoft.Extensions.EnvironmentAbstractions +{ + public struct DirectoryPath + { + public string Value { get; } + + public DirectoryPath(string value) + { + Value = value; + } + + public DirectoryPath WithSubDirectories(params string[] paths) + { + string[] insertValueInFront = new string[paths.Length + 1]; + insertValueInFront[0] = Value; + Array.Copy(paths, 0, insertValueInFront, 1, paths.Length); + + return new DirectoryPath(Path.Combine(insertValueInFront)); + } + + public FilePath WithFile(string fileName) + { + return new FilePath(Path.Combine(Value, fileName)); + } + + public string ToQuotedString() + { + return $"\"{Value}\""; + } + + public override string ToString() + { + return ToQuotedString(); + } + + public DirectoryPath GetParentPath() + { + return new DirectoryPath(Directory.GetParent(Path.GetFullPath(Value)).FullName); + } + } +} diff --git a/src/Microsoft.DotNet.InternalAbstractions/FilePath.cs b/src/Microsoft.DotNet.InternalAbstractions/FilePath.cs new file mode 100644 index 000000000..9dbbf8c83 --- /dev/null +++ b/src/Microsoft.DotNet.InternalAbstractions/FilePath.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.Extensions.EnvironmentAbstractions +{ + public struct FilePath + { + public string Value { get; } + + public FilePath(string value) + { + Value = value; + } + + public string ToQuotedString() + { + return $"\"{Value}\""; + } + + public override string ToString() + { + return ToQuotedString(); + } + + public DirectoryPath GetDirectoryPath() + { + return new DirectoryPath(Path.GetDirectoryName(Value)); + } + } +} diff --git a/src/dotnet/BundledTargetFramework.cs b/src/dotnet/BundledTargetFramework.cs new file mode 100644 index 000000000..e819a613a --- /dev/null +++ b/src/dotnet/BundledTargetFramework.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.Versioning; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli +{ + internal static class BundledTargetFramework + { + public static string GetTargetFrameworkMoniker() + { + TargetFrameworkAttribute targetFrameworkAttribute = typeof(BundledTargetFramework) + .GetTypeInfo() + .Assembly + .GetCustomAttribute(); + + return NuGetFramework + .Parse(targetFrameworkAttribute.FrameworkName) + .GetShortFolderName(); + } + } +} diff --git a/src/dotnet/Properties/AssemblyInfo.cs b/src/dotnet/Properties/AssemblyInfo.cs index d7ae131d6..ede900dd4 100644 --- a/src/dotnet/Properties/AssemblyInfo.cs +++ b/src/dotnet/Properties/AssemblyInfo.cs @@ -17,3 +17,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("dotnet-sln-remove.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("dotnet-msbuild.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("dotnet-run.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.ToolPackageObtainer.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/dotnet/ToolPackageObtainer/IPackageToProjectFileAdder.cs b/src/dotnet/ToolPackageObtainer/IPackageToProjectFileAdder.cs new file mode 100644 index 000000000..00a8d2e66 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/IPackageToProjectFileAdder.cs @@ -0,0 +1,12 @@ +// 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 Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal interface IPackageToProjectFileAdder + { + void Add(FilePath projectPath, string packageId); + } +} diff --git a/src/dotnet/ToolPackageObtainer/IProjectRestorer.cs b/src/dotnet/ToolPackageObtainer/IProjectRestorer.cs new file mode 100644 index 000000000..d53898427 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/IProjectRestorer.cs @@ -0,0 +1,15 @@ +// 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 Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal interface IProjectRestorer + { + void Restore( + FilePath projectPath, + DirectoryPath assetJsonOutput, + FilePath? nugetconfig); + } +} diff --git a/src/dotnet/ToolPackageObtainer/PackageObtainException.cs b/src/dotnet/ToolPackageObtainer/PackageObtainException.cs new file mode 100644 index 000000000..db15e8b1b --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/PackageObtainException.cs @@ -0,0 +1,22 @@ +// 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; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class PackageObtainException : Exception + { + public PackageObtainException() + { + } + + public PackageObtainException(string message) : base(message) + { + } + + public PackageObtainException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/src/dotnet/ToolPackageObtainer/PackageVersion.cs b/src/dotnet/ToolPackageObtainer/PackageVersion.cs new file mode 100644 index 000000000..da7d6aa3d --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/PackageVersion.cs @@ -0,0 +1,28 @@ +// 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; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class PackageVersion + { + public PackageVersion(string packageVersion) + { + if (packageVersion == null) + { + Value = Path.GetRandomFileName(); + IsPlaceholder = true; + } + else + { + Value = packageVersion; + IsPlaceholder = false; + } + } + + public bool IsPlaceholder { get; } + public string Value { get; } + public bool IsConcreteValue => !IsPlaceholder; + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfiguration.cs b/src/dotnet/ToolPackageObtainer/ToolConfiguration.cs new file mode 100644 index 000000000..1b0a32cb3 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfiguration.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class ToolConfiguration + { + public ToolConfiguration( + string commandName, + string toolAssemblyEntryPoint) + { + if (string.IsNullOrWhiteSpace(commandName)) + { + throw new ArgumentNullException(nameof(commandName), "Cannot be null or whitespace"); + } + + EnsureNoInvalidFilenameCharacters(commandName, nameof(toolAssemblyEntryPoint)); + + if (string.IsNullOrWhiteSpace(toolAssemblyEntryPoint)) + { + throw new ArgumentNullException(nameof(toolAssemblyEntryPoint), "Cannot be null or whitespace"); + } + + CommandName = commandName; + ToolAssemblyEntryPoint = toolAssemblyEntryPoint; + } + + private void EnsureNoInvalidFilenameCharacters(string commandName, string nameOfParam) + { + // https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names + char[] invalidCharactors = {'/', '<', '>', ':', '"', '/', '\\', '|', '?', '*'}; + if (commandName.IndexOfAny(invalidCharactors) != -1) + { + throw new ArgumentException( + paramName: nameof(nameOfParam), + message: "Cannot contain following character " + new string(invalidCharactors)); + } + } + + + public string CommandName { get; } + public string ToolAssemblyEntryPoint { get; } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfigurationAndExecutableDirectory.cs b/src/dotnet/ToolPackageObtainer/ToolConfigurationAndExecutableDirectory.cs new file mode 100644 index 000000000..4dc6a220d --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfigurationAndExecutableDirectory.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class ToolConfigurationAndExecutableDirectory + { + public ToolConfigurationAndExecutableDirectory( + ToolConfiguration toolConfiguration, + DirectoryPath executableDirectory) + { + Configuration = toolConfiguration; + ExecutableDirectory = executableDirectory; + } + + public ToolConfiguration Configuration { get; } + public DirectoryPath ExecutableDirectory { get; } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliTool.cs b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliTool.cs new file mode 100644 index 000000000..b64db01e2 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliTool.cs @@ -0,0 +1,13 @@ +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Microsoft.DotNet.ToolPackageObtainer.ToolConfigurationDeserialization +{ + [DebuggerStepThrough] + [XmlRoot(Namespace = "", IsNullable = false)] + public class DotNetCliTool + { + [XmlArrayItem("Command", IsNullable = false)] + public DotNetCliToolCommand[] Commands { get; set; } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliToolCommand.cs b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliToolCommand.cs new file mode 100644 index 000000000..159f6e993 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserialization/DotNetCliToolCommand.cs @@ -0,0 +1,21 @@ +using System; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Microsoft.DotNet.ToolPackageObtainer.ToolConfigurationDeserialization +{ + [Serializable] + [DebuggerStepThrough] + [XmlType(AnonymousType = true)] + public class DotNetCliToolCommand + { + [XmlAttribute] + public string Name { get; set; } + + [XmlAttribute] + public string EntryPoint { get; set; } + + [XmlAttribute] + public string Runner { get; set; } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserializer.cs b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserializer.cs new file mode 100644 index 000000000..534cbf50c --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfigurationDeserializer.cs @@ -0,0 +1,61 @@ +// 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.Xml; +using System.Xml.Serialization; +using Microsoft.DotNet.ToolPackageObtainer.ToolConfigurationDeserialization; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal static class ToolConfigurationDeserializer + { + public static ToolConfiguration Deserialize(string pathToXml) + { + var serializer = new XmlSerializer(typeof(DotNetCliTool)); + + DotNetCliTool dotNetCliTool; + + using (var fs = new FileStream(pathToXml, FileMode.Open)) + { + var reader = XmlReader.Create(fs); + + try + { + dotNetCliTool = (DotNetCliTool)serializer.Deserialize(reader); + } + catch (InvalidOperationException e) when (e.InnerException is XmlException) + { + throw new ToolConfigurationException( + "Failed to retrive tool configuration exception, configuration is malformed xml. " + + e.InnerException.Message); + } + } + + if (dotNetCliTool.Commands.Length != 1) + { + throw new ToolConfigurationException( + "Failed to retrive tool configuration exception, one and only one command is supported."); + } + + if (dotNetCliTool.Commands[0].Runner != "dotnet") + { + throw new ToolConfigurationException( + "Failed to retrive tool configuration exception, only dotnet as runner is supported."); + } + + var commandName = dotNetCliTool.Commands[0].Name; + var toolAssemblyEntryPoint = dotNetCliTool.Commands[0].EntryPoint; + + try + { + return new ToolConfiguration(commandName, toolAssemblyEntryPoint); + } + catch (ArgumentException e) + { + throw new ToolConfigurationException("Configuration content error. " + e.Message); + } + } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolConfigurationException.cs b/src/dotnet/ToolPackageObtainer/ToolConfigurationException.cs new file mode 100644 index 000000000..074010aa0 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolConfigurationException.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class ToolConfigurationException : ArgumentException + { + public ToolConfigurationException(string message) : base(message) + { + } + } +} diff --git a/src/dotnet/ToolPackageObtainer/ToolPackageObtainer.cs b/src/dotnet/ToolPackageObtainer/ToolPackageObtainer.cs new file mode 100644 index 000000000..faf4ed461 --- /dev/null +++ b/src/dotnet/ToolPackageObtainer/ToolPackageObtainer.cs @@ -0,0 +1,190 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ToolPackageObtainer +{ + internal class ToolPackageObtainer + { + private readonly Lazy _bundledTargetFrameworkMoniker; + private readonly Func _getTempProjectPath; + private readonly IPackageToProjectFileAdder _packageToProjectFileAdder; + private readonly IProjectRestorer _projectRestorer; + private readonly DirectoryPath _toolsPath; + + public ToolPackageObtainer( + DirectoryPath toolsPath, + Func getTempProjectPath, + Lazy bundledTargetFrameworkMoniker, + IPackageToProjectFileAdder packageToProjectFileAdder, + IProjectRestorer projectRestorer + ) + { + _getTempProjectPath = getTempProjectPath; + _bundledTargetFrameworkMoniker = bundledTargetFrameworkMoniker; + _projectRestorer = projectRestorer ?? throw new ArgumentNullException(nameof(projectRestorer)); + _packageToProjectFileAdder = packageToProjectFileAdder ?? + throw new ArgumentNullException(nameof(packageToProjectFileAdder)); + _toolsPath = toolsPath; + } + + public ToolConfigurationAndExecutableDirectory ObtainAndReturnExecutablePath( + string packageId, + string packageVersion = null, + FilePath? nugetconfig = null, + string targetframework = null) + { + if (packageId == null) + { + throw new ArgumentNullException(nameof(packageId)); + } + + if (targetframework == null) + { + targetframework = _bundledTargetFrameworkMoniker.Value; + } + + var packageVersionOrPlaceHolder = new PackageVersion(packageVersion); + + DirectoryPath individualToolVersion = + CreateIndividualToolVersionDirectory(packageId, packageVersionOrPlaceHolder); + + FilePath tempProjectPath = CreateTempProject( + packageId, + packageVersionOrPlaceHolder, + targetframework, + individualToolVersion); + + if (packageVersionOrPlaceHolder.IsPlaceholder) + { + InvokeAddPackageRestore( + nugetconfig, + tempProjectPath, + packageId); + } + + InvokeRestore(nugetconfig, tempProjectPath, individualToolVersion); + + if (packageVersionOrPlaceHolder.IsPlaceholder) + { + var concreteVersion = + new DirectoryInfo( + Directory.GetDirectories( + individualToolVersion.WithSubDirectories(packageId).Value).Single()).Name; + DirectoryPath concreteVersionIndividualToolVersion = + individualToolVersion.GetParentPath().WithSubDirectories(concreteVersion); + Directory.Move(individualToolVersion.Value, concreteVersionIndividualToolVersion.Value); + + individualToolVersion = concreteVersionIndividualToolVersion; + packageVersion = concreteVersion; + } + + ToolConfiguration toolConfiguration = GetConfiguration(packageId, packageVersion, individualToolVersion); + + return new ToolConfigurationAndExecutableDirectory( + toolConfiguration, + individualToolVersion.WithSubDirectories( + packageId, + packageVersion, + "tools", + targetframework)); + } + + private static ToolConfiguration GetConfiguration( + string packageId, + string packageVersion, + DirectoryPath individualToolVersion) + { + FilePath toolConfigurationPath = + individualToolVersion + .WithSubDirectories(packageId, packageVersion, "tools") + .WithFile("DotnetToolsConfig.xml"); + + ToolConfiguration toolConfiguration = + ToolConfigurationDeserializer.Deserialize(toolConfigurationPath.Value); + + return toolConfiguration; + } + + private void InvokeRestore( + FilePath? nugetconfig, + FilePath tempProjectPath, + DirectoryPath individualToolVersion) + { + _projectRestorer.Restore(tempProjectPath, individualToolVersion, nugetconfig); + } + + private FilePath CreateTempProject( + string packageId, + PackageVersion packageVersion, + string targetframework, + DirectoryPath individualToolVersion) + { + FilePath tempProjectPath = _getTempProjectPath(); + if (Path.GetExtension(tempProjectPath.Value) != "csproj") + { + tempProjectPath = new FilePath(Path.ChangeExtension(tempProjectPath.Value, "csproj")); + } + + EnsureDirectoryExists(tempProjectPath.GetDirectoryPath()); + var tempProjectContent = new XDocument( + new XElement("Project", + new XAttribute("Sdk", "Microsoft.NET.Sdk"), + new XElement("PropertyGroup", + new XElement("TargetFramework", targetframework), + new XElement("RestorePackagesPath", individualToolVersion.Value), + new XElement("DisableImplicitFrameworkReferences", "true") + ), + packageVersion.IsConcreteValue + ? new XElement("ItemGroup", + new XElement("PackageReference", + new XAttribute("Include", packageId), + new XAttribute("Version", packageVersion.Value) + )) + : null)); + + File.WriteAllText(tempProjectPath.Value, + tempProjectContent.ToString()); + + return tempProjectPath; + } + + private void InvokeAddPackageRestore( + FilePath? nugetconfig, + FilePath tempProjectPath, + string packageId) + { + if (nugetconfig != null) + { + File.Copy( + nugetconfig.Value.Value, + tempProjectPath + .GetDirectoryPath() + .WithFile("nuget.config") + .Value); + } + + _packageToProjectFileAdder.Add(tempProjectPath, packageId); + } + + private DirectoryPath CreateIndividualToolVersionDirectory( + string packageId, + PackageVersion packageVersion) + { + DirectoryPath individualTool = _toolsPath.WithSubDirectories(packageId); + DirectoryPath individualToolVersion = individualTool.WithSubDirectories(packageVersion.Value); + EnsureDirectoryExists(individualToolVersion); + return individualToolVersion; + } + + private static void EnsureDirectoryExists(DirectoryPath path) + { + if (!Directory.Exists(path.Value)) + { + Directory.CreateDirectory(path.Value); + } + } + } +} diff --git a/src/dotnet/commands/dotnet-install/PackageToProjectFileAdder.cs b/src/dotnet/commands/dotnet-install/PackageToProjectFileAdder.cs new file mode 100644 index 000000000..21de01ddb --- /dev/null +++ b/src/dotnet/commands/dotnet-install/PackageToProjectFileAdder.cs @@ -0,0 +1,49 @@ +// 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.Collections.Generic; +using Microsoft.DotNet.ToolPackageObtainer; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.Cli +{ + + internal class PackageToProjectFileAdder : IPackageToProjectFileAdder + { + public void Add(FilePath projectPath, string packageId) + { + if (packageId == null) + { + throw new ArgumentNullException(nameof(packageId)); + } + + var argsToPassToRestore = new List + { + projectPath.Value, + "package", + packageId, + "--no-restore" + }; + + var command = new DotNetCommandFactory(alwaysRunOutOfProc: true) + .Create( + "add", + argsToPassToRestore) + .CaptureStdOut() + .CaptureStdErr(); + + var result = command.Execute(); + if (result.ExitCode != 0) + { + throw new PackageObtainException("Failed to add package. " + + $"{Environment.NewLine}WorkingDirectory: " + + result.StartInfo.WorkingDirectory + + $"{Environment.NewLine}Arguments: " + + result.StartInfo.Arguments + + $"{Environment.NewLine}Output: " + + result.StdErr + result.StdOut); + } + } + } +} diff --git a/src/dotnet/commands/dotnet-install/ProjectRestorer.cs b/src/dotnet/commands/dotnet-install/ProjectRestorer.cs new file mode 100644 index 000000000..a9717ed69 --- /dev/null +++ b/src/dotnet/commands/dotnet-install/ProjectRestorer.cs @@ -0,0 +1,55 @@ +// 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.Collections.Generic; +using Microsoft.DotNet.ToolPackageObtainer; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.Cli +{ + internal class ProjectRestorer : IProjectRestorer + { + public void Restore( + FilePath projectPath, + DirectoryPath assetJsonOutput, + FilePath? nugetconfig) + { + var argsToPassToRestore = new List(); + + argsToPassToRestore.Add(projectPath.ToQuotedString()); + if (nugetconfig != null) + { + argsToPassToRestore.Add("--configfile"); + argsToPassToRestore.Add(nugetconfig.Value.ToQuotedString()); + } + + argsToPassToRestore.AddRange(new List + { + "--runtime", + RuntimeEnvironment.GetRuntimeIdentifier(), + $"/p:BaseIntermediateOutputPath={assetJsonOutput.ToQuotedString()}" + }); + + var command = new DotNetCommandFactory(alwaysRunOutOfProc: true) + .Create( + "restore", + argsToPassToRestore) + .CaptureStdOut() + .CaptureStdErr(); + + var result = command.Execute(); + if (result.ExitCode != 0) + { + throw new PackageObtainException("Failed to restore package. " + + $"{Environment.NewLine}WorkingDirectory: " + + result.StartInfo.WorkingDirectory + + $"{Environment.NewLine}Arguments: " + + result.StartInfo.Arguments + + $"{Environment.NewLine}Output: " + + result.StdErr + result.StdOut); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Tests.sln b/test/Microsoft.DotNet.Cli.Tests.sln index fd8afc849..9140c1153 100644 --- a/test/Microsoft.DotNet.Cli.Tests.sln +++ b/test/Microsoft.DotNet.Cli.Tests.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26419.0 +VisualStudioVersion = 15.0.27004.2008 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 @@ -74,13 +74,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-store.Tests", "dotne EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-back-compat.Tests", "dotnet-back-compat.Tests\dotnet-back-compat.Tests.csproj", "{27351B2F-325B-4843-9F4C-BC53FD06A7B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-remove-package.Tests", "dotnet-remove-package.Tests\dotnet-remove-package.Tests.csproj", "{CF517B15-B307-4660-87D5-CC036ADECD4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-remove-package.Tests", "dotnet-remove-package.Tests\dotnet-remove-package.Tests.csproj", "{CF517B15-B307-4660-87D5-CC036ADECD4B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.MSBuildSdkResolver.Tests", "Microsoft.DotNet.MSBuildSdkResolver.Tests\Microsoft.DotNet.MSBuildSdkResolver.Tests.csproj", "{42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet-clean.Tests", "dotnet-clean.Tests\dotnet-clean.Tests.csproj", "{D9A582B8-1FE2-45D5-B139-0BA828FE3691}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-clean.Tests", "dotnet-clean.Tests\dotnet-clean.Tests.csproj", "{D9A582B8-1FE2-45D5-B139-0BA828FE3691}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.DotNet.TestFramework", "Microsoft.DotNet.TestFramework\Microsoft.DotNet.TestFramework.csproj", "{D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.TestFramework", "Microsoft.DotNet.TestFramework\Microsoft.DotNet.TestFramework.csproj", "{D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.DotNet.ToolPackageObtainer.Tests", "Microsoft.DotNet.ToolPackageObtainer.Tests\Microsoft.DotNet.ToolPackageObtainer.Tests.csproj", "{C2A907A3-677B-4C73-9AA4-E53613E13C78}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -490,16 +492,16 @@ Global {27351B2F-325B-4843-9F4C-BC53FD06A7B5}.Release|x86.Build.0 = Release|Any CPU {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x64.ActiveCfg = Debug|x64 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x64.Build.0 = Debug|x64 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x86.ActiveCfg = Debug|x86 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x86.Build.0 = Debug|x86 + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x64.Build.0 = Debug|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x86.ActiveCfg = Debug|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Debug|x86.Build.0 = Debug|Any CPU {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|Any CPU.ActiveCfg = Release|Any CPU {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|Any CPU.Build.0 = Release|Any CPU - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x64.ActiveCfg = Release|x64 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x64.Build.0 = Release|x64 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x86.ActiveCfg = Release|x86 - {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x86.Build.0 = Release|x86 + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x64.ActiveCfg = Release|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x64.Build.0 = Release|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x86.ActiveCfg = Release|Any CPU + {CF517B15-B307-4660-87D5-CC036ADECD4B}.Release|x86.Build.0 = Release|Any CPU {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|Any CPU.Build.0 = Debug|Any CPU {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -514,28 +516,40 @@ Global {42A0CAB4-FFAD-47D4-9880-C0F4EDCF93DE}.Release|x86.Build.0 = Release|Any CPU {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x64.ActiveCfg = Debug|x64 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x64.Build.0 = Debug|x64 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x86.ActiveCfg = Debug|x86 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x86.Build.0 = Debug|x86 + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x64.Build.0 = Debug|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Debug|x86.Build.0 = Debug|Any CPU {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|Any CPU.Build.0 = Release|Any CPU - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x64.ActiveCfg = Release|x64 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x64.Build.0 = Release|x64 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x86.ActiveCfg = Release|x86 - {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x86.Build.0 = Release|x86 + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x64.ActiveCfg = Release|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x64.Build.0 = Release|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x86.ActiveCfg = Release|Any CPU + {D9A582B8-1FE2-45D5-B139-0BA828FE3691}.Release|x86.Build.0 = Release|Any CPU {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x64.ActiveCfg = Debug|x64 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x64.Build.0 = Debug|x64 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x86.ActiveCfg = Debug|x86 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x86.Build.0 = Debug|x86 + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x64.Build.0 = Debug|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Debug|x86.Build.0 = Debug|Any CPU {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|Any CPU.ActiveCfg = Release|Any CPU {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|Any CPU.Build.0 = Release|Any CPU - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x64.ActiveCfg = Release|x64 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x64.Build.0 = Release|x64 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x86.ActiveCfg = Release|x86 - {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x86.Build.0 = Release|x86 + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x64.ActiveCfg = Release|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x64.Build.0 = Release|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x86.ActiveCfg = Release|Any CPU + {D23AFC9C-8FD5-45DD-A84C-0E6528C42F0E}.Release|x86.Build.0 = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|x64.Build.0 = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Debug|x86.Build.0 = Debug|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|Any CPU.Build.0 = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|x64.ActiveCfg = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|x64.Build.0 = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|x86.ActiveCfg = Release|Any CPU + {C2A907A3-677B-4C73-9AA4-E53613E13C78}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,4 +559,7 @@ Global {5767D8F0-4ED9-4083-8BDC-ED9E65AA86EF} = {15DDC326-69C3-4081-8AA1-B578B2BDE2C6} {92BA9F90-E25B-4A1C-9598-2295D3DFC12F} = {BB393A93-1770-4753-B7D6-56F0DD378177} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7AF6777A-0133-453A-B302-FDD974B8F39C} + EndGlobalSection EndGlobal diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigGolden.xml b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigGolden.xml new file mode 100644 index 000000000..cd4f20107 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigGolden.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMalformed.xml b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMalformed.xml new file mode 100644 index 000000000..0aa8173ad --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMalformed.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMissing.xml b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMissing.xml new file mode 100644 index 000000000..87134da29 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/DotnetToolsConfigMissing.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/Microsoft.DotNet.ToolPackageObtainer.Tests.csproj b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/Microsoft.DotNet.ToolPackageObtainer.Tests.csproj new file mode 100644 index 000000000..cf8b5f9cd --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/Microsoft.DotNet.ToolPackageObtainer.Tests.csproj @@ -0,0 +1,56 @@ + + + $(CliTargetFramework) + $(CLI_SharedFrameworkVersion) + true + ../../tools/Key.snk + true + true + $(AssetTargetFallback);dotnet5.4;portable-net451+win8 + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + $(BaseOutputPath)/TestAsset/SampleGlobalTool + + + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/DotnetToolsConfig.xml b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/DotnetToolsConfig.xml new file mode 100644 index 000000000..b9c18a1e7 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/DotnetToolsConfig.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/Program.cs b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/Program.cs new file mode 100644 index 000000000..1140102df --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/Program.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; + +namespace consoledemo +{ + class Program + { + static void Main(string[] args) + { + var greeting = "Hello World from Global Tool"; + Console.WriteLine(greeting); + } + } +} diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/consoledemo.csproj b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/consoledemo.csproj new file mode 100644 index 000000000..4e16cac58 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/consoledemo.csproj @@ -0,0 +1,7 @@ + + + Exe + netcoreapp2.1 + consoledemo + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/includepublish.nuspec b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/includepublish.nuspec new file mode 100644 index 000000000..89686425e --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/SampleGlobalTool/includepublish.nuspec @@ -0,0 +1,13 @@ + + + + global.tool.console.demo + 1.0.4 + test app + testauthor + + + + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolConfigurationDeserializerTests.cs b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolConfigurationDeserializerTests.cs new file mode 100644 index 000000000..4c81f787a --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolConfigurationDeserializerTests.cs @@ -0,0 +1,52 @@ +// 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.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using NuGet.Protocol.Core.Types; +using Xunit; + +namespace Microsoft.DotNet.ToolPackageObtainer.Tests +{ + public class ToolConfigurationDeserializerTests + { + [Fact] + public void GivenXmlPathItShouldGetToolConfiguration() + { + ToolConfiguration toolConfiguration = ToolConfigurationDeserializer.Deserialize("DotnetToolsConfigGolden.xml"); + + toolConfiguration.CommandName.Should().Be("sayhello"); + toolConfiguration.ToolAssemblyEntryPoint.Should().Be("console.dll"); + } + + [Fact] + public void GivenMalformedPathItThrows() + { + Action a = () => ToolConfigurationDeserializer.Deserialize("DotnetToolsConfigMalformed.xml"); + a.ShouldThrow() + .And.Message.Should() + .Contain("Failed to retrive tool configuration exception, configuration is malformed xml"); + } + + [Fact] + public void GivenMissingContentItThrows() + { + Action a = () => ToolConfigurationDeserializer.Deserialize("DotnetToolsConfigMissing.xml"); + a.ShouldThrow() + .And.Message.Should() + .Contain("Configuration content error"); + } + + [Fact] + public void GivenInvalidCharAsFileNameItThrows() + { + Action a = () => new ToolConfiguration("na***me", "my.dll"); + a.ShouldThrow() + .And.Message.Should() + .Contain("Cannot contain following character"); + } + } +} diff --git a/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolPackageObtainerTests.cs b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolPackageObtainerTests.cs new file mode 100644 index 000000000..01606f251 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackageObtainer.Tests/ToolPackageObtainerTests.cs @@ -0,0 +1,208 @@ +// 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 FluentAssertions; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.Extensions.EnvironmentAbstractions; +using Microsoft.DotNet.Cli; +using Xunit; + +namespace Microsoft.DotNet.ToolPackageObtainer.Tests +{ + public class ToolPackageObtainerTests : TestBase + { + [Fact] + public void GivenNugetConfigAndPackageNameAndVersionAndTargetFrameworkWhenCallItCanDownloadThePackage() + { + FilePath nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); + var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); + + var packageObtainer = + ConstructDefaultPackageObtainer(toolsPath); + ToolConfigurationAndExecutableDirectory toolConfigurationAndExecutableDirectory = packageObtainer.ObtainAndReturnExecutablePath( + packageId: TestPackageId, + packageVersion: TestPackageVersion, + nugetconfig: nugetConfigPath, + targetframework: _testTargetframework); + + var executable = toolConfigurationAndExecutableDirectory + .ExecutableDirectory + .WithFile( + toolConfigurationAndExecutableDirectory + .Configuration + .ToolAssemblyEntryPoint); + + File.Exists(executable.Value) + .Should() + .BeTrue(executable + " should have the executable"); + } + + [Fact] + public void GivenNugetConfigAndPackageNameAndVersionAndTargetFrameworkWhenCallItCreateAssetFile() + { + var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); + var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); + + var packageObtainer = + ConstructDefaultPackageObtainer(toolsPath); + ToolConfigurationAndExecutableDirectory toolConfigurationAndExecutableDirectory = + packageObtainer.ObtainAndReturnExecutablePath( + packageId: TestPackageId, + packageVersion: TestPackageVersion, + nugetconfig: nugetConfigPath, + targetframework: _testTargetframework); + + var assetJsonPath = toolConfigurationAndExecutableDirectory + .ExecutableDirectory + .GetParentPath() + .GetParentPath() + .GetParentPath() + .GetParentPath() + .WithFile("project.assets.json").Value; + + File.Exists(assetJsonPath) + .Should() + .BeTrue(assetJsonPath + " should be created"); + } + + [Fact] + public void GivenAllButNoNugetConfigFilePathItCanDownloadThePackage() + { + var uniqueTempProjectPath = GetUniqueTempProjectPathEachTest(); + var tempProjectDirectory = uniqueTempProjectPath.GetDirectoryPath(); + var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); + var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); + + Directory.CreateDirectory(tempProjectDirectory.Value); + + /* + * No nuget config means you don't need nuget config passed in during call + * NuGet needs a way to find the package, in production, it will keep look up folders for Nuget.Config + * and use the feed there. + * In test, we don't want NuGet to keep look up, so we just copy paste beside the project. + */ + File.Copy(nugetConfigPath.Value, + tempProjectDirectory.WithFile("nuget.config").Value); + + var packageObtainer = + new ToolPackageObtainer( + new DirectoryPath(toolsPath), + () => uniqueTempProjectPath, + new Lazy(), + new PackageToProjectFileAdder(), + new ProjectRestorer()); + ToolConfigurationAndExecutableDirectory toolConfigurationAndExecutableDirectory = packageObtainer.ObtainAndReturnExecutablePath( + packageId: TestPackageId, + packageVersion: TestPackageVersion, + targetframework: _testTargetframework); + + var executable = toolConfigurationAndExecutableDirectory + .ExecutableDirectory + .WithFile( + toolConfigurationAndExecutableDirectory + .Configuration + .ToolAssemblyEntryPoint); + + File.Exists(executable.Value) + .Should() + .BeTrue(executable + " should have the executable"); + } + + [Fact] + public void GivenAllButNoPackageVersionItCanDownloadThePackage() + { + var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); + var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); + + var packageObtainer = + ConstructDefaultPackageObtainer(toolsPath); + ToolConfigurationAndExecutableDirectory toolConfigurationAndExecutableDirectory = packageObtainer.ObtainAndReturnExecutablePath( + packageId: TestPackageId, + nugetconfig: nugetConfigPath, + targetframework: _testTargetframework); + + var executable = toolConfigurationAndExecutableDirectory + .ExecutableDirectory + .WithFile( + toolConfigurationAndExecutableDirectory + .Configuration + .ToolAssemblyEntryPoint); + + File.Exists(executable.Value) + .Should() + .BeTrue(executable + " should have the executable"); + } + + [Fact] + public void GivenAllButNoTargetFrameworkItCanDownloadThePackage() + { + var nugetConfigPath = WriteNugetConfigFileToPointToTheFeed(); + var toolsPath = Path.Combine(Directory.GetCurrentDirectory(), Path.GetRandomFileName()); + + var packageObtainer = + new ToolPackageObtainer( + new DirectoryPath(toolsPath), + GetUniqueTempProjectPathEachTest, + new Lazy(() => BundledTargetFramework.GetTargetFrameworkMoniker()), + new PackageToProjectFileAdder(), + new ProjectRestorer()); + ToolConfigurationAndExecutableDirectory toolConfigurationAndExecutableDirectory = + packageObtainer.ObtainAndReturnExecutablePath( + packageId: TestPackageId, + packageVersion: TestPackageVersion, + nugetconfig: nugetConfigPath); + + var executable = toolConfigurationAndExecutableDirectory + .ExecutableDirectory + .WithFile( + toolConfigurationAndExecutableDirectory + .Configuration + .ToolAssemblyEntryPoint); + + File.Exists(executable.Value) + .Should() + .BeTrue(executable + " should have the executable"); + } + + private static readonly Func GetUniqueTempProjectPathEachTest = () => + { + var tempProjectDirectory = + new DirectoryPath(Path.GetTempPath()).WithSubDirectories(Path.GetRandomFileName()); + var tempProjectPath = + tempProjectDirectory.WithFile(Path.GetRandomFileName() + ".csproj"); + return tempProjectPath; + }; + + private static ToolPackageObtainer ConstructDefaultPackageObtainer(string toolsPath) + { + return new ToolPackageObtainer( + new DirectoryPath(toolsPath), + GetUniqueTempProjectPathEachTest, + new Lazy(), + new PackageToProjectFileAdder(), + new ProjectRestorer()); + } + + private static FilePath WriteNugetConfigFileToPointToTheFeed() + { + var nugetConfigName = Path.GetRandomFileName() + ".config"; + var executeDirectory = + Path.GetDirectoryName( + System.Reflection + .Assembly + .GetExecutingAssembly() + .Location); + NuGetConfig.Write( + directory: executeDirectory, + configname: nugetConfigName, + localFeedPath: Path.Combine(executeDirectory, "TestAssetLocalNugetFeed")); + return new FilePath(Path.GetFullPath(nugetConfigName)); + } + + private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker(); + private const string TestPackageVersion = "1.0.4"; + private const string TestPackageId = "global.tool.console.demo"; + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs index f6f05bd51..356eab384 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs @@ -23,5 +23,23 @@ namespace Microsoft.DotNet.Tools.Test.Utilities File.WriteAllText(path, contents); } + + public static void Write(string directory, string configname, string localFeedPath) + { + const string template = @" + + + + + + + + +"; + + var path = Path.Combine(directory, configname); + + File.WriteAllText(path, string.Format(template, localFeedPath)); + } } } diff --git a/test/dotnet.Tests/BundledTargetFramworkTests.cs b/test/dotnet.Tests/BundledTargetFramworkTests.cs new file mode 100644 index 000000000..4f67d03fa --- /dev/null +++ b/test/dotnet.Tests/BundledTargetFramworkTests.cs @@ -0,0 +1,39 @@ +// 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 Microsoft.DotNet.Cli; +using Microsoft.DotNet.Tools.Test.Utilities; +using Xunit; +using FluentAssertions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Tests +{ + public class BundledTargetFrameworkTests : TestBase + { + [Fact] + public void VersionCommandDisplaysCorrectVersion() + { + var filePath = Path.Combine( + AppContext.BaseDirectory, + "ExpectedTargetFrameworkMoniker.txt"); + var targetFrameworkMoniker = GetTargetFrameworkMonikerFromFile(filePath); + var shortFolderName = NuGetFramework + .Parse(targetFrameworkMoniker) + .GetShortFolderName(); + BundledTargetFramework + .GetTargetFrameworkMoniker() + .Should().Be(shortFolderName); + } + + private static string GetTargetFrameworkMonikerFromFile(string versionFilePath) + { + using (var reader = new StreamReader(File.OpenRead(versionFilePath))) + { + return reader.ReadLine(); + } + } + } +} diff --git a/test/dotnet.Tests/dotnet.Tests.csproj b/test/dotnet.Tests/dotnet.Tests.csproj index 66872e208..35c0fa458 100644 --- a/test/dotnet.Tests/dotnet.Tests.csproj +++ b/test/dotnet.Tests/dotnet.Tests.csproj @@ -52,6 +52,23 @@ + + + $(IntermediateOutputPath)ExpectedTargetFrameworkMoniker.txt + $(TargetFrameworkMoniker) + + $([System.IO.File]::ReadAllText($(ExpectedTargetFrameworkMonikerFileInIntermediateFolder))) + + false + true + + + + + + + +