diff --git a/src/Microsoft.DotNet.Cli.Utils/CommandPermission.cs b/src/Microsoft.DotNet.Cli.Utils/CommandPermission.cs new file mode 100644 index 000000000..d26d2f640 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/CommandPermission.cs @@ -0,0 +1,29 @@ +// 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.Runtime.InteropServices; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal class FilePermissionSetter : IFilePermissionSetter + { + public void SetUserExecutionPermission(string path) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return; + } + + CommandResult result = new CommandFactory() + .Create("chmod", new[] { "u+x", path }) + .CaptureStdOut() + .CaptureStdErr() + .Execute(); + + if (result.ExitCode != 0) + { + throw new FilePermissionSettingException(result.StdErr); + } + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/FilePermissionSettingException.cs b/src/Microsoft.DotNet.Cli.Utils/FilePermissionSettingException.cs new file mode 100644 index 000000000..b7aaaaba8 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/FilePermissionSettingException.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; +using System.Runtime.Serialization; + +namespace Microsoft.DotNet.Cli.Utils +{ + [Serializable] + internal class FilePermissionSettingException : Exception + { + public FilePermissionSettingException() + { + } + + public FilePermissionSettingException(string message) : base(message) + { + } + + public FilePermissionSettingException(string message, Exception innerException) : base(message, innerException) + { + } + + protected FilePermissionSettingException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Microsoft.DotNet.Cli.Utils/IFilePermissionSetter.cs b/src/Microsoft.DotNet.Cli.Utils/IFilePermissionSetter.cs new file mode 100644 index 000000000..9753fc232 --- /dev/null +++ b/src/Microsoft.DotNet.Cli.Utils/IFilePermissionSetter.cs @@ -0,0 +1,7 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + internal interface IFilePermissionSetter + { + void SetUserExecutionPermission(string path); + } +} diff --git a/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs b/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs index 512037f33..c46f8e37a 100644 --- a/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs +++ b/src/Microsoft.DotNet.InternalAbstractions/FileWrapper.cs @@ -51,6 +51,11 @@ namespace Microsoft.Extensions.EnvironmentAbstractions File.Move(source, destination); } + public void Copy(string sourceFileName, string destFileName) + { + File.Copy(sourceFileName, destFileName); + } + public void Delete(string path) { File.Delete(path); diff --git a/src/Microsoft.DotNet.InternalAbstractions/IFile.cs b/src/Microsoft.DotNet.InternalAbstractions/IFile.cs index a04f70dcd..044297b6e 100644 --- a/src/Microsoft.DotNet.InternalAbstractions/IFile.cs +++ b/src/Microsoft.DotNet.InternalAbstractions/IFile.cs @@ -27,6 +27,8 @@ namespace Microsoft.Extensions.EnvironmentAbstractions void Move(string source, string destination); + void Copy(string source, string destination); + void Delete(string path); } } diff --git a/src/dotnet/CommonLocalizableStrings.resx b/src/dotnet/CommonLocalizableStrings.resx index ad89d2c03..f4c170343 100644 --- a/src/dotnet/CommonLocalizableStrings.resx +++ b/src/dotnet/CommonLocalizableStrings.resx @@ -640,4 +640,10 @@ setx PATH "%PATH%;{0}" Format version is missing. This tool may not be supported in this SDK version. Please contact the author of the tool. + + More than one packaged shim is available: {0}. + + + Failed to read NuGet LockFile for tool package '{0}': {1} + diff --git a/src/dotnet/ShellShim/AppHostShimMaker.cs b/src/dotnet/ShellShim/AppHostShimMaker.cs new file mode 100644 index 000000000..9b5eaf2de --- /dev/null +++ b/src/dotnet/ShellShim/AppHostShimMaker.cs @@ -0,0 +1,53 @@ +// 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 System.Runtime.InteropServices; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.PlatformAbstractions; +using Microsoft.DotNet.Tools.Common; +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ShellShim +{ + internal class AppHostShellShimMaker : IAppHostShellShimMaker + { + private const string ApphostNameWithoutExtension = "apphost"; + private readonly string _appHostSourceDirectory; + private readonly IFilePermissionSetter _filePermissionSetter; + + public AppHostShellShimMaker(string appHostSourceDirectory = null, IFilePermissionSetter filePermissionSetter = null) + { + _appHostSourceDirectory = + appHostSourceDirectory + ?? Path.Combine(ApplicationEnvironment.ApplicationBasePath, "AppHostTemplate"); + + _filePermissionSetter = + filePermissionSetter + ?? new FilePermissionSetter(); + } + + public void CreateApphostShellShim(FilePath entryPoint, FilePath shimPath) + { + string appHostSourcePath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + appHostSourcePath = Path.Combine(_appHostSourceDirectory, ApphostNameWithoutExtension + ".exe"); + } + else + { + appHostSourcePath = Path.Combine(_appHostSourceDirectory, ApphostNameWithoutExtension); + } + + var appHostDestinationFilePath = shimPath.Value; + var appBinaryFilePath = PathUtility.GetRelativePath(appHostDestinationFilePath, entryPoint.Value); + + EmbedAppNameInHost.EmbedAndReturnModifiedAppHostPath( + appHostSourceFilePath: appHostSourcePath, + appHostDestinationFilePath: appHostDestinationFilePath, + appBinaryFilePath: appBinaryFilePath); + + _filePermissionSetter.SetUserExecutionPermission(appHostDestinationFilePath); + } + } +} diff --git a/src/dotnet/ShellShim/IApphostShellShimMaker.cs b/src/dotnet/ShellShim/IApphostShellShimMaker.cs new file mode 100644 index 000000000..962e68881 --- /dev/null +++ b/src/dotnet/ShellShim/IApphostShellShimMaker.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.EnvironmentAbstractions; + +namespace Microsoft.DotNet.ShellShim +{ + internal interface IAppHostShellShimMaker + { + void CreateApphostShellShim(FilePath entryPoint, FilePath shimPath); + } +} diff --git a/src/dotnet/ShellShim/IShellShimRepository.cs b/src/dotnet/ShellShim/IShellShimRepository.cs index 737b548df..e8049fa6e 100644 --- a/src/dotnet/ShellShim/IShellShimRepository.cs +++ b/src/dotnet/ShellShim/IShellShimRepository.cs @@ -1,12 +1,13 @@ // 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.Collections.Generic; using Microsoft.Extensions.EnvironmentAbstractions; namespace Microsoft.DotNet.ShellShim { internal interface IShellShimRepository { - void CreateShim(FilePath targetExecutablePath, string commandName); + void CreateShim(FilePath targetExecutablePath, string commandName, IReadOnlyList packagedShims = null); void RemoveShim(string commandName); } diff --git a/src/dotnet/ShellShim/ShellShimRepository.cs b/src/dotnet/ShellShim/ShellShimRepository.cs index a7b890525..121582cf6 100644 --- a/src/dotnet/ShellShim/ShellShimRepository.cs +++ b/src/dotnet/ShellShim/ShellShimRepository.cs @@ -21,16 +21,24 @@ namespace Microsoft.DotNet.ShellShim private const string ApphostNameWithoutExtension = "apphost"; private readonly DirectoryPath _shimsDirectory; - private readonly string _appHostSourceDirectory; + private readonly IFileSystem _fileSystem; + private readonly IAppHostShellShimMaker _appHostShellShimMaker; + private readonly IFilePermissionSetter _filePermissionSetter; - public ShellShimRepository(DirectoryPath shimsDirectory, string appHostSourcePath = null) + public ShellShimRepository( + DirectoryPath shimsDirectory, + string appHostSourceDirectory = null, + IFileSystem fileSystem = null, + IAppHostShellShimMaker appHostShellShimMaker = null, + IFilePermissionSetter filePermissionSetter = null) { _shimsDirectory = shimsDirectory; - _appHostSourceDirectory = appHostSourcePath ?? Path.Combine(ApplicationEnvironment.ApplicationBasePath, - "AppHostTemplate"); + _fileSystem = fileSystem ?? new FileSystemWrapper(); + _appHostShellShimMaker = appHostShellShimMaker ?? new AppHostShellShimMaker(appHostSourceDirectory: appHostSourceDirectory); + _filePermissionSetter = filePermissionSetter ?? new FilePermissionSetter(); } - public void CreateShim(FilePath targetExecutablePath, string commandName) + public void CreateShim(FilePath targetExecutablePath, string commandName, IReadOnlyList packagedShims = null) { if (string.IsNullOrEmpty(targetExecutablePath.Value)) { @@ -54,19 +62,27 @@ namespace Microsoft.DotNet.ShellShim { try { - if (!Directory.Exists(_shimsDirectory.Value)) + if (!_fileSystem.Directory.Exists(_shimsDirectory.Value)) { - Directory.CreateDirectory(_shimsDirectory.Value); + _fileSystem.Directory.CreateDirectory(_shimsDirectory.Value); } - CreateApphostShim( - commandName, - entryPoint: targetExecutablePath); - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (TryGetPackagedShim(packagedShims, commandName, out FilePath? packagedShim)) { - SetUserExecutionPermission(GetShimPath(commandName)); + _fileSystem.File.Copy(packagedShim.Value.Value, GetShimPath(commandName).Value); + _filePermissionSetter.SetUserExecutionPermission(GetShimPath(commandName).Value); } + else + { + _appHostShellShimMaker.CreateApphostShellShim( + targetExecutablePath, + GetShimPath(commandName)); + } + } + catch (FilePermissionSettingException ex) + { + throw new ShellShimException( + string.Format(CommonLocalizableStrings.FailedSettingShimPermissions, ex.Message)); } catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException) { @@ -80,7 +96,7 @@ namespace Microsoft.DotNet.ShellShim } }, rollback: () => { - foreach (var file in GetShimFiles(commandName).Where(f => File.Exists(f.Value))) + foreach (var file in GetShimFiles(commandName).Where(f => _fileSystem.File.Exists(f.Value))) { File.Delete(file.Value); } @@ -94,10 +110,10 @@ namespace Microsoft.DotNet.ShellShim action: () => { try { - foreach (var file in GetShimFiles(commandName).Where(f => File.Exists(f.Value))) + foreach (var file in GetShimFiles(commandName).Where(f => _fileSystem.File.Exists(f.Value))) { var tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - File.Move(file.Value, tempPath); + _fileSystem.File.Move(file.Value, tempPath); files[file.Value] = tempPath; } } @@ -115,38 +131,17 @@ namespace Microsoft.DotNet.ShellShim commit: () => { foreach (var value in files.Values) { - File.Delete(value); + _fileSystem.File.Delete(value); } }, rollback: () => { foreach (var kvp in files) { - File.Move(kvp.Value, kvp.Key); + _fileSystem.File.Move(kvp.Value, kvp.Key); } }); } - private void CreateApphostShim(string commandName, FilePath entryPoint) - { - string appHostSourcePath; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - appHostSourcePath = Path.Combine(_appHostSourceDirectory, ApphostNameWithoutExtension + ".exe"); - } - else - { - appHostSourcePath = Path.Combine(_appHostSourceDirectory, ApphostNameWithoutExtension); - } - - var appHostDestinationFilePath = GetShimPath(commandName).Value; - var appBinaryFilePath = PathUtility.GetRelativePath(appHostDestinationFilePath, entryPoint.Value); - - EmbedAppNameInHost.EmbedAndReturnModifiedAppHostPath( - appHostSourceFilePath: appHostSourcePath, - appHostDestinationFilePath: appHostDestinationFilePath, - appBinaryFilePath: appBinaryFilePath); - } - private class StartupOptions { public string appRoot { get; set; } @@ -159,7 +154,7 @@ namespace Microsoft.DotNet.ShellShim private bool ShimExists(string commandName) { - return GetShimFiles(commandName).Any(p => File.Exists(p.Value)); + return GetShimFiles(commandName).Any(p => _fileSystem.File.Exists(p.Value)); } private IEnumerable GetShimFiles(string commandName) @@ -184,24 +179,37 @@ namespace Microsoft.DotNet.ShellShim } } - private static void SetUserExecutionPermission(FilePath path) + private bool TryGetPackagedShim( + IReadOnlyList packagedShims, + string commandName, + out FilePath? packagedShim) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + packagedShim = null; + + if (packagedShims != null && packagedShims.Count > 0) { - return; + FilePath[] candidatepackagedShim = + packagedShims + .Where(s => string.Equals( + Path.GetFileName(s.Value), + Path.GetFileName(GetShimPath(commandName).Value))).ToArray(); + + if (candidatepackagedShim.Length > 1) + { + throw new ShellShimException( + string.Format( + CommonLocalizableStrings.MoreThanOnePackagedShimAvailable, + string.Join(';', candidatepackagedShim))); + } + + if (candidatepackagedShim.Length == 1) + { + packagedShim = candidatepackagedShim.Single(); + return true; + } } - CommandResult result = new CommandFactory() - .Create("chmod", new[] { "u+x", path.Value }) - .CaptureStdOut() - .CaptureStdErr() - .Execute(); - - if (result.ExitCode != 0) - { - throw new ShellShimException( - string.Format(CommonLocalizableStrings.FailedSettingShimPermissions, result.StdErr)); - } + return false; } } } diff --git a/src/dotnet/ToolPackage/IToolPackage.cs b/src/dotnet/ToolPackage/IToolPackage.cs index ebf351487..0b0a16c1e 100644 --- a/src/dotnet/ToolPackage/IToolPackage.cs +++ b/src/dotnet/ToolPackage/IToolPackage.cs @@ -20,6 +20,8 @@ namespace Microsoft.DotNet.ToolPackage IEnumerable Warnings { get; } + IReadOnlyList PackagedShims { get; } + void Uninstall(); } } diff --git a/src/dotnet/ToolPackage/LockFileMatchChecker.cs b/src/dotnet/ToolPackage/LockFileMatchChecker.cs index efa4af6b6..03d0a6db5 100644 --- a/src/dotnet/ToolPackage/LockFileMatchChecker.cs +++ b/src/dotnet/ToolPackage/LockFileMatchChecker.cs @@ -1,6 +1,7 @@ // 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.Collections.Generic; using System.Linq; using NuGet.ProjectModel; @@ -22,36 +23,60 @@ namespace Microsoft.DotNet.ToolPackage string[] entryPointPathInArray = SplitPathByDirectorySeparator(targetRelativeFilePath); return entryPointPathInArray.Length >= 1 - && PathInLockFileDirectoriesStartWithToolsAndFollowsTwoSubFolder() - && SubPathMatchesTargetFilePath(); + && PathInLockFileDirectoriesStartWithToolsAndFollowsTwoSubFolder( + pathInLockFilePathInArray, + entryPointPathInArray) + && SubPathMatchesTargetFilePath(pathInLockFilePathInArray, entryPointPathInArray); + } - bool SubPathMatchesTargetFilePath() + /// + /// Check if LockFileItem is under targetRelativePath directory. + /// The path in LockFileItem is in pattern tools/TFM/RID/my/tool.dll. Tools/TFM/RID is selected by NuGet. + /// And there will be only one TFM/RID combination. + /// When "my/folder/of/tool/tools.dll" part under targetRelativePath "my/folder/of" or "my/folder", return true. + /// + internal static bool MatchesDirectoryPath(LockFileItem lockFileItem, string targetRelativePath) + { + string[] pathInLockFilePathInArray = SplitPathByDirectorySeparator(lockFileItem.Path); + string[] targetDirectoryPathInArray = SplitPathByDirectorySeparator(targetRelativePath); + + return pathInLockFilePathInArray[0] == "tools" + && SubPathMatchesTargetFilePath(pathInLockFilePathInArray, targetDirectoryPathInArray); + } + + private static bool SubPathMatchesTargetFilePath(string[] pathInLockFilePathInArray, string[] targetInArray) + { + string[] pathAfterToolsTfmRid = pathInLockFilePathInArray.Skip(3).ToArray(); + return !targetInArray + .Where((directoryOnEveryLevel, i) => directoryOnEveryLevel != pathAfterToolsTfmRid[i]) + .Any(); + } + + private static bool PathInLockFileDirectoriesStartWithToolsAndFollowsTwoSubFolder( + string[] pathInLockFilePathInArray, + string[] targetInArray) + { + if (pathInLockFilePathInArray.Length - targetInArray.Length != 3) { - string[] pathAfterToolsTfmRid = pathInLockFilePathInArray.Skip(3).ToArray(); - return !pathAfterToolsTfmRid - .Where((directoryOnEveryLevel, i) => directoryOnEveryLevel != entryPointPathInArray[i]) - .Any(); + return false; } - bool PathInLockFileDirectoriesStartWithToolsAndFollowsTwoSubFolder() + if (pathInLockFilePathInArray[0] != "tools") { - if (pathInLockFilePathInArray.Length - entryPointPathInArray.Length != 3) - { - return false; - } - - if (pathInLockFilePathInArray[0] != "tools") - { - return false; - } - - return true; + return false; } - string[] SplitPathByDirectorySeparator(string path) + return true; + } + + private static string[] SplitPathByDirectorySeparator(string path) + { + if (string.IsNullOrEmpty(path)) { - return path.Split('\\', '/'); + return new string[0]; } + + return path.Split('\\', '/'); } } } diff --git a/src/dotnet/ToolPackage/ToolPackageInstance.cs b/src/dotnet/ToolPackage/ToolPackageInstance.cs index 4dd9733fb..c8be8d5ca 100644 --- a/src/dotnet/ToolPackage/ToolPackageInstance.cs +++ b/src/dotnet/ToolPackage/ToolPackageInstance.cs @@ -7,13 +7,17 @@ using Microsoft.DotNet.Tools; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.ProjectModel; using NuGet.Versioning; +using Microsoft.DotNet.Cli.Utils; namespace Microsoft.DotNet.ToolPackage { // This is named "ToolPackageInstance" because "ToolPackage" would conflict with the namespace internal class ToolPackageInstance : IToolPackage { + private const string PackagedShimsDirectoryConvention = "shims"; + public IEnumerable Warnings => _toolConfiguration.Value.Warnings; + public PackageId Id { get; private set; } public NuGetVersion Version { get; private set; } @@ -28,12 +32,21 @@ namespace Microsoft.DotNet.ToolPackage } } + public IReadOnlyList PackagedShims + { + get + { + return _packagedShims.Value; + } + } + private const string AssetsFileName = "project.assets.json"; private const string ToolSettingsFileName = "DotnetToolSettings.xml"; private IToolPackageStore _store; private Lazy> _commands; private Lazy _toolConfiguration; + private Lazy> _packagedShims; public ToolPackageInstance( IToolPackageStore store, @@ -43,6 +56,7 @@ namespace Microsoft.DotNet.ToolPackage { _store = store ?? throw new ArgumentNullException(nameof(store)); _commands = new Lazy>(GetCommands); + _packagedShims = new Lazy>(GetPackagedShims); Id = id; Version = version ?? throw new ArgumentNullException(nameof(version)); @@ -107,11 +121,11 @@ namespace Microsoft.DotNet.ToolPackage try { var commands = new List(); - var lockFile = new LockFileFormat().Read(PackageDirectory.WithFile(AssetsFileName).Value); - var library = FindLibraryInLockFile(lockFile); + LockFile lockFile = new LockFileFormat().Read(PackageDirectory.WithFile(AssetsFileName).Value); + LockFileTargetLibrary library = FindLibraryInLockFile(lockFile); ToolConfiguration configuration = _toolConfiguration.Value; - var entryPointFromLockFile = FindItemInTargetLibrary(library, configuration.ToolAssemblyEntryPoint); + LockFileItem entryPointFromLockFile = FindItemInTargetLibrary(library, configuration.ToolAssemblyEntryPoint); if (entryPointFromLockFile == null) { throw new ToolConfigurationException( @@ -125,11 +139,7 @@ namespace Microsoft.DotNet.ToolPackage commands.Add(new CommandSettings( configuration.CommandName, "dotnet", - PackageDirectory - .WithSubDirectories( - Id.ToString(), - library.Version.ToNormalizedString()) - .WithFile(entryPointFromLockFile.Path))); + LockFileRelativePathToFullFilePath(entryPointFromLockFile.Path, library))); return commands; } @@ -143,6 +153,15 @@ namespace Microsoft.DotNet.ToolPackage } } + private FilePath LockFileRelativePathToFullFilePath(string lockFileRelativePath, LockFileTargetLibrary library) + { + return PackageDirectory + .WithSubDirectories( + Id.ToString(), + library.Version.ToNormalizedString()) + .WithFile(lockFileRelativePath); + } + private ToolConfiguration GetToolConfiguration() { try @@ -161,6 +180,55 @@ namespace Microsoft.DotNet.ToolPackage } } + private IReadOnlyList GetPackagedShims() + { + LockFileTargetLibrary library; + try + { + LockFile lockFile = new LockFileFormat().Read(PackageDirectory.WithFile(AssetsFileName).Value); + library = FindLibraryInLockFile(lockFile); + } + catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException) + { + throw new ToolPackageException( + string.Format( + CommonLocalizableStrings.FailedToReadNuGetLockFile, + Id, + ex.Message), + ex); + } + + IEnumerable filesUnderShimsDirectory = library + ?.ToolsAssemblies + ?.Where(t => LockFileMatcher.MatchesDirectoryPath(t, PackagedShimsDirectoryConvention)); + + if (filesUnderShimsDirectory == null) + { + return Array.Empty(); + } + + IEnumerable allAvailableShimRuntimeIdentifiers = filesUnderShimsDirectory + .Select(f => f.Path.Split('\\', '/')?[4]) // ex: "tools/netcoreapp2.1/any/shims/osx-x64/demo" osx-x64 is at [4] + .Where(f => !string.IsNullOrEmpty(f)); + + if (new FrameworkDependencyFile().TryGetMostFitRuntimeIdentifier( + DotnetFiles.VersionFileObject.BuildRid, + allAvailableShimRuntimeIdentifiers.ToArray(), + out var mostFitRuntimeIdentifier)) + { + return library + ?.ToolsAssemblies + ?.Where(l => + LockFileMatcher.MatchesDirectoryPath(l, $"{PackagedShimsDirectoryConvention}/{mostFitRuntimeIdentifier}")) + .Select(l => LockFileRelativePathToFullFilePath(l.Path, library)).ToArray() + ?? Array.Empty(); + } + else + { + return Array.Empty(); + } + } + private ToolConfiguration DeserializeToolConfiguration(string ToolSettingsFileName, LockFileTargetLibrary library) { var dotnetToolSettings = FindItemInTargetLibrary(library, ToolSettingsFileName); diff --git a/src/dotnet/commands/dotnet-tool/install/ToolInstallCommand.cs b/src/dotnet/commands/dotnet-tool/install/ToolInstallCommand.cs index 42bcb8790..845644239 100644 --- a/src/dotnet/commands/dotnet-tool/install/ToolInstallCommand.cs +++ b/src/dotnet/commands/dotnet-tool/install/ToolInstallCommand.cs @@ -142,7 +142,7 @@ namespace Microsoft.DotNet.Tools.Tool.Install foreach (var command in package.Commands) { - shellShimRepository.CreateShim(command.Executable, command.Name); + shellShimRepository.CreateShim(command.Executable, command.Name, package.PackagedShims); } scope.Complete(); diff --git a/src/dotnet/xlf/CommonLocalizableStrings.cs.xlf b/src/dotnet/xlf/CommonLocalizableStrings.cs.xlf index aa36da3b1..8e1334b72 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.cs.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.cs.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.de.xlf b/src/dotnet/xlf/CommonLocalizableStrings.de.xlf index c944f6a40..f79fd7412 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.de.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.de.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.es.xlf b/src/dotnet/xlf/CommonLocalizableStrings.es.xlf index 58718eb15..ffb99ee4e 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.es.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.es.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.fr.xlf b/src/dotnet/xlf/CommonLocalizableStrings.fr.xlf index 8b6d109c8..13142465f 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.fr.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.fr.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.it.xlf b/src/dotnet/xlf/CommonLocalizableStrings.it.xlf index 7d37d0324..cf4725a56 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.it.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.it.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.ja.xlf b/src/dotnet/xlf/CommonLocalizableStrings.ja.xlf index f3806381e..25533c941 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.ja.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.ja.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.ko.xlf b/src/dotnet/xlf/CommonLocalizableStrings.ko.xlf index 18e4e1ebc..d6ccc832f 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.ko.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.ko.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.pl.xlf b/src/dotnet/xlf/CommonLocalizableStrings.pl.xlf index c93085baa..1dc4ed34a 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.pl.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.pl.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.pt-BR.xlf b/src/dotnet/xlf/CommonLocalizableStrings.pt-BR.xlf index 64d6bb597..282310d02 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.pt-BR.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.pt-BR.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.ru.xlf b/src/dotnet/xlf/CommonLocalizableStrings.ru.xlf index 84a2a6885..6d8268d94 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.ru.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.ru.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.tr.xlf b/src/dotnet/xlf/CommonLocalizableStrings.tr.xlf index 158df3e77..07e8e75b8 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.tr.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.tr.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.zh-Hans.xlf b/src/dotnet/xlf/CommonLocalizableStrings.zh-Hans.xlf index bb980cac5..3b5ab3369 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.zh-Hans.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.zh-Hans.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/src/dotnet/xlf/CommonLocalizableStrings.zh-Hant.xlf b/src/dotnet/xlf/CommonLocalizableStrings.zh-Hant.xlf index a8751bbee..4b1a80094 100644 --- a/src/dotnet/xlf/CommonLocalizableStrings.zh-Hant.xlf +++ b/src/dotnet/xlf/CommonLocalizableStrings.zh-Hant.xlf @@ -883,6 +883,16 @@ setx PATH "%PATH%;{0}" Failed to add '{0}' to the PATH environment variable. Please add this directory to your PATH to use tools installed with 'dotnet tool install'. + + More than one packaged shim is available: {0}. + More than one packaged shim is available: {0}. + + + + Failed to read NuGet LockFile for tool package '{0}': {1} + Failed to read NuGet LockFile for tool package '{0}': {1} + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Configurer.UnitTests/GivenAFunctionReturnStringAndFakeFileSystem.cs b/test/Microsoft.DotNet.Configurer.UnitTests/GivenAFunctionReturnStringAndFakeFileSystem.cs index 3844e07c1..7f9645ce0 100644 --- a/test/Microsoft.DotNet.Configurer.UnitTests/GivenAFunctionReturnStringAndFakeFileSystem.cs +++ b/test/Microsoft.DotNet.Configurer.UnitTests/GivenAFunctionReturnStringAndFakeFileSystem.cs @@ -134,6 +134,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests { throw new UnauthorizedAccessException(); } + + public void Copy(string source, string destination) + { + throw new UnauthorizedAccessException(); + } } private class NoPermissionDirectoryFake : IDirectory diff --git a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs index c224254e0..2113d9d17 100644 --- a/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs +++ b/test/Microsoft.DotNet.Configurer.UnitTests/GivenANuGetCacheSentinel.cs @@ -286,6 +286,11 @@ namespace Microsoft.DotNet.Configurer.UnitTests { throw new NotImplementedException(); } + + public void Copy(string source, string destination) + { + throw new NotImplementedException(); + } } private class MockStream : MemoryStream diff --git a/test/Microsoft.DotNet.ShellShim.Tests/ShellShimRepositoryTests.cs b/test/Microsoft.DotNet.ShellShim.Tests/ShellShimRepositoryTests.cs index 15b9d4f7c..90a3bb83d 100644 --- a/test/Microsoft.DotNet.ShellShim.Tests/ShellShimRepositoryTests.cs +++ b/test/Microsoft.DotNet.ShellShim.Tests/ShellShimRepositoryTests.cs @@ -119,7 +119,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -161,7 +161,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -198,7 +198,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -223,7 +223,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -252,7 +252,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -288,7 +288,7 @@ namespace Microsoft.DotNet.ShellShim.Tests IShellShimRepository shellShimRepository; if (testMockBehaviorIsInSync) { - shellShimRepository = new ShellShimRepositoryMock(new DirectoryPath(pathToShim)); + shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); } else { @@ -315,6 +315,67 @@ namespace Microsoft.DotNet.ShellShim.Tests Directory.EnumerateFileSystemEntries(pathToShim).Should().BeEmpty(); } + [Fact] + public void WhenPackagedShimProvidedItCopies() + { + const string tokenToIdentifyCopiedShim = "packagedShim"; + + var shellCommandName = nameof(ShellShimRepositoryTests) + Path.GetRandomFileName(); + var pathToShim = GetNewCleanFolderUnderTempRoot(); + var packagedShimFolder = GetNewCleanFolderUnderTempRoot(); + var dummyShimPath = Path.Combine(packagedShimFolder, shellCommandName); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dummyShimPath = dummyShimPath + ".exe"; + } + + File.WriteAllText(dummyShimPath, tokenToIdentifyCopiedShim); + + ShellShimRepository shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); + + shellShimRepository.CreateShim( + new FilePath("dummy.dll"), + shellCommandName, + new[] {new FilePath(dummyShimPath)}); + + var createdShim = Directory.EnumerateFileSystemEntries(pathToShim).Single(); + File.ReadAllText(createdShim).Should().Contain(tokenToIdentifyCopiedShim); + } + + [Fact] + public void WhenMutipleSameNamePackagedShimProvidedItThrows() + { + const string tokenToIdentifyCopiedShim = "packagedShim"; + + var shellCommandName = nameof(ShellShimRepositoryTests) + Path.GetRandomFileName(); + var pathToShim = GetNewCleanFolderUnderTempRoot(); + var packagedShimFolder = GetNewCleanFolderUnderTempRoot(); + var dummyShimPath = Path.Combine(packagedShimFolder, shellCommandName); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + dummyShimPath = dummyShimPath + ".exe"; + } + + File.WriteAllText(dummyShimPath, tokenToIdentifyCopiedShim); + ShellShimRepository shellShimRepository = GetShellShimRepositoryWithMockMaker(pathToShim); + + FilePath[] filePaths = new[] { new FilePath(dummyShimPath), new FilePath("path" + dummyShimPath) }; + + Action a = () => shellShimRepository.CreateShim( + new FilePath("dummy.dll"), + shellCommandName, + new[] { new FilePath(dummyShimPath), new FilePath("path" + dummyShimPath) }); + + a.ShouldThrow() + .And.Message + .Should().Contain( + string.Format( + CommonLocalizableStrings.MoreThanOnePackagedShimAvailable, + string.Join(';', filePaths))); + } + private static void MakeNameConflictingCommand(string pathToPlaceShim, string shellCommandName) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -440,5 +501,12 @@ namespace Microsoft.DotNet.ShellShim.Tests return CleanFolderUnderTempRoot.FullName; } + + private ShellShimRepository GetShellShimRepositoryWithMockMaker(string pathToShim) + { + return new ShellShimRepository( + new DirectoryPath(pathToShim), + appHostShellShimMaker: new AppHostShellShimMakerMock()); + } } } diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/LockFileMatcherTests.cs b/test/Microsoft.DotNet.ToolPackage.Tests/LockFileMatcherTests.cs index 0a0a0cb03..dd658842f 100644 --- a/test/Microsoft.DotNet.ToolPackage.Tests/LockFileMatcherTests.cs +++ b/test/Microsoft.DotNet.ToolPackage.Tests/LockFileMatcherTests.cs @@ -10,7 +10,6 @@ namespace Microsoft.DotNet.ToolPackage.Tests { public class LockFileMatcherTests : TestBase { - [Theory] [InlineData("tools/netcoreapp1.1/any/tool.dll", "tool.dll", true)] [InlineData(@"tools\netcoreapp1.1\any\subDirectory\tool.dll", "subDirectory/tool.dll", true)] @@ -24,5 +23,20 @@ namespace Microsoft.DotNet.ToolPackage.Tests LockFileMatcher.MatchesFile(new LockFileItem(pathInLockFileItem), targetRelativeFilePath) .Should().Be(shouldMatch); } + + + [Theory] + [InlineData("tools/netcoreapp1.1/any/tool.dll", "", true)] + [InlineData(@"tools\netcoreapp1.1\any\subDirectory\tool.dll", "subDirectory", true)] + [InlineData(@"tools\netcoreapp1.1\any\subDirectory\tool.dll", "sub", false)] + [InlineData("tools/netcoreapp1.1/any/subDirectory/tool.dll", "any/subDirectory", false)] + public void MatchesDirectoryPathTests( + string pathInLockFileItem, + string targetRelativeFilePath, + bool shouldMatch) + { + LockFileMatcher.MatchesDirectoryPath(new LockFileItem(pathInLockFileItem), targetRelativeFilePath) + .Should().Be(shouldMatch); + } } } diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/Microsoft.DotNet.ToolPackage.Tests.csproj b/test/Microsoft.DotNet.ToolPackage.Tests/Microsoft.DotNet.ToolPackage.Tests.csproj index 27ff23b4b..ad85df299 100644 --- a/test/Microsoft.DotNet.ToolPackage.Tests/Microsoft.DotNet.ToolPackage.Tests.csproj +++ b/test/Microsoft.DotNet.ToolPackage.Tests/Microsoft.DotNet.ToolPackage.Tests.csproj @@ -45,6 +45,11 @@ + + + + + @@ -59,4 +64,17 @@ + + + + $(BaseOutputPath)/TestAsset/SampleGlobalToolWithShim + + + + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/DotnetToolSettings.xml b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/DotnetToolSettings.xml new file mode 100644 index 000000000..92142c78d --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/DotnetToolSettings.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/Program.cs b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/Program.cs new file mode 100644 index 000000000..1140102df --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/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.ToolPackage.Tests/SampleGlobalToolWithShim/consoledemo.csproj b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/consoledemo.csproj new file mode 100644 index 000000000..4e16cac58 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/consoledemo.csproj @@ -0,0 +1,7 @@ + + + Exe + netcoreapp2.1 + consoledemo + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim new file mode 100644 index 000000000..8ba74aee5 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim @@ -0,0 +1 @@ +packagedshim \ No newline at end of file diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim.exe b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim.exe new file mode 100644 index 000000000..8ba74aee5 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/dummyshim.exe @@ -0,0 +1 @@ +packagedshim \ No newline at end of file diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/includepublish.nuspec b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/includepublish.nuspec new file mode 100644 index 000000000..b2ed9d349 --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/SampleGlobalToolWithShim/includepublish.nuspec @@ -0,0 +1,20 @@ + + + + global.tool.console.demo.with.shim + 1.0.4 + test app + testauthor + + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.ToolPackage.Tests/ToolPackageInstanceTests.cs b/test/Microsoft.DotNet.ToolPackage.Tests/ToolPackageInstanceTests.cs new file mode 100644 index 000000000..3c035e17f --- /dev/null +++ b/test/Microsoft.DotNet.ToolPackage.Tests/ToolPackageInstanceTests.cs @@ -0,0 +1,130 @@ +// 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 System.Reflection; +using System.Transactions; +using FluentAssertions; +using Microsoft.DotNet.Tools.Test.Utilities; +using Microsoft.DotNet.Cli; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.DotNet.Tools; +using Microsoft.DotNet.Tools.Tool.Install; +using Microsoft.DotNet.Tools.Tests.ComponentMocks; +using Microsoft.Extensions.DependencyModel.Tests; +using Microsoft.Extensions.EnvironmentAbstractions; +using NuGet.Versioning; +using Xunit; + +namespace Microsoft.DotNet.ToolPackage.Tests +{ + public class ToolPackageInstanceTests : TestBase + { + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GivenAnInstalledPackageUninstallRemovesThePackage(bool testMockBehaviorIsInSync) + { + var source = GetTestLocalFeedPath(); + + var (store, installer, reporter, fileSystem) = Setup( + useMock: testMockBehaviorIsInSync, + feeds: GetMockFeedsForSource(source)); + + var package = installer.InstallPackage( + packageId: TestPackageId, + versionRange: VersionRange.Parse(TestPackageVersion), + targetFramework: _testTargetframework, + additionalFeeds: new[] {source}); + + package.PackagedShims.Should().ContainSingle(f => f.Value.Contains("demo.exe") || f.Value.Contains("demo")); + + package.Uninstall(); + } + + private static FilePath GetUniqueTempProjectPathEachTest() + { + var tempProjectDirectory = + new DirectoryPath(Path.GetTempPath()).WithSubDirectories(Path.GetRandomFileName()); + var tempProjectPath = + tempProjectDirectory.WithFile(Path.GetRandomFileName() + ".csproj"); + return tempProjectPath; + } + + private static IEnumerable GetMockFeedsForSource(string source) + { + return new[] + { + new MockFeed + { + Type = MockFeedType.ImplicitAdditionalFeed, + Uri = source, + Packages = new List + { + new MockFeedPackage + { + PackageId = TestPackageId.ToString(), + Version = TestPackageVersion + } + } + } + }; + } + + private static (IToolPackageStore, IToolPackageInstaller, BufferedReporter, IFileSystem) Setup( + bool useMock, + IEnumerable feeds = null, + FilePath? tempProject = null, + DirectoryPath? offlineFeed = null) + { + var root = new DirectoryPath(Path.Combine(TempRoot.Root, Path.GetRandomFileName())); + var reporter = new BufferedReporter(); + + IFileSystem fileSystem; + IToolPackageStore store; + IToolPackageInstaller installer; + if (useMock) + { + var packagedShimsMap = new Dictionary> + { + [TestPackageId] = new FilePath[] {new FilePath("path/demo.exe")} + }; + + fileSystem = new FileSystemMockBuilder().Build(); + store = new ToolPackageStoreMock(root, fileSystem); + installer = new ToolPackageInstallerMock( + fileSystem: fileSystem, + store: store, + projectRestorer: new ProjectRestorerMock( + fileSystem: fileSystem, + reporter: reporter, + feeds: feeds), + packagedShimsMap: packagedShimsMap); + } + else + { + fileSystem = new FileSystemWrapper(); + store = new ToolPackageStore(root); + installer = new ToolPackageInstaller( + store: store, + projectRestorer: new ProjectRestorer(reporter), + tempProject: tempProject ?? GetUniqueTempProjectPathEachTest(), + offlineFeed: offlineFeed ?? new DirectoryPath("does not exist")); + } + + store.Root.Value.Should().Be(Path.GetFullPath(root.Value)); + + return (store, installer, reporter, fileSystem); + } + + private static string GetTestLocalFeedPath() => + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed"); + + private readonly string _testTargetframework = BundledTargetFramework.GetTargetFrameworkMoniker(); + private const string TestPackageVersion = "1.0.4"; + private static readonly PackageId TestPackageId = new PackageId("global.tool.console.demo.with.shim"); + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/AppHostShellShimMakerMock.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/AppHostShellShimMakerMock.cs new file mode 100644 index 000000000..1dca12162 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/AppHostShellShimMakerMock.cs @@ -0,0 +1,38 @@ +// 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.DotNet.ShellShim; +using Microsoft.Extensions.EnvironmentAbstractions; +using Newtonsoft.Json; + +namespace Microsoft.DotNet.Tools.Tests.ComponentMocks +{ + internal class AppHostShellShimMakerMock : IAppHostShellShimMaker + { + private static IFileSystem _fileSystem; + + public AppHostShellShimMakerMock(IFileSystem fileSystem = null) + { + _fileSystem = fileSystem ?? new FileSystemWrapper(); + } + + public void CreateApphostShellShim(FilePath entryPoint, FilePath shimPath) + { + var shim = new FakeShim + { + Runner = "dotnet", + ExecutablePath = entryPoint.Value + }; + + _fileSystem.File.WriteAllText( + shimPath.Value, + JsonConvert.SerializeObject(shim)); + } + + public class FakeShim + { + public string Runner { get; set; } + public string ExecutablePath { get; set; } + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ShellShimRepositoryMock.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ShellShimRepositoryMock.cs deleted file mode 100644 index d67167d0e..000000000 --- a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ShellShimRepositoryMock.cs +++ /dev/null @@ -1,104 +0,0 @@ -// 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.Runtime.InteropServices; -using Microsoft.DotNet.Cli; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.DotNet.ShellShim; -using Microsoft.Extensions.EnvironmentAbstractions; -using Newtonsoft.Json; - -namespace Microsoft.DotNet.Tools.Tests.ComponentMocks -{ - internal class ShellShimRepositoryMock : IShellShimRepository - { - private static IFileSystem _fileSystem; - private readonly DirectoryPath _pathToPlaceShim; - - public ShellShimRepositoryMock(DirectoryPath pathToPlaceShim, IFileSystem fileSystem = null) - { - _pathToPlaceShim = pathToPlaceShim; - _fileSystem = fileSystem ?? new FileSystemWrapper(); - } - - public void CreateShim(FilePath targetExecutablePath, string commandName) - { - if (ShimExists(commandName)) - { - throw new ShellShimException( - string.Format(CommonLocalizableStrings.ShellShimConflict, - commandName)); - } - - TransactionalAction.Run( - action: () => { - var shim = new FakeShim - { - Runner = "dotnet", - ExecutablePath = targetExecutablePath.Value - }; - - _fileSystem.File.WriteAllText( - GetShimPath(commandName).Value, - JsonConvert.SerializeObject(shim)); - }, - rollback: () => { - _fileSystem.File.Delete(GetShimPath(commandName).Value); - }); - } - - public void RemoveShim(string commandName) - { - var originalShimPath = GetShimPath(commandName); - if (!_fileSystem.File.Exists(originalShimPath.Value)) - { - return; - } - - string tempShimPath = null; - TransactionalAction.Run( - action: () => { - var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - _fileSystem.File.Move(originalShimPath.Value, tempFile); - tempShimPath = tempFile; - }, - commit: () => { - if (tempShimPath != null) - { - _fileSystem.File.Delete(tempShimPath); - } - }, - rollback: () => { - if (tempShimPath != null) - { - _fileSystem.File.Move(tempShimPath, originalShimPath.Value); - } - }); - } - - private bool ShimExists(string commandName) - { - return _fileSystem.File.Exists(GetShimPath(commandName).Value); - } - - private FilePath GetShimPath(string shellCommandName) - { - var shimPath = Path.Combine(_pathToPlaceShim.Value, shellCommandName); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - shimPath += ".exe"; - } - - return new FilePath(shimPath); - } - - public class FakeShim - { - public string Runner { get; set; } - public string ExecutablePath { get; set; } - } - } -} diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageInstallerMock.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageInstallerMock.cs index facc85132..9737ff858 100644 --- a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageInstallerMock.cs +++ b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageInstallerMock.cs @@ -23,19 +23,22 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks private readonly IFileSystem _fileSystem; private readonly Action _installCallback; private readonly Dictionary> _warningsMap; + private readonly Dictionary> _packagedShimsMap; public ToolPackageInstallerMock( IFileSystem fileSystem, IToolPackageStore store, IProjectRestorer projectRestorer, Action installCallback = null, - Dictionary> warningsMap = null) + Dictionary> warningsMap = null, + Dictionary> packagedShimsMap = null) { _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); _store = store ?? throw new ArgumentNullException(nameof(store)); _projectRestorer = projectRestorer ?? throw new ArgumentNullException(nameof(projectRestorer)); _installCallback = installCallback; _warningsMap = warningsMap ?? new Dictionary>(); + _packagedShimsMap = packagedShimsMap ?? new Dictionary>(); } public IToolPackage InstallPackage(PackageId packageId, @@ -92,7 +95,10 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks IEnumerable warnings = null; _warningsMap.TryGetValue(packageId, out warnings); - return new ToolPackageMock(_fileSystem, packageId, version, packageDirectory, warnings: warnings); + IReadOnlyList packedShims = null; + _packagedShimsMap.TryGetValue(packageId, out packedShims); + + return new ToolPackageMock(_fileSystem, packageId, version, packageDirectory, warnings: warnings, packagedShims: packedShims); }, rollback: () => { if (rollbackDirectory != null && _fileSystem.Directory.Exists(rollbackDirectory)) diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageMock.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageMock.cs index 565e140e4..43d430b08 100644 --- a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageMock.cs +++ b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageMock.cs @@ -18,6 +18,7 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks private Lazy> _commands; private Action _uninstallCallback; private IEnumerable _warnings; + private readonly IReadOnlyList _packagedShims; public ToolPackageMock( IFileSystem fileSystem, @@ -25,7 +26,8 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks NuGetVersion version, DirectoryPath packageDirectory, Action uninstallCallback = null, - IEnumerable warnings = null) + IEnumerable warnings = null, + IReadOnlyList packagedShims = null) { _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); Id = id; @@ -34,6 +36,7 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks _commands = new Lazy>(GetCommands); _uninstallCallback = uninstallCallback; _warnings = warnings ?? new List(); + _packagedShims = packagedShims ?? new List(); } public PackageId Id { get; private set; } @@ -52,6 +55,14 @@ namespace Microsoft.DotNet.Tools.Tests.ComponentMocks public IEnumerable Warnings => _warnings; + public IReadOnlyList PackagedShims + { + get + { + return _packagedShims; + } + } + public void Uninstall() { var rootDirectory = PackageDirectory.GetParentPath(); diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs index 6adce5ca9..f75d3471b 100644 --- a/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Mock/FileSystemMockBuilder.cs @@ -132,6 +132,20 @@ namespace Microsoft.Extensions.DependencyModel.Tests _files.Remove(path); } + + public void Copy(string source, string destination) + { + if (!Exists(source)) + { + throw new FileNotFoundException("source does not exist."); + } + if (Exists(destination)) + { + throw new IOException("destination exists."); + } + + _files[destination] = _files[source]; + } } private class DirectoryMock : IDirectory diff --git a/test/dotnet.Tests/CommandTests/ToolInstallCommandTests.cs b/test/dotnet.Tests/CommandTests/ToolInstallCommandTests.cs index 05cca5f33..6ba8a385f 100644 --- a/test/dotnet.Tests/CommandTests/ToolInstallCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/ToolInstallCommandTests.cs @@ -46,7 +46,11 @@ namespace Microsoft.DotNet.Tests.Commands _fileSystem = new FileSystemMockBuilder().Build(); _toolPackageStore = new ToolPackageStoreMock(new DirectoryPath(PathToPlacePackages), _fileSystem); _createShellShimRepository = - (nonGlobalLocation) => new ShellShimRepositoryMock(new DirectoryPath(PathToPlaceShim), _fileSystem); + (nonGlobalLocation) => new ShellShimRepository( + new DirectoryPath(PathToPlaceShim), + fileSystem: _fileSystem, + appHostShellShimMaker: new AppHostShellShimMakerMock(_fileSystem), + filePermissionSetter: new NoOpFilePermissionSetter()); _environmentPathInstructionMock = new EnvironmentPathInstructionMock(_reporter, PathToPlaceShim); _createToolPackageStoreAndInstaller = (_) => (_toolPackageStore, CreateToolPackageInstaller()); @@ -71,7 +75,7 @@ namespace Microsoft.DotNet.Tests.Commands // It is hard to simulate shell behavior. Only Assert shim can point to executable dll _fileSystem.File.Exists(ExpectedCommandPath()).Should().BeTrue(); - var deserializedFakeShim = JsonConvert.DeserializeObject( + var deserializedFakeShim = JsonConvert.DeserializeObject( _fileSystem.File.ReadAllText(ExpectedCommandPath())); _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); @@ -117,7 +121,7 @@ namespace Microsoft.DotNet.Tests.Commands _fileSystem.File.Exists(ExpectedCommandPath()) .Should().BeTrue(); var deserializedFakeShim = - JsonConvert.DeserializeObject( + JsonConvert.DeserializeObject( _fileSystem.File.ReadAllText(ExpectedCommandPath())); _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); } @@ -455,6 +459,42 @@ namespace Microsoft.DotNet.Tests.Commands _reporter.Lines.Should().NotContain(l => l.Contains(EnvironmentPathInstructionMock.MockInstructionText)); } + [Fact] + public void AndPackagedShimIsProvidedWhenRunWithPackageIdItCreateShimUsingPackagedShim() + { + var extension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty; + var prepackagedShimPath = "packagedShimDirectory/" + ProjectRestorerMock.FakeCommandName + extension; + var tokenToIdentifyPackagedShim = "packagedShim"; + _fileSystem.File.WriteAllText(prepackagedShimPath, tokenToIdentifyPackagedShim); + + var result = Parser.Instance.Parse($"dotnet tool install --tool-path /tmp/folder {PackageId}"); + var appliedCommand = result["dotnet"]["tool"]["install"]; + var parser = Parser.Instance; + var parseResult = parser.ParseFrom("dotnet tool", new[] {"install", "-g", PackageId}); + + var packagedShimsMap = new Dictionary> + { + [new PackageId(PackageId)] = new[] {new FilePath(prepackagedShimPath)} + }; + + var installCommand = new ToolInstallCommand(appliedCommand, + parseResult, + (_) => (_toolPackageStore, new ToolPackageInstallerMock( + fileSystem: _fileSystem, + store: _toolPackageStore, + packagedShimsMap: packagedShimsMap, + projectRestorer: new ProjectRestorerMock( + fileSystem: _fileSystem, + reporter: _reporter))), + _createShellShimRepository, + new EnvironmentPathInstructionMock(_reporter, PathToPlaceShim), + _reporter); + + installCommand.Execute().Should().Be(0); + + _fileSystem.File.ReadAllText(ExpectedCommandPath()).Should().Be(tokenToIdentifyPackagedShim); + } + private IToolPackageInstaller CreateToolPackageInstaller( IEnumerable feeds = null, Action installCallback = null) @@ -476,5 +516,12 @@ namespace Microsoft.DotNet.Tests.Commands "pathToPlace", ProjectRestorerMock.FakeCommandName + extension); } + + private class NoOpFilePermissionSetter : IFilePermissionSetter + { + public void SetUserExecutionPermission(string path) + { + } + } } } diff --git a/test/dotnet.Tests/CommandTests/ToolUninstallCommandTests.cs b/test/dotnet.Tests/CommandTests/ToolUninstallCommandTests.cs index 2e47036ac..902d4eb1d 100644 --- a/test/dotnet.Tests/CommandTests/ToolUninstallCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/ToolUninstallCommandTests.cs @@ -22,6 +22,7 @@ using Xunit; using Parser = Microsoft.DotNet.Cli.Parser; using LocalizableStrings = Microsoft.DotNet.Tools.Tool.Uninstall.LocalizableStrings; using InstallLocalizableStrings = Microsoft.DotNet.Tools.Tool.Install.LocalizableStrings; +using Microsoft.DotNet.ShellShim; namespace Microsoft.DotNet.Tests.Commands { @@ -203,7 +204,10 @@ namespace Microsoft.DotNet.Tests.Commands result["dotnet"]["tool"]["install"], result, (_) => (store, packageInstallerMock), - (_) => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + (_) => new ShellShimRepository( + new DirectoryPath(ShimsDirectory), + fileSystem: _fileSystem, + appHostShellShimMaker: new AppHostShellShimMakerMock(_fileSystem)), _environmentPathInstructionMock, _reporter); } @@ -219,7 +223,10 @@ namespace Microsoft.DotNet.Tests.Commands new DirectoryPath(ToolsDirectory), _fileSystem, uninstallCallback), - (_) => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + (_) => new ShellShimRepository( + new DirectoryPath(ShimsDirectory), + fileSystem: _fileSystem, + appHostShellShimMaker: new AppHostShellShimMakerMock(_fileSystem)), _reporter); } } diff --git a/test/dotnet.Tests/CommandTests/ToolUpdateCommandTests.cs b/test/dotnet.Tests/CommandTests/ToolUpdateCommandTests.cs index 4be734335..32a64274f 100644 --- a/test/dotnet.Tests/CommandTests/ToolUpdateCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/ToolUpdateCommandTests.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.EnvironmentAbstractions; using Xunit; using Parser = Microsoft.DotNet.Cli.Parser; using LocalizableStrings = Microsoft.DotNet.Tools.Tool.Update.LocalizableStrings; +using Microsoft.DotNet.ShellShim; namespace Microsoft.DotNet.Tests.Commands { @@ -139,7 +140,7 @@ namespace Microsoft.DotNet.Tests.Commands _mockFeeds ), installCallback: () => throw new ToolConfigurationException("Simulated error"))), - _ => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + _ => GetMockedShellShimRepository(), _reporter); Action a = () => command.Execute(); @@ -168,7 +169,7 @@ namespace Microsoft.DotNet.Tests.Commands _mockFeeds ), installCallback: () => throw new ToolConfigurationException("Simulated error"))), - _ => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + _ => GetMockedShellShimRepository(), _reporter); Action a = () => command.Execute(); @@ -216,7 +217,7 @@ namespace Microsoft.DotNet.Tests.Commands _reporter, _mockFeeds ))), - (_) => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + (_) => GetMockedShellShimRepository(), _environmentPathInstructionMock, _reporter); } @@ -236,8 +237,16 @@ namespace Microsoft.DotNet.Tests.Commands _reporter, _mockFeeds ))), - (_) => new ShellShimRepositoryMock(new DirectoryPath(ShimsDirectory), _fileSystem), + (_) => GetMockedShellShimRepository(), _reporter); } + + private ShellShimRepository GetMockedShellShimRepository() + { + return new ShellShimRepository( + new DirectoryPath(ShimsDirectory), + fileSystem: _fileSystem, + appHostShellShimMaker: new AppHostShellShimMakerMock(_fileSystem)); + } } }