File: Commands\Tool\Execute\ToolExecuteCommand.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 Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
using Microsoft.DotNet.Cli.Commands.Tool.Install;
using Microsoft.DotNet.Cli.Commands.Tool.Restore;
using Microsoft.DotNet.Cli.Commands.Tool.Run;
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.Configuration;
using NuGet.Versioning;

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

internal sealed class ToolExecuteCommand : CommandBase<ToolExecuteCommandDefinitionBase>
{
    private readonly PackageIdentityWithRange _packageToolIdentityArgument;
    private readonly IEnumerable<string> _forwardArguments;
    private readonly bool _allowRollForward;
    private readonly string? _configFile;
    private readonly string[] _sources;
    private readonly string[] _addSource;
    private readonly VerbosityOptions _verbosity;
    private readonly IToolPackageDownloader _toolPackageDownloader = ToolPackageFactory.CreateToolPackageStoresAndDownloader().downloader;

    private readonly RestoreActionConfig _restoreActionConfig;

    private readonly ToolManifestFinder _toolManifestFinder;

    public ToolExecuteCommand(ParseResult result, ToolManifestFinder? toolManifestFinder = null, string? currentWorkingDirectory = null)
        : base(result)
    {
        _packageToolIdentityArgument = result.GetValue(Definition.PackageIdentityArgument);
        _forwardArguments = result.GetValue(Definition.CommandArgument) ?? [];
        _allowRollForward = result.GetValue(Definition.RollForwardOption);
        _configFile = result.GetValue(Definition.ConfigOption);
        _sources = result.GetValue(Definition.SourceOption) ?? [];
        _addSource = result.GetValue(Definition.AddSourceOption) ?? [];
        _verbosity = result.GetValue(Definition.VerbosityOption);
        _restoreActionConfig = Definition.RestoreOptions.ToRestoreActionConfig(result);
        _toolManifestFinder = toolManifestFinder ?? new ToolManifestFinder(new DirectoryPath(currentWorkingDirectory ?? Directory.GetCurrentDirectory()));
    }

    public override int Execute()
    {
        var versionRange = VersionRangeUtilities.GetVersionRange(
            _packageToolIdentityArgument.VersionRange?.OriginalString,
            _parseResult.GetValue(Definition.VersionOption),
            _parseResult.GetValue(Definition.PrereleaseOption));

        PackageId packageId = new PackageId(_packageToolIdentityArgument.Id);

        var toolLocationActivity = Activities.Source.StartActivity("find-tool");
        toolLocationActivity?.SetTag("tool.package.id", packageId.ToString());
        toolLocationActivity?.SetTag("tool.package.version", versionRange?.ToString() ?? "latest");

        //  Look in local tools manifest first, but only if version is not specified
        if (versionRange == null)
        {
            var localToolsResolverCache = new LocalToolsResolverCache();

            if (_toolManifestFinder.TryFindPackageId(packageId, out var toolManifestPackage))
            {
                toolLocationActivity?.SetTag("tool.exec.kind", "local");
                toolLocationActivity?.Stop();

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

                var restoreResult = toolPackageRestorer.InstallPackage(toolManifestPackage, _configFile == null ? null : new FilePath(_configFile));

                if (!restoreResult.IsSuccess)
                {
                    Reporter.Error.WriteLine(restoreResult.Message.Red());
                    return 1;
                }

                if (restoreResult.SaveToCache is not null)
                {
                    localToolsResolverCache.Save(new Dictionary<RestoredCommandIdentifier, ToolCommand>
                    {
                        { restoreResult.SaveToCache.Value.restoredCommandIdentifier, restoreResult.SaveToCache.Value.toolCommand }
                    });
                }

                var localToolsCommandResolver = new LocalToolsCommandResolver(
                    _toolManifestFinder,
                    localToolsResolverCache);

                return ToolRunCommand.ExecuteCommand(localToolsCommandResolver, toolManifestPackage.CommandNames.Single().Value, _forwardArguments, _allowRollForward);
            }
        }

        var packageLocation = new PackageLocation(
                nugetConfig: _configFile != null ? new(_configFile) : null,
                sourceFeedOverrides: _sources,
                additionalFeeds: _addSource);

        (var bestVersion, var packageSource) = _toolPackageDownloader.GetNuGetVersion(packageLocation, packageId, _verbosity, versionRange, _restoreActionConfig);
        toolLocationActivity?.SetTag("tool.exec.kind", "one-shot");
        toolLocationActivity?.Stop();

        //  TargetFramework is null, which means to use the current framework.  Global tools can override the target framework to use (or select assets for),
        //  but we don't support this for local or one-shot tools.
        if (!_toolPackageDownloader.TryGetDownloadedTool(packageId, bestVersion, targetFramework: null, verbosity: _verbosity, out var toolPackage))
        {
            //  We've already determined which source we will use and will use it to download the package.
            //  So set the package location here to override the source feeds to just the source we already resolved to.
            //  This does mean that we won't work with feeds that have a primary package but where the RID-specific packages are on
            //  other feeds, but this is probably OK.
            var downloadPackageLocation = new PackageLocation(
                nugetConfig: _configFile != null ? new(_configFile) : null,
                sourceFeedOverrides: _sources,
                additionalFeeds: _addSource,
                packageSourceOverrides: [packageSource]);

            toolPackage = _toolPackageDownloader.InstallPackage(
                downloadPackageLocation,
                packageId: packageId,
                verbosity: _verbosity,
                versionRange: new VersionRange(bestVersion, true, bestVersion, true),
                isGlobalToolRollForward: false,
                restoreActionConfig: _restoreActionConfig);
        }

        using var toolExecuteActivity = Activities.Source.StartActivity("execute-tool");
        toolExecuteActivity?.SetTag("tool.package.id", packageId.ToString());
        toolExecuteActivity?.SetTag("tool.package.version", toolPackage.Version.ToString());
        toolExecuteActivity?.SetTag("tool.runner", toolPackage.Command.Runner);
        var commandSpec = ToolCommandSpecCreator.CreateToolCommandSpec(toolPackage.Command.Name.Value, toolPackage.Command.Executable.Value, toolPackage.Command.Runner, _allowRollForward, _forwardArguments);
        var command = CommandFactoryUsingResolver.Create(commandSpec);
        var result = command.Execute();
        return result.ExitCode;
    }
}