File: Commands\Tool\Install\ToolInstallGlobalOrToolPathCommand.cs
Web Access
Project: ..\..\..\src\Cli\dotnet\dotnet.csproj (dotnet)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.CommandLine;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.Versioning;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.ShellShim;
using Microsoft.DotNet.Cli.Commands.Tool.Update;
using Microsoft.DotNet.Cli.Commands.Tool.Uninstall;
using Microsoft.DotNet.Cli.Commands.Tool.List;
 
namespace Microsoft.DotNet.Cli.Commands.Tool.Install;
 
internal delegate IShellShimRepository CreateShellShimRepository(string appHostSourceDirectory, DirectoryPath? nonGlobalLocation = null);
 
internal delegate (IToolPackageStore, IToolPackageStoreQuery, IToolPackageDownloader) CreateToolPackageStoresAndDownloader(
    DirectoryPath? nonGlobalLocation = null,
    IEnumerable<string>? forwardRestoreArguments = null);
 
internal class ToolInstallGlobalOrToolPathCommand : CommandBase
{
    private readonly IEnvironmentPathInstruction _environmentPathInstruction;
    private readonly IReporter _reporter;
    private readonly CreateShellShimRepository _createShellShimRepository;
    private readonly CreateToolPackageStoresAndDownloaderAndUninstaller _createToolPackageStoreDownloaderUninstaller;
    private readonly ShellShimTemplateFinder _shellShimTemplateFinder;
    private readonly IToolPackageStoreQuery? _store;
 
    private readonly PackageId? _packageId;
    private readonly string? _configFilePath;
    private readonly string? _framework;
    private readonly string[]? _source;
    private readonly string[]? _addSource;
    private readonly bool _global;
    private readonly VerbosityOptions _verbosity;
    private readonly string? _toolPath;
    private readonly string? _architectureOption;
    private readonly IEnumerable<string> _forwardRestoreArguments;
    private readonly bool _allowRollForward;
    private readonly bool _allowPackageDowngrade;
    private readonly bool _updateAll;
    private readonly string? _currentWorkingDirectory;
    private readonly bool? _verifySignatures;
 
    internal readonly RestoreActionConfig _restoreActionConfig;
 
    public ToolInstallGlobalOrToolPathCommand(
        ParseResult parseResult,
        PackageId? packageId = null,
        CreateToolPackageStoresAndDownloaderAndUninstaller? createToolPackageStoreDownloaderUninstaller = null,
        CreateShellShimRepository? createShellShimRepository = null,
        IEnvironmentPathInstruction? environmentPathInstruction = null,
        IReporter? reporter = null,
        INuGetPackageDownloader? nugetPackageDownloader = null,
        IToolPackageStoreQuery? store = null,
        string? currentWorkingDirectory = null,
        bool? verifySignatures = null)
        : base(parseResult)
    {
        _verifySignatures = verifySignatures;
        _currentWorkingDirectory = currentWorkingDirectory;
 
        var packageIdArgument = parseResult.GetValue(ToolInstallCommandParser.PackageIdentityArgument).Id;
 
        _packageId = packageId ?? (packageIdArgument is not null ? new PackageId(packageIdArgument) : null);
        _configFilePath = parseResult.GetValue(ToolInstallCommandParser.ConfigOption);
        _framework = parseResult.GetValue(ToolInstallCommandParser.FrameworkOption);
        _source = parseResult.GetValue(ToolInstallCommandParser.SourceOption);
        _addSource = parseResult.GetValue(ToolInstallCommandParser.AddSourceOption);
        _global = parseResult.GetValue(ToolInstallCommandParser.GlobalOption);
        _verbosity = GetValueOrDefault(ToolInstallCommandParser.VerbosityOption, VerbosityOptions.minimal, parseResult);
        _toolPath = parseResult.GetValue(ToolInstallCommandParser.ToolPathOption);
        _architectureOption = parseResult.GetValue(ToolInstallCommandParser.ArchitectureOption);
 
        _forwardRestoreArguments = parseResult.OptionValuesToBeForwarded(ToolInstallCommandParser.GetCommand());
 
        _environmentPathInstruction = environmentPathInstruction
            ?? EnvironmentPathFactory.CreateEnvironmentPathInstruction();
        _createShellShimRepository = createShellShimRepository ?? ShellShimRepositoryFactory.CreateShellShimRepository;
        var tempDir = new DirectoryPath(PathUtilities.CreateTempSubdirectory());
        var configOption = parseResult.GetValue(ToolInstallCommandParser.ConfigOption);
        var sourceOption = parseResult.GetValue(ToolInstallCommandParser.AddSourceOption);
        var packageSourceLocation = new PackageSourceLocation(string.IsNullOrEmpty(configOption) ? null : new FilePath(configOption), additionalSourceFeeds: sourceOption, basePath: _currentWorkingDirectory);
        _restoreActionConfig = new RestoreActionConfig(DisableParallel: parseResult.GetValue(ToolCommandRestorePassThroughOptions.DisableParallelOption),
            NoCache: parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoCacheOption) || parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoHttpCacheOption),
            IgnoreFailedSources: parseResult.GetValue(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption),
            Interactive: parseResult.GetValue(ToolCommandRestorePassThroughOptions.InteractiveRestoreOption));
        nugetPackageDownloader ??= new NuGetPackageDownloader.NuGetPackageDownloader(tempDir, verboseLogger: new NullLogger(), restoreActionConfig: _restoreActionConfig, verbosityOptions: _verbosity, verifySignatures: verifySignatures ?? true, shouldUsePackageSourceMapping: true);
        _shellShimTemplateFinder = new ShellShimTemplateFinder(nugetPackageDownloader, tempDir, packageSourceLocation);
        _store = store;
 
        _allowRollForward = parseResult.GetValue(ToolInstallCommandParser.RollForwardOption);
 
        _allowPackageDowngrade = parseResult.GetValue(ToolInstallCommandParser.AllowPackageDowngradeOption);
        _createToolPackageStoreDownloaderUninstaller = createToolPackageStoreDownloaderUninstaller ??
                                              ToolPackageFactory.CreateToolPackageStoresAndDownloaderAndUninstaller;
        _updateAll = parseResult.GetValue(ToolUpdateCommandParser.UpdateAllOption);
 
        _reporter = reporter ?? Reporter.Output;
    }
 
    public static T GetValueOrDefault<T>(Option<T> option, T defaultOption, ParseResult parseResult)
    {
        if (parseResult.GetResult(option) is { } result &&
            result.GetValueOrDefault<T>() is { } t)
        {
            return t;
        }
 
        return defaultOption;
    }
 
    public override int Execute()
    {
        if (_updateAll)
        {
            var toolListCommand = new ToolListGlobalOrToolPathCommand(_parseResult, toolPath => { return _store; });
            var toolIds = toolListCommand.GetPackages(null, null);
            foreach (var toolId in toolIds)
            {
                ExecuteInstallCommand(new PackageId(toolId.Id.ToString()));
            }
            return 0;
        }
 
        if (_packageId is null)
        {
            throw new GracefulException(CliCommandStrings.ToolInstallPackageIdMissing);
        }
 
        return ExecuteInstallCommand((PackageId)_packageId);
    }
 
    private int ExecuteInstallCommand(PackageId packageId)
    {
        using var _activity = Activities.Source.StartActivity("install-tool");
        _activity?.DisplayName = $"Install {packageId}";
        _activity?.SetTag("tool.package.id", packageId);
 
        if (!string.IsNullOrEmpty(_configFilePath) && !File.Exists(_configFilePath))
        {
            throw new GracefulException(string.Format(CliCommandStrings.ToolInstallNuGetConfigurationFileDoesNotExist, Path.GetFullPath(_configFilePath)));
        }
 
        DirectoryPath? toolPath = null;
        if (!string.IsNullOrEmpty(_toolPath))
        {
            toolPath = new DirectoryPath(_toolPath);
        }
 
        VersionRange? versionRange = _parseResult.GetVersionRange();
 
        (IToolPackageStore toolPackageStore,
         IToolPackageStoreQuery toolPackageStoreQuery,
         IToolPackageDownloader toolPackageDownloader,
         IToolPackageUninstaller toolPackageUninstaller) = _createToolPackageStoreDownloaderUninstaller(toolPath, _forwardRestoreArguments, _currentWorkingDirectory);
 
        var appHostSourceDirectory = ShellShimTemplateFinder.GetDefaultAppHostSourceDirectory();
        IShellShimRepository shellShimRepository = _createShellShimRepository(appHostSourceDirectory, toolPath);
 
        IToolPackage? oldPackageNullable = GetOldPackage(toolPackageStoreQuery, packageId);
 
        if (oldPackageNullable is not null)
        {
            NuGetVersion nugetVersion = GetBestMatchNugetVersion(packageId, versionRange, toolPackageDownloader);
            _activity?.DisplayName = $"Install {packageId}@{nugetVersion}";
            _activity?.SetTag("tool.package.id", packageId);
            _activity?.SetTag("tool.package.version", nugetVersion);
 
            if (ToolVersionAlreadyInstalled(oldPackageNullable, nugetVersion))
            {
                _reporter.WriteLine(string.Format(CliCommandStrings.ToolAlreadyInstalled, oldPackageNullable.Id, oldPackageNullable.Version.ToNormalizedString()).Green());
                return 0;
            }
        }
 
        TransactionalAction.Run(() =>
        {
            if (oldPackageNullable is not null)
            {
                RunWithHandlingUninstallError(() =>
                {
                    shellShimRepository.RemoveShim(oldPackageNullable.Command);
                    toolPackageUninstaller.Uninstall(oldPackageNullable.PackageDirectory);
                }, packageId);
            }
 
            RunWithHandlingInstallError(() =>
            {
                var toolPackageDownloaderActivity = Activities.Source.StartActivity("download-tool-package");
                IToolPackage newInstalledPackage = toolPackageDownloader.InstallPackage(
                    new PackageLocation(nugetConfig: GetConfigFile(), sourceFeedOverrides: _source, additionalFeeds: _addSource),
                    packageId: packageId,
                    versionRange: versionRange,
                    targetFramework: _framework,
                    verbosity: _verbosity,
                    isGlobalTool: true,
                    isGlobalToolRollForward: _allowRollForward,
                    verifySignatures: _verifySignatures ?? true,
                    restoreActionConfig: _restoreActionConfig
                );
                toolPackageDownloaderActivity?.Dispose();
 
                EnsureVersionIsHigher(oldPackageNullable, newInstalledPackage, _allowPackageDowngrade);
 
                NuGetFramework? framework;
                if (string.IsNullOrEmpty(_framework) && newInstalledPackage.Frameworks.Count() > 0)
                {
                    framework = newInstalledPackage.Frameworks
                        .Where(f => f.Version < new NuGetVersion(Product.Version).Version)
                        .MaxBy(f => f.Version);
                }
                else
                {
                    framework = string.IsNullOrEmpty(_framework) ? null : NuGetFramework.Parse(_framework);
                }
                var shimActivity = Activities.Source.StartActivity("create-shell-shim");
                string appHostSourceDirectory = _shellShimTemplateFinder.ResolveAppHostSourceDirectoryAsync(_architectureOption, framework, RuntimeInformation.ProcessArchitecture).Result;
                shellShimRepository.CreateShim(newInstalledPackage.Command, newInstalledPackage.PackagedShims);
                shimActivity?.Dispose();
 
                foreach (string w in newInstalledPackage.Warnings)
                {
                    _reporter.WriteLine(w.Yellow());
                }
                if (_global)
                {
                    _environmentPathInstruction.PrintAddPathInstructionIfPathDoesNotExist();
                }
 
                PrintSuccessMessage(oldPackageNullable, newInstalledPackage);
            }, packageId);
        });
 
        return 0;
    }
 
    private NuGetVersion GetBestMatchNugetVersion(PackageId packageId, VersionRange? versionRange, IToolPackageDownloader toolPackageDownloader)
    {
        return toolPackageDownloader.GetNuGetVersion(
            packageLocation: new PackageLocation(nugetConfig: GetConfigFile(), sourceFeedOverrides: _source, additionalFeeds: _addSource),
            packageId: packageId,
            versionRange: versionRange,
            verbosity: _verbosity,
            restoreActionConfig: _restoreActionConfig
        ).version;
    }
 
    private static bool ToolVersionAlreadyInstalled(IToolPackage? oldPackageNullable, NuGetVersion nuGetVersion)
    {
        return oldPackageNullable != null && oldPackageNullable.Version == nuGetVersion;
    }
 
    private static void EnsureVersionIsHigher(IToolPackage? oldPackageNullable, IToolPackage newInstalledPackage, bool allowDowngrade)
    {
        if (oldPackageNullable != null && newInstalledPackage.Version < oldPackageNullable.Version && !allowDowngrade)
        {
            throw new GracefulException(
            [
                string.Format(CliCommandStrings.UpdateToLowerVersion,
                    newInstalledPackage.Version.ToNormalizedString(),
                    oldPackageNullable.Version.ToNormalizedString())
            ], isUserError: false);
        }
    }
 
    private static void RunWithHandlingInstallError(Action installAction, PackageId packageId)
    {
        try
        {
            installAction();
        }
        catch (Exception ex)
            when (InstallToolCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex))
        {
            var message = new List<string>
            {
                string.Format(CliCommandStrings.UpdateToolFailed, packageId)
            };
            message.AddRange(InstallToolCommandLowLevelErrorConverter.GetUserFacingMessages(ex, packageId));
 
            throw new GracefulException(
                messages: message,
                verboseMessages: [ex.ToString()],
                isUserError: false);
        }
    }
 
    private static void RunWithHandlingUninstallError(Action uninstallAction, PackageId packageId)
    {
        try
        {
            uninstallAction();
        }
        catch (Exception ex)
            when (ToolUninstallCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex))
        {
            var message = new List<string>
            {
                string.Format(CliCommandStrings.UpdateToolFailed, packageId)
            };
            message.AddRange(ToolUninstallCommandLowLevelErrorConverter.GetUserFacingMessages(ex, packageId));
 
            throw new GracefulException(
                messages: message,
                verboseMessages: [ex.ToString()],
                isUserError: false);
        }
    }
 
    private FilePath? GetConfigFile()
    {
        FilePath? configFile = null;
        if (!string.IsNullOrEmpty(_configFilePath))
        {
            configFile = new FilePath(_configFilePath);
        }
 
        return configFile;
    }
 
    private static IToolPackage? GetOldPackage(IToolPackageStoreQuery toolPackageStoreQuery, PackageId packageId)
    {
        IToolPackage? oldPackageNullable;
        try
        {
            oldPackageNullable = toolPackageStoreQuery.EnumeratePackageVersions(packageId).SingleOrDefault();
        }
        catch (InvalidOperationException)
        {
            throw new GracefulException(messages:
            [
                string.Format(CliCommandStrings.ToolUpdateToolHasMultipleVersionsInstalled, packageId)
            ], isUserError: false);
        }
 
        return oldPackageNullable;
    }
 
    private void PrintSuccessMessage(IToolPackage? oldPackage, IToolPackage newInstalledPackage)
    {
        if (!_verbosity.IsQuiet())
        {
            if (oldPackage == null)
            {
                _reporter.WriteLine(
                    string.Format(
                        CliCommandStrings.ToolInstallInstallationSucceeded,
                        newInstalledPackage.Command.Name,
                        newInstalledPackage.Id,
                        newInstalledPackage.Version.ToNormalizedString()).Green());
            }
            else if (oldPackage.Version != newInstalledPackage.Version)
            {
                _reporter.WriteLine(
                    string.Format(
                        CliCommandStrings.ToolUpdateUpdateSucceeded,
                        newInstalledPackage.Id,
                        oldPackage.Version.ToNormalizedString(),
                        newInstalledPackage.Version.ToNormalizedString()).Green());
            }
            else
            {
                _reporter.WriteLine(
                    string.Format(
                        newInstalledPackage.Version.IsPrerelease ?
                        CliCommandStrings.UpdateSucceededPreVersionNoChange : CliCommandStrings.UpdateSucceededStableVersionNoChange,
                        newInstalledPackage.Id, newInstalledPackage.Version).Green());
            }
        }
    }
}