File: Commands\Package\Add\PackageAddCommand.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.CommandLine;
using Microsoft.DotNet.Cli.Commands.MSBuild;
using Microsoft.DotNet.Cli.Commands.NuGet;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.ProjectTools;

namespace Microsoft.DotNet.Cli.Commands.Package.Add;

internal sealed class PackageAddCommand : CommandBase<PackageAddCommandDefinitionBase>
{
    private readonly PackageIdentityWithRange _packageId;

    public PackageAddCommand(ParseResult parseResult)
        : base(parseResult)
    {
        _packageId = parseResult.GetValue(Definition.PackageIdArgument);
    }

    public override int Execute()
    {
        var (fileOrDirectory, allowedAppKinds) = PackageCommandParser.ProcessPathOptions(Definition.FileOption, Definition.ProjectOption, Definition.GetProjectOrFileArgument(), _parseResult);

        bool isFileBasedApp = allowedAppKinds.HasFlag(AppKinds.FileBased) && VirtualProjectBuilder.IsValidEntryPointPath(fileOrDirectory);

        Debug.Assert(isFileBasedApp || allowedAppKinds.HasFlag(AppKinds.ProjectBased));

        string projectFilePath;
        if (!File.Exists(fileOrDirectory))
        {
            Debug.Assert(!isFileBasedApp);
            projectFilePath = MsbuildProject.GetProjectFileFromDirectory(fileOrDirectory);
        }
        else
        {
            projectFilePath = fileOrDirectory;
        }

        if (isFileBasedApp)
        {
            projectFilePath = Path.GetFullPath(projectFilePath);
        }

        var tempDgFilePath = string.Empty;

        if (!_parseResult.GetValue(Definition.NoRestoreOption))
        {
            try
            {
                // Create a Dependency Graph file for the project
                tempDgFilePath = Path.GetTempFileName();
            }
            catch (IOException ioex)
            {
                // Catch IOException from Path.GetTempFileName() and throw a graceful exception to the user.
                throw new GracefulException(string.Format(CliCommandStrings.CmdDGFileIOException, projectFilePath), ioex);
            }

            GetProjectDependencyGraph(projectFilePath, tempDgFilePath, isFileBasedApp);
        }

        var args = TransformArgs(
            _packageId,
            tempDgFilePath,
            projectFilePath);

        var result = NuGetCommand.Run(args, isFileBasedApp);

        DisposeTemporaryFile(tempDgFilePath);

        return result;
    }

    private static void GetProjectDependencyGraph(string projectFilePath, string dgFilePath, bool isFileBasedApp)
    {
        int result;
        if (isFileBasedApp)
        {
            result = new VirtualProjectBuildingCommand(
                projectFilePath,
                MSBuildArgs
                    .FromProperties(new Dictionary<string, string>
                    {
                        { "RestoreGraphOutputPath", dgFilePath },
                        { "RestoreRecursive", "false" },
                        { "RestoreDotnetCliToolReferences", "false" },
                    }.AsReadOnly())
                    .CloneWithVerbosity(VerbosityOptions.quiet)
                    .CloneWithAdditionalTargets("GenerateRestoreGraphFile"))
            {
                NoRestore = true,
                NoCache = true,
                NoWriteBuildMarkers = true,
            }.Execute();
        }
        else
        {
            result = new MSBuildForwardingApp(
                [
                    // Pass the project file path
                    projectFilePath,

                    // Pass the task as generate restore Dependency Graph file
                    "-target:GenerateRestoreGraphFile",

                    // Pass Dependency Graph file output path
                    $"-property:RestoreGraphOutputPath=\"{dgFilePath}\"",

                    // Turn off recursive restore
                    "-property:RestoreRecursive=false",

                    // Turn off restore for Dotnet cli tool references so that we do not generate extra dg specs
                    "-property:RestoreDotnetCliToolReferences=false",

                    // Output should not include MSBuild version header
                    "--nologo",

                    // Set verbosity to quiet to avoid cluttering the output for this 'inner' build
                    "-v:quiet"
                ]).Execute();
        }

        if (result != 0)
        {
            throw new GracefulException(string.Format(CliCommandStrings.CmdDGFileException, projectFilePath));
        }
    }

    private static void DisposeTemporaryFile(string filePath)
    {
        if (File.Exists(filePath))
        {
            File.Delete(filePath);
        }
    }

    private string[] TransformArgs(PackageIdentityWithRange packageId, string tempDgFilePath, string projectFilePath)
    {
        List<string> args = [
            "package",
            "add",
            "--package",
            packageId.Id,
            "--project",
            projectFilePath
        ];

        if (packageId.HasVersion)
        {
            args.Add("--version");
            args.Add(packageId.VersionRange.OriginalString ?? string.Empty);
        }

        args.AddRange(_parseResult
            .OptionValuesToBeForwarded()
            .SelectMany(a => a.Split(' ', 2)));

        if (_parseResult.GetValue(Definition.NoRestoreOption))
        {
            args.Add("--no-restore");
        }
        else
        {
            args.Add("--dg-file");
            args.Add(tempDgFilePath);
        }

        return [.. args];
    }
}