From 4e8e21b23c0d48034892febfcf33763f7f4d62af Mon Sep 17 00:00:00 2001 From: tmat Date: Wed, 30 Jul 2025 11:12:17 -0700 Subject: [PATCH 1/2] Fix /bl option parsing, use it for build logging --- .../dotnet-watch/Build/BuildReporter.cs | 4 +- .../dotnet-watch/Build/EvaluationResult.cs | 3 +- .../CommandLine/CommandLineOptions.cs | 111 ++++++++++++++---- .../CommandLine/EnvironmentOptions.cs | 10 +- .../dotnet-watch/CommandLine/GlobalOptions.cs | 6 + .../HotReload/HotReloadDotNetWatcher.cs | 14 ++- .../HotReload/ScopedCssFileHandler.cs | 4 +- src/BuiltInTools/dotnet-watch/Program.cs | 2 +- .../Properties/launchSettings.json | 6 +- .../dotnet-watch/Watch/BuildEvaluator.cs | 2 +- .../Watch/MsBuildFileSetFactory.cs | 5 - .../Build/EvaluationTests.cs | 6 +- .../CommandLine/BinaryLoggerTests.cs | 72 ++++++++++++ .../CommandLine/CommandLineOptionsTests.cs | 80 +++++++++++-- .../CommandLine/EnvironmentOptionsTests.cs | 29 +++++ .../CommandLine/ProgramTests.cs | 1 + .../HotReload/ApplyDeltaTests.cs | 53 +++++++++ .../TestUtilities/MockFileSetFactory.cs | 2 +- .../TestUtilities/WatchableApp.cs | 9 +- 19 files changed, 352 insertions(+), 67 deletions(-) create mode 100644 test/dotnet-watch.Tests/CommandLine/BinaryLoggerTests.cs create mode 100644 test/dotnet-watch.Tests/CommandLine/EnvironmentOptionsTests.cs diff --git a/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs b/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs index 5a06112ab5a1..52f60f7144de 100644 --- a/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs +++ b/src/BuiltInTools/dotnet-watch/Build/BuildReporter.cs @@ -7,13 +7,13 @@ namespace Microsoft.DotNet.Watch; -internal sealed class BuildReporter(IReporter reporter, EnvironmentOptions environmentOptions) +internal sealed class BuildReporter(IReporter reporter, GlobalOptions options, EnvironmentOptions environmentOptions) { public IReporter Reporter => reporter; public EnvironmentOptions EnvironmentOptions => environmentOptions; public Loggers GetLoggers(string projectPath, string operationName) - => new(reporter, environmentOptions.GetTestBinLogPath(projectPath, operationName)); + => new(reporter, environmentOptions.GetBinLogPath(projectPath, operationName, options)); public void ReportWatchedFiles(Dictionary fileItems) { diff --git a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs index ee209e05b03a..0b753ebdea60 100644 --- a/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs +++ b/src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs @@ -38,11 +38,12 @@ public void WatchFiles(FileWatcher fileWatcher) string rootProjectPath, IEnumerable buildArguments, IReporter reporter, + GlobalOptions options, EnvironmentOptions environmentOptions, bool restore, CancellationToken cancellationToken) { - var buildReporter = new BuildReporter(reporter, environmentOptions); + var buildReporter = new BuildReporter(reporter, options, environmentOptions); // See https://github.com/dotnet/project-system/blob/main/docs/well-known-project-properties.md diff --git a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs index b8dc3a12003e..6e59a17a3cac 100644 --- a/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs +++ b/src/BuiltInTools/dotnet-watch/CommandLine/CommandLineOptions.cs @@ -5,6 +5,8 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Data; +using System.Diagnostics; +using Microsoft.Build.Logging; using Microsoft.DotNet.Cli; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Extensions; @@ -15,19 +17,28 @@ internal sealed class CommandLineOptions { public const string DefaultCommand = "run"; + private static readonly ImmutableArray s_binaryLogOptionNames = ["-bl", "/bl", "-binaryLogger", "--binaryLogger", "/binaryLogger"]; + public bool List { get; init; } - required public GlobalOptions GlobalOptions { get; init; } + public required GlobalOptions GlobalOptions { get; init; } public string? ProjectPath { get; init; } public string? TargetFramework { get; init; } public bool NoLaunchProfile { get; init; } public string? LaunchProfileName { get; init; } - public string? ExplicitCommand { get; init; } - + /// + /// Arguments passed to . + /// public required IReadOnlyList CommandArguments { get; init; } + + /// + /// Arguments passed to `dotnet build` and to design-time build evaluation. + /// public required IReadOnlyList BuildArguments { get; init; } + public string? ExplicitCommand { get; init; } + public string Command => ExplicitCommand ?? DefaultCommand; // this option is referenced from inner logic and so needs to be reference-able @@ -64,6 +75,12 @@ internal sealed class CommandLineOptions var launchProfileOption = new Option("--launch-profile", "-lp") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false }; var noLaunchProfileOption = new Option("--no-launch-profile") { Hidden = true, Arity = ArgumentArity.Zero }; + //var binaryLogOption = new Option(s_binaryLogOptionNames[0], [.. s_binaryLogOptionNames]) + //{ + // Arity = ArgumentArity.ZeroOrOne, + // CustomParser = static r => r.Tokens.FirstOrDefault()?.ToString() ?? "" + //}; + var rootCommand = new RootCommand(Resources.Help) { Directives = { new EnvironmentVariablesDirective() }, @@ -78,6 +95,7 @@ internal sealed class CommandLineOptions rootCommand.Options.Add(shortProjectOption); rootCommand.Options.Add(launchProfileOption); rootCommand.Options.Add(noLaunchProfileOption); + //rootCommand.Options.Add(binaryLogOption); // We process all tokens that do not match any of the above options // to find the subcommand (the first unmatched token preceding "--") @@ -128,6 +146,7 @@ internal sealed class CommandLineOptions Output = output, Error = output }); + if (!rootCommandInvoked) { // help displayed: @@ -145,10 +164,16 @@ internal sealed class CommandLineOptions } } - var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand); + var commandArguments = GetCommandArguments(parseResult, watchOptions, explicitCommand, out var binLogToken, out var binLogPath); // We assume that forwarded options, if any, are intended for dotnet build. - var buildArguments = buildOptions.Select(option => ((IForwardedOption)option).GetForwardingFunction()(parseResult)).SelectMany(args => args).ToArray(); + var buildArguments = buildOptions.Select(option => ((IForwardedOption)option).GetForwardingFunction()(parseResult)).SelectMany(args => args).ToList(); + + if (binLogToken != null) + { + buildArguments.Add(binLogToken); + } + var targetFrameworkOption = (Option?)buildOptions.SingleOrDefault(option => option.Name == "--framework"); return new() @@ -160,6 +185,7 @@ internal sealed class CommandLineOptions NoHotReload = parseResult.GetValue(noHotReloadOption), NonInteractive = parseResult.GetValue(NonInteractiveOption), Verbose = parseResult.GetValue(verboseOption), + BinaryLogPath = ParseBinaryLogFilePath(binLogPath), }, CommandArguments = commandArguments, @@ -173,12 +199,36 @@ internal sealed class CommandLineOptions }; } + /// + /// 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 + /// + internal static string? ParseBinaryLogFilePath(string? value) + => value switch + { + 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") + }; + private static IReadOnlyList GetCommandArguments( ParseResult parseResult, IReadOnlyList