File: NuGet\Commands\RestoreCommand.cs
Web Access
Project: src\src\Aspire.Managed\Aspire.Managed.csproj (aspire-managed)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using System.CommandLine;
using NuGet.Commands;
using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel;
using NuGet.Protocol.Core.Types;
using NuGet.RuntimeModel;
using NuGet.Versioning;
 
namespace Aspire.Managed.NuGet.Commands;
 
/// <summary>
/// Restore command - restores NuGet packages without requiring a .csproj file.
/// Uses NuGet's RestoreRunner to produce a project.assets.json file.
/// </summary>
public static class RestoreCommand
{
    private const string RuntimeIdentifierGraphFileName = "RuntimeIdentifierGraph.json";
    private const string RuntimeIdentifierGraphResourceName = "Aspire.Managed.RuntimeIdentifierGraph.json";
 
    /// <summary>
    /// Creates the restore command.
    /// </summary>
    public static Command Create()
    {
        var command = new Command("restore", "Restore NuGet packages");
 
        var packageOption = new Option<string[]>("--package", "-p")
        {
            Description = "Package reference as 'PackageId,Version' (can specify multiple)",
            Required = true,
            AllowMultipleArgumentsPerToken = true
        };
        command.Options.Add(packageOption);
 
        var frameworkOption = new Option<string>("--framework", "-f")
        {
            Description = "Target framework (default: net10.0)",
            DefaultValueFactory = _ => "net10.0"
        };
        command.Options.Add(frameworkOption);
 
        var runtimeIdentifierOption = new Option<string?>("--runtime-identifier", "--rid")
        {
            Description = "Runtime identifier to restore for"
        };
        command.Options.Add(runtimeIdentifierOption);
 
        var outputOption = new Option<string>("--output", "-o")
        {
            Description = "Output directory for project.assets.json",
            DefaultValueFactory = _ => "./obj"
        };
        command.Options.Add(outputOption);
 
        var sourceOption = new Option<string[]>("--source")
        {
            Description = "NuGet feed URL (can specify multiple)",
            DefaultValueFactory = _ => Array.Empty<string>(),
            AllowMultipleArgumentsPerToken = true
        };
        command.Options.Add(sourceOption);
 
        var configOption = new Option<string?>("--nuget-config")
        {
            Description = "Path to nuget.config file"
        };
        command.Options.Add(configOption);
 
        var workingDirOption = new Option<string?>("--working-dir", "-w")
        {
            Description = "Working directory for nuget.config discovery"
        };
        command.Options.Add(workingDirOption);
 
        var noNugetOrgOption = new Option<bool>("--no-nuget-org")
        {
            Description = "Don't add nuget.org as fallback source"
        };
        command.Options.Add(noNugetOrgOption);
 
        var verboseOption = new Option<bool>("--verbose")
        {
            Description = "Enable verbose output"
        };
        command.Options.Add(verboseOption);
 
        command.SetAction(async (parseResult, ct) =>
        {
            // Note: ?? is used for null-safety even with DefaultValueFactory because GetValue returns T?
            var packageArgs = parseResult.GetValue(packageOption) ?? [];
            var framework = parseResult.GetValue(frameworkOption)!;
            var runtimeIdentifier = parseResult.GetValue(runtimeIdentifierOption);
            var output = parseResult.GetValue(outputOption)!;
            var sources = parseResult.GetValue(sourceOption) ?? [];
            var nugetConfigPath = parseResult.GetValue(configOption);
            var workingDir = parseResult.GetValue(workingDirOption);
            var noNugetOrg = parseResult.GetValue(noNugetOrgOption);
            var verbose = parseResult.GetValue(verboseOption);
 
            // Parse packages (format: PackageId,Version)
            var packages = new List<(string Id, string Version)>();
            foreach (var pkgArg in packageArgs)
            {
                if (verbose)
                {
                    Console.WriteLine($"Parsing package argument: {pkgArg}");
                }
                var parts = pkgArg.Split(',', 2, StringSplitOptions.RemoveEmptyEntries);
                if (parts.Length < 2)
                {
                    Console.Error.WriteLine($"Error: Package argument '{pkgArg}' must be in format 'PackageId,Version'");
                    return 1;
                }
                packages.Add((parts[0], parts[1]));
            }
 
            return await ExecuteRestoreAsync(packages, framework, runtimeIdentifier, output, sources, nugetConfigPath, workingDir, noNugetOrg, verbose).ConfigureAwait(false);
        });
 
        return command;
    }
 
    private static async Task<int> ExecuteRestoreAsync(
        List<(string Id, string Version)> packages,
        string framework,
        string? runtimeIdentifier,
        string output,
        string[] cliSources,
        string? nugetConfigPath,
        string? workingDir,
        bool noNugetOrg,
        bool verbose)
    {
        var outputPath = Path.GetFullPath(output);
        Directory.CreateDirectory(outputPath);
 
        var logger = new NuGetLogger(verbose);
 
        try
        {
            // Load NuGet settings once — handles working dir, config file, and machine-wide settings.
            var machineWideSettings = new XPlatMachineWideSetting();
            var settings = Settings.LoadDefaultSettings(workingDir, nugetConfigPath, machineWideSettings);
 
            if (verbose)
            {
                Console.WriteLine($"Restoring {packages.Count} packages for {framework}");
                Console.WriteLine($"Output: {outputPath}");
                if (!string.IsNullOrWhiteSpace(runtimeIdentifier))
                {
                    Console.WriteLine($"Runtime identifier: {runtimeIdentifier}");
                }
                if (workingDir is not null)
                {
                    Console.WriteLine($"Working dir: {workingDir}");
                }
                if (nugetConfigPath is not null)
                {
                    Console.WriteLine($"NuGet config: {nugetConfigPath}");
                }
            }
 
            // Resolve package sources using NuGet's PackageSourceProvider
            var packageSources = ResolvePackageSources(settings, cliSources, noNugetOrg);
 
            var nugetFramework = NuGetFramework.Parse(framework);
 
            // Build PackageSpec and DependencyGraphSpec
            var packageSpec = BuildPackageSpec(packages, nugetFramework, runtimeIdentifier, outputPath, packageSources, settings);
 
            var dgSpec = new DependencyGraphSpec();
            dgSpec.AddProject(packageSpec);
            dgSpec.AddRestore(packageSpec.RestoreMetadata.ProjectUniqueName);
 
            // Pass settings to the provider so it reuses our pre-loaded settings
            var providerCache = new RestoreCommandProvidersCache();
            var dgProvider = new DependencyGraphSpecRequestProvider(providerCache, dgSpec, settings);
 
            // Run restore — let NuGet handle source credentials, parallel execution, etc.
            using var cacheContext = new SourceCacheContext();
            var restoreArgs = new RestoreArgs
            {
                CacheContext = cacheContext,
                Log = logger,
                PreLoadedRequestProviders = [dgProvider],
                DisableParallel = Environment.ProcessorCount == 1,
                AllowNoOp = false,
                MachineWideSettings = machineWideSettings,
            };
 
            // Initialize NuGet's trust store with embedded trusted root certificates
            // so that package signature verification works on Linux.
            TrustedRootsHelper.InitializeTrustStore();
 
            var results = await RestoreRunner.RunAsync(restoreArgs).ConfigureAwait(false);
            var summary = results.Count > 0 ? results[0] : null;
 
            if (summary is null)
            {
                Console.Error.WriteLine("Error: Restore returned no results");
                return 1;
            }
 
            if (!summary.Success)
            {
                var errors = string.Join(Environment.NewLine,
                    summary.Errors?.Select(e => e.Message) ?? ["Unknown error"]);
                Console.Error.WriteLine($"Error: Restore failed: {errors}");
                return 1;
            }
 
            var assetsPath = Path.Combine(outputPath, "project.assets.json");
            Console.WriteLine($"Restore completed successfully");
            Console.WriteLine($"Assets file: {assetsPath}");
            return 0;
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"Error: {ex.Message}");
            if (verbose)
            {
                Console.Error.WriteLine(ex.ToString());
            }
 
            return 1;
        }
    }
 
    private static List<PackageSource> ResolvePackageSources(ISettings settings, string[] cliSources, bool noNugetOrg)
    {
        // Load enabled sources from NuGet config
        var provider = new PackageSourceProvider(settings);
        var sources = provider.LoadPackageSources().Where(s => s.IsEnabled).ToList();
 
        // Append CLI --source values (matching NuGet's behavior of merging, not replacing)
        foreach (var cliSource in cliSources)
        {
            if (!sources.Any(s => s.Source.Equals(cliSource, StringComparison.OrdinalIgnoreCase)))
            {
                sources.Add(new PackageSource(cliSource));
            }
        }
 
        // Add nuget.org as a fallback source unless opted out
        if (!noNugetOrg)
        {
            const string nugetOrgUrl = "https://api.nuget.org/v3/index.json";
            if (!sources.Any(s => s.Source.Equals(nugetOrgUrl, StringComparison.OrdinalIgnoreCase)))
            {
                Console.WriteLine("Note: Adding nuget.org as fallback package source. Use --no-nuget-org to disable.");
                sources.Add(new PackageSource(nugetOrgUrl, "nuget.org"));
            }
        }
 
        return sources;
    }
 
    private static PackageSpec BuildPackageSpec(
        List<(string Id, string Version)> packages,
        NuGetFramework framework,
        string? runtimeIdentifier,
        string outputPath,
        List<PackageSource> sources,
        ISettings settings)
    {
        var projectName = "AspireRestore";
        var projectPath = Path.Combine(outputPath, "project.json");
        var tfmShort = framework.GetShortFolderName();
        var runtimeIdentifierGraphPath = !string.IsNullOrWhiteSpace(runtimeIdentifier)
            ? EnsureRuntimeIdentifierGraphPath(outputPath)
            : null;
 
        var dependencies = packages.Select(p => new LibraryDependency
        {
            LibraryRange = new LibraryRange(
                p.Id,
                VersionRange.Parse(p.Version),
                LibraryDependencyTarget.Package)
        }).ToImmutableArray();
 
        var tfInfo = new TargetFrameworkInformation
        {
            FrameworkName = framework,
            TargetAlias = tfmShort,
            Dependencies = dependencies,
            RuntimeIdentifierGraphPath = runtimeIdentifierGraphPath
        };
 
        var restoreMetadata = new ProjectRestoreMetadata
        {
            ProjectUniqueName = projectName,
            ProjectName = projectName,
            ProjectPath = projectPath,
            ProjectStyle = ProjectStyle.PackageReference,
            OutputPath = outputPath,
            PackagesPath = SettingsUtility.GetGlobalPackagesFolder(settings),
            OriginalTargetFrameworks = [tfmShort],
            ConfigFilePaths = settings.GetConfigFilePaths().ToList(),
        };
 
        foreach (var source in sources)
        {
            restoreMetadata.Sources.Add(source);
        }
 
        restoreMetadata.TargetFrameworks.Add(new ProjectRestoreMetadataFrameworkInfo(framework)
        {
            TargetAlias = tfmShort
        });
 
        var packageSpec = new PackageSpec([tfInfo])
        {
            Name = projectName,
            FilePath = projectPath,
            RestoreMetadata = restoreMetadata,
        };
 
        if (!string.IsNullOrWhiteSpace(runtimeIdentifier))
        {
            packageSpec.RuntimeGraph = new RuntimeGraph([new RuntimeDescription(runtimeIdentifier)]);
        }
 
        return packageSpec;
    }
 
    private static string EnsureRuntimeIdentifierGraphPath(string outputPath)
    {
        var graphPath = Path.Combine(outputPath, RuntimeIdentifierGraphFileName);
        if (File.Exists(graphPath))
        {
            return graphPath;
        }
 
        using var resourceStream = typeof(RestoreCommand).Assembly.GetManifestResourceStream(RuntimeIdentifierGraphResourceName)
            ?? throw new InvalidOperationException($"Embedded runtime identifier graph resource '{RuntimeIdentifierGraphResourceName}' was not found.");
        using var fileStream = File.Create(graphPath);
        resourceStream.CopyTo(fileStream);
 
        return graphPath;
    }
}