|
// 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 Microsoft.Build.Evaluation;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Logging;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.Commands.Restore;
using Microsoft.DotNet.Cli.Commands.Run.LaunchSettings;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
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; }
/// <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; }
public ReadOnlyDictionary<string, string>? RestoreProperties { 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.
/// </summary>
public MSBuildArgs MSBuildArgs { get; }
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; }
/// <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,
bool noRestore,
bool noCache,
bool interactive,
MSBuildArgs msbuildArgs,
string[] applicationArgs,
bool readCodeFromStdin,
IReadOnlyDictionary<string, string> environmentVariables,
ReadOnlyDictionary<string, string>? msbuildRestoreProperties)
{
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;
ApplicationArgs = applicationArgs;
Interactive = interactive;
NoRestore = noRestore;
NoCache = noCache;
MSBuildArgs = SetupSilentBuildArgs(msbuildArgs);
EnvironmentVariables = environmentVariables;
RestoreProperties = msbuildRestoreProperties;
}
public int Execute()
{
if (!TryGetLaunchProfileSettingsIfNeeded(out var launchSettings))
{
return 1;
}
Func<ProjectCollection, ProjectInstance>? projectFactory = null;
RunProperties? cachedRunProperties = null;
VirtualProjectBuildingCommand? virtualCommand = null;
if (ShouldBuild)
{
if (string.Equals("true", launchSettings?.DotNetRunMessages, StringComparison.OrdinalIgnoreCase))
{
Reporter.Output.WriteLine(CliCommandStrings.RunCommandBuilding);
}
EnsureProjectIsBuilt(out projectFactory, out cachedRunProperties, out virtualCommand);
}
else
{
if (NoCache)
{
throw new GracefulException(CliCommandStrings.CannotCombineOptions, RunCommandParser.NoCacheOption.Name, RunCommandParser.NoBuildOption.Name);
}
if (EntryPointFileFullPath is not null)
{
Debug.Assert(!ReadCodeFromStdin);
virtualCommand = CreateVirtualCommand();
virtualCommand.MarkArtifactsFolderUsed();
var cacheEntry = virtualCommand.GetPreviousCacheEntry();
projectFactory = CanUseRunPropertiesForCscBuiltProgram(BuildLevel.None, cacheEntry) ? null : virtualCommand.CreateProjectInstance;
cachedRunProperties = cacheEntry?.Run;
}
}
try
{
ICommand targetCommand = GetTargetCommand(projectFactory, cachedRunProperties);
ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
// Env variables specified on command line override those specified in launch profile:
foreach (var (name, value) in EnvironmentVariables)
{
targetCommand.EnvironmentVariable(name, value);
}
// Send telemetry about the run operation
SendRunTelemetry(launchSettings, virtualCommand);
// 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);
}
}
internal void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
{
if (launchSettings == null)
{
return;
}
if (!string.IsNullOrEmpty(launchSettings.ApplicationUrl))
{
targetCommand.EnvironmentVariable("ASPNETCORE_URLS", launchSettings.ApplicationUrl);
}
targetCommand.EnvironmentVariable("DOTNET_LAUNCH_PROFILE", launchSettings.LaunchProfileName);
foreach (var entry in launchSettings.EnvironmentVariables)
{
string value = Environment.ExpandEnvironmentVariables(entry.Value);
//NOTE: MSBuild variables are not expanded like they are in VS
targetCommand.EnvironmentVariable(entry.Key, value);
}
if (!NoLaunchProfileArguments && string.IsNullOrEmpty(targetCommand.CommandArgs) && launchSettings.CommandLineArgs != null)
{
targetCommand.SetCommandArgs(launchSettings.CommandLineArgs);
}
}
internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel? launchSettingsModel)
{
launchSettingsModel = default;
if (NoLaunchProfile)
{
return true;
}
var launchSettingsPath = ReadCodeFromStdin ? null : TryFindLaunchSettings(projectOrEntryPointFilePath: ProjectFileFullPath ?? EntryPointFileFullPath!, launchProfile: LaunchProfile);
if (launchSettingsPath is null)
{
return true;
}
if (!RunCommandVerbosity.IsQuiet())
{
Reporter.Output.WriteLine(string.Format(CliCommandStrings.UsingLaunchSettingsFromMessage, launchSettingsPath));
}
string profileName = string.IsNullOrEmpty(LaunchProfile) ? CliCommandStrings.DefaultLaunchProfileDisplayName : LaunchProfile;
try
{
var applyResult = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsPath, LaunchProfile);
if (!applyResult.Success)
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName, applyResult.FailureReason).Bold().Red());
}
else
{
launchSettingsModel = applyResult.LaunchSettings;
}
}
catch (IOException ex)
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName).Bold().Red());
Reporter.Error.WriteLine(ex.Message.Bold().Red());
return false;
}
return true;
static string? TryFindLaunchSettings(string projectOrEntryPointFilePath, string? launchProfile)
{
var buildPathContainer = Path.GetDirectoryName(projectOrEntryPointFilePath)!;
string propsDirectory;
// VB.NET projects store the launch settings file in the
// "My Project" directory instead of a "Properties" directory.
// TODO: use the `AppDesignerFolder` MSBuild property instead, which captures this logic already
if (string.Equals(Path.GetExtension(projectOrEntryPointFilePath), ".vbproj", StringComparison.OrdinalIgnoreCase))
{
propsDirectory = "My Project";
}
else
{
propsDirectory = "Properties";
}
string launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(buildPathContainer, propsDirectory);
bool hasLaunchSetttings = File.Exists(launchSettingsPath);
string appName = Path.GetFileNameWithoutExtension(projectOrEntryPointFilePath);
string runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(buildPathContainer, appName);
bool hasRunJson = File.Exists(runJsonPath);
if (hasLaunchSetttings)
{
if (hasRunJson)
{
Reporter.Output.WriteLine(string.Format(CliCommandStrings.RunCommandWarningRunJsonNotUsed, runJsonPath, launchSettingsPath).Yellow());
}
return launchSettingsPath;
}
if (hasRunJson)
{
return runJsonPath;
}
if (!string.IsNullOrEmpty(launchProfile))
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile, launchProfile, $"""
{launchSettingsPath}
{runJsonPath}
""").Bold().Red());
}
return null;
}
}
private void EnsureProjectIsBuilt(out Func<ProjectCollection, ProjectInstance>? projectFactory, out RunProperties? cachedRunProperties, out VirtualProjectBuildingCommand? virtualCommand)
{
int buildResult;
if (EntryPointFileFullPath is not null)
{
virtualCommand = CreateVirtualCommand();
buildResult = virtualCommand.Execute();
projectFactory = CanUseRunPropertiesForCscBuiltProgram(virtualCommand.LastBuild.Level, virtualCommand.LastBuild.Cache?.PreviousEntry) ? null : virtualCommand.CreateProjectInstance;
cachedRunProperties = virtualCommand.LastBuild.Cache?.CurrentEntry.Run;
}
else
{
Debug.Assert(ProjectFileFullPath is not null);
projectFactory = null;
cachedRunProperties = null;
virtualCommand = null;
buildResult = new RestoringCommand(
MSBuildArgs.CloneWithExplicitArgs([ProjectFileFullPath, .. MSBuildArgs.OtherMSBuildArgs]),
NoRestore,
advertiseWorkloadUpdates: false
).Execute();
}
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 CreateVirtualCommand()
{
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>
/// <returns></returns>
private MSBuildArgs SetupSilentBuildArgs(MSBuildArgs msbuildArgs)
{
msbuildArgs = msbuildArgs.CloneWithAdditionalArgs("-nologo");
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);
}
}
internal ICommand GetTargetCommand(Func<ProjectCollection, ProjectInstance>? projectFactory, RunProperties? cachedRunProperties)
{
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.");
return CreateCommandFromRunProperties(cachedRunProperties.WithApplicationArguments(ApplicationArgs));
}
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.");
return CreateCommandForCscBuiltProgram(EntryPointFileFullPath);
}
Reporter.Verbose.WriteLine("Getting target command: evaluating project.");
FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. MSBuildArgs.OtherMSBuildArgs], "dotnet-run");
var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger);
ValidatePreconditions(project);
InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs);
logger?.ReallyShutdown();
var runProperties = RunProperties.FromProject(project).WithApplicationArguments(ApplicationArgs);
var command = CreateCommandFromRunProperties(runProperties);
return command;
static ProjectInstance EvaluateProject(string? projectFilePath, Func<ProjectCollection, ProjectInstance>? projectFactory, MSBuildArgs msbuildArgs, ILogger? binaryLogger)
{
Debug.Assert(projectFilePath is not null || projectFactory is not null);
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
var collection = new ProjectCollection(globalProperties: globalProperties, loggers: binaryLogger is null ? null : [binaryLogger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
if (projectFilePath is not null)
{
return collection.LoadProject(projectFilePath).CreateProjectInstance();
}
Debug.Assert(projectFactory is not null);
return projectFactory(collection);
}
static void ValidatePreconditions(ProjectInstance project)
{
if (string.IsNullOrWhiteSpace(project.GetPropertyValue("TargetFramework")))
{
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)
{
var artifactsPath = VirtualProjectBuildingCommand.GetArtifactsPath(entryPointFileFullPath);
var exePath = Path.Join(artifactsPath, "bin", "debug", Path.GetFileNameWithoutExtension(entryPointFileFullPath) + FileNameSuffixes.CurrentPlatform.Exe);
var commandSpec = new CommandSpec(path: exePath, args: null);
var command = CommandFactoryUsingResolver.Create(commandSpec)
.WorkingDirectory(Path.GetDirectoryName(entryPointFileFullPath));
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)
{
List<ILogger> loggersForBuild = [
TerminalLogger.CreateTerminalOrConsoleLogger([$"--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);
}
}
}
internal static void ThrowUnableToRunError(ProjectInstance project)
{
string targetFrameworks = project.GetPropertyValue("TargetFrameworks");
if (!string.IsNullOrEmpty(targetFrameworks))
{
string targetFramework = project.GetPropertyValue("TargetFramework");
if (string.IsNullOrEmpty(targetFramework))
{
throw new GracefulException(CliCommandStrings.RunCommandExceptionUnableToRunSpecifyFramework, "--framework");
}
}
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();
}
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 (VirtualProjectBuildingCommand.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)
{
if (parseResult.UsingRunCommandShorthandProjectOption())
{
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(RunCommandParser.ApplicationArguments)?.ToList();
LoggerUtility.SeparateBinLogArguments(applicationArguments, out var binLogArgs, out var nonBinLogArgs);
var msbuildProperties = parseResult.OptionValuesToBeForwarded(RunCommandParser.GetCommand()).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(RunCommandParser.ProjectOption);
string? fileOption = parseResult.GetValue(RunCommandParser.FileOption);
if (projectOption != null && fileOption != null)
{
throw new GracefulException(CliCommandStrings.CannotCombineOptions, RunCommandParser.ProjectOption.Name, RunCommandParser.FileOption.Name);
}
string[] args = [.. nonBinLogArgs];
string? projectFilePath = DiscoverProjectFilePath(
filePath: fileOption,
projectFileOrDirectoryPath: projectOption,
readCodeFromStdin: readCodeFromStdin,
ref args,
out string? entryPointFilePath);
bool noBuild = parseResult.HasOption(RunCommandParser.NoBuildOption);
string launchProfile = parseResult.GetValue(RunCommandParser.LaunchProfileOption) ?? string.Empty;
if (readCodeFromStdin && entryPointFilePath != null)
{
Debug.Assert(projectFilePath is null && entryPointFilePath is "-");
if (noBuild)
{
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.NoBuildOption.Name);
}
if (!string.IsNullOrWhiteSpace(launchProfile))
{
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.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 = VirtualProjectBuildingCommand.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.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), RunCommandParser.VerbosityOption);
var command = new RunCommand(
noBuild: noBuild,
projectFileFullPath: projectFilePath,
entryPointFileFullPath: entryPointFilePath,
launchProfile: launchProfile,
noLaunchProfile: parseResult.HasOption(RunCommandParser.NoLaunchProfileOption),
noLaunchProfileArguments: parseResult.HasOption(RunCommandParser.NoLaunchProfileArgumentsOption),
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),
noCache: parseResult.HasOption(RunCommandParser.NoCacheOption),
interactive: parseResult.GetValue(RunCommandParser.InteractiveOption),
msbuildArgs: msbuildArgs,
applicationArgs: args,
readCodeFromStdin: readCodeFromStdin,
environmentVariables: parseResult.GetValue(CommonOptions.EnvOption) ?? ImmutableDictionary<string, string>.Empty,
msbuildRestoreProperties: parseResult.GetValue(CommonOptions.RestorePropertiesOption)
);
return command;
}
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(
ProjectLaunchSettingsModel? launchSettings,
VirtualProjectBuildingCommand? virtualCommand)
{
try
{
if (virtualCommand != null)
{
SendFileBasedTelemetry(launchSettings, virtualCommand);
}
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(
ProjectLaunchSettingsModel? launchSettings,
VirtualProjectBuildingCommand virtualCommand)
{
Debug.Assert(EntryPointFileFullPath != null);
var projectIdentifier = RunTelemetry.GetFileBasedIdentifier(EntryPointFileFullPath, Sha256Hasher.Hash);
var directives = virtualCommand.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: virtualCommand.LastBuild.Level is BuildLevel.All,
usedRoslynCompiler: virtualCommand.LastBuild.Level is BuildLevel.Csc);
}
/// <summary>
/// Builds and sends telemetry data for project-based app runs.
/// </summary>
private void SendProjectBasedTelemetry(ProjectLaunchSettingsModel? 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;
}
}
|