File: Commands\Tool\Install\ToolInstallLocalCommand.cs
Web Access
Project: src\src\sdk\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 System.Diagnostics;
using Microsoft.DotNet.Cli.Commands.Tool.Update;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.ToolManifest;
using Microsoft.DotNet.Cli.ToolPackage;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Versioning;

namespace Microsoft.DotNet.Cli.Commands.Tool.Install;

internal sealed class ToolInstallLocalCommand : CommandBase<ToolUpdateInstallCommandDefinition>
{
    private readonly IToolManifestFinder _toolManifestFinder;
    private readonly IToolManifestEditor _toolManifestEditor;
    private readonly ILocalToolsResolverCache _localToolsResolverCache;
    private readonly ToolInstallLocalInstaller _toolLocalPackageInstaller;
    private readonly IReporter _reporter;
    private readonly PackageIdentityWithRange? _packageIdentityWithRange;
    private readonly bool _allowPackageDowngrade;

    private readonly string? _explicitManifestFile;
    private readonly bool _createManifestIfNeeded;
    private readonly bool _allowRollForward;
    private readonly bool _updateAll;

    internal RestoreActionConfig restoreActionConfig;

    public ToolInstallLocalCommand(
        ParseResult parseResult,
        IToolPackageDownloader? toolPackageDownloader = null,
        IToolManifestFinder? toolManifestFinder = null,
        IToolManifestEditor? toolManifestEditor = null,
        ILocalToolsResolverCache? localToolsResolverCache = null,
        IReporter? reporter = null,
        string? runtimeJsonPathForTests = null)
        : base(parseResult)
    {
        if (Definition is ToolUpdateCommandDefinition updateDef)
        {
            _updateAll = parseResult.GetValue(updateDef.UpdateAllOption);
            _packageIdentityWithRange = parseResult.GetValue(updateDef.PackageIdentityArgument);
        }
        else
        {
            var installDef = (ToolInstallCommandDefinition)Definition;
            _packageIdentityWithRange = parseResult.GetValue(installDef.PackageIdentityArgument);
            _createManifestIfNeeded = parseResult.GetValue(installDef.CreateManifestIfNeededOption);
            _allowRollForward = parseResult.GetValue(installDef.RollForwardOption);
        }

        _explicitManifestFile = parseResult.GetValue(Definition.ToolManifestOption);

        _reporter = reporter ?? Reporter.Output;

        _toolManifestFinder = toolManifestFinder ?? new ToolManifestFinder(new DirectoryPath(Directory.GetCurrentDirectory()));
        _toolManifestEditor = toolManifestEditor ?? new ToolManifestEditor();
        _localToolsResolverCache = localToolsResolverCache ?? new LocalToolsResolverCache();

        restoreActionConfig = Definition.RestoreOptions.ToRestoreActionConfig(parseResult);

        _toolLocalPackageInstaller = new ToolInstallLocalInstaller(
            configFilePath: parseResult.GetValue(Definition.ConfigOption),
            sources: parseResult.GetValue(Definition.AddSourceOption),
            sourceFeedOverrides: parseResult.GetValue(Definition.SourceOption),
            verbosity: parseResult.GetValue(Definition.VerbosityOption),
            toolPackageDownloader,
            runtimeJsonPathForTests,
            restoreActionConfig);

        _allowPackageDowngrade = parseResult.GetValue(Definition.AllowPackageDowngradeOption);
    }

    public override int Execute()
    {
        if (_updateAll)
        {
            foreach (var (manifestPackage, _) in ((IToolManifestInspector)_toolManifestFinder).Inspect())
            {
                ExecuteInstallCommand(manifestPackage.PackageId, versionRange: null);
            }

            return 0;
        }

        // package id must be specified (UpdateToolCommandInvalidAllAndPackageId is reported otherwise):
        Debug.Assert(_packageIdentityWithRange != null);
        var packageId = new PackageId(_packageIdentityWithRange.Value.Id);

        var versionRange = VersionRangeUtilities.GetVersionRange(
            _packageIdentityWithRange.Value.VersionRange?.OriginalString,
            _parseResult.GetValue(Definition.VersionOption),
            _parseResult.GetValue(Definition.PrereleaseOption));

        return ExecuteInstallCommand(packageId, versionRange);
    }

    private int ExecuteInstallCommand(PackageId packageId, VersionRange? versionRange)
    {
        FilePath manifestFile = GetManifestFilePath();

        (FilePath? manifestFileOptional, string warningMessage) =
            _toolManifestFinder.ExplicitManifestOrFindManifestContainPackageId(_explicitManifestFile, packageId);

        if (warningMessage != null)
        {
            _reporter.WriteLine(warningMessage.Yellow());
        }

        manifestFile = manifestFileOptional ?? GetManifestFilePath();
        var existingPackageWithPackageId = _toolManifestFinder.Find(manifestFile).Where(p => p.PackageId.Equals(packageId));

        if (!existingPackageWithPackageId.Any())
        {
            return InstallNewTool(manifestFile, packageId, versionRange);
        }

        var existingPackage = existingPackageWithPackageId.Single();
        var toolDownloadedPackage = _toolLocalPackageInstaller.Install(manifestFile, packageId, versionRange);

        InstallToolUpdate(existingPackage, toolDownloadedPackage, manifestFile, packageId);

        _localToolsResolverCache.SaveToolPackage(
            toolDownloadedPackage,
            _toolLocalPackageInstaller.TargetFrameworkToInstall);

        return 0;
    }

    public int InstallToolUpdate(ToolManifestPackage existingPackage, IToolPackage toolDownloadedPackage, FilePath manifestFile, PackageId packageId)
    {
        if (existingPackage.Version > toolDownloadedPackage.Version && !_allowPackageDowngrade)
        {
            throw new GracefulException(
                [
                    string.Format(
                        CliCommandStrings.UpdateLocalToolToLowerVersion,
                        toolDownloadedPackage.Version.ToNormalizedString(),
                        existingPackage.Version.ToNormalizedString(),
                        manifestFile.Value)
                ],
                isUserError: false);
        }
        else if (existingPackage.Version == toolDownloadedPackage.Version)
        {
            _reporter.WriteLine(
                string.Format(
                    CliCommandStrings.UpdateLocaToolSucceededVersionNoChange,
                    toolDownloadedPackage.Id,
                    existingPackage.Version.ToNormalizedString(),
                    manifestFile.Value));
        }
        else
        {
            _toolManifestEditor.Edit(
                manifestFile,
                packageId,
                toolDownloadedPackage.Version,
                [toolDownloadedPackage.Command.Name]);
            _reporter.WriteLine(
                string.Format(
                    CliCommandStrings.UpdateLocalToolSucceeded,
                    toolDownloadedPackage.Id,
                    existingPackage.Version.ToNormalizedString(),
                    toolDownloadedPackage.Version.ToNormalizedString(),
                    manifestFile.Value).Green());
        }

        _localToolsResolverCache.SaveToolPackage(
            toolDownloadedPackage,
            _toolLocalPackageInstaller.TargetFrameworkToInstall);

        return 0;
    }

    public int InstallNewTool(FilePath manifestFile, PackageId packageId, VersionRange? versionRange)
    {
        IToolPackage toolDownloadedPackage =
            _toolLocalPackageInstaller.Install(manifestFile, packageId, versionRange);

        _toolManifestEditor.Add(
            manifestFile,
            toolDownloadedPackage.Id,
            toolDownloadedPackage.Version,
            [toolDownloadedPackage.Command.Name],
            _allowRollForward);

        _localToolsResolverCache.SaveToolPackage(
            toolDownloadedPackage,
            _toolLocalPackageInstaller.TargetFrameworkToInstall);

        _reporter.WriteLine(
            string.Format(
                CliCommandStrings.LocalToolInstallationSucceeded,
                toolDownloadedPackage.Command.Name,
                toolDownloadedPackage.Id,
                toolDownloadedPackage.Version.ToNormalizedString(),
                manifestFile.Value).Green());

        return 0;
    }

    public FilePath GetManifestFilePath()
    {
        return string.IsNullOrWhiteSpace(_explicitManifestFile)
            ? _toolManifestFinder.FindFirst(_createManifestIfNeeded)
            : new FilePath(_explicitManifestFile);
    }
}