File: Commands\Tool\Restore\ToolRestoreCommand.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.

#nullable disable

using System.CommandLine;
using Microsoft.DotNet.Cli.CommandLine;
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.Frameworks;
using NuGet.Versioning;

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

internal class ToolRestoreCommand : CommandBase<ToolRestoreCommandDefinition>
{
    private readonly string _configFilePath;
    private readonly IReporter _errorReporter;
    private readonly ILocalToolsResolverCache _localToolsResolverCache;
    private readonly IToolManifestFinder _toolManifestFinder;
    private readonly IFileSystem _fileSystem;
    private readonly IReporter _reporter;
    private readonly string[] _sources;
    private readonly IToolPackageDownloader _toolPackageDownloader;
    private readonly VerbosityOptions _verbosity;
    private readonly RestoreActionConfig _restoreActionConfig;

    public ToolRestoreCommand(
        ParseResult result,
        IToolPackageDownloader toolPackageDownloader = null,
        IToolManifestFinder toolManifestFinder = null,
        ILocalToolsResolverCache localToolsResolverCache = null,
        IFileSystem fileSystem = null,
        IReporter reporter = null)
        : base(result)
    {
        if (toolPackageDownloader == null)
        {
            (IToolPackageStore,
                IToolPackageStoreQuery,
                IToolPackageDownloader downloader) toolPackageStoresAndInstaller
                    = ToolPackageFactory.CreateToolPackageStoresAndDownloader();
            _toolPackageDownloader = toolPackageStoresAndInstaller.downloader;
        }
        else
        {
            _toolPackageDownloader = toolPackageDownloader;
        }

        _toolManifestFinder
            = toolManifestFinder
              ?? new ToolManifestFinder(new DirectoryPath(Directory.GetCurrentDirectory()));

        _localToolsResolverCache = localToolsResolverCache ?? new LocalToolsResolverCache();
        _fileSystem = fileSystem ?? new FileSystemWrapper();

        _reporter = reporter ?? Reporter.Output;
        _errorReporter = reporter ?? Reporter.Error;

        _configFilePath = result.GetValue(Definition.ConfigOption);
        _sources = result.GetValue(Definition.AddSourceOption);
        _verbosity = result.GetValue(Definition.VerbosityOption);
        if (!result.HasOption(Definition.VerbosityOption) && result.GetValue(Definition.RestoreOptions.InteractiveOption))
        {
            _verbosity = VerbosityOptions.minimal;
        }

        _restoreActionConfig = Definition.RestoreOptions.ToRestoreActionConfig(result);
    }

    public override int Execute()
    {
        FilePath? customManifestFileLocation = GetCustomManifestFileLocation();

        FilePath? configFile = null;
        if (!string.IsNullOrEmpty(_configFilePath))
        {
            configFile = new FilePath(_configFilePath);
        }

        IReadOnlyCollection<ToolManifestPackage> packagesFromManifest;
        try
        {
            packagesFromManifest = _toolManifestFinder.Find(customManifestFileLocation);
        }
        catch (ToolManifestCannotBeFoundException e)
        {
            if (CommandLoggingContext.IsVerbose)
            {
                _reporter.WriteLine(string.Join(Environment.NewLine, e.VerboseMessage).Yellow());
            }

            _reporter.WriteLine(e.Message.Yellow());
            _reporter.WriteLine(CliCommandStrings.NoToolsWereRestored.Yellow());
            return 0;
        }

        var toolPackageRestorer = new ToolPackageRestorer(
            _toolPackageDownloader,
            _sources,
            overrideSources: [],
            _verbosity,
            _restoreActionConfig,
            _localToolsResolverCache,
            _fileSystem);

        ToolRestoreResult[] toolRestoreResults =
            [.. packagesFromManifest
                .AsEnumerable()
                .Select(package => toolPackageRestorer.InstallPackage(package, configFile))];

        Dictionary<RestoredCommandIdentifier, ToolCommand> downloaded =
            toolRestoreResults.Select(result => result.SaveToCache)
                .Where(item => item is not null)
                .ToDictionary(pair => pair.Value.restoredCommandIdentifier, pair => pair.Value.toolCommand);

        EnsureNoCommandNameCollision(downloaded);

        _localToolsResolverCache.Save(downloaded);

        return PrintConclusionAndReturn(toolRestoreResults);
    }

    private int PrintConclusionAndReturn(ToolRestoreResult[] toolRestoreResults)
    {
        if (toolRestoreResults.Any(r => !r.IsSuccess))
        {
            _reporter.WriteLine();
            _errorReporter.WriteLine(string.Join(
                Environment.NewLine,
                toolRestoreResults.Where(r => !r.IsSuccess).Select(r => r.Message)).Red());

            var successMessage = toolRestoreResults.Where(r => r.IsSuccess).Select(r => r.Message);
            if (successMessage.Any())
            {
                _reporter.WriteLine();
                _reporter.WriteLine(string.Join(Environment.NewLine, successMessage));
                
                // Display warnings for successful restorations even in partial failure case
                var warnings = toolRestoreResults.Where(r => r.IsSuccess && !string.IsNullOrEmpty(r.Warning)).Select(r => r.Warning);
                if (warnings.Any())
                {
                    _reporter.WriteLine();
                    _reporter.WriteLine(string.Join(Environment.NewLine, warnings).Yellow());
                }
            }

            _errorReporter.WriteLine(Environment.NewLine +
                                     (toolRestoreResults.Any(r => r.IsSuccess)
                                         ? CliCommandStrings.RestorePartiallyFailed
                                         : CliCommandStrings.RestoreFailed).Red());

            return 1;
        }
        else
        {
            _reporter.WriteLine(string.Join(Environment.NewLine,
                toolRestoreResults.Where(r => r.IsSuccess).Select(r => r.Message)));
            
            // Display warnings for newer versions available
            var warnings = toolRestoreResults.Where(r => r.IsSuccess && !string.IsNullOrEmpty(r.Warning)).Select(r => r.Warning);
            if (warnings.Any())
            {
                _reporter.WriteLine();
                _reporter.WriteLine(string.Join(Environment.NewLine, warnings).Yellow());
            }
            
            _reporter.WriteLine();
            _reporter.WriteLine(CliCommandStrings.LocalToolsRestoreWasSuccessful.Green());

            return 0;
        }
    }

    private FilePath? GetCustomManifestFileLocation()
    {
        string customFile = _parseResult.GetValue(Definition.ToolManifestOption);
        FilePath? customManifestFileLocation;
        if (!string.IsNullOrEmpty(customFile))
        {
            customManifestFileLocation = new FilePath(customFile);
        }
        else
        {
            customManifestFileLocation = null;
        }

        return customManifestFileLocation;
    }

    private static void EnsureNoCommandNameCollision(Dictionary<RestoredCommandIdentifier, ToolCommand> dictionary)
    {
        string[] errors = [.. dictionary
            .Select(pair => (pair.Key.PackageId, pair.Key.CommandName))
            .GroupBy(packageIdAndCommandName => packageIdAndCommandName.CommandName)
            .Where(grouped => grouped.Count() > 1)
            .Select(nonUniquePackageIdAndCommandNames =>
                string.Format(CliCommandStrings.PackagesCommandNameCollisionConclusion,
                    string.Join(Environment.NewLine,
                        nonUniquePackageIdAndCommandNames.Select(
                            p => "\t" + string.Format(
                                CliCommandStrings.PackagesCommandNameCollisionForOnePackage,
                                p.CommandName.Value,
                                p.PackageId.ToString())))))];

        if (errors.Any())
        {
            throw new ToolPackageException(string.Join(Environment.NewLine, errors));
        }
    }

    public struct ToolRestoreResult
    {
        public (RestoredCommandIdentifier restoredCommandIdentifier, ToolCommand toolCommand)? SaveToCache { get; }
        public bool IsSuccess { get; }
        public string Message { get; }
        public string Warning { get; }

        private ToolRestoreResult(
            (RestoredCommandIdentifier, ToolCommand)? saveToCache,
            bool isSuccess, string message, string warning = null)
        {
            if (string.IsNullOrWhiteSpace(message))
            {
                throw new ArgumentException("message", nameof(message));
            }

            SaveToCache = saveToCache;
            IsSuccess = isSuccess;
            Message = message;
            Warning = warning;
        }

        public static ToolRestoreResult Success(
            (RestoredCommandIdentifier, ToolCommand)? saveToCache,
            string message,
            string warning = null)
        {
            return new ToolRestoreResult(saveToCache, true, message, warning);
        }

        public static ToolRestoreResult Failure(string message)
        {
            return new ToolRestoreResult(null, false, message, null);
        }

        public static ToolRestoreResult Failure(
            PackageId packageId,
            ToolPackageException toolPackageException)
        {
            return new ToolRestoreResult(null, false,
                string.Format(CliCommandStrings.PackageFailedToRestore,
                    packageId.ToString(), toolPackageException.ToString()), null);
        }
    }
}