File: src\sdk\src\Cli\dotnet\Commands\Run\AotRunCommand.cs
Web Access
Project: src\sdk\src\Cli\dotnet-aot\dotnet-aot.csproj (dotnet-aot)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if CLI_AOT
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using System.Security;
using System.Text.Json;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.FileBasedPrograms;
using Microsoft.DotNet.ProjectTools;

namespace Microsoft.DotNet.Cli.Commands.Run;

/// <summary>
/// Handles eligible file-based application launches inside the Native AOT CLI.
/// </summary>
internal static class AotRunCommand
{
    /// <summary>
    /// Configures the Native AOT implementation for the <c>run</c> command.
    /// </summary>
    /// <param name="command">The shared run command definition.</param>
    internal static void ConfigureCommand(RunCommandDefinition command)
        => command.SetAction(parseResult => Execute(parseResult, Launch));

    /// <summary>
    /// Executes an eligible file-based application through the Native AOT launcher.
    /// </summary>
    /// <param name="parseResult">The parsed run invocation.</param>
    /// <returns>The launched process exit code.</returns>
    internal static int Execute(ParseResult parseResult)
        => Execute(parseResult, Launch);

    /// <summary>
    /// Plans and executes an eligible file-based application using an injected launcher.
    /// </summary>
    /// <param name="parseResult">The parsed run invocation.</param>
    /// <param name="launch">Launches the committed invocation.</param>
    /// <param name="currentDirectory">The current directory used for discovery and relative paths.</param>
    /// <returns>The launcher exit code.</returns>
    /// <exception cref="CommandNotAvailableInAotException">The invocation cannot be handled safely by the Native AOT path.</exception>
    /// <exception cref="GracefulException">Managed project discovery reports a user-facing input error.</exception>
    internal static int Execute(
        ParseResult parseResult,
        Func<AotRunInvocation, int> launch,
        string? currentDirectory = null)
    {
        currentDirectory ??= Environment.CurrentDirectory;
        var definition = (RunCommandDefinition)parseResult.CommandResult.Command;
        if (!TryGetEligibleInvocationInputs(
            parseResult,
            definition,
            currentDirectory,
            out bool noBuild,
            out string? entryPointFileFullPath,
            out string[]? applicationArguments,
            out IReadOnlyDictionary<string, string>? environmentVariables,
            out string fallbackReason))
        {
            throw CreateManagedFallbackException(fallbackReason);
        }

        LaunchProfileReadResult profileResult = ReadLaunchProfile(
            parseResult,
            definition,
            entryPointFileFullPath);

        string command;
        string commandArguments;
        string? workingDirectory;
        string? artifactsPath = null;
        RunProperties? validatedRunProperties = null;
        RunTier runTier;
        RunDecisionReason decisionReason;
        RunPlan? plan = null;
        if (noBuild && profileResult.Profile is not ExecutableLaunchProfile)
        {
            artifactsPath = VirtualProjectBuilder.GetArtifactsPath(entryPointFileFullPath);
            RunPlan noBuildPlan = FileBasedAppRunPlan.AnalyzeAotNoBuildSynthetic(
                entryPointFileFullPath,
                artifactsPath);
            if (noBuildPlan.Tier == RunTier.LaunchOnly)
            {
                plan = noBuildPlan;
            }
        }

        bool executableCanBypassCache = noBuild &&
            profileResult.Profile is ExecutableLaunchProfile;
        // A failed synthetic no-build check can still use an authoritative cached RunProperties
        // contract, so continue to full cache validation before falling back.
        if (plan is null && !executableCanBypassCache)
        {
            if (!TryGetRuntimeVersion(out string? runtimeVersion))
            {
                throw CreateManagedFallbackException("the runtime version could not be read");
            }

            artifactsPath ??= VirtualProjectBuilder.GetArtifactsPath(entryPointFileFullPath);
            RunPlan cachedPlan = FileBasedAppRunPlan.AnalyzeCachedLaunch(
                entryPointFileFullPath,
                artifactsPath,
                CreateGlobalProperties(parseResult, definition),
                Product.Version,
                runtimeVersion);
            if (cachedPlan.Tier == RunTier.CachedLaunch)
            {
                plan = cachedPlan;
            }
        }

        if (profileResult.Profile is ExecutableLaunchProfile executableProfile &&
            (executableCanBypassCache || plan?.Tier == RunTier.CachedLaunch))
        {
            if (noBuild)
            {
                artifactsPath = null;
            }
            command = executableProfile.ExecutablePath;
            commandArguments = CommonRunHelpers.CombineRunArguments(
                baseArguments: null,
                applicationArguments,
                executableProfile.CommandLineArgs);
            workingDirectory = executableProfile.WorkingDirectory
                ?? Path.GetDirectoryName(entryPointFileFullPath)!;
            runTier = RunTier.LaunchOnly;
            decisionReason = RunDecisionReason.ExecutableLaunchProfile;
        }
        else if (plan is { Launch: { } launchInfo })
        {
            validatedRunProperties = launchInfo.RunProperties;
            command = launchInfo.Command;
            commandArguments = CommonRunHelpers.CombineRunArguments(
                validatedRunProperties?.Arguments,
                applicationArguments,
                profileResult.Profile?.CommandLineArgs,
                appendApplicationArgumentsToBase: validatedRunProperties is not null);
            workingDirectory = validatedRunProperties?.WorkingDirectory ?? currentDirectory;
            runTier = plan.Tier;
            decisionReason = plan.Reason;
        }
        else
        {
            throw CreateManagedFallbackException("no eligible cached launch contract was found");
        }

        var launchEnvironment = new Dictionary<string, string?>(StringComparer.Ordinal);
        if (profileResult.Profile is not ExecutableLaunchProfile)
        {
            string? rootVariableName = EnvironmentVariableNames.TryGetDotNetRootVariableName(
                validatedRunProperties?.RuntimeIdentifier ?? RuntimeInformation.RuntimeIdentifier,
                validatedRunProperties?.DefaultAppHostRuntimeIdentifier ?? RuntimeInformation.RuntimeIdentifier,
                validatedRunProperties?.TargetFrameworkVersion ?? $"v{Product.TargetFrameworkVersion}");
            if (rootVariableName is not null && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(rootVariableName)))
            {
                if (string.IsNullOrEmpty(NativeEntryPoint.DotnetRoot))
                {
                    throw CreateManagedFallbackException("the dotnet root could not be determined");
                }

                launchEnvironment[rootVariableName] = NativeEntryPoint.DotnetRoot;
            }
        }

        CommonRunHelpers.ApplyLaunchEnvironmentVariables(
            profileResult.Profile,
            environmentVariables,
            (name, value) => launchEnvironment[name] = value);

        profileResult.WriteMessages();
        if (!noBuild && profileResult.Profile?.DotNetRunMessages == true)
        {
            Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding);
        }
        Reporter.Verbose.WriteLine($"AOT run tier: {runTier} ({decisionReason}).");

        if (artifactsPath is not null)
        {
            FileBasedAppRunPlan.MarkArtifactsPathUsed(artifactsPath);
        }

        int exitCode = launch(new AotRunInvocation(
            command,
            commandArguments,
            launchEnvironment,
            workingDirectory,
            artifactsPath));
        return exitCode;
    }

    private static int Launch(AotRunInvocation invocation)
    {
        var commandSpec = new CommandSpec(
            invocation.Command,
            invocation.CommandArguments);
        Microsoft.DotNet.Cli.Utils.Command command = CommandFactoryUsingResolver.Create(commandSpec);
        command.WorkingDirectory(invocation.WorkingDirectory);
        foreach ((string name, string? value) in invocation.EnvironmentVariables)
        {
            command.EnvironmentVariable(name, value);
        }

        ConsoleCancelEventHandler cancelHandler = static (_, eventArgs) => eventArgs.Cancel = true;
        Console.CancelKeyPress += cancelHandler;
        try
        {
            return command.Execute().ExitCode;
        }
        finally
        {
            Console.CancelKeyPress -= cancelHandler;
        }
    }

    private static LaunchProfileReadResult ReadLaunchProfile(
        ParseResult parseResult,
        RunCommandDefinition definition,
        string entryPointFileFullPath)
    {
        var messages = new List<(string Message, bool IsError)>();
        string? launchProfile = parseResult.GetValue(definition.LaunchProfileOption);
        LaunchProfileParseResult result = CommonRunHelpers.ReadLaunchProfile(
            entryPointFileFullPath,
            launchProfile,
            parseResult.HasOption(definition.NoLaunchProfileOption),
            // Explicit verbosity is not eligible for the AOT path, so every reachable invocation
            // has the managed command's default non-quiet run verbosity.
            reportUsingLaunchSettings: true,
            (message, isError) => messages.Add((message, isError)));
        if (result.FailureReason is not null)
        {
            messages.Add((string.Format(
                CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings,
                LaunchProfileParser.GetLaunchProfileDisplayName(launchProfile),
                result.FailureReason).Bold().Red(), IsError: true));
        }

        return new LaunchProfileReadResult(result.Profile, messages);
    }

    private static Dictionary<string, string> CreateGlobalProperties(
        ParseResult parseResult,
        RunCommandDefinition definition)
    {
        Dictionary<string, string> globalProperties = CommonRunHelpers.CreateFileBasedRunGlobalProperties();
        // Mirror the managed option's --property:NuGetInteractive forwarding without constructing
        // MSBuildArgs solely for cache validation.
        globalProperties["NuGetInteractive"] = parseResult.GetValue(definition.InteractiveOption) ? "true" : "false";
        return globalProperties;
    }

    private static bool TryGetRuntimeVersion([NotNullWhen(true)] out string? runtimeVersion)
    {
        runtimeVersion = null;
        try
        {
            using var stream = File.OpenRead(Path.Join(SdkPaths.SdkDirectory, "dotnet.runtimeconfig.json"));
            using JsonDocument document = JsonDocument.Parse(stream);
            runtimeVersion = document.RootElement
                .GetProperty("runtimeOptions")
                .GetProperty("framework")
                .GetProperty("version")
                .GetString();
            return !string.IsNullOrWhiteSpace(runtimeVersion);
        }
        catch (Exception exception)
        {
            Reporter.Verbose.WriteLine($"Failed to read the runtime version: {exception}");
            return false;
        }
    }

    private static bool TryGetEligibleInvocationInputs(
        ParseResult parseResult,
        RunCommandDefinition definition,
        string currentDirectory,
        out bool noBuild,
        [NotNullWhen(true)] out string? entryPointFileFullPath,
        [NotNullWhen(true)] out string[]? applicationArguments,
        [NotNullWhen(true)] out IReadOnlyDictionary<string, string>? environmentVariables,
        out string fallbackReason)
    {
        noBuild = parseResult.HasOption(definition.NoBuildOption);
        entryPointFileFullPath = null;
        applicationArguments = null;
        environmentVariables = null;
        fallbackReason = string.Empty;

        if (GetUnsupportedOption(parseResult, definition) is { } unsupportedOption)
        {
            fallbackReason = $"option '{unsupportedOption.Name}' is not supported by the native path";
            return false;
        }

        string[] parsedApplicationArguments = parseResult.GetValue(definition.ApplicationArguments) ?? [];
        if (!CommonRunHelpers.TrySplitApplicationArgumentsAtDoubleDash(
            parseResult,
            parsedApplicationArguments,
            out int argumentCountBeforeDoubleDash,
            out string[] argumentsAfterDoubleDash))
        {
            fallbackReason = "application arguments could not be separated at '--'";
            return false;
        }

        if (argumentCountBeforeDoubleDash > 0 && parsedApplicationArguments[0] == "-")
        {
            fallbackReason = "standard-input source code requires the managed run implementation";
            return false;
        }

        string? entryPointPath = parseResult.GetValue(definition.FileOption);
        if (string.IsNullOrEmpty(entryPointPath))
        {
            string? projectFilePath;
            try
            {
                projectFilePath = CommonRunHelpers.TryFindSingleProjectInDirectory(currentDirectory);
            }
            catch (Exception exception) when (
                exception is IOException or
                UnauthorizedAccessException or
                SecurityException)
            {
                fallbackReason = "the current directory could not be searched safely";
                return false;
            }

            if (projectFilePath is not null)
            {
                fallbackReason = "the current directory contains a project";
                return false;
            }

            if (argumentCountBeforeDoubleDash == 0)
            {
                throw new GracefulException(CliCommandStrings.RunCommandExceptionNoProjects, currentDirectory, "--project");
            }

            if (argumentCountBeforeDoubleDash != 1)
            {
                fallbackReason = "positional file discovery did not identify exactly one entry-point argument";
                return false;
            }

            entryPointPath = parsedApplicationArguments[0];
        }
        else if (argumentCountBeforeDoubleDash != 0)
        {
            fallbackReason = "application arguments before '--' are ambiguous with an explicit --file option";
            return false;
        }

        try
        {
            entryPointFileFullPath = Path.GetFullPath(entryPointPath, currentDirectory);
        }
        catch (Exception exception) when (exception is ArgumentException or NotSupportedException or SecurityException)
        {
            fallbackReason = "the entry-point path could not be normalized";
            return false;
        }

        if (!VirtualProjectBuilder.IsValidEntryPointPath(entryPointFileFullPath))
        {
            if (string.IsNullOrEmpty(parseResult.GetValue(definition.FileOption)))
            {
                throw new GracefulException(CliCommandStrings.RunCommandExceptionNoProjects, currentDirectory, "--project");
            }

            fallbackReason = "the entry-point path is not a supported C# file";
            return false;
        }

        applicationArguments = argumentsAfterDoubleDash;
        environmentVariables = parseResult.GetValue(definition.EnvOption)
            ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        fallbackReason = string.Empty;
        return true;
    }

    private static Option? GetUnsupportedOption(ParseResult parseResult, RunCommandDefinition definition)
        => parseResult.CommandResult.Children
            .OfType<OptionResult>()
            .FirstOrDefault(optionResult =>
                !optionResult.Implicit
                && optionResult.Option != definition.FileOption
                && optionResult.Option != definition.LaunchProfileOption
                && optionResult.Option != definition.NoLaunchProfileOption
                && optionResult.Option != definition.NoBuildOption
                && optionResult.Option != definition.NoRestoreOption
                && optionResult.Option != definition.EnvOption)
            ?.Option;

    private static CommandNotAvailableInAotException CreateManagedFallbackException(string reason)
    {
        Reporter.Verbose.WriteLine($"AOT run is falling back to the managed CLI because {reason}.");
        return new CommandNotAvailableInAotException();
    }

}
#endif