55using System . CommandLine ;
66using System . CommandLine . Parsing ;
77using System . Data ;
8+ using System . Diagnostics ;
9+ using Microsoft . Build . Logging ;
810using Microsoft . DotNet . Cli ;
911using Microsoft . DotNet . Cli . Commands . Run ;
1012using Microsoft . DotNet . Cli . Extensions ;
@@ -15,19 +17,28 @@ internal sealed class CommandLineOptions
1517{
1618 public const string DefaultCommand = "run" ;
1719
20+ private static readonly ImmutableArray < string > s_binaryLogOptionNames = [ "-bl" , "/bl" , "-binaryLogger" , "--binaryLogger" , "/binaryLogger" ] ;
21+
1822 public bool List { get ; init ; }
19- required public GlobalOptions GlobalOptions { get ; init ; }
23+ public required GlobalOptions GlobalOptions { get ; init ; }
2024
2125 public string ? ProjectPath { get ; init ; }
2226 public string ? TargetFramework { get ; init ; }
2327 public bool NoLaunchProfile { get ; init ; }
2428 public string ? LaunchProfileName { get ; init ; }
2529
26- public string ? ExplicitCommand { get ; init ; }
27-
30+ /// <summary>
31+ /// Arguments passed to <see cref="Command"/>.
32+ /// </summary>
2833 public required IReadOnlyList < string > CommandArguments { get ; init ; }
34+
35+ /// <summary>
36+ /// Arguments passed to `dotnet build` and to design-time build evaluation.
37+ /// </summary>
2938 public required IReadOnlyList < string > BuildArguments { get ; init ; }
3039
40+ public string ? ExplicitCommand { get ; init ; }
41+
3142 public string Command => ExplicitCommand ?? DefaultCommand ;
3243
3344 // this option is referenced from inner logic and so needs to be reference-able
@@ -128,6 +139,7 @@ internal sealed class CommandLineOptions
128139 Output = output ,
129140 Error = output
130141 } ) ;
142+
131143 if ( ! rootCommandInvoked )
132144 {
133145 // help displayed:
@@ -145,10 +157,16 @@ internal sealed class CommandLineOptions
145157 }
146158 }
147159
148- var commandArguments = GetCommandArguments ( parseResult , watchOptions , explicitCommand ) ;
160+ var commandArguments = GetCommandArguments ( parseResult , watchOptions , explicitCommand , out var binLogToken , out var binLogPath ) ;
149161
150162 // We assume that forwarded options, if any, are intended for dotnet build.
151- var buildArguments = buildOptions . Select ( option => ( ( IForwardedOption ) option ) . GetForwardingFunction ( ) ( parseResult ) ) . SelectMany ( args => args ) . ToArray ( ) ;
163+ var buildArguments = buildOptions . Select ( option => ( ( IForwardedOption ) option ) . GetForwardingFunction ( ) ( parseResult ) ) . SelectMany ( args => args ) . ToList ( ) ;
164+
165+ if ( binLogToken != null )
166+ {
167+ buildArguments . Add ( binLogToken ) ;
168+ }
169+
152170 var targetFrameworkOption = ( Option < string > ? ) buildOptions . SingleOrDefault ( option => option . Name == "--framework" ) ;
153171
154172 return new ( )
@@ -160,6 +178,7 @@ internal sealed class CommandLineOptions
160178 NoHotReload = parseResult . GetValue ( noHotReloadOption ) ,
161179 NonInteractive = parseResult . GetValue ( NonInteractiveOption ) ,
162180 Verbose = parseResult . GetValue ( verboseOption ) ,
181+ BinaryLogPath = ParseBinaryLogFilePath ( binLogPath ) ,
163182 } ,
164183
165184 CommandArguments = commandArguments ,
@@ -173,12 +192,36 @@ internal sealed class CommandLineOptions
173192 } ;
174193 }
175194
195+ /// <summary>
196+ /// Parses the value of msbuild option `-binaryLogger[:[LogFile=]output.binlog[;ProjectImports={None,Embed,ZipFile}]]`.
197+ /// Emulates https://github.com/dotnet/msbuild/blob/7f69ea906c29f2478cc05423484ad185de66e124/src/Build/Logging/BinaryLogger/BinaryLogger.cs#L481.
198+ /// See https://github.com/dotnet/msbuild/issues/12256
199+ /// </summary>
200+ internal static string ? ParseBinaryLogFilePath ( string ? value )
201+ => value switch
202+ {
203+ null => null ,
204+ _ => ( from parameter in value . Split ( ';' , StringSplitOptions . RemoveEmptyEntries )
205+ where ! string . Equals ( parameter , "ProjectImports=None" , StringComparison . OrdinalIgnoreCase ) &&
206+ ! string . Equals ( parameter , "ProjectImports=Embed" , StringComparison . OrdinalIgnoreCase ) &&
207+ ! string . Equals ( parameter , "ProjectImports=ZipFile" , StringComparison . OrdinalIgnoreCase ) &&
208+ ! string . Equals ( parameter , "OmitInitialInfo" , StringComparison . OrdinalIgnoreCase )
209+ let path = ( parameter . StartsWith ( "LogFile=" , StringComparison . OrdinalIgnoreCase ) ? parameter [ "LogFile=" . Length ..] : parameter ) . Trim ( '"' )
210+ let pathWithExtension = path . EndsWith ( ".binlog" , StringComparison . OrdinalIgnoreCase ) ? path : $ "{ path } .binlog"
211+ select pathWithExtension )
212+ . LastOrDefault ( "msbuild.binlog" )
213+ } ;
214+
176215 private static IReadOnlyList < string > GetCommandArguments (
177216 ParseResult parseResult ,
178217 IReadOnlyList < Option > watchOptions ,
179- Command ? explicitCommand )
218+ Command ? explicitCommand ,
219+ out string ? binLogToken ,
220+ out string ? binLogPath )
180221 {
181222 var arguments = new List < string > ( ) ;
223+ binLogToken = null ;
224+ binLogPath = null ;
182225
183226 foreach ( var child in parseResult . CommandResult . Children )
184227 {
@@ -199,23 +242,11 @@ private static IReadOnlyList<string> GetCommandArguments(
199242 continue ;
200243 }
201244
202- // Some options _may_ be computed or have defaults, so not all may have an IdentifierToken.
203- // For those that do not, use the Option's Name instead.
204- var optionNameToForward = optionResult . IdentifierToken ? . Value ?? optionResult . Option . Name ;
245+ var optionNameToForward = GetOptionNameToForward ( optionResult ) ;
205246 if ( optionResult . Tokens . Count == 0 && ! optionResult . Implicit )
206247 {
207248 arguments . Add ( optionNameToForward ) ;
208249 }
209- else if ( optionResult . Option . Name == "--property" )
210- {
211- foreach ( var token in optionResult . Tokens )
212- {
213- // While dotnet-build allows "/p Name=Value", dotnet-msbuild does not.
214- // Any command that forwards args to dotnet-msbuild will fail if we don't use colon.
215- // See https://github.com/dotnet/sdk/issues/44655.
216- arguments . Add ( $ "{ optionNameToForward } :{ token . Value } ") ;
217- }
218- }
219250 else
220251 {
221252 foreach ( var token in optionResult . Tokens )
@@ -227,8 +258,6 @@ private static IReadOnlyList<string> GetCommandArguments(
227258 }
228259 }
229260
230- var tokens = parseResult . UnmatchedTokens . ToArray ( ) ;
231-
232261 // Assuming that all tokens after "--" are unmatched:
233262 var dashDashIndex = IndexOf ( parseResult . Tokens , t => t . Value == "--" ) ;
234263 var unmatchedTokensBeforeDashDash = parseResult . UnmatchedTokens . Count - ( dashDashIndex >= 0 ? parseResult . Tokens . Count - dashDashIndex - 1 : 0 ) ;
@@ -240,10 +269,32 @@ private static IReadOnlyList<string> GetCommandArguments(
240269 {
241270 var token = parseResult . UnmatchedTokens [ i ] ;
242271
243- if ( i < unmatchedTokensBeforeDashDash && ! seenCommand && token == explicitCommand ? . Name )
272+ if ( i < unmatchedTokensBeforeDashDash )
244273 {
245- seenCommand = true ;
246- continue ;
274+ if ( ! seenCommand && token == explicitCommand ? . Name )
275+ {
276+ seenCommand = true ;
277+ continue ;
278+ }
279+
280+ // Workaround: commands do not have forwarding option for -bl
281+ // https://github.com/dotnet/sdk/issues/49989
282+ foreach ( var name in s_binaryLogOptionNames )
283+ {
284+ if ( token . StartsWith ( name , StringComparison . OrdinalIgnoreCase ) )
285+ {
286+ if ( token . Length == name . Length )
287+ {
288+ binLogToken = token ;
289+ binLogPath = "" ;
290+ }
291+ else if ( token . Length > name . Length + 1 && token [ name . Length ] == ':' )
292+ {
293+ binLogToken = token ;
294+ binLogPath = token [ ( name . Length + 1 ) ..] ;
295+ }
296+ }
297+ }
247298 }
248299
249300 if ( ! dashDashInserted && i >= unmatchedTokensBeforeDashDash )
@@ -258,6 +309,11 @@ private static IReadOnlyList<string> GetCommandArguments(
258309 return arguments ;
259310 }
260311
312+ private static string GetOptionNameToForward ( OptionResult optionResult )
313+ // Some options _may_ be computed or have defaults, so not all may have an IdentifierToken.
314+ // For those that do not, use the Option's Name instead.
315+ => optionResult . IdentifierToken ? . Value ?? optionResult . Option . Name ;
316+
261317 private static Command ? TryGetSubcommand ( ParseResult parseResult )
262318 {
263319 // Assuming that all tokens after "--" are unmatched:
0 commit comments