File: Commands\Run\RunCommand.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.Collections.Immutable;
using System.Collections.ObjectModel;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.FileBasedPrograms;
using Microsoft.DotNet.ProjectTools;
using Microsoft.DotNet.Utilities;

namespace Microsoft.DotNet.Cli.Commands.Run;

public class RunCommand
{
    public bool NoBuild { get; }

    /// <summary>
    /// Full path to a project file to run.
    /// <see langword="null"/> if running without a project file
    /// (then <see cref="EntryPointFileFullPath"/> is not <see langword="null"/>).
    /// </summary>
    public string? ProjectFileFullPath { get; }

    /// <summary>
    /// Full path to an entry-point <c>.cs</c> file to run without a project file.
    /// </summary>
    public string? EntryPointFileFullPath { get; }

    public string ProjectOrEntryPointPath =>
        ProjectFileFullPath ?? EntryPointFileFullPath!;

    /// <summary>
    /// Whether <c>dotnet run -</c> is being executed.
    /// In that case, <see cref="EntryPointFileFullPath"/> points to a temporary file
    /// containing all text read from the standard input.
    /// </summary>
    public bool ReadCodeFromStdin { get; }

    /// <summary>
    /// unparsed/arbitrary CLI tokens to be passed to the running application
    /// </summary>
    public string[] ApplicationArgs { get; set; }
    public bool NoRestore { get; }
    public bool NoCache { get; }

    /// <summary>
    /// Parsed structure representing the MSBuild arguments that will be used to build the project.
    ///
    /// Note: This property has a private setter and is mutated within the class when framework selection modifies it.
    /// This mutability is necessary to allow the command to update MSBuild arguments after construction based on framework selection.
    /// </summary>
    public MSBuildArgs MSBuildArgs { get; private set; }
    public bool Interactive { get; }

    /// <summary>
    /// Environment variables specified on command line via -e option.
    /// </summary>
    public IReadOnlyDictionary<string, string> EnvironmentVariables { get; }

    private bool ShouldBuild => !NoBuild;

    public string? LaunchProfile { get; }
    public bool NoLaunchProfile { get; }

    /// <summary>
    /// The verbosity of the run-portion of this command specifically. If implicit builds are performed, they will always happen
    /// at a quiet verbosity by default, but it's important that we enable separate verbosity for the run command itself.
    /// </summary>
    public VerbosityOptions RunCommandVerbosity { get; private set; }

    /// <summary>
    /// True to ignore command line arguments specified by launch profile.
    /// </summary>
    public bool NoLaunchProfileArguments { get; }

    /// <summary>
    /// Device identifier to use for running the application.
    /// </summary>
    public string? Device { get; }

    /// <summary>
    /// Whether to list available devices and exit.
    /// </summary>
    public bool ListDevices { get; }

    /// <summary>
    /// Tracks whether restore was performed during device selection phase.
    /// If true, we should skip restore in the build phase to avoid redundant work.
    /// </summary>
    private bool _restoreDoneForDeviceSelection;

    /// <param name="applicationArgs">unparsed/arbitrary CLI tokens to be passed to the running application</param>
    public RunCommand(
        bool noBuild,
        string? projectFileFullPath,
        string? entryPointFileFullPath,
        string? launchProfile,
        bool noLaunchProfile,
        bool noLaunchProfileArguments,
        string? device,
        bool listDevices,
        bool noRestore,
        bool noCache,
        bool interactive,
        MSBuildArgs msbuildArgs,
        string[] applicationArgs,
        bool readCodeFromStdin,
        IReadOnlyDictionary<string, string> environmentVariables)
    {
        Debug.Assert(projectFileFullPath is null ^ entryPointFileFullPath is null);
        Debug.Assert(!readCodeFromStdin || entryPointFileFullPath is not null);

        NoBuild = noBuild;
        ProjectFileFullPath = projectFileFullPath;
        EntryPointFileFullPath = entryPointFileFullPath;
        ReadCodeFromStdin = readCodeFromStdin;
        LaunchProfile = launchProfile;
        NoLaunchProfile = noLaunchProfile;
        NoLaunchProfileArguments = noLaunchProfileArguments;
        Device = device;
        ListDevices = listDevices;
        ApplicationArgs = applicationArgs;
        Interactive = interactive;
        NoRestore = noRestore;
        NoCache = noCache;
        MSBuildArgs = SetupSilentBuildArgs(msbuildArgs);
        EnvironmentVariables = environmentVariables;
    }

    public int Execute()
    {
        if (NoBuild && NoCache)
        {
            throw new GracefulException(CliCommandStrings.CannotCombineOptions, RunCommandDefinition.NoCacheOptionName, RunCommandDefinition.NoBuildOptionName);
        }

        // Create a single logger for all MSBuild operations (device selection + build/run)
        // File-based runs (.cs files) don't support device selection and should use the existing logger behavior
        FacadeLogger? logger = ProjectFileFullPath is not null 
            ? LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run")
            : null;
        try
        {
            // Pre-run evaluation: Handle target framework and device selection for project-based scenarios
            using var selector = ProjectFileFullPath is not null
                ? new RunCommandSelector(ProjectFileFullPath, Interactive, MSBuildArgs, EnvironmentVariables, logger)
                : null;
            if (selector is not null && !TrySelectTargetFrameworkAndDeviceIfNeeded(selector))
            {
                // If --list-devices was specified, this is a successful exit
                return ListDevices ? 0 : 1;
            }

            // For file-based projects, check for multi-targeting before building
            if (EntryPointFileFullPath is not null && !TrySelectTargetFrameworkForFileBasedProject())
            {
                return 1;
            }

            var launchProfileParseResult = ReadLaunchProfileSettings();
            if (launchProfileParseResult.FailureReason != null)
            {
                Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, LaunchProfileParser.GetLaunchProfileDisplayName(LaunchProfile), launchProfileParseResult.FailureReason).Bold().Red());
            }

            Func<ProjectCollection, ProjectInstance>? projectFactory = null;
            RunProperties? cachedRunProperties = null;
            VirtualProjectBuildingCommand? projectBuilder = null;
            if (ShouldBuild)
            {
                if (launchProfileParseResult.Profile?.DotNetRunMessages == true)
                {
                    Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding);
                }

                EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out projectBuilder, selector?.IntermediateOutputPath, selector?.HasRuntimeEnvironmentVariableSupport ?? false);
            }
            else if (EntryPointFileFullPath is not null && launchProfileParseResult.Profile is not ExecutableLaunchProfile)
            {
                // The entry-point is not used to run the application if the launch profile specifies Executable command. 

                Debug.Assert(!ReadCodeFromStdin);
                projectBuilder = CreateProjectBuilder();
                projectBuilder.MarkArtifactsFolderUsed();

                var cacheEntry = projectBuilder.GetPreviousCacheEntry();
                projectFactory = CanUseRunPropertiesForCscBuiltProgram(BuildLevel.None, cacheEntry) ? null : projectBuilder.CreateProjectInstance;
                cachedRunProperties = cacheEntry?.Run;
            }

            // Deploy step: Call DeployToDevice target if available
            // This must run even with --no-build, as the user may have selected a different device
            if (selector is not null && !selector.TryDeployToDevice())
            {
                // Only error if we have a valid project (not a .sln file, etc.)
                if (selector.HasValidProject)
                {
                    throw new GracefulException(CliCommandStrings.RunCommandDeployFailed);
                }
            }

            var targetCommand = GetTargetCommand(launchProfileParseResult.Profile, projectFactory, cachedRunProperties, logger);

            // Send telemetry about the run operation
            SendRunTelemetry(launchProfileParseResult.Profile, projectBuilder);

            // Ignore Ctrl-C for the remainder of the command's execution
            Console.CancelKeyPress += (sender, e) => { e.Cancel = true; };

            return targetCommand.Execute().ExitCode;
        }
        catch (InvalidProjectFileException e)
        {
            throw new GracefulException(
                string.Format(CliCommandStrings.RunCommandSpecifiedFileIsNotAValidProject, ProjectFileFullPath),
                e);
        }
        finally
        {
            logger?.ReallyShutdown();
        }
    }

    internal ICommand GetTargetCommand(LaunchProfile? launchSettings, Func<ProjectCollection, ProjectInstance>? projectFactory, RunProperties? cachedRunProperties, FacadeLogger? logger)
        => launchSettings switch
        {
            null => GetTargetCommandForProject(launchSettings: null, projectFactory, cachedRunProperties, logger),
            ProjectLaunchProfile projectSettings => GetTargetCommandForProject(projectSettings, projectFactory, cachedRunProperties, logger),
            ExecutableLaunchProfile executableSettings => GetTargetCommandForExecutable(executableSettings),
            _ => throw new InvalidOperationException()
        };

    /// <summary>
    /// Checks if target framework selection and device selection are needed.
    /// Uses a single RunCommandSelector instance for both operations, re-evaluating
    /// the project after framework selection to get the correct device list.
    /// </summary>
    /// <param name="selector">The RunCommandSelector instance to use for selection</param>
    /// <returns>True if we can continue, false if we should exit</returns>
    private bool TrySelectTargetFrameworkAndDeviceIfNeeded(RunCommandSelector selector)
    {
        var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
        
        // If user specified --device on command line, add it to global properties and MSBuildArgs
        if (!string.IsNullOrWhiteSpace(Device))
        {
            globalProperties["Device"] = Device;
            var properties = new Dictionary<string, string> { { "Device", Device } };
            selector.InvalidateGlobalProperties(properties);
            var additionalProperties = new ReadOnlyDictionary<string, string>(properties);
            MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
        }

        // Optimization: If BOTH framework AND device are already specified (and we're not listing devices), 
        // we can skip both framework selection and device selection entirely
        bool hasFramework = globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework);
        bool hasDevice = globalProperties.TryGetValue("Device", out var preSpecifiedDevice) && !string.IsNullOrWhiteSpace(preSpecifiedDevice);
        
        if (!ListDevices && hasFramework && hasDevice)
        {
            // Both framework and device are pre-specified
            return true;
        }

        // Step 1: Select target framework if needed
        if (!selector.TrySelectTargetFramework(out string? selectedFramework))
        {
            return false;
        }

        if (selectedFramework is not null)
        {
            ApplySelectedFramework(selectedFramework);
            
            // Re-evaluate project with the selected framework so device selection sees the right devices
            var properties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
            selector.InvalidateGlobalProperties(properties);
        }

        // Step 2: Check if device is now pre-specified after framework selection
        if (!ListDevices && hasDevice)
        {
            // Device was pre-specified, we can skip device selection
            return true;
        }

        // Step 3: Select device if needed
        if (selector.TrySelectDevice(
            ListDevices,
            NoRestore,
            out string? selectedDevice,
            out string? runtimeIdentifier,
            out _restoreDoneForDeviceSelection))
        {
            // If a device was selected (either by user or by prompt), apply it to MSBuildArgs
            if (selectedDevice is not null)
            {
                var properties = new Dictionary<string, string> { { "Device", selectedDevice } };

                // If the device provided a RuntimeIdentifier, add it too
                if (!string.IsNullOrEmpty(runtimeIdentifier))
                {
                    properties["RuntimeIdentifier"] = runtimeIdentifier;

                    // If the device added a RuntimeIdentifier, we need to re-restore with that RID
                    // because the previous restore (if any) didn't include it
                    _restoreDoneForDeviceSelection = false;
                }

                // Update the selector's global properties so DeployToDevice and other targets see the Device
                selector.InvalidateGlobalProperties(properties);
                var additionalProperties = new ReadOnlyDictionary<string, string>(properties);
                MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
            }

            // If ListDevices was set, we return true but the caller will exit after listing
            return !ListDevices;
        }

        return false;
    }

    /// <summary>
    /// Checks if target framework selection is needed for file-based projects.
    /// Parses directives from the source file to detect multi-targeting.
    /// </summary>
    /// <returns>True if we can continue, false if we should exit</returns>
    private bool TrySelectTargetFrameworkForFileBasedProject()
    {
        Debug.Assert(EntryPointFileFullPath is not null);

        var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(MSBuildArgs);
        
        // If a framework is already specified via --framework, no need to check
        if (globalProperties.TryGetValue("TargetFramework", out var existingFramework) && !string.IsNullOrWhiteSpace(existingFramework))
        {
            return true;
        }

        // Get frameworks from source file directives
        var frameworks = GetTargetFrameworksFromSourceFile(EntryPointFileFullPath);
        if (frameworks is [])
        {
            return true; // Not multi-targeted
        }

        // Use RunCommandSelector to handle multi-target selection (or single framework selection)
        if (RunCommandSelector.TrySelectTargetFramework(frameworks, Interactive, out string? selectedFramework))
        {
            ApplySelectedFramework(selectedFramework);
            return true;
        }

        return false;
    }

    /// <summary>
    /// Parses a source file to extract target frameworks from directives.
    /// </summary>
    /// <returns>Array of frameworks if TargetFrameworks is specified, empty array otherwise</returns>
    private static string[] GetTargetFrameworksFromSourceFile(string sourceFilePath)
    {
        var value = VirtualProjectBuilder.GetPropertyFromSourceFile(sourceFilePath, "TargetFrameworks");
        return value?.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) ?? [];
    }

    /// <summary>
    /// Applies the selected target framework to MSBuildArgs if a framework was provided.
    /// </summary>
    /// <param name="selectedFramework">The framework to apply, or null if no framework selection was needed</param>
    private void ApplySelectedFramework(string? selectedFramework)
    {
        // If selectedFramework is null, it means no framework selection was needed
        // (e.g., user already specified --framework, or single-target project)
        if (selectedFramework is not null)
        {
            var additionalProperties = new ReadOnlyDictionary<string, string>(
                new Dictionary<string, string> { { "TargetFramework", selectedFramework } });
            MSBuildArgs = MSBuildArgs.CloneWithAdditionalProperties(additionalProperties);
        }
    }

    private ICommand GetTargetCommandForExecutable(ExecutableLaunchProfile launchSettings)
    {
        var workingDirectory = launchSettings.WorkingDirectory ?? Path.GetDirectoryName(ProjectOrEntryPointPath);

        var commandArgs = (NoLaunchProfileArguments || ApplicationArgs is not [])
            ? ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(ApplicationArgs)
            : launchSettings.CommandLineArgs ?? "";

        var commandSpec = new CommandSpec(launchSettings.ExecutablePath, commandArgs);
        var command = CommandFactoryUsingResolver.Create(commandSpec)
            .WorkingDirectory(workingDirectory);

        SetEnvironmentVariables(command, launchSettings);

        return command;
    }

    private void SetEnvironmentVariables(ICommand command, LaunchProfile? launchSettings)
    {
        // Handle Project-specific settings
        if (launchSettings is ProjectLaunchProfile projectSettings)
        {
            if (!string.IsNullOrEmpty(projectSettings.ApplicationUrl))
            {
                command.EnvironmentVariable("ASPNETCORE_URLS", projectSettings.ApplicationUrl);
            }
        }

        if (launchSettings != null)
        {
            command.EnvironmentVariable("DOTNET_LAUNCH_PROFILE", launchSettings.LaunchProfileName);

            foreach (var entry in launchSettings.EnvironmentVariables)
            {
                command.EnvironmentVariable(entry.Key, entry.Value);
            }
        }

        // Env variables specified on command line override those specified in launch profile:
        foreach (var (name, value) in EnvironmentVariables)
        {
            command.EnvironmentVariable(name, value);
        }
    }

    internal LaunchProfileParseResult ReadLaunchProfileSettings()
    {
        if (NoLaunchProfile)
        {
            return LaunchProfileParseResult.Success(model: null);
        }

        var launchSettingsPath = ReadCodeFromStdin
            ? null
            : LaunchSettings.TryFindLaunchSettingsFile(
                projectOrEntryPointFilePath: ProjectFileFullPath ?? EntryPointFileFullPath!,
                launchProfile: LaunchProfile,
                static (message, isError) => (isError ? Reporter.Error : Reporter.Output).WriteLine(message));

        if (launchSettingsPath is null)
        {
            return LaunchProfileParseResult.Success(model: null);
        }

        if (!RunCommandVerbosity.IsQuiet())
        {
            Reporter.Error.WriteLine(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath));
        }

        return LaunchSettings.ReadProfileSettingsFromFile(launchSettingsPath, LaunchProfile);
    }

    private void EnsureProjectIsBuilt(out Func<ProjectCollection, ProjectInstance>? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? projectBuilder, string? intermediateOutputPath, bool hasRuntimeEnvironmentVariableSupport)
    {
        int buildResult;
        if (EntryPointFileFullPath is not null)
        {
            projectBuilder = CreateProjectBuilder();
            buildResult = projectBuilder.Execute();
            projectFactory = CanUseRunPropertiesForCscBuiltProgram(projectBuilder.LastBuild.Level, projectBuilder.LastBuild.Cache?.PreviousEntry) ? null : projectBuilder.CreateProjectInstance;
            cachedRunProperties = projectBuilder.LastRunProperties ?? projectBuilder.LastBuild.Cache?.CurrentEntry.Run;
        }
        else
        {
            Debug.Assert(ProjectFileFullPath is not null);

            projectFactory = null;
            cachedRunProperties = null;
            projectBuilder = null;

            // Create temporary props file for environment variables only if the project has opted in.
            // This avoids invalidating incremental builds for projects that don't consume the items.
            // Use IntermediateOutputPath from earlier project evaluation (via RunCommandSelector), defaulting to "obj" if not available.
            string? envPropsFile = hasRuntimeEnvironmentVariableSupport
                ? EnvironmentVariablesToMSBuild.CreatePropsFile(ProjectFileFullPath, EnvironmentVariables, intermediateOutputPath)
                : null;
            try
            {
                var buildArgs = MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]);
                buildArgs = EnvironmentVariablesToMSBuild.AddPropsFileToArgs(buildArgs, envPropsFile);
                buildResult = new RestoringCommand(
                    buildArgs,
                    NoRestore || _restoreDoneForDeviceSelection,
                    advertiseWorkloadUpdates: false
                ).Execute();
            }
            finally
            {
                // Clean up temporary props file
                EnvironmentVariablesToMSBuild.DeletePropsFile(envPropsFile);
            }
        }

        if (buildResult != 0)
        {
            Reporter.Error.WriteLine();
            throw new GracefulException(CliCommandStrings.RunCommandException);
        }
    }

    private static bool CanUseRunPropertiesForCscBuiltProgram(BuildLevel level, RunFileBuildCacheEntry? previousCache)
    {
        return level == BuildLevel.Csc ||
            (level == BuildLevel.None && previousCache?.BuildLevel == BuildLevel.Csc);
    }

    private VirtualProjectBuildingCommand CreateProjectBuilder()
    {
        Debug.Assert(EntryPointFileFullPath != null);

        var args = MSBuildArgs.RequestedTargets is null or []
            ? MSBuildArgs.CloneWithAdditionalTargets(Constants.Build, Constants.ComputeRunArguments, Constants.CoreCompile)
            : MSBuildArgs.CloneWithAdditionalTargets(Constants.ComputeRunArguments, Constants.CoreCompile);

        return new(
            entryPointFileFullPath: EntryPointFileFullPath,
            msbuildArgs: args)
        {
            NoRestore = NoRestore,
            NoCache = NoCache,
        };
    }

    /// <summary>
    /// Applies run-specific customization to the MSBuild arguments
    /// that will be used to build the project. `run` wants to operate silently if possible,
    /// so we disable as much MSBuild output as possible, unless we're forced to be interactive.
    /// </summary>
    private MSBuildArgs SetupSilentBuildArgs(MSBuildArgs msbuildArgs)
    {
        msbuildArgs = msbuildArgs.CloneWithNoLogo(true);

        if (msbuildArgs.Verbosity is VerbosityOptions userVerbosity)
        {
            // if the user had a desired verbosity, we use that for the run command
            RunCommandVerbosity = userVerbosity;
            return msbuildArgs;
        }
        else
        {
            // Apply defaults if the user didn't expressly set the verbosity.
            // Setting RunCommandVerbosity to minimal ensures that we keep the previous launchsettings
            // and related diagnostics messages on by default.
            RunCommandVerbosity = VerbosityOptions.minimal;
            return msbuildArgs.CloneWithVerbosity(VerbosityOptions.quiet);
        }
    }

    private ICommand GetTargetCommandForProject(ProjectLaunchProfile? launchSettings, Func<ProjectCollection, ProjectInstance>? projectFactory, RunProperties? cachedRunProperties, FacadeLogger? logger)
    {
        ICommand command;
        if (cachedRunProperties != null)
        {
            // We can skip project evaluation if we already evaluated the project during virtual build
            // or we have cached run properties in previous run (and this is a --no-build or skip-msbuild run).
            Reporter.Verbose.WriteLine("Getting target command: from cache.");
            command = CreateCommandFromRunProperties(cachedRunProperties.WithApplicationArguments(ApplicationArgs));
        }
        else if (projectFactory is null && ProjectFileFullPath is null)
        {
            // If we are running a file-based app and projectFactory is null, it means csc was used instead of full msbuild.
            // So we can skip project evaluation to continue the optimized path.
            Debug.Assert(EntryPointFileFullPath is not null);
            Reporter.Verbose.WriteLine("Getting target command: for csc-built program.");
            command = CreateCommandForCscBuiltProgram(EntryPointFileFullPath, ApplicationArgs);
        }
        else
        {
            Reporter.Verbose.WriteLine("Getting target command: evaluating project.");
    
            ProjectInstance project;
            try
            {
                project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger);
                ValidatePreconditions(project);
                InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs, EnvironmentVariables);
            }
            finally
            {
                    }

            var runProperties = RunProperties.FromProject(project).WithApplicationArguments(ApplicationArgs);
            command = CreateCommandFromRunProperties(runProperties);
        }

        SetEnvironmentVariables(command, launchSettings);

        if (!NoLaunchProfileArguments && string.IsNullOrEmpty(command.CommandArgs) && launchSettings?.CommandLineArgs != null)
        {
            command.SetCommandArgs(launchSettings.CommandLineArgs);
        }

        return command;

        static ProjectInstance EvaluateProject(string? projectFilePath, Func<ProjectCollection, ProjectInstance>? projectFactory, MSBuildArgs msbuildArgs, ILogger? binaryLogger)
        {
            var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
            var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);

            if (projectFactory != null)
            {
                return projectFactory(collection);
            }

            try
            {
                return collection.LoadProject(projectFilePath).CreateProjectInstance();
            }
            catch (InvalidProjectFileException e)
            {
                throw new GracefulException(string.Format(CliCommandStrings.RunCommandSpecifiedFileIsNotAValidProject, projectFilePath), e);
            }
        }

        static void ValidatePreconditions(ProjectInstance project)
        {
            // there must be some kind of TFM available to run a project
            if (string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework")) && string.IsNullOrEmpty(project.GetPropertyValue("TargetFrameworks")))
            {
                ThrowUnableToRunError(project);
            }
        }

        static ICommand CreateCommandFromRunProperties(RunProperties runProperties)
        {
            CommandSpec commandSpec = new(runProperties.Command, runProperties.Arguments);

            var command = CommandFactoryUsingResolver.Create(commandSpec)
                .WorkingDirectory(runProperties.WorkingDirectory);

            SetRootVariableName(
                command,
                runtimeIdentifier: runProperties.RuntimeIdentifier,
                defaultAppHostRuntimeIdentifier: runProperties.DefaultAppHostRuntimeIdentifier,
                targetFrameworkVersion: runProperties.TargetFrameworkVersion);

            return command;
        }

        static void SetRootVariableName(ICommand command, string runtimeIdentifier, string defaultAppHostRuntimeIdentifier, string targetFrameworkVersion)
        {
            var rootVariableName = EnvironmentVariableNames.TryGetDotNetRootVariableName(
                runtimeIdentifier,
                defaultAppHostRuntimeIdentifier,
                targetFrameworkVersion);
            if (rootVariableName != null && string.IsNullOrEmpty(Environment.GetEnvironmentVariable(rootVariableName)))
            {
                command.EnvironmentVariable(rootVariableName, Path.GetDirectoryName(new Muxer().MuxerPath));
            }
        }

        static ICommand CreateCommandForCscBuiltProgram(string entryPointFileFullPath, string[] args)
        {
            var artifactsPath = VirtualProjectBuilder.GetArtifactsPath(entryPointFileFullPath);
            var exePath = Path.Join(artifactsPath, "bin", "debug", Path.GetFileNameWithoutExtension(entryPointFileFullPath) + FileNameSuffixes.CurrentPlatform.Exe);
            var commandSpec = new CommandSpec(path: exePath, args: ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args));
            var command = CommandFactoryUsingResolver.Create(commandSpec);

            SetRootVariableName(
                command,
                runtimeIdentifier: RuntimeInformation.RuntimeIdentifier,
                defaultAppHostRuntimeIdentifier: RuntimeInformation.RuntimeIdentifier,
                targetFrameworkVersion: $"v{VirtualProjectBuildingCommand.TargetFrameworkVersion}");

            return command;
        }

        static void InvokeRunArgumentsTarget(ProjectInstance project, bool noBuild, FacadeLogger? binaryLogger, MSBuildArgs buildArgs, IReadOnlyDictionary<string, string> environmentVariables)
        {
            // Only add environment variables as MSBuild items if the project has opted in via capability
            if (project.GetItems(Constants.ProjectCapability)
                .Any(item => string.Equals(item.EvaluatedInclude, Constants.RuntimeEnvironmentVariableSupport, StringComparison.OrdinalIgnoreCase)))
            {
                EnvironmentVariablesToMSBuild.AddAsItems(project, environmentVariables);
            }

            List<ILogger> loggersForBuild = [
                CommonRunHelpers.GetConsoleLogger(
                    buildArgs.CloneWithExplicitArgs([$"--verbosity:{LoggerVerbosity.Quiet.ToString().ToLowerInvariant()}", ..buildArgs.OtherMSBuildArgs])
                )
            ];
            if (binaryLogger is not null)
            {
                loggersForBuild.Add(binaryLogger);
            }

            if (!project.Build([Constants.ComputeRunArguments], loggers: loggersForBuild, remoteLoggers: null, out _))
            {
                throw new GracefulException(CliCommandStrings.RunCommandEvaluationExceptionBuildFailed, Constants.ComputeRunArguments);
            }
        }
    }

    [DoesNotReturn]
    internal static void ThrowUnableToRunError(ProjectInstance project)
    {
        throw new GracefulException(
                string.Format(
                    CliCommandStrings.RunCommandExceptionUnableToRun,
                    project.GetPropertyValue("MSBuildProjectFullPath"),
                    Product.TargetFrameworkVersion,
                    project.GetPropertyValue("OutputType")));
    }

    private static string? DiscoverProjectFilePath(string? filePath, string? projectFileOrDirectoryPath, bool readCodeFromStdin, ref string[] args, out string? entryPointFilePath)
    {
        // If `--file` is explicitly specified, just use that.
        if (filePath != null)
        {
            Debug.Assert(projectFileOrDirectoryPath == null);
            entryPointFilePath = Path.GetFullPath(filePath);
            return null;
        }

        bool emptyProjectOption = false;
        if (string.IsNullOrWhiteSpace(projectFileOrDirectoryPath))
        {
            emptyProjectOption = true;
            projectFileOrDirectoryPath = Directory.GetCurrentDirectory();
        }

        // Normalize path separators to handle Windows-style paths on non-Windows platforms.
        // This is supported for backward compatibility in 'dotnet run' only, not for all CLI commands.
        // Converting backslashes to forward slashes allows PowerShell scripts using Windows-style paths
        // to work cross-platform, maintaining compatibility with .NET 9 behavior.
        if (Path.DirectorySeparatorChar != '\\')
        {
            projectFileOrDirectoryPath = projectFileOrDirectoryPath.Replace('\\', '/');
        }

        string? projectFilePath = Directory.Exists(projectFileOrDirectoryPath)
            ? TryFindSingleProjectInDirectory(projectFileOrDirectoryPath)
            : projectFileOrDirectoryPath;

        // Check if the project file actually exists when it's specified as a direct file path
        if (projectFilePath is not null && !emptyProjectOption && !File.Exists(projectFilePath))
        {
            throw new GracefulException(CliCommandStrings.CmdNonExistentFileErrorDescription, projectFilePath);
        }

        // If no project exists in the directory and no --project was given,
        // try to resolve an entry-point file instead.
        entryPointFilePath = projectFilePath is null && emptyProjectOption
            ? TryFindEntryPointFilePath(readCodeFromStdin, ref args)
            : null;

        if (entryPointFilePath is null && projectFilePath is null)
        {
            throw new GracefulException(CliCommandStrings.RunCommandExceptionNoProjects, projectFileOrDirectoryPath, "--project");
        }

        return projectFilePath;

        static string? TryFindSingleProjectInDirectory(string directory)
        {
            string[] projectFiles = Directory.GetFiles(directory, "*.*proj");

            if (projectFiles.Length == 0)
            {
                return null;
            }

            if (projectFiles.Length > 1)
            {
                throw new GracefulException(CliCommandStrings.RunCommandExceptionMultipleProjects, directory);
            }

            return projectFiles[0];
        }

        static string? TryFindEntryPointFilePath(bool readCodeFromStdin, ref string[] args)
        {
            if (args is not [{ } arg, ..])
            {
                return null;
            }

            if (!readCodeFromStdin)
            {
                if (VirtualProjectBuilder.IsValidEntryPointPath(arg))
                {
                    arg = Path.GetFullPath(arg);
                }
                else
                {
                    return null;
                }
            }

            args = args[1..];
            return arg;
        }
    }

    public static RunCommand FromArgs(string[] args)
    {
        var parseResult = Parser.Parse(["dotnet", "run", .. args]);
        return FromParseResult(parseResult);
    }

    public static RunCommand FromParseResult(ParseResult parseResult)
    {
        var definition = (RunCommandDefinition)parseResult.CommandResult.Command;

        if (UsingRunCommandShorthandProjectOption(parseResult))
        {
            Reporter.Output.WriteLine(CliCommandStrings.RunCommandProjectAbbreviationDeprecated.Yellow());
            parseResult = ModifyParseResultForShorthandProjectOption(parseResult);
        }

        // if the application arguments contain any binlog args then we need to remove them from the application arguments and apply
        // them to the restore args.
        // this is because we can't model the binlog command structure in MSbuild in the System.CommandLine parser, but we need
        // bl information to synchronize the restore and build logger configurations
        var applicationArguments = parseResult.GetValue(definition.ApplicationArguments)?.ToList();

        LoggerUtility.SeparateBinLogArguments(applicationArguments, out var binLogArgs, out var nonBinLogArgs);

        var msbuildProperties = parseResult.OptionValuesToBeForwarded(definition).ToList();
        if (binLogArgs.Count > 0)
        {
            msbuildProperties.AddRange(binLogArgs);
        }

        // Only consider `-` to mean "read code from stdin" if it is before double dash `--`
        // (otherwise it should be forwarded to the target application as its command-line argument).
        bool readCodeFromStdin = nonBinLogArgs is ["-", ..] &&
            parseResult.Tokens.TakeWhile(static t => t.Type != TokenType.DoubleDash)
                .Any(static t => t is { Type: TokenType.Argument, Value: "-" });

        string? projectOption = parseResult.GetValue(definition.ProjectOption);
        string? fileOption = parseResult.GetValue(definition.FileOption);

        if (projectOption != null && fileOption != null)
        {
            throw new GracefulException(CliCommandStrings.CannotCombineOptions, definition.ProjectOption.Name, definition.FileOption.Name);
        }

        string[] args = [.. nonBinLogArgs];
        string? projectFilePath = DiscoverProjectFilePath(
            filePath: fileOption,
            projectFileOrDirectoryPath: projectOption,
            readCodeFromStdin: readCodeFromStdin,
            ref args,
            out string? entryPointFilePath);

        // Warn if an argument looks like a file-based program entry point but we're falling back to project-based run.
        // This helps users who accidentally run `dotnet run file.cs` in a directory containing a project file.
        // Do not warn if --project or --file was explicitly specified.
        // Only consider arguments that appear before '--' in the command line.
        if (projectFilePath is not null && projectOption is null && fileOption is null && !readCodeFromStdin)
        {
            var argValuesBeforeDoubleDash = parseResult.Tokens
                .TakeWhile(static t => t.Type != TokenType.DoubleDash)
                .Where(static t => t.Type == TokenType.Argument)
                .Select(static t => t.Value)
                .ToHashSet();

            foreach (var arg in args)
            {
                if (!argValuesBeforeDoubleDash.Contains(arg))
                {
                    continue;
                }

                if (VirtualProjectBuilder.IsValidEntryPointPath(arg))
                {
                    Reporter.Error.WriteLine(
                        string.Format(CliCommandStrings.RunCommandWarningFileArgumentPassedToProject, arg, projectFilePath).Yellow());
                    break;
                }

                if (arg.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
                {
                    Reporter.Error.WriteLine(
                        string.Format(CliCommandStrings.RunCommandWarningCsFileArgumentPassedToProject, arg, projectFilePath).Yellow());
                    break;
                }
            }
        }

        bool noBuild = parseResult.HasOption(definition.NoBuildOption);
        string launchProfile = parseResult.GetValue(definition.LaunchProfileOption) ?? string.Empty;

        if (readCodeFromStdin && entryPointFilePath != null)
        {
            Debug.Assert(projectFilePath is null && entryPointFilePath is "-");

            if (noBuild)
            {
                throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, definition.NoBuildOption.Name);
            }

            if (!string.IsNullOrWhiteSpace(launchProfile))
            {
                throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, definition.LaunchProfileOption.Name);
            }

            // If '-' is specified as the input file, read all text from stdin into a temporary file and use that as the entry point.
            // We create a new directory for each file so other files are not included in the compilation.
            // We fail if the file already exists to avoid reusing the same file for multiple stdin runs (in case the random name is duplicate).
            string directory = VirtualProjectBuilder.GetTempSubpath(Path.GetRandomFileName());
            VirtualProjectBuildingCommand.CreateTempSubdirectory(directory);
            entryPointFilePath = Path.Join(directory, "app.cs");
            using (var stdinStream = Console.OpenStandardInput())
            using (var fileStream = new FileStream(entryPointFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
            {
                stdinStream.CopyTo(fileStream);
            }

            Debug.Assert(nonBinLogArgs[0] == "-");
            nonBinLogArgs[0] = entryPointFilePath;
        }

        var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(
            msbuildProperties,
            CommonOptions.CreatePropertyOption(),
            CommonOptions.CreateRestorePropertyOption(),
            CommonOptions.CreateMSBuildTargetOption(),
            definition.VerbosityOption);

        var command = new RunCommand(
            noBuild: noBuild,
            projectFileFullPath: projectFilePath,
            entryPointFileFullPath: entryPointFilePath,
            launchProfile: launchProfile,
            noLaunchProfile: parseResult.HasOption(definition.NoLaunchProfileOption),
            noLaunchProfileArguments: parseResult.HasOption(definition.NoLaunchProfileArgumentsOption),
            device: parseResult.GetValue(definition.DeviceOption),
            listDevices: parseResult.HasOption(definition.ListDevicesOption),
            noRestore: parseResult.HasOption(definition.NoRestoreOption) || parseResult.HasOption(definition.NoBuildOption),
            noCache: parseResult.HasOption(definition.NoCacheOption),
            interactive: parseResult.GetValue(definition.InteractiveOption),
            msbuildArgs: msbuildArgs,
            applicationArgs: args,
            readCodeFromStdin: readCodeFromStdin,
            environmentVariables: parseResult.GetValue(definition.EnvOption) ?? ImmutableDictionary<string, string>.Empty
        );

        return command;

        bool UsingRunCommandShorthandProjectOption(ParseResult parseResult)
        {
            if (parseResult.HasOption(definition.PropertyOption) && parseResult.GetValue(definition.PropertyOption)!.Any())
            {
                var projVals = parseResult.GetRunCommandShorthandProjectValues();
                if (projVals?.Any() is true)
                {
                    if (projVals.Count() != 1 || parseResult.HasOption(definition.ProjectOption))
                    {
                        throw new GracefulException(CliStrings.OnlyOneProjectAllowed);
                    }
                    return true;
                }
            }
            return false;
        }
    }

    public static int Run(ParseResult parseResult)
    {
        parseResult.HandleDebugSwitch();

        return FromParseResult(parseResult).Execute();
    }

    public static ParseResult ModifyParseResultForShorthandProjectOption(ParseResult parseResult)
    {
        // we know the project is going to be one of the following forms:
        //   -p:project
        //   -p project
        // so try to find those and filter them out of the arguments array
        var possibleProject = parseResult.GetRunCommandShorthandProjectValues()!.FirstOrDefault()!; // ! are ok because of precondition check in method called before this.
        var tokensMinusProject = new List<string>();
        var nextTokenMayBeProject = false;
        foreach (var token in parseResult.Tokens)
        {
            if (token.Value == "-p")
            {
                // skip this token, if the next token _is_ the project then we'll skip that too
                // if the next token _isn't_ the project then we'll backfill
                nextTokenMayBeProject = true;
                continue;
            }
            else if (token.Value == possibleProject && nextTokenMayBeProject)
            {
                // skip, we've successfully stripped this option and value entirely
                nextTokenMayBeProject = false;
                continue;
            }
            else if (token.Value.StartsWith("-p") && token.Value.EndsWith(possibleProject))
            {
                // both option and value in the same token, skip and carry on
            }
            else
            {
                if (nextTokenMayBeProject)
                {
                    //we skipped a -p, so backfill it
                    tokensMinusProject.Add("-p");
                }
                nextTokenMayBeProject = false;
            }

            tokensMinusProject.Add(token.Value);
        }

        tokensMinusProject.Add("--project");
        tokensMinusProject.Add(possibleProject);

        var tokensToParse = tokensMinusProject.ToArray();
        var newParseResult = Parser.Parse(tokensToParse);
        return newParseResult;
    }

    /// <summary>
    /// Sends telemetry about the run operation.
    /// </summary>
    private void SendRunTelemetry(
        LaunchProfile? launchSettings,
        VirtualProjectBuildingCommand? projectBuilder)
    {
        try
        {
            if (projectBuilder != null)
            {
                SendFileBasedTelemetry(launchSettings, projectBuilder);
            }
            else
            {
                SendProjectBasedTelemetry(launchSettings);
            }
        }
        catch (Exception ex)
        {
            // Silently ignore telemetry errors to not affect the run operation
            if (CommandLoggingContext.IsVerbose)
            {
                Reporter.Verbose.WriteLine($"Failed to send run telemetry: {ex}");
            }
        }
    }

    /// <summary>
    /// Builds and sends telemetry data for file-based app runs.
    /// </summary>
    private void SendFileBasedTelemetry(
        LaunchProfile? launchSettings,
        VirtualProjectBuildingCommand projectBuilder)
    {
        Debug.Assert(EntryPointFileFullPath != null);
        var projectIdentifier = RunTelemetry.GetFileBasedIdentifier(EntryPointFileFullPath, Sha256Hasher.Hash);

        var directives = projectBuilder.EvaluatedDirectives;

        if (directives.IsDefault)
        {
            directives = projectBuilder.Directives;
        }

        if (directives.IsDefault)
        {
            directives = [];
        }

        var sdkCount = RunTelemetry.CountSdks(directives);
        var packageReferenceCount = RunTelemetry.CountPackageReferences(directives);
        var projectReferenceCount = RunTelemetry.CountProjectReferences(directives);
        var additionalPropertiesCount = RunTelemetry.CountAdditionalProperties(directives);

        RunTelemetry.TrackRunEvent(
            isFileBased: true,
            projectIdentifier: projectIdentifier,
            launchProfile: LaunchProfile,
            noLaunchProfile: NoLaunchProfile,
            launchSettings: launchSettings,
            sdkCount: sdkCount,
            packageReferenceCount: packageReferenceCount,
            projectReferenceCount: projectReferenceCount,
            additionalPropertiesCount: additionalPropertiesCount,
            usedMSBuild: projectBuilder.LastBuild.Level is BuildLevel.All,
            usedRoslynCompiler: projectBuilder.LastBuild.Level is BuildLevel.Csc);
    }

    /// <summary>
    /// Builds and sends telemetry data for project-based app runs.
    /// </summary>
    private void SendProjectBasedTelemetry(LaunchProfile? launchSettings)
    {
        Debug.Assert(ProjectFileFullPath != null);
        var projectIdentifier = RunTelemetry.GetProjectBasedIdentifier(ProjectFileFullPath, GetRepositoryRoot(), Sha256Hasher.Hash);

        // Get package and project reference counts for project-based apps
        int packageReferenceCount = 0;
        int projectReferenceCount = 0;

        // Try to get project information for telemetry if we built the project
        if (ShouldBuild)
        {
            try
            {
                var globalProperties = MSBuildArgs.GlobalProperties?.ToDictionary() ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
                globalProperties[Constants.EnableDefaultItems] = "false";
                globalProperties[Constants.MSBuildExtensionsPath] = AppContext.BaseDirectory;

                using var collection = new ProjectCollection(globalProperties: globalProperties);
                var project = collection.LoadProject(ProjectFileFullPath).CreateProjectInstance();

                packageReferenceCount = RunTelemetry.CountPackageReferences(project);
                projectReferenceCount = RunTelemetry.CountProjectReferences(project);
            }
            catch
            {
                // If project evaluation fails for telemetry, use defaults
                // We don't want telemetry collection to affect the run operation
            }
        }

        RunTelemetry.TrackRunEvent(
            isFileBased: false,
            projectIdentifier: projectIdentifier,
            launchProfile: LaunchProfile,
            noLaunchProfile: NoLaunchProfile,
            launchSettings: launchSettings,
            packageReferenceCount: packageReferenceCount,
            projectReferenceCount: projectReferenceCount);
    }

    /// <summary>
    /// Attempts to find the repository root directory.
    /// </summary>
    /// <returns>Repository root path if found, null otherwise</returns>
    private string? GetRepositoryRoot()
    {
        try
        {
            var currentDir = ProjectFileFullPath != null
                ? Path.GetDirectoryName(ProjectFileFullPath)
                : Directory.GetCurrentDirectory();

            while (currentDir != null)
            {
                if (Directory.Exists(Path.Combine(currentDir, ".git")))
                {
                    return currentDir;
                }
                currentDir = Directory.GetParent(currentDir)?.FullName;
            }
        }
        catch
        {
            // Ignore errors when trying to find repo root
        }

        return null;
    }
}