Summary
The SDK decides which command-line tokens belong to MSBuild and which belong to the process a verb launches. It does this in four different places, with three different policies, none of which consult MSBuild's own parser. Every time MSBuild adds a switch, the SDK silently misroutes it until someone notices and patches an allowlist.
MSBuild already exposes the parsing routine needed to stop this. dotnet/msbuild#12836 extracted the command-line parsing out of XMake into a reusable CommandLineParser, and src/MSBuild/AssemblyInfo.cs carries [assembly: InternalsVisibleTo("dotnet", ...)] specifically so the SDK can call it:
Microsoft.Build.CommandLine.Experimental.CommandLineParser
.Parse(IEnumerable<string> args) -> CommandLineSwitchesAccessor
CommandLineSwitchesAccessor exposes typed properties for every switch — MultiThreaded, NodeReuse, MaxCpuCount, GraphBuild, IsolateProjects, Restore, BinaryLogger, TerminalLogger, and so on.
Current state
| Location |
Policy |
LoggerUtility.SeparateLoggerArguments |
Hardcoded allowlist: bl, noConsoleLogger, tl, ll, tlp, clp. Used by build/publish/pack/clean/restore, run, and test under the Microsoft.Testing.Platform runner. |
Test/VSTest/TestCommand.cs |
Forwards all unmatched tokens to MSBuild. |
dotnet-watch CommandLineOptions.GetCommandArguments |
Modeled forwarding options only, plus a hand-extracted -bl (s_binaryLogOptionNames). |
MSBuildArgs.AnalyzeMSBuildArguments |
Re-parses tokens against a small hand-built set of Option objects. |
Consequences
Bugs in this class are found one switch at a time:
Each fix is a separate patch to a separate allowlist. Switches such as -nr/-nodeReuse, -graph, and -isolate are still misrouted today by the verbs that route unmatched tokens away from MSBuild, and any switch MSBuild adds in the future starts out broken.
Proposal
Replace the hand-maintained allowlists with a single classification helper backed by CommandLineParser, and have all four call sites use it. The routing policy for each verb — which recognized MSBuild switches a verb should intercept rather than forward to the launched process — stays an SDK decision, but "is this an MSBuild switch at all" stops being one.
Open questions
Two properties of the current API make it unusable as a drop-in, and would need to be addressed on the MSBuild side first:
-
Parse throws on unrecognized switches. It ends in CommandLineSwitches.ThrowErrors(), and unknown tokens raise UnknownSwitchError. The SDK's use case is the opposite: a token MSBuild does not recognize is a valid argument for the test application or the launched app. This needs a non-throwing mode that reports unrecognized tokens rather than failing.
-
Parse reads response files. It calls GatherAllSwitches, which walks parent directories looking for Directory.Build.rsp and MSBuild.rsp. For classifying tokens that the SDK has already received, that is unwanted I/O, and it would make argument routing depend on response file contents. A parse-only overload would be needed.
Raising as a design discussion rather than a concrete work item, since the shape depends on what MSBuild is willing to expose.
/cc @baronfel
Summary
The SDK decides which command-line tokens belong to MSBuild and which belong to the process a verb launches. It does this in four different places, with three different policies, none of which consult MSBuild's own parser. Every time MSBuild adds a switch, the SDK silently misroutes it until someone notices and patches an allowlist.
MSBuild already exposes the parsing routine needed to stop this. dotnet/msbuild#12836 extracted the command-line parsing out of
XMakeinto a reusableCommandLineParser, andsrc/MSBuild/AssemblyInfo.cscarries[assembly: InternalsVisibleTo("dotnet", ...)]specifically so the SDK can call it:CommandLineSwitchesAccessorexposes typed properties for every switch —MultiThreaded,NodeReuse,MaxCpuCount,GraphBuild,IsolateProjects,Restore,BinaryLogger,TerminalLogger, and so on.Current state
LoggerUtility.SeparateLoggerArgumentsbl,noConsoleLogger,tl,ll,tlp,clp. Used bybuild/publish/pack/clean/restore,run, andtestunder the Microsoft.Testing.Platform runner.Test/VSTest/TestCommand.csdotnet-watchCommandLineOptions.GetCommandArguments-bl(s_binaryLogOptionNames).MSBuildArgs.AnalyzeMSBuildArgumentsOptionobjects.Consequences
Bugs in this class are found one switch at a time:
LoggerUtilityallowlist.--binaryLoggerMSBuild option in the CLI command parsers so thatwatchdoesn't have to do custom handling #49989 —dotnet watchhad no forwarding option for-bl, worked around with a bespoke token scan inCommandLineOptions.-mt/-multiThreadedwas passed to the test application and to the launched app instead of MSBuild. Fixed by adding entries to the same allowlist, plus a second fix indotnet watchmirroring its-blworkaround.Each fix is a separate patch to a separate allowlist. Switches such as
-nr/-nodeReuse,-graph, and-isolateare still misrouted today by the verbs that route unmatched tokens away from MSBuild, and any switch MSBuild adds in the future starts out broken.Proposal
Replace the hand-maintained allowlists with a single classification helper backed by
CommandLineParser, and have all four call sites use it. The routing policy for each verb — which recognized MSBuild switches a verb should intercept rather than forward to the launched process — stays an SDK decision, but "is this an MSBuild switch at all" stops being one.Open questions
Two properties of the current API make it unusable as a drop-in, and would need to be addressed on the MSBuild side first:
Parsethrows on unrecognized switches. It ends inCommandLineSwitches.ThrowErrors(), and unknown tokens raiseUnknownSwitchError. The SDK's use case is the opposite: a token MSBuild does not recognize is a valid argument for the test application or the launched app. This needs a non-throwing mode that reports unrecognized tokens rather than failing.Parsereads response files. It callsGatherAllSwitches, which walks parent directories looking forDirectory.Build.rspandMSBuild.rsp. For classifying tokens that the SDK has already received, that is unwanted I/O, and it would make argument routing depend on response file contents. A parse-only overload would be needed.Raising as a design discussion rather than a concrete work item, since the shape depends on what MSBuild is willing to expose.
/cc @baronfel