File: Commands\RestoreCommand.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// 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.Globalization;
using System.Text.Json;
using Aspire.Cli.Configuration;
using Aspire.Cli.DotNet;
using Aspire.Cli.Interaction;
using Aspire.Cli.Projects;
using Aspire.Cli.Resources;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Commands;
 
/// <summary>
/// Restores dependencies for .NET AppHost projects and generates SDK code for guest (non-.NET) AppHost projects.
/// For guest AppHosts, always regenerates without checking the hash, unlike <c>aspire run</c> which
/// skips code generation when the package hash is unchanged.
/// </summary>
internal sealed class RestoreCommand : BaseCommand
{
    internal override HelpGroup HelpGroup => HelpGroup.AppCommands;
 
    protected override bool UpdateNotificationsEnabled => true;
 
    private readonly IProjectLocator _projectLocator;
    private readonly IAppHostProjectFactory _projectFactory;
    private readonly ILanguageDiscovery _languageDiscovery;
    private readonly IDotNetCliRunner _runner;
    private readonly IDotNetSdkInstaller _sdkInstaller;
    private readonly IInteractionService _interactionService;
    private readonly ILogger<RestoreCommand> _logger;
 
    private static readonly OptionWithLegacy<FileInfo?> s_appHostOption = new("--apphost", "--project", SharedCommandStrings.AppHostOptionDescription);
 
    public RestoreCommand(
        IProjectLocator projectLocator,
        IAppHostProjectFactory projectFactory,
        ILanguageDiscovery languageDiscovery,
        IDotNetCliRunner runner,
        IDotNetSdkInstaller sdkInstaller,
        IInteractionService interactionService,
        ILogger<RestoreCommand> logger,
        CommonCommandServices services)
        : base("restore", RestoreCommandStrings.Description, services)
    {
        _projectLocator = projectLocator;
        _projectFactory = projectFactory;
        _languageDiscovery = languageDiscovery;
        _runner = runner;
        _sdkInstaller = sdkInstaller;
        _interactionService = interactionService;
        _logger = logger;
 
        Options.Add(s_appHostOption);
    }
 
    protected override async Task<CommandResult> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
    {
        var passedAppHostProjectFile = parseResult.GetValue(s_appHostOption);
 
        try
        {
            using var activity = Telemetry.StartDiagnosticActivity(Name);
 
            FileInfo? effectiveAppHostFile = null;
            GuestAppHostProject? configOnlyGuestProject = null;
            DirectoryInfo? configOnlyProjectDirectory = null;
 
            try
            {
                var searchResult = await _projectLocator.UseOrFindAppHostProjectFileAsync(
                    passedAppHostProjectFile,
                    MultipleAppHostProjectsFoundBehavior.Prompt,
                    createSettingsFile: false,
                    cancellationToken);
 
                effectiveAppHostFile = searchResult.SelectedProjectFile;
            }
            catch (ProjectLocatorException ex) when (ex.FailureReason is ProjectLocatorFailureReason.NoProjectFileFound or ProjectLocatorFailureReason.ProjectFileDoesntExist)
            {
                (configOnlyGuestProject, configOnlyProjectDirectory) = TryResolveConfigOnlyGuestProject(passedAppHostProjectFile);
 
                if (configOnlyGuestProject is null || configOnlyProjectDirectory is null)
                {
                    throw;
                }
            }
 
            if (configOnlyGuestProject is not null && configOnlyProjectDirectory is not null)
            {
                _logger.LogDebug(
                    "Restoring SDK code for config-only guest AppHost in {Directory}",
                    configOnlyProjectDirectory.FullName);
 
                var success = await _interactionService.ShowStatusAsync(
                    RestoreCommandStrings.RestoringSdkCode,
                    async () => await configOnlyGuestProject.BuildAndGenerateSdkAsync(configOnlyProjectDirectory, cancellationToken: cancellationToken),
                    emoji: KnownEmojis.Gear);
 
                if (success)
                {
                    _interactionService.DisplaySuccess(
                        string.Format(CultureInfo.CurrentCulture, RestoreCommandStrings.RestoreSucceeded, AspireConfigFile.FileName));
                    return CommandResult.Success();
                }
 
                return CommandResult.Failure(CliExitCodes.FailedToBuildArtifacts);
            }
 
            if (effectiveAppHostFile is null)
            {
                return CommandResult.Failure(CliExitCodes.FailedToFindProject);
            }
 
            var project = _projectFactory.TryGetProject(effectiveAppHostFile);
 
            if (project is null)
            {
                return CommandResult.Failure(CliExitCodes.FailedToFindProject, RestoreCommandStrings.UnrecognizedAppHostType);
            }
 
            if (project is DotNetAppHostProject)
            {
                if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, Telemetry, cancellationToken: cancellationToken))
                {
                    return CommandResult.Failure(CliExitCodes.SdkNotInstalled);
                }
 
                var appHostDirectory = effectiveAppHostFile.Directory!;
                _logger.LogDebug("Restoring packages for {AppHost} in {Directory}", effectiveAppHostFile.FullName, appHostDirectory.FullName);
 
                var restoreExitCode = await _interactionService.ShowStatusAsync(
                    RestoreCommandStrings.RestoringSdkCode,
                    async () => await _runner.RestoreAsync(effectiveAppHostFile, new ProcessInvocationOptions(), cancellationToken),
                    emoji: KnownEmojis.Gear);
 
                if (restoreExitCode == 0)
                {
                    _interactionService.DisplaySuccess(
                        string.Format(CultureInfo.CurrentCulture, RestoreCommandStrings.RestoreSucceeded, effectiveAppHostFile.Name));
                    return CommandResult.Success();
                }
 
                return CommandResult.Failure(CliExitCodes.FailedToBuildArtifacts);
            }
 
            if (project is GuestAppHostProject guestProject)
            {
                var directory = effectiveAppHostFile.Directory!;
                _logger.LogDebug("Restoring SDK code for {AppHost} in {Directory}", effectiveAppHostFile.FullName, directory.FullName);
 
                var success = await _interactionService.ShowStatusAsync(
                    RestoreCommandStrings.RestoringSdkCode,
                    async () => await guestProject.BuildAndGenerateSdkAsync(directory, cancellationToken: cancellationToken),
                    emoji: KnownEmojis.Gear);
 
                if (success)
                {
                    _interactionService.DisplaySuccess(
                        string.Format(CultureInfo.CurrentCulture, RestoreCommandStrings.RestoreSucceeded, effectiveAppHostFile.Name));
                    return CommandResult.Success();
                }
 
                return CommandResult.Failure(CliExitCodes.FailedToBuildArtifacts);
            }
 
            return CommandResult.Failure(CliExitCodes.FailedToFindProject, RestoreCommandStrings.UnrecognizedAppHostType);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            return CommandResult.Cancelled();
        }
        catch (ProjectLocatorException ex)
        {
            return HandleProjectLocatorException(ex, InteractionService, Telemetry);
        }
        catch (Exception ex)
        {
            var errorMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.UnexpectedErrorOccurred, ex.Message);
            Telemetry.RecordError(errorMessage, ex);
            return CommandResult.Failure(CliExitCodes.FailedToBuildArtifacts, errorMessage);
        }
    }
 
    private (GuestAppHostProject? Project, DirectoryInfo? Directory) TryResolveConfigOnlyGuestProject(FileInfo? passedAppHostProjectFile)
    {
        var searchDirectory = GetFallbackSearchDirectory(passedAppHostProjectFile);
        if (searchDirectory is null)
        {
            return (null, null);
        }
 
        while (searchDirectory is not null)
        {
            AspireConfigFile? config;
            try
            {
                config = AspireConfigFile.Load(searchDirectory.FullName);
            }
            catch (JsonException ex)
            {
                _logger.LogDebug(ex, "Ignoring invalid config while resolving config-only guest AppHost in {Directory}", searchDirectory.FullName);
                return (null, null);
            }
 
            if (config is not null)
            {
                if (!string.IsNullOrWhiteSpace(config.AppHost?.Path))
                {
                    return (null, null);
                }
 
                if (!string.IsNullOrWhiteSpace(config.AppHost?.Language))
                {
                    var language = _languageDiscovery.GetLanguageById(config.AppHost.Language);
                    if (language is null)
                    {
                        _logger.LogDebug("Configured AppHost language '{Language}' is not available for config-only restore in {Directory}", config.AppHost.Language, searchDirectory.FullName);
                        return (null, null);
                    }
 
                    if (_projectFactory.GetProject(language) is GuestAppHostProject guestProject)
                    {
                        _logger.LogInformation(
                            "Using config-only guest AppHost restore for language {Language} in {Directory}",
                            language.LanguageId.Value,
                            searchDirectory.FullName);
                        return (guestProject, searchDirectory);
                    }
 
                    return (null, null);
                }
            }
 
            searchDirectory = searchDirectory.Parent;
        }
 
        return (null, null);
    }
 
    private DirectoryInfo? GetFallbackSearchDirectory(FileInfo? passedAppHostProjectFile)
    {
        if (passedAppHostProjectFile is null)
        {
            return ExecutionContext.WorkingDirectory;
        }
 
        if (Directory.Exists(passedAppHostProjectFile.FullName))
        {
            return new DirectoryInfo(passedAppHostProjectFile.FullName);
        }
 
        return null;
    }
}