Skip to content
71 changes: 21 additions & 50 deletions src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Data;
Expand All @@ -17,8 +16,6 @@ internal sealed class CommandLineOptions
{
public const string DefaultCommand = "run";

private static readonly ImmutableArray<string> s_binaryLogOptionNames = ["-bl", "/bl", "-binaryLogger", "--binaryLogger", "/binaryLogger"];

public bool List { get; init; }
public required GlobalOptions GlobalOptions { get; init; }

Expand Down Expand Up @@ -157,17 +154,14 @@ internal sealed class CommandLineOptions
}
}

var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand, out var binLogToken, out var binLogPath);
var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand);

// We assume that forwarded options, if any, are intended for dotnet build.
var buildArguments = buildOptions.Select(option => ((IForwardedOption)option).GetForwardingFunction()(parseResult)).SelectMany(args => args).ToList();

if (binLogToken != null)
{
buildArguments.Add(binLogToken);
}

var targetFrameworkOption = (Option<string>?)buildOptions.SingleOrDefault(option => option.Name == "--framework");
var binaryLoggerOption = (Option<BinaryLoggerParameters?>?)buildOptions.SingleOrDefault(option => option.Name == "--binaryLogger");
var binaryLoggerOptions = binaryLoggerOption != null ? parseResult.GetValue(binaryLoggerOption) : null;

return new()
{
Expand All @@ -178,7 +172,7 @@ internal sealed class CommandLineOptions
NoHotReload = parseResult.GetValue(noHotReloadOption),
NonInteractive = parseResult.GetValue(NonInteractiveOption),
Verbose = parseResult.GetValue(verboseOption),
BinaryLogPath = ParseBinaryLogFilePath(binLogPath),
BinaryLogPath = GetBinaryLogFilePath(binaryLoggerOptions),
},

CommandArguments = commandArguments,
Expand All @@ -193,35 +187,31 @@ internal sealed class CommandLineOptions
}

/// <summary>
/// Parses the value of msbuild option `-binaryLogger[:[LogFile=]output.binlog[;ProjectImports={None,Embed,ZipFile}]]`.
/// Emulates https://github.com/dotnet/msbuild/blob/7f69ea906c29f2478cc05423484ad185de66e124/src/Build/Logging/BinaryLogger/BinaryLogger.cs#L481.
/// See https://github.com/dotnet/msbuild/issues/12256
/// Gets the binary log file path from the binary logger options.
/// </summary>
internal static string? ParseBinaryLogFilePath(string? value)
=> value switch
internal static string? GetBinaryLogFilePath(BinaryLoggerParameters? parameters)
{
if (parameters == null)
{
null => null,
_ => (from parameter in value.Split(';', StringSplitOptions.RemoveEmptyEntries)
where !string.Equals(parameter, "ProjectImports=None", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(parameter, "ProjectImports=Embed", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(parameter, "ProjectImports=ZipFile", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(parameter, "OmitInitialInfo", StringComparison.OrdinalIgnoreCase)
let path = (parameter.StartsWith("LogFile=", StringComparison.OrdinalIgnoreCase) ? parameter["LogFile=".Length..] : parameter).Trim('"')
let pathWithExtension = path.EndsWith(".binlog", StringComparison.OrdinalIgnoreCase) ? path : $"{path}.binlog"
select pathWithExtension)
.LastOrDefault("msbuild.binlog")
};
return null;
}

if (!string.IsNullOrEmpty(parameters.LogFile))
{
return parameters.LogFile.EndsWith(".binlog", StringComparison.OrdinalIgnoreCase)
? parameters.LogFile
: $"{parameters.LogFile}.binlog";
}

return "msbuild.binlog";
}

private static IReadOnlyList<string> GetCommandArguments(
ParseResult parseResult,
IReadOnlyList<Option> watchOptions,
Command? explicitCommand,
out string? binLogToken,
out string? binLogPath)
Command? explicitCommand)
{
var arguments = new List<string>();
binLogToken = null;
binLogPath = null;

foreach (var child in parseResult.CommandResult.Children)
{
Expand Down Expand Up @@ -276,25 +266,6 @@ private static IReadOnlyList<string> GetCommandArguments(
seenCommand = true;
continue;
}

// Workaround: commands do not have forwarding option for -bl
// https://github.com/dotnet/sdk/issues/49989
foreach (var name in s_binaryLogOptionNames)
{
if (token.StartsWith(name, StringComparison.OrdinalIgnoreCase))
{
if (token.Length == name.Length)
{
binLogToken = token;
binLogPath = "";
}
else if (token.Length > name.Length + 1 && token[name.Length] == ':')
{
binLogToken = token;
binLogPath = token[(name.Length + 1)..];
}
}
}
}

if (!dashDashInserted && i >= unmatchedTokensBeforeDashDash)
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Build/BuildCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.ArchitectureOption);
command.Options.Add(CommonOptions.OperatingSystemOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(TargetOption);

command.SetAction(BuildCommand.Run);
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Clean/CleanCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.ArtifactsPathOption);
command.Options.Add(NoLogoOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(TargetOption);
command.Subcommands.Add(CleanFileBasedAppArtifactsCommandParser.Command);

Expand Down
2 changes: 1 addition & 1 deletion src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ private static MSBuildArgs ConcatTelemetryLogger(MSBuildArgs msbuildArgs)
/// Mostly intended for quick/one-shot usage - most 'core' SDK commands should do more hands-on parsing.
/// </summary>
public MSBuildForwardingApp(IEnumerable<string> rawMSBuildArgs, string? msbuildPath = null) : this(
MSBuildArgs.AnalyzeMSBuildArguments(rawMSBuildArgs.ToArray(), CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), CommonOptions.VerbosityOption()),
MSBuildArgs.AnalyzeMSBuildArguments(rawMSBuildArgs.ToArray(), CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), CommonOptions.VerbosityOption(), CommonOptions.BinaryLoggerOption),
msbuildPath)
{
}
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Pack/PackCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.VersionSuffixOption);
command.Options.Add(ConfigurationOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(TargetOption);

// Don't include runtime option because we want to include it specifically and allow the short version ("-r") to be used
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Publish/PublishCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.ArchitectureOption);
command.Options.Add(CommonOptions.OperatingSystemOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(TargetOption);

command.SetAction(PublishCommand.Run);
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Restore/RestoreCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private static Command ConstructCommand()

command.Arguments.Add(SlnOrProjectOrFileArgument);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);

command.Options.AddRange(FullRestoreOptions());

Expand Down
3 changes: 2 additions & 1 deletion src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ public override RunApiOutput Execute()
applicationArgs: [],
readCodeFromStdin: false,
environmentVariables: ReadOnlyDictionary<string, string>.Empty,
msbuildRestoreProperties: ReadOnlyDictionary<string, string>.Empty);
msbuildRestoreProperties: ReadOnlyDictionary<string, string>.Empty,
binaryLoggerParameters: null);

runCommand.TryGetLaunchProfileSettingsIfNeeded(out var launchSettings);
var targetCommand = (Utils.Command)runCommand.GetTargetCommand(buildCommand.CreateProjectInstance);
Expand Down
36 changes: 19 additions & 17 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ public class RunCommand

public ReadOnlyDictionary<string, string>? RestoreProperties { get; }

/// <summary>
/// Binary logger parameters, if specified on the command line.
/// </summary>
public BinaryLoggerParameters? BinaryLoggerParameters { get; }

/// <summary>
/// unparsed/arbitrary CLI tokens to be passed to the running application
/// </summary>
Expand Down Expand Up @@ -89,7 +94,8 @@ public RunCommand(
string[] applicationArgs,
bool readCodeFromStdin,
IReadOnlyDictionary<string, string> environmentVariables,
ReadOnlyDictionary<string, string>? msbuildRestoreProperties)
ReadOnlyDictionary<string, string>? msbuildRestoreProperties,
BinaryLoggerParameters? binaryLoggerParameters)
{
Debug.Assert(projectFileFullPath is null ^ entryPointFileFullPath is null);
Debug.Assert(!readCodeFromStdin || entryPointFileFullPath is not null);
Expand All @@ -108,6 +114,7 @@ public RunCommand(
MSBuildArgs = SetupSilentBuildArgs(msbuildArgs);
EnvironmentVariables = environmentVariables;
RestoreProperties = msbuildRestoreProperties;
BinaryLoggerParameters = binaryLoggerParameters;
}

public int Execute()
Expand Down Expand Up @@ -356,7 +363,7 @@ internal ICommand GetTargetCommand(Func<ProjectCollection, ProjectInstance>? pro
return CreateCommandForCscBuiltProgram(EntryPointFileFullPath);
}

FacadeLogger? logger = LoggerUtility.DetermineBinlogger([..MSBuildArgs.OtherMSBuildArgs], "dotnet-run");
FacadeLogger? logger = LoggerUtility.CreateBinaryLogger(BinaryLoggerParameters, "dotnet-run");
var project = EvaluateProject(ProjectFileFullPath, projectFactory, MSBuildArgs, logger);
ValidatePreconditions(project);
InvokeRunArgumentsTarget(project, NoBuild, logger, MSBuildArgs);
Expand Down Expand Up @@ -596,23 +603,14 @@ public static RunCommand FromParseResult(ParseResult parseResult)
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
// Get application arguments (no need to separate binary log args since they're now handled as a ForwardedOption)
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 ["-", ..] &&
bool readCodeFromStdin = applicationArguments is ["-", ..] &&
parseResult.Tokens.TakeWhile(static t => t.Type != TokenType.DoubleDash)
.Any(static t => t is { Type: TokenType.Argument, Value: "-" });

Expand All @@ -624,7 +622,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
throw new GracefulException(CliCommandStrings.CannotCombineOptions, RunCommandParser.ProjectOption.Name, RunCommandParser.FileOption.Name);
}

string[] args = [.. nonBinLogArgs];
string[] args = [.. applicationArguments ?? []];
string? projectFilePath = DiscoverProjectFilePath(
filePath: fileOption,
projectFileOrDirectoryPath: projectOption,
Expand Down Expand Up @@ -661,8 +659,11 @@ public static RunCommand FromParseResult(ParseResult parseResult)
stdinStream.CopyTo(fileStream);
}

Debug.Assert(nonBinLogArgs[0] == "-");
nonBinLogArgs[0] = entryPointFilePath;
Debug.Assert(applicationArguments?[0] == "-");
if (applicationArguments != null)
{
applicationArguments[0] = entryPointFilePath;
}
}

var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(msbuildProperties, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), RunCommandParser.VerbosityOption);
Expand All @@ -681,7 +682,8 @@ public static RunCommand FromParseResult(ParseResult parseResult)
applicationArgs: args,
readCodeFromStdin: readCodeFromStdin,
environmentVariables: parseResult.GetValue(CommonOptions.EnvOption) ?? ImmutableDictionary<string, string>.Empty,
msbuildRestoreProperties: parseResult.GetValue(CommonOptions.RestorePropertiesOption)
msbuildRestoreProperties: parseResult.GetValue(CommonOptions.RestorePropertiesOption),
binaryLoggerParameters: parseResult.GetValue(CommonOptions.BinaryLoggerOption)
);

return command;
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Run/RunCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.ArchitectureOption);
command.Options.Add(CommonOptions.OperatingSystemOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(CommonOptions.ArtifactsPathOption);
command.Options.Add(CommonOptions.EnvOption);

Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet/Commands/Store/StoreCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static Command ConstructCommand()
command.Options.Add(CommonOptions.VerbosityOption());
command.Options.Add(CommonOptions.CurrentRuntimeOption(CliCommandStrings.CurrentRuntimeOptionDescription));
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);

command.SetAction(StoreCommand.Run);

Expand Down
10 changes: 7 additions & 3 deletions src/Cli/dotnet/Commands/Test/MSBuildUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModul
Path.GetDirectoryName(solutionModel.Description)! :
SolutionAndProjectUtility.GetRootDirectory(solutionFilePath);

FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb);
FacadeLogger? logger = LoggerUtility.CreateBinaryLogger(buildOptions.BinaryLoggerParameters, dotnetTestVerb);
var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), loggers: logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);

ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = GetProjectsProperties(collection, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions);
Expand All @@ -50,7 +50,7 @@ public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModul
return (Array.Empty<ParallelizableTestModuleGroupWithSequentialInnerModules>(), isBuiltOrRestored);
}

FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb);
FacadeLogger? logger = LoggerUtility.CreateBinaryLogger(buildOptions.BinaryLoggerParameters, dotnetTestVerb);
var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs([.. buildOptions.MSBuildArgs]), logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);

IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, buildOptions.NoLaunchProfile);
Expand All @@ -71,6 +71,9 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult, int degreeOf
parseResult.GetValue(TestingPlatformOptions.SolutionOption),
parseResult.GetValue(TestingPlatformOptions.DirectoryOption));

// Get the binary logger parameters from the parse result
var binaryLoggerParameters = parseResult.GetValue(CommonOptions.BinaryLoggerOption);

return new BuildOptions(
pathOptions,
parseResult.GetValue(CommonOptions.NoRestoreOption),
Expand All @@ -80,7 +83,8 @@ public static BuildOptions GetBuildOptions(ParseResult parseResult, int degreeOf
parseResult.GetValue(TestingPlatformOptions.NoLaunchProfileArgumentsOption),
degreeOfParallelism,
otherArgs,
msbuildArgs);
msbuildArgs,
binaryLoggerParameters);
}

private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOptions buildOptions)
Expand Down
8 changes: 6 additions & 2 deletions src/Cli/dotnet/Commands/Test/Options.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Cli;

namespace Microsoft.DotNet.Cli.Commands.Test;

internal record TestOptions(bool HasFilterMode, bool IsHelp);
Expand All @@ -14,5 +16,7 @@ internal record BuildOptions(
Utils.VerbosityOptions? Verbosity,
bool NoLaunchProfile,
bool NoLaunchProfileArguments,
int DegreeOfParallelism, List<string> UnmatchedTokens,
IEnumerable<string> MSBuildArgs);
int DegreeOfParallelism,
List<string> UnmatchedTokens,
IEnumerable<string> MSBuildArgs,
BinaryLoggerParameters? BinaryLoggerParameters);
2 changes: 2 additions & 0 deletions src/Cli/dotnet/Commands/Test/TestCommandParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ private static Command GetTestingPlatformCliCommand()
command.Options.Add(TestingPlatformOptions.OutputOption);
command.Options.Add(TestingPlatformOptions.NoLaunchProfileOption);
command.Options.Add(TestingPlatformOptions.NoLaunchProfileArgumentsOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(MTPTargetOption);

return command;
Expand Down Expand Up @@ -294,6 +295,7 @@ private static Command GetVSTestCliCommand()
command.Options.Add(CommonOptions.ArchitectureOption);
command.Options.Add(CommonOptions.OperatingSystemOption);
command.Options.Add(CommonOptions.DisableBuildServersOption);
command.Options.Add(CommonOptions.BinaryLoggerOption);
command.Options.Add(VsTestTargetOption);
command.SetAction(TestCommand.Run);

Expand Down
Loading
Loading