From 78278012511926aa159d891d7a95cb8924b285d2 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 26 Nov 2025 15:09:09 +0100 Subject: [PATCH 01/10] Removing FEATURE_GET_COMMANDLINE constant --- src/Build.UnitTests/Utilities_Tests.cs | 10 +- src/Build/BackEnd/Client/MSBuildClient.cs | 19 +-- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 7 +- .../BackEnd/Node/ServerNodeBuildCommand.cs | 12 -- src/Build/CompatibilitySuppressions.xml | 47 ++++++- src/Directory.BeforeCommon.targets | 1 - .../CommandLineSwitches_Tests.cs | 6 +- .../ProjectSchemaValidationHandler_Tests.cs | 10 +- src/MSBuild.UnitTests/XMake_Tests.cs | 49 ++------ src/MSBuild/MSBuildClientApp.cs | 25 +--- src/MSBuild/XMake.cs | 115 +++--------------- src/Shared/BuildEnvironmentHelper.cs | 7 ++ 12 files changed, 99 insertions(+), 209 deletions(-) diff --git a/src/Build.UnitTests/Utilities_Tests.cs b/src/Build.UnitTests/Utilities_Tests.cs index a29466e852c..880fbe49e59 100644 --- a/src/Build.UnitTests/Utilities_Tests.cs +++ b/src/Build.UnitTests/Utilities_Tests.cs @@ -80,17 +80,9 @@ public void CommentsInPreprocessing() env.SetEnvironmentVariable("MSBUILDLOADALLFILESASWRITEABLE", "1"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe """ + inputFile.Path + - (NativeMethodsShared.IsUnixLike ? @""" -pp:""" : @""" /pp:""") + outputFile.Path + @"""") - .ShouldBe(MSBuildApp.ExitType.Success); -#else Assert.Equal( MSBuildApp.ExitType.Success, - MSBuildApp.Execute( - new[] { @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', - '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'})); -#endif + MSBuildApp.Execute([ @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'])); bool foundDoNotModify = false; foreach (string line in File.ReadLines(outputFile.Path)) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 9bd05788271..e1df9945e91 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -48,11 +48,7 @@ public sealed class MSBuildClient /// The command line to process. /// The first argument on the command line is assumed to be the name/path of the executable, and is ignored. /// -#if FEATURE_GET_COMMANDLINE - private readonly string _commandLine; -#else private readonly string[] _commandLine; -#endif /// /// The MSBuild client execution result. @@ -112,13 +108,7 @@ public sealed class MSBuildClient /// on the command line is assumed to be the name/path of the executable, and is ignored /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. - public MSBuildClient( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation) + public MSBuildClient(string[] commandLine, string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); @@ -162,12 +152,7 @@ private void CreateNodePipeStream() public MSBuildClientExitResult Execute(CancellationToken cancellationToken) { // Command line in one string used only in human readable content. - string descriptiveCommandLine = -#if FEATURE_GET_COMMANDLINE - _commandLine; -#else - string.Join(" ", _commandLine); -#endif + string descriptiveCommandLine = string.Join(" ", _commandLine); CommunicationsUtilities.Trace("Executing build with command line '{0}'", descriptiveCommandLine); diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index e5b1e76f412..49ed1d610dd 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -25,12 +25,7 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// /// A callback used to execute command line build. /// - public delegate (int exitCode, string exitType) BuildCallback( -#if FEATURE_GET_COMMANDLINE - string commandLine); -#else - string[] commandLine); -#endif + public delegate (int exitCode, string exitType) BuildCallback(string[] commandLine); private readonly BuildCallback _buildFunction; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index fc5bca7e920..ab067c7d4ad 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -14,11 +14,7 @@ namespace Microsoft.Build.BackEnd /// internal sealed class ServerNodeBuildCommand : INodePacket { -#if FEATURE_GET_COMMANDLINE - private string _commandLine = default!; -#else private string[] _commandLine = default!; -#endif private string _startupDirectory = default!; private Dictionary _buildProcessEnvironment = default!; private CultureInfo _culture = default!; @@ -34,11 +30,7 @@ internal sealed class ServerNodeBuildCommand : INodePacket /// /// Command line including arguments /// -#if FEATURE_GET_COMMANDLINE - public string CommandLine => _commandLine; -#else public string[] CommandLine => _commandLine; -#endif /// /// The startup directory @@ -79,11 +71,7 @@ private ServerNodeBuildCommand() } public ServerNodeBuildCommand( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else string[] commandLine, -#endif string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, diff --git a/src/Build/CompatibilitySuppressions.xml b/src/Build/CompatibilitySuppressions.xml index 0497d618a92..e1a53b3ea2e 100644 --- a/src/Build/CompatibilitySuppressions.xml +++ b/src/Build/CompatibilitySuppressions.xml @@ -1,3 +1,48 @@  - \ No newline at end of file + + + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 73b91fe6c96..db68730447e 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -31,7 +31,6 @@ $(DefineConstants);FEATURE_ENVIRONMENT_SYSTEMDIRECTORY $(DefineConstants);FEATURE_FILE_TRACKER $(DefineConstants);FEATURE_GAC - $(DefineConstants);FEATURE_GET_COMMANDLINE $(DefineConstants);FEATURE_HANDLEPROCESSCORRUPTEDSTATEEXCEPTIONS $(DefineConstants);FEATURE_HTTP_LISTENER $(DefineConstants);FEATURE_INSTALLED_MSBUILD diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index a2de7a8fb1c..81e5053216d 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -1548,11 +1548,7 @@ public void ProcessInvalidTargetSwitch() using TestEnvironment testEnvironment = TestEnvironment.Create(); string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"msbuild.exe " + project + " /t:foo.bar").ShouldBe(MSBuildApp.ExitType.SwitchError); -#else - MSBuildApp.Execute(new[] { @"msbuild.exe", project, "/t:foo.bar" }).ShouldBe(MSBuildApp.ExitType.SwitchError); -#endif + MSBuildApp.Execute([@"msbuild.exe", project, "/t:foo.bar"]).ShouldBe(MSBuildApp.ExitType.SwitchError); } /// diff --git a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs index 7a224860a2f..f091499933d 100644 --- a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs +++ b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs @@ -52,7 +52,7 @@ public void VerifyInvalidProjectSchema() "); string quotedProjectFilename = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFilename + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFilename, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); } finally { @@ -95,7 +95,7 @@ public void VerifyInvalidSchemaItself1() "); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -155,7 +155,7 @@ public void VerifyInvalidSchemaItself2() string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -203,7 +203,7 @@ public void VerifyValidProjectSchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( @@ -256,7 +256,7 @@ public void VerifyInvalidImportNotCaughtBySchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, "/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 4173c747739..e30c412e7e2 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -562,12 +562,7 @@ public void GetLengthOfSwitchIndicatorTest() [InlineData(@"/h")] public void Help(string indicator) { - MSBuildApp.Execute( -#if FEATURE_GET_COMMANDLINE - @$"c:\bin\msbuild.exe {indicator} ") -#else - new[] { @"c:\bin\msbuild.exe", indicator }) -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", indicator]) .ShouldBe(MSBuildApp.ExitType.Success); } @@ -660,19 +655,11 @@ public void VersionSwitchDisableChangeWave() public void ErrorCommandLine() { string oldValueForMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe -junk").ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(@"msbuild.exe -t").ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(@"msbuild.exe @bogus.rsp").ShouldBe(MSBuildApp.ExitType.InitializationError); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", "-junk" }).ShouldBe(MSBuildApp.ExitType.SwitchError); - MSBuildApp.Execute(new[] { @"msbuild.exe", "-t" }).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"c:\bin\msbuild.exe", "-junk"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "-t"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "@bogus.rsp"]).ShouldBe(MSBuildApp.ExitType.InitializationError); - MSBuildApp.Execute(new[] { @"msbuild.exe", "@bogus.rsp" }).ShouldBe(MSBuildApp.ExitType.InitializationError); -#endif Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", oldValueForMSBuildLoadMicrosoftTargetsReadOnly); } @@ -1153,11 +1140,7 @@ public void TestEnvironmentTest() sw.WriteLine(projectString); } // Should pass -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", quotedProjectFileName }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", quotedProjectFileName]).ShouldBe(MSBuildApp.ExitType.Success); } finally { @@ -1190,21 +1173,15 @@ public void MSBuildEngineLogger() { sw.WriteLine(projectString); } -#if FEATURE_GET_COMMANDLINE // Should pass - MSBuildApp.Execute(@$"c:\bin\msbuild.exe /logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}"" /verbosity:detailed " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); - -#else - // Should pass - MSBuildApp.Execute( - new[] - { + MSBuildApp + .Execute([ NativeMethodsShared.IsWindows ? @"c:\bin\msbuild.exe" : "/msbuild.exe", @$"/logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}""", "/verbosity:detailed", - quotedProjectFileName - }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + quotedProjectFileName]) + .ShouldBe(MSBuildApp.ExitType.Success); + File.Exists(logFile).ShouldBeTrue(); var logFileContents = File.ReadAllText(logFile); @@ -2946,11 +2923,7 @@ public void ThrowsWhenMaxCpuCountTooLargeForMultiThreadedAndForceAllTasksOutOfPr testEnvironment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe " + project + " / m:257 /mt").ShouldBe(MSBuildApp.ExitType.SwitchError); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", project, "/m:257 /mt" }).ShouldBe(MSBuildApp.ExitType.SwitchError); -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", project, "/m:257 /mt"]).ShouldBe(MSBuildApp.ExitType.SwitchError); } private string CopyMSBuild() diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index 3eeb975bc40..33100583fe2 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -34,18 +34,12 @@ internal static class MSBuildClientApp /// /// The locations of msbuild exe/dll and dotnet.exe would be automatically detected if called from dotnet or msbuild cli. Calling this function from other executables might not work. /// - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, CancellationToken cancellationToken) { string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; return Execute( - commandLine, + commandLineArgs, msbuildLocation, cancellationToken); } @@ -53,7 +47,7 @@ public static MSBuildApp.ExitType Execute( /// /// This is the entry point for the MSBuild client. /// - /// The command line to process. The first argument + /// The command line to process. The first argument /// on the command line is assumed to be the name/path of the executable, and /// is ignored. /// Full path to current MSBuild.exe if executable is MSBuild.exe, @@ -61,16 +55,9 @@ public static MSBuildApp.ExitType Execute( /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation, - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbuildLocation, CancellationToken cancellationToken) { - MSBuildClient msbuildClient = new MSBuildClient(commandLine, msbuildLocation); + MSBuildClient msbuildClient = new MSBuildClient(commandLineArgs, msbuildLocation); MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || @@ -84,7 +71,7 @@ public static MSBuildApp.ExitType Execute( } // Server is busy, fallback to old behavior. - return MSBuildApp.Execute(commandLine); + return MSBuildApp.Execute(commandLineArgs); } if (exitResult.MSBuildClientExitType == MSBuildClientExitType.Success && diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index f6bb284bbe3..5f952fda3d7 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -233,14 +233,14 @@ private static void HandleConfigurationException(Exception ex) #if FEATURE_APPDOMAIN [LoaderOptimization(LoaderOptimization.MultiDomain)] #endif -#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter - public static int Main( -#if !FEATURE_GET_COMMANDLINE - string[] args -#endif - ) -#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + public static int Main(string[] args) { + // When invoked from SDK, insert the command executable path as the first element of the args array. + if (BuildEnvironmentHelper.IsRunningOnCoreClr) + { + args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..args]; + } + // Setup the console UI. using AutomaticEncodingRestorer _ = new(); SetConsoleUI(); @@ -263,35 +263,18 @@ string[] args if ( Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine)) -#else - ConstructArrayArg(args))) -#endif + CanRunServerBasedOnCommandLineSwitches(args)) { Console.CancelKeyPress += Console_CancelKeyPress; // Use the client app to execute build in msbuild server. Opt-in feature. - exitCode = ((s_initialized && MSBuildClientApp.Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine, -#else - ConstructArrayArg(args), -#endif - s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && MSBuildClientApp.Execute(args, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { // return 0 on success, non-zero on failure - exitCode = ((s_initialized && Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine) -#else - ConstructArrayArg(args)) -#endif - == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && Execute(args) == ExitType.Success) ? 0 : 1); } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") @@ -310,12 +293,7 @@ string[] args /// /// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway. /// - private static bool CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) { bool canRunServer = true; try @@ -353,23 +331,6 @@ private static bool CanRunServerBasedOnCommandLineSwitches( return canRunServer; } -#if !FEATURE_GET_COMMANDLINE - /// - /// Insert the command executable path as the first element of the args array. - /// - /// - /// - private static string[] ConstructArrayArg(string[] args) - { - string[] newArgArray = new string[args.Length + 1]; - - newArgArray[0] = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - Array.Copy(args, 0, newArgArray, 1, args.Length); - - return newArgArray; - } -#endif // !FEATURE_GET_COMMANDLINE - /// /// Append output file with elapsedTime /// @@ -623,12 +584,7 @@ private static void DebuggerLaunchCheck() /// is ignored. /// A value of type ExitType that indicates whether the build succeeded, /// or the manner in which it failed. - public static ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + public static ExitType Execute(string[] commandLine) { DebuggerLaunchCheck(); @@ -645,9 +601,7 @@ public static ExitType Execute( // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); -#if FEATURE_GET_COMMANDLINE ErrorUtilities.VerifyThrowArgumentLength(commandLine); -#endif AppDomain.CurrentDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler; @@ -659,14 +613,11 @@ public static ExitType Execute( TextWriter targetsWriter = null; try { -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStart(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStart(string.Join(" ", commandLine)); } -#endif + Console.CancelKeyPress += cancelHandler; // check the operating system the code is running on @@ -769,11 +720,7 @@ public static ExitType Execute( ref getTargetResult, ref getResultOutputFile, recursing: false, -#if FEATURE_GET_COMMANDLINE - commandLine); -#else - string.Join(' ', commandLine)); -#endif + string.Join(" ", commandLine)); CommandLineSwitches.SwitchesFromResponseFiles = null; @@ -1073,14 +1020,10 @@ public static ExitType Execute( preprocessWriter?.Dispose(); targetsWriter?.Dispose(); -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStop(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStop(string.Join(" ", commandLine)); } -#endif } /********************************************************************************************************************** * WARNING: Do NOT add any more catch blocks above! @@ -1305,11 +1248,7 @@ internal static bool BuildProject( #if FEATURE_REPORTFILEACCESSES bool reportFileAccesses, #endif -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else string[] commandLine) -#endif { // Set limitation for multithreaded and MSBUILDFORCEALLTASKSOUTOFPROC=1. Max is 256 because of unique task host id generation. if (multiThreaded && Traits.Instance.ForceAllTasksOutOfProcToTaskHost) @@ -1564,11 +1503,8 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) if (!Traits.Instance.EscapeHatches.DoNotSendDeferredMessagesToBuildManager) { var commandLineString = -#if FEATURE_GET_COMMANDLINE - commandLine; -#else string.Join(" ", commandLine); -#endif + messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLineString)); // Log a message for every response file and include it in log @@ -2002,25 +1938,16 @@ internal static void SetConsoleUI() /// /// Combined bag of switches. private static void GatherAllSwitches( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else string[] commandLine, -#endif - out CommandLineSwitches switchesFromAutoResponseFile, out CommandLineSwitches switchesNotFromAutoResponseFile, out string fullCommandLine) + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine) { ResetGatheringSwitchesState(); -#if FEATURE_GET_COMMANDLINE - // split the command line on (unquoted) whitespace - var commandLineArgs = QuotingUtilities.SplitUnquoted(commandLine); - - s_exeName = FileUtilities.FixFilePath(QuotingUtilities.Unquote(commandLineArgs[0])); -#else var commandLineArgs = new List(commandLine); s_exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; -#endif #if USE_MSBUILD_DLL_EXTN var msbuildExtn = ".dll"; @@ -2035,11 +1962,7 @@ private static void GatherAllSwitches( // discard the first piece, because that's the path to the executable -- the rest are args commandLineArgs.RemoveAt(0); -#if FEATURE_GET_COMMANDLINE - fullCommandLine = $"'{commandLine}'"; -#else - fullCommandLine = $"'{string.Join(' ', commandLine)}'"; -#endif + fullCommandLine = $"'{string.Join(" ", commandLine)}'"; // parse the command line, and flag syntax errors and obvious switch errors switchesNotFromAutoResponseFile = new CommandLineSwitches(); diff --git a/src/Shared/BuildEnvironmentHelper.cs b/src/Shared/BuildEnvironmentHelper.cs index 3a0c945eb7d..6c9b8a0979a 100644 --- a/src/Shared/BuildEnvironmentHelper.cs +++ b/src/Shared/BuildEnvironmentHelper.cs @@ -38,6 +38,13 @@ internal sealed class BuildEnvironmentHelper /// private static readonly string[] s_msBuildExeNames = { "MSBuild.exe", "MSBuild.dll" }; + public static bool IsRunningOnCoreClr { get; } = +#if NET + true; +#else + false; +#endif + /// /// Gets the cached Build Environment instance. /// From cdf1a91cb65613a7539a2112f8884ac66e86fed2 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 26 Nov 2025 15:22:36 +0100 Subject: [PATCH 02/10] Update src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs index f091499933d..82fa56a588c 100644 --- a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs +++ b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs @@ -256,7 +256,7 @@ public void VerifyInvalidImportNotCaughtBySchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, "/validate:\"{msbuildTempXsdFilenames[0]}\""])); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( From 8997bc3c0d5bb2da2810618488d2ad705e2a1311 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 26 Nov 2025 15:24:16 +0100 Subject: [PATCH 03/10] Update src/MSBuild/XMake.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/MSBuild/XMake.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 5f952fda3d7..2dccdee2ede 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -235,7 +235,7 @@ private static void HandleConfigurationException(Exception ex) #endif public static int Main(string[] args) { - // When invoked from SDK, insert the command executable path as the first element of the args array. + // When running on CoreCLR (.NET), insert the command executable path as the first element of the args array. if (BuildEnvironmentHelper.IsRunningOnCoreClr) { args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..args]; From c974b5278f2ad06b48c6ac8840c38eea40df7ee9 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 3 Dec 2025 14:16:55 +0100 Subject: [PATCH 04/10] Fixed 'args' array handling in .NETFx scenarios --- src/MSBuild/XMake.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 5f952fda3d7..a0f57bfa25b 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -236,10 +236,9 @@ private static void HandleConfigurationException(Exception ex) public static int Main(string[] args) { // When invoked from SDK, insert the command executable path as the first element of the args array. - if (BuildEnvironmentHelper.IsRunningOnCoreClr) - { - args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..args]; - } + args = BuildEnvironmentHelper.IsRunningOnCoreClr ? + [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args] : + QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); // Setup the console UI. using AutomaticEncodingRestorer _ = new(); @@ -601,8 +600,6 @@ public static ExitType Execute(string[] commandLine) // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); - ErrorUtilities.VerifyThrowArgumentLength(commandLine); - AppDomain.CurrentDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler; ExitType exitType = ExitType.Success; @@ -1960,7 +1957,10 @@ private static void GatherAllSwitches( } // discard the first piece, because that's the path to the executable -- the rest are args - commandLineArgs.RemoveAt(0); + if (commandLineArgs.Count > 0) + { + commandLineArgs.RemoveAt(0); + } fullCommandLine = $"'{string.Join(" ", commandLine)}'"; @@ -2498,8 +2498,8 @@ private static bool ProcessCommandLineSwitches( bool useTerminalLogger = ProcessTerminalLoggerConfiguration(commandLineSwitches, out string aggregatedTerminalLoggerParameters); // This is temporary until we can remove the need for the environment variable. - // DO NOT use this environment variable for any new features as it will be removed without further notice. - Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); + // DO NOT use this environment variable for any new features as it will be removed without further notice. + Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); DisplayVersionMessageIfNeeded(recursing, useTerminalLogger, commandLineSwitches); From 4f396e4144610c355c1a2ad1f298914b0c222f6c Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 3 Dec 2025 14:22:27 +0100 Subject: [PATCH 05/10] Fixing comment --- src/MSBuild/XMake.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 4b03d152a5f..9e4effdd47f 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -235,7 +235,7 @@ private static void HandleConfigurationException(Exception ex) #endif public static int Main(string[] args) { - // When invoked from SDK, insert the command executable path as the first element of the args array. + // When running on CoreCLR(.NET), insert the command executable path as the first element of the args array. args = BuildEnvironmentHelper.IsRunningOnCoreClr ? [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args] : QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); From 9fbb20134800d8ad83d40555340aa0f3043c1cb2 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Thu, 4 Dec 2025 11:39:31 +0100 Subject: [PATCH 06/10] CmdLine parsing was extracted from XMake and the implementation is visible to dotnet --- .../CommandLineSwitches_Tests.cs | 69 +- src/MSBuild.UnitTests/XMake_Tests.cs | 70 +- src/MSBuild/AssemblyInfo.cs | 1 + src/MSBuild/CommandLine/CommandLineParser.cs | 615 ++++++++++++++++++ .../CommandLineSwitchException.cs | 0 .../{ => CommandLine}/CommandLineSwitches.cs | 0 src/MSBuild/MSBuild.csproj | 7 +- src/MSBuild/XMake.cs | 605 +---------------- 8 files changed, 702 insertions(+), 665 deletions(-) create mode 100644 src/MSBuild/CommandLine/CommandLineParser.cs rename src/MSBuild/{ => CommandLine}/CommandLineSwitchException.cs (100%) rename src/MSBuild/{ => CommandLine}/CommandLineSwitches.cs (100%) diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index 81e5053216d..88c5d591c53 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -622,7 +622,9 @@ public void FeatureAvailibilitySwitchIdentificationTest(string switchName) public void TargetsSwitchParameter() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets:targets.txt" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets:targets.txt"], switches); switches.HaveErrors().ShouldBeFalse(); switches[CommandLineSwitches.ParameterizedSwitch.Targets].ShouldBe(new[] { "targets.txt" }); @@ -632,7 +634,9 @@ public void TargetsSwitchParameter() public void TargetsSwitchDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets /targets" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets /targets"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -709,8 +713,9 @@ public void LowPrioritySwitchIdentificationTests(string lowpriority) public void GraphBuildSwitchCanHaveParameters() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph", "/graph:true; NoBuild ;; ;", "/graph:foo" }, switches); + parser.GatherCommandLineSwitches(["/graph", "/graph:true; NoBuild ;; ;", "/graph:foo"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(new[] { "true", " NoBuild ", " ", "foo" }); @@ -721,8 +726,9 @@ public void GraphBuildSwitchCanHaveParameters() public void GraphBuildSwitchCanBeParameterless() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph" }, switches); + parser.GatherCommandLineSwitches(["/graph"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(Array.Empty()); @@ -733,8 +739,9 @@ public void GraphBuildSwitchCanBeParameterless() public void InputResultsCachesSupportsMultipleOccurrence() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/irc", "/irc:a;b", "/irc:c;d" }, switches); + parser.GatherCommandLineSwitches(["/irc", "/irc:a;b", "/irc:c;d"], switches); switches[CommandLineSwitches.ParameterizedSwitch.InputResultsCaches].ShouldBe(new[] { null, "a", "b", "c", "d" }); @@ -745,8 +752,9 @@ public void InputResultsCachesSupportsMultipleOccurrence() public void OutputResultsCache() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a" }, switches); + parser.GatherCommandLineSwitches(["/orc:a"], switches); switches[CommandLineSwitches.ParameterizedSwitch.OutputResultsCache].ShouldBe(new[] { "a" }); @@ -757,8 +765,9 @@ public void OutputResultsCache() public void OutputResultsCachesDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a", "/orc:b" }, switches); + parser.GatherCommandLineSwitches(["/orc:a", "/orc:b"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -1288,8 +1297,9 @@ public void ExtractAnyLoggerParameterPickLast() public void ProcessWarnAsErrorSwitchNotSpecified() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "" }), commandLineSwitches); + parser.GatherCommandLineSwitches([""], commandLineSwitches); Assert.Null(MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches)); } @@ -1303,16 +1313,17 @@ public void ProcessWarnAsErrorSwitchWithCodes() ISet expectedWarningsAsErrors = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnaserror: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnaserror:A,b,C", // Repeats of different case "\"/warnaserror:, ,,\"", // Empty items "/err:D,d;E,e", // A different source with new items and uses the short form "/warnaserror:a", // A different source with a single duplicate "/warnaserror:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1328,12 +1339,13 @@ public void ProcessWarnAsErrorSwitchWithCodes() public void ProcessWarnAsErrorSwitchEmptySwitchClearsSet() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1351,13 +1363,14 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() ISet expectedWarningsAsErors = new HashSet(StringComparer.OrdinalIgnoreCase) { "e", "f", "g" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", "/warnaserror:e;f;g", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1373,8 +1386,9 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() public void ProcessWarnAsErrorSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/warnaserror"], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1390,10 +1404,11 @@ public void ProcessWarnAsErrorSwitchEmpty() public void ProcessWarnAsMessageSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); // Set "expanded" content to match the placeholder so the verify can use the exact resource string as "expected." string command = "{0}"; - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnasmessage" }), commandLineSwitches, command); + parser.GatherCommandLineSwitches(["/warnasmessage"], commandLineSwitches, command); VerifySwitchError(commandLineSwitches, "/warnasmessage", AssemblyResources.GetString("MissingWarnAsMessageParameterError")); } @@ -1410,13 +1425,15 @@ public void ProcessEnvironmentVariableSwitch() env.SetEnvironmentVariable("ENVIRONMENTVARIABLE", string.Empty); CommandLineSwitches commandLineSwitches = new(); + CommandLineParser parser = new CommandLineParser(); + string fullCommandLine = "msbuild validProject.csproj %ENVIRONMENTVARIABLE%"; - MSBuildApp.GatherCommandLineSwitches(new List() { "validProject.csproj", "%ENVIRONMENTVARIABLE%" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["validProject.csproj", "%ENVIRONMENTVARIABLE%"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); commandLineSwitches = new(); fullCommandLine = "msbuild %ENVIRONMENTVARIABLE% validProject.csproj"; - MSBuildApp.GatherCommandLineSwitches(new List() { "%ENVIRONMENTVARIABLE%", "validProject.csproj" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["%ENVIRONMENTVARIABLE%", "validProject.csproj"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); } } @@ -1430,16 +1447,17 @@ public void ProcessWarnAsMessageSwitchWithCodes() ISet expectedWarningsAsMessages = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnasmessage: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnasmessage:A,b,C", // Repeats of different case "\"/warnasmessage:, ,,\"", // Empty items "/nowarn:D,d;E,e", // A different source with new items and uses the short form "/warnasmessage:a", // A different source with a single duplicate "/warnasmessage:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsMessages = MSBuildApp.ProcessWarnAsMessageSwitch(commandLineSwitches); @@ -1455,8 +1473,9 @@ public void ProcessWarnAsMessageSwitchWithCodes() public void ProcessProfileEvaluationEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/profileevaluation" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/profileevaluation"], commandLineSwitches); commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ProfileEvaluation][0].ShouldBe("no-file"); } diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index e30c412e7e2..f36fa01e38b 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -96,11 +96,9 @@ public XMakeAppTests(ITestOutputHelper output) public void GatherCommandLineSwitchesTwoProperties() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/p:a=b", "/p:c=d" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/p:a=b", "/p:c=d"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -111,13 +109,9 @@ public void GatherCommandLineSwitchesTwoProperties() public void GatherCommandLineSwitchesAnyDash() { var switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List { - "-p:a=b", - "--p:maxcpucount=8" - }; - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["-p:a=b", "--p:maxcpucount=8"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -128,11 +122,9 @@ public void GatherCommandLineSwitchesAnyDash() public void GatherCommandLineSwitchesMaxCpuCountWithArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:2" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:2"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[0].ShouldBe("2"); @@ -145,11 +137,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:3", "/m" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:3", "/m"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[1].ShouldBe(Convert.ToString(NativeMethodsShared.GetLogicalCoreCount())); @@ -165,11 +155,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgumentButWithColon() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters.Length.ShouldBe(0); @@ -459,44 +447,44 @@ public void ExtractSwitchParametersTest() { string commandLineArg = "\"/p:foo=\"bar"; string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "/p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); // this test is totally unreal -- we'd never attempt to extract switch parameters if the leading character is not a // switch indicator (either '-' or '/') -- here the leading character is a double-quote commandLineArg = "\"\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "/p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -505,37 +493,37 @@ public void ExtractSwitchParametersTestDoubleDash() { var commandLineArg = "\"--p:foo=\"bar"; var unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "--p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "--p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -548,11 +536,11 @@ public void GetLengthOfSwitchIndicatorTest() var commandLineSwitchWithNoneOrIncorrectIndicator = "zSwitch"; - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); } [Theory] diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index f93e8a6db00..49249f7e410 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -10,6 +10,7 @@ [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +[assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, // so that we don't run into known security issues with loading libraries from unsafe locations diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs new file mode 100644 index 00000000000..291f58da04d --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -0,0 +1,615 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; + +#nullable disable + +namespace Microsoft.Build.CommandLine +{ + internal class CommandLineParser + { + /// + /// String replacement pattern to support paths in response files. + /// + private const string responseFilePathReplacement = "%MSBuildThisFileDirectory%"; + + /// + /// The name of an auto-response file to search for in the project directory and above. + /// + private const string directoryResponseFileName = "Directory.Build.rsp"; + + /// + /// The name of the auto-response file. + /// + private const string autoResponseFileName = "MSBuild.rsp"; + + /// + /// Used to keep track of response files to prevent them from + /// being included multiple times (or even recursively). + /// + private List includedResponseFiles; + + internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); + + public (CommandLineSwitches commandLineSwitches, CommandLineSwitches responseFileSwitches) Parse(IEnumerable commandLineArgs) + { + GatherAllSwitches( + commandLineArgs, + out CommandLineSwitches responseFileSwitches, + out CommandLineSwitches commandLineSwitches, + out _, + out _); + + return (commandLineSwitches, responseFileSwitches); + } + + /// + /// Gets all specified switches, from the command line, as well as all + /// response files, including the auto-response file. + /// + /// + /// + /// + /// + /// Combined bag of switches. + internal void GatherAllSwitches( + IEnumerable commandLineArgs, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine, + out string exeName) + { + ResetGatheringSwitchesState(); + + // discard the first piece, because that's the path to the executable -- the rest are args + commandLineArgs = commandLineArgs.Skip(1); + + exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + +#if USE_MSBUILD_DLL_EXTN + var msbuildExtn = ".dll"; +#else + var msbuildExtn = ".exe"; +#endif + if (!exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) + { + exeName += msbuildExtn; + } + + fullCommandLine = $"'{string.Join(" ", commandLineArgs)}'"; + + // parse the command line, and flag syntax errors and obvious switch errors + switchesNotFromAutoResponseFile = new CommandLineSwitches(); + GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile, fullCommandLine); + + // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the + // switches on the command line + switchesFromAutoResponseFile = new CommandLineSwitches(); + if (!switchesNotFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + GatherAutoResponseFileSwitches(exePath, switchesFromAutoResponseFile, fullCommandLine); + } + } + + /// + /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and + /// flags syntax errors, and other obvious switch errors. + /// + /// + /// Internal for unit testing only. + /// + internal void GatherCommandLineSwitches(IEnumerable commandLineArgs, CommandLineSwitches commandLineSwitches, string commandLine = "") + { + foreach (string commandLineArg in commandLineArgs) + { + string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); + + if (unquotedCommandLineArg.Length > 0) + { + // response file switch starts with @ + if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) + { + GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches, commandLine); + } + else + { + string switchName; + string switchParameters; + + // all switches should start with - or / or -- unless a project is being specified + if (!ValidateSwitchIndicatorInUnquotedArgument(unquotedCommandLineArg) || FileUtilities.LooksLikeUnixFilePath(unquotedCommandLineArg)) + { + switchName = null; + // add a (fake) parameter indicator for later parsing + switchParameters = $":{commandLineArg}"; + } + else + { + // check if switch has parameters (look for the : parameter indicator) + int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); + + // get the length of the beginning sequence considered as a switch indicator (- or / or --) + int switchIndicatorsLength = GetLengthOfSwitchIndicator(unquotedCommandLineArg); + + // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the + // leading - or / or --) and the parameter indicator (if the switch has parameters); the parameters (if any) + // follow the parameter indicator + if (switchParameterIndicator == -1) + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength); + switchParameters = string.Empty; + } + else + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength, switchParameterIndicator - switchIndicatorsLength); + switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator, switchIndicatorsLength); + } + } + + // Special case: for the switches "/m" (or "/maxCpuCount") and "/bl" (or "/binarylogger") we wish to pretend we saw a default argument + // This allows a subsequent /m:n on the command line to override it. + // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. + // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu + // for backwards compatibility. + if (string.IsNullOrEmpty(switchParameters)) + { + if (string.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) + { + int numberOfCpus = NativeMethodsShared.GetLogicalCoreCount(); + switchParameters = $":{numberOfCpus}"; + } + else if (string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase)) + { + // we have to specify at least one parameter otherwise it's impossible to distinguish the situation + // where /bl is not specified at all vs. where /bl is specified without the file name. + switchParameters = ":msbuild.binlog"; + } + else if (string.Equals(switchName, "prof", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "profileevaluation", StringComparison.OrdinalIgnoreCase)) + { + switchParameters = ":no-file"; + } + } + + if (CommandLineSwitches.IsParameterlessSwitch(switchName, out var parameterlessSwitch, out var duplicateSwitchErrorMessage)) + { + GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out var parameterizedSwitch, out duplicateSwitchErrorMessage, out var multipleParametersAllowed, out var missingParametersErrorMessage, out var unquoteParameters, out var allowEmptyParameters)) + { + GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg, allowEmptyParameters, commandLine); + } + else + { + commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg, commandLine); + } + } + } + } + } + + /// + /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses + /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. + /// + /// + /// + private void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches, string commandLine) + { + try + { + string responseFile = FileUtilities.FixFilePath(unquotedCommandLineArg.Substring(1)); + + if (responseFile.Length == 0) + { + commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg, commandLine); + } + else if (!FileSystems.Default.FileExists(responseFile)) + { + commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg, commandLine); + } + else + { + // normalize the response file path to help catch multiple (or recursive) inclusions + responseFile = Path.GetFullPath(responseFile); + // NOTE: for network paths or mapped paths, normalization is not guaranteed to work + + bool isRepeatedResponseFile = false; + + foreach (string includedResponseFile in includedResponseFiles) + { + if (string.Equals(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase)) + { + commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg, commandLine); + isRepeatedResponseFile = true; + break; + } + } + + if (!isRepeatedResponseFile) + { + var responseFileDirectory = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(responseFile)); + includedResponseFiles.Add(responseFile); + + List argsFromResponseFile; + +#if FEATURE_ENCODING_DEFAULT + using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#else + using (StreamReader responseFileContents = FileUtilities.OpenRead(responseFile)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#endif + { + argsFromResponseFile = new List(); + + while (responseFileContents.Peek() != -1) + { + // ignore leading whitespace on each line + string responseFileLine = responseFileContents.ReadLine().TrimStart(); + + // skip comment lines beginning with # + if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) + { + // Allow special case to support a path relative to the .rsp file being processed. + responseFileLine = Regex.Replace(responseFileLine, responseFilePathReplacement, + responseFileDirectory, RegexOptions.IgnoreCase); + + // treat each line of the response file like a command line i.e. args separated by whitespace + argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); + } + } + } + + CommandLineSwitches.SwitchesFromResponseFiles.Add((responseFile, string.Join(" ", argsFromResponseFile))); + + GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches, commandLine); + } + } + } + catch (NotSupportedException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (SecurityException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (UnauthorizedAccessException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (IOException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + } + + /// + /// Called when a switch that doesn't take parameters is detected on the command line. + /// + /// + /// + /// + /// + /// + private static void GatherParameterlessCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + string unquotedCommandLineArg, + string commandLine) + { + // switch should not have any parameters + if (switchParameters.Length == 0) + { + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || + (duplicateSwitchErrorMessage == null)) + { + commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg, commandLine); + } + } + + /// + /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the + /// switch parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static void GatherParameterizedCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + bool multipleParametersAllowed, + string missingParametersErrorMessage, + bool unquoteParameters, + string unquotedCommandLineArg, + bool allowEmptyParameters, + string commandLine) + { + if (// switch must have parameters + (switchParameters.Length > 1) || + // unless the parameters are optional + (missingParametersErrorMessage == null)) + { + // skip the parameter indicator (if any) + if (switchParameters.Length > 0) + { + switchParameters = switchParameters.Substring(1); + } + + if (parameterizedSwitch == CommandLineSwitches.ParameterizedSwitch.Project && IsEnvironmentVariable(switchParameters)) + { + commandLineSwitches.SetSwitchError("EnvironmentVariableAsSwitch", unquotedCommandLineArg, commandLine); + } + + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || + (duplicateSwitchErrorMessage == null)) + { + // save the parameters after unquoting and splitting them if necessary + if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters, allowEmptyParameters)) + { + // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional + if (missingParametersErrorMessage != null) + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + + /// + /// Identifies if there is rsp files near the project file + /// + /// true if there autoresponse file was found + internal bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) + { + bool found = false; + + var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); + + if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + // gather any switches from the first Directory.Build.rsp found in the project directory or above + string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); + + found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); + + // Don't look for more response files if it's only in the same place we already looked (next to the exe) + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + if (!string.Equals(projectDirectory, exePath, StringComparison.OrdinalIgnoreCase)) + { + // this combines any found, with higher precedence, with the switches from the original auto response file switches + found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); + } + } + + return found; + } + + private static string GetProjectDirectory(string[] projectSwitchParameters) + { + string projectDirectory = "."; + ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); + + if (projectSwitchParameters.Length == 1) + { + var projectFile = FileUtilities.FixFilePath(projectSwitchParameters[0]); + + if (FileSystems.Default.DirectoryExists(projectFile)) + { + // the provided argument value is actually the directory + projectDirectory = projectFile; + } + else + { + InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); + projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); + } + } + + return projectDirectory; + } + + /// + /// Extracts a switch's parameters after processing all quoting around the switch. + /// + /// + /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". + /// + /// + /// + /// + /// + /// + /// + /// The given switch's parameters (with interesting quoting preserved). + internal static string ExtractSwitchParameters( + string commandLineArg, + string unquotedCommandLineArg, + int doubleQuotesRemovedFromArg, + string switchName, + int switchParameterIndicator, + int switchIndicatorsLength) + { + + // find the parameter indicator again using the quoted arg + // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a + // parameter indicator cannot be escaped or made into a literal + int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); + + // check if there is any quoting in the name portion of the switch + string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); + + ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), + "The switch name extracted from either the partially or completely unquoted arg should be the same."); + + ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, + "The name portion of the switch cannot contain more quoting than the arg itself."); + + string switchParameters; + // if quoting in the name portion of the switch was terminated + if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) + { + // get the parameters exactly as specified on the command line i.e. including quoting + switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); + } + else + { + // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) + // terminates the switch parameters + int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); + if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && + ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) + { + // then the parameters are not quoted in any interesting way, so use the unquoted parameters + switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); + } + else + { + // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name + // portion of the switch + switchParameters = $":\"{commandLineArg.Substring(quotedSwitchParameterIndicator + 1)}"; + } + } + + ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); + + return switchParameters; + } + + /// + /// Checks whether envVar is an environment variable. MSBuild uses + /// Environment.ExpandEnvironmentVariables(string), which only + /// considers %-delimited variables. + /// + /// A possible environment variable + /// Whether envVar is an environment variable + private static bool IsEnvironmentVariable(string envVar) + { + return envVar.StartsWith("%") && envVar.EndsWith("%") && envVar.Length > 1; + } + + /// + /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the + /// switches from the auto-response file with the switches passed in. + /// Returns true if the response file was found. + /// + private bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + string autoResponseFile = Path.Combine(path, autoResponseFileName); + return GatherAutoResponseFileSwitchesFromFullPath(autoResponseFile, switchesFromAutoResponseFile, commandLine); + } + + private bool GatherAutoResponseFileSwitchesFromFullPath(string autoResponseFile, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + bool found = false; + + // if the auto-response file does not exist, only use the switches on the command line + if (FileSystems.Default.FileExists(autoResponseFile)) + { + found = true; + GatherResponseFileSwitch($"@{autoResponseFile}", switchesFromAutoResponseFile, commandLine); + + // if the "/noautoresponse" switch was set in the auto-response file, flag an error + if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", + switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse), commandLine); + } + + // Throw errors found in the response file + switchesFromAutoResponseFile.ThrowErrors(); + } + + return found; + } + + /// + /// Checks whether an argument given as a parameter starts with valid indicator, + ///
which means, whether switch begins with one of: "/", "-", "--" + ///
+ /// Command line argument with beginning indicator (e.g. --help). + ///
This argument has to be unquoted, otherwise the first character will always be a quote character " + /// true if argument's beginning matches one of possible indicators + ///
false if argument's beginning doesn't match any of correct indicator + ///
+ private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) + { + return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" + || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); + } + + /// + /// Gets the length of the switch indicator (- or / or --) + ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. + ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. + ///
+ /// Unquoted argument with leading indicator and name + /// Correct length of used indicator + ///
0 if no leading sequence recognized as correct indicator
+ /// Internal for testing purposes + internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) + { + if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) + { + return 2; + } + else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) + { + return 1; + } + else + { + return 0; + } + } + + public void ResetGatheringSwitchesState() + { + includedResponseFiles = new List(); + CommandLineSwitches.SwitchesFromResponseFiles = new(); + } + } +} diff --git a/src/MSBuild/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs similarity index 100% rename from src/MSBuild/CommandLineSwitchException.cs rename to src/MSBuild/CommandLine/CommandLineSwitchException.cs diff --git a/src/MSBuild/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs similarity index 100% rename from src/MSBuild/CommandLineSwitches.cs rename to src/MSBuild/CommandLine/CommandLineSwitches.cs diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index e43039e8e6c..d923d5bc84f 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -131,8 +131,9 @@ - - + + + @@ -207,7 +208,7 @@ - + diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 9e4effdd47f..27ba886afba 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -143,6 +143,8 @@ public enum ExitType private static readonly char[] s_commaSemicolon = { ',', ';' }; + private static CommandLineParser commandLineParser; + /// /// Static constructor /// @@ -158,6 +160,7 @@ static MSBuildApp() // any configuration file exceptions can be caught here. // //////////////////////////////////////////////////////////////////////////////// s_exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); + commandLineParser = new CommandLineParser(); s_initialized = true; } @@ -297,9 +300,9 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) bool canRunServer = true; try { - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine); + commandLineParser.GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine, out s_exeName); CommandLineSwitches commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); - if (CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) + if (commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) { commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } @@ -672,7 +675,7 @@ public static ExitType Execute(string[] commandLine) bool reportFileAccesses = false; #endif - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _); + commandLineParser.GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _, out s_exeName); bool buildCanBeInvoked = ProcessCommandLineSwitches( switchesFromAutoResponseFile, @@ -1163,14 +1166,7 @@ private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs ///
private static void ResetBuildState() { - ResetGatheringSwitchesState(); - } - - private static void ResetGatheringSwitchesState() - { - s_includedResponseFiles = new List(); - usingSwitchesFromAutoResponseFile = false; - CommandLineSwitches.SwitchesFromResponseFiles = new(); + commandLineParser.ResetGatheringSwitchesState(); } /// @@ -1507,7 +1503,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLineString)); // Log a message for every response file and include it in log - foreach (var responseFilePath in s_includedResponseFiles) + foreach (var responseFilePath in commandLineParser.IncludedResponseFiles) { messagesToLogInBuildLoggers.Add( new BuildManager.DeferredBuildMessage( @@ -1927,501 +1923,11 @@ internal static void SetConsoleUI() #endif } - /// - /// Gets all specified switches, from the command line, as well as all - /// response files, including the auto-response file. - /// - /// - /// - /// - /// - /// Combined bag of switches. - private static void GatherAllSwitches( - string[] commandLine, - out CommandLineSwitches switchesFromAutoResponseFile, - out CommandLineSwitches switchesNotFromAutoResponseFile, - out string fullCommandLine) - { - ResetGatheringSwitchesState(); - - var commandLineArgs = new List(commandLine); - - s_exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - -#if USE_MSBUILD_DLL_EXTN - var msbuildExtn = ".dll"; -#else - var msbuildExtn = ".exe"; -#endif - if (!s_exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) - { - s_exeName += msbuildExtn; - } - - // discard the first piece, because that's the path to the executable -- the rest are args - commandLineArgs.RemoveAt(0); - - fullCommandLine = $"'{string.Join(" ", commandLine)}'"; - - // parse the command line, and flag syntax errors and obvious switch errors - switchesNotFromAutoResponseFile = new CommandLineSwitches(); - GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile, fullCommandLine); - - // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the - // switches on the command line - switchesFromAutoResponseFile = new CommandLineSwitches(); - if (!switchesNotFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - GatherAutoResponseFileSwitches(s_exePath, switchesFromAutoResponseFile, fullCommandLine); - } - } - - /// - /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and - /// flags syntax errors, and other obvious switch errors. - /// - /// - /// Internal for unit testing only. - /// - internal static void GatherCommandLineSwitches(List commandLineArgs, CommandLineSwitches commandLineSwitches, string commandLine = "") - { - foreach (string commandLineArg in commandLineArgs) - { - string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - - if (unquotedCommandLineArg.Length > 0) - { - // response file switch starts with @ - if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) - { - GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches, commandLine); - } - else - { - string switchName; - string switchParameters; - - // all switches should start with - or / or -- unless a project is being specified - if (!ValidateSwitchIndicatorInUnquotedArgument(unquotedCommandLineArg) || FileUtilities.LooksLikeUnixFilePath(unquotedCommandLineArg)) - { - switchName = null; - // add a (fake) parameter indicator for later parsing - switchParameters = $":{commandLineArg}"; - } - else - { - // check if switch has parameters (look for the : parameter indicator) - int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); - - // get the length of the beginning sequence considered as a switch indicator (- or / or --) - int switchIndicatorsLength = GetLengthOfSwitchIndicator(unquotedCommandLineArg); - - // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the - // leading - or / or --) and the parameter indicator (if the switch has parameters); the parameters (if any) - // follow the parameter indicator - if (switchParameterIndicator == -1) - { - switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength); - switchParameters = string.Empty; - } - else - { - switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength, switchParameterIndicator - switchIndicatorsLength); - switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator, switchIndicatorsLength); - } - } - - // Special case: for the switches "/m" (or "/maxCpuCount") and "/bl" (or "/binarylogger") we wish to pretend we saw a default argument - // This allows a subsequent /m:n on the command line to override it. - // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. - // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu - // for backwards compatibility. - if (string.IsNullOrEmpty(switchParameters)) - { - if (string.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) - { - int numberOfCpus = NativeMethodsShared.GetLogicalCoreCount(); - switchParameters = $":{numberOfCpus}"; - } - else if (string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase)) - { - // we have to specify at least one parameter otherwise it's impossible to distinguish the situation - // where /bl is not specified at all vs. where /bl is specified without the file name. - switchParameters = ":msbuild.binlog"; - } - else if (string.Equals(switchName, "prof", StringComparison.OrdinalIgnoreCase) || - string.Equals(switchName, "profileevaluation", StringComparison.OrdinalIgnoreCase)) - { - switchParameters = ":no-file"; - } - } - - if (CommandLineSwitches.IsParameterlessSwitch(switchName, out var parameterlessSwitch, out var duplicateSwitchErrorMessage)) - { - GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out var parameterizedSwitch, out duplicateSwitchErrorMessage, out var multipleParametersAllowed, out var missingParametersErrorMessage, out var unquoteParameters, out var allowEmptyParameters)) - { - GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg, allowEmptyParameters, commandLine); - } - else - { - commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg, commandLine); - } - } - } - } - } - - /// - /// Extracts a switch's parameters after processing all quoting around the switch. - /// - /// - /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". - /// - /// - /// - /// - /// - /// - /// - /// The given switch's parameters (with interesting quoting preserved). - internal static string ExtractSwitchParameters( - string commandLineArg, - string unquotedCommandLineArg, - int doubleQuotesRemovedFromArg, - string switchName, - int switchParameterIndicator, - int switchIndicatorsLength) - { - - // find the parameter indicator again using the quoted arg - // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a - // parameter indicator cannot be escaped or made into a literal - int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); - - // check if there is any quoting in the name portion of the switch - string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); - - ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), - "The switch name extracted from either the partially or completely unquoted arg should be the same."); - - ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, - "The name portion of the switch cannot contain more quoting than the arg itself."); - - string switchParameters; - // if quoting in the name portion of the switch was terminated - if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) - { - // get the parameters exactly as specified on the command line i.e. including quoting - switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); - } - else - { - // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) - // terminates the switch parameters - int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); - if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && - ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) - { - // then the parameters are not quoted in any interesting way, so use the unquoted parameters - switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); - } - else - { - // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name - // portion of the switch - switchParameters = $":\"{commandLineArg.Substring(quotedSwitchParameterIndicator + 1)}"; - } - } - - ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); - - return switchParameters; - } - - /// - /// Used to keep track of response files to prevent them from - /// being included multiple times (or even recursively). - /// - private static List s_includedResponseFiles; - - /// - /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses - /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. - /// - /// - /// - private static void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches, string commandLine) - { - try - { - string responseFile = FileUtilities.FixFilePath(unquotedCommandLineArg.Substring(1)); - - if (responseFile.Length == 0) - { - commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg, commandLine); - } - else if (!FileSystems.Default.FileExists(responseFile)) - { - commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg, commandLine); - } - else - { - // normalize the response file path to help catch multiple (or recursive) inclusions - responseFile = Path.GetFullPath(responseFile); - // NOTE: for network paths or mapped paths, normalization is not guaranteed to work - - bool isRepeatedResponseFile = false; - - foreach (string includedResponseFile in s_includedResponseFiles) - { - if (string.Equals(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase)) - { - commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg, commandLine); - isRepeatedResponseFile = true; - break; - } - } - - if (!isRepeatedResponseFile) - { - var responseFileDirectory = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(responseFile)); - s_includedResponseFiles.Add(responseFile); - - List argsFromResponseFile; - -#if FEATURE_ENCODING_DEFAULT - using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. -#else - using (StreamReader responseFileContents = FileUtilities.OpenRead(responseFile)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. -#endif - { - argsFromResponseFile = new List(); - - while (responseFileContents.Peek() != -1) - { - // ignore leading whitespace on each line - string responseFileLine = responseFileContents.ReadLine().TrimStart(); - - // skip comment lines beginning with # - if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) - { - // Allow special case to support a path relative to the .rsp file being processed. - responseFileLine = Regex.Replace(responseFileLine, responseFilePathReplacement, - responseFileDirectory, RegexOptions.IgnoreCase); - - // treat each line of the response file like a command line i.e. args separated by whitespace - argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); - } - } - } - - CommandLineSwitches.SwitchesFromResponseFiles.Add((responseFile, string.Join(" ", argsFromResponseFile))); - - GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches, commandLine); - } - } - } - catch (NotSupportedException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (SecurityException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (UnauthorizedAccessException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - catch (IOException e) - { - commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); - } - } - - /// - /// Called when a switch that doesn't take parameters is detected on the command line. - /// - /// - /// - /// - /// - /// - private static void GatherParameterlessCommandLineSwitch( - CommandLineSwitches commandLineSwitches, - CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, - string switchParameters, - string duplicateSwitchErrorMessage, - string unquotedCommandLineArg, - string commandLine) - { - // switch should not have any parameters - if (switchParameters.Length == 0) - { - // check if switch is duplicated, and if that's allowed - if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || - (duplicateSwitchErrorMessage == null)) - { - commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); - } - else - { - commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - } - else - { - commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg, commandLine); - } - } - - /// - /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the - /// switch parameters. - /// - /// - /// - /// - /// - /// - /// - /// - /// - private static void GatherParameterizedCommandLineSwitch( - CommandLineSwitches commandLineSwitches, - CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, - string switchParameters, - string duplicateSwitchErrorMessage, - bool multipleParametersAllowed, - string missingParametersErrorMessage, - bool unquoteParameters, - string unquotedCommandLineArg, - bool allowEmptyParameters, - string commandLine) - { - if (// switch must have parameters - (switchParameters.Length > 1) || - // unless the parameters are optional - (missingParametersErrorMessage == null)) - { - // skip the parameter indicator (if any) - if (switchParameters.Length > 0) - { - switchParameters = switchParameters.Substring(1); - } - - if (parameterizedSwitch == CommandLineSwitches.ParameterizedSwitch.Project && IsEnvironmentVariable(switchParameters)) - { - commandLineSwitches.SetSwitchError("EnvironmentVariableAsSwitch", unquotedCommandLineArg, commandLine); - } - - // check if switch is duplicated, and if that's allowed - if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || - (duplicateSwitchErrorMessage == null)) - { - // save the parameters after unquoting and splitting them if necessary - if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters, allowEmptyParameters)) - { - // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional - if (missingParametersErrorMessage != null) - { - commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); - } - } - } - else - { - commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); - } - } - else - { - commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); - } - } - - /// - /// Checks whether envVar is an environment variable. MSBuild uses - /// Environment.ExpandEnvironmentVariables(string), which only - /// considers %-delimited variables. - /// - /// A possible environment variable - /// Whether envVar is an environment variable - private static bool IsEnvironmentVariable(string envVar) - { - return envVar.StartsWith("%") && envVar.EndsWith("%") && envVar.Length > 1; - } - - /// - /// The name of the auto-response file. - /// - private const string autoResponseFileName = "MSBuild.rsp"; - - /// - /// The name of an auto-response file to search for in the project directory and above. - /// - private const string directoryResponseFileName = "Directory.Build.rsp"; - - /// - /// String replacement pattern to support paths in response files. - /// - private const string responseFilePathReplacement = "%MSBuildThisFileDirectory%"; - - /// - /// Whether switches from the auto-response file are being used. - /// - internal static bool usingSwitchesFromAutoResponseFile = false; - /// /// Indicates that this process is working as a server. /// private static bool s_isServerNode; - /// - /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the - /// switches from the auto-response file with the switches passed in. - /// Returns true if the response file was found. - /// - private static bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) - { - string autoResponseFile = Path.Combine(path, autoResponseFileName); - return GatherAutoResponseFileSwitchesFromFullPath(autoResponseFile, switchesFromAutoResponseFile, commandLine); - } - - private static bool GatherAutoResponseFileSwitchesFromFullPath(string autoResponseFile, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) - { - bool found = false; - - // if the auto-response file does not exist, only use the switches on the command line - if (FileSystems.Default.FileExists(autoResponseFile)) - { - found = true; - GatherResponseFileSwitch($"@{autoResponseFile}", switchesFromAutoResponseFile, commandLine); - - // if the "/noautoresponse" switch was set in the auto-response file, flag an error - if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", - switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse), commandLine); - } - - if (switchesFromAutoResponseFile.HaveAnySwitchesBeenSet()) - { - // we picked up some switches from the auto-response file - usingSwitchesFromAutoResponseFile = true; - } - - // Throw errors found in the response file - switchesFromAutoResponseFile.ThrowErrors(); - } - - return found; - } - /// /// Coordinates the processing of all detected switches. It gathers information necessary to invoke the build engine, and /// performs deeper error checking on the switches and their parameters. @@ -2559,7 +2065,7 @@ private static bool ProcessCommandLineSwitches( } else { - bool foundProjectAutoResponseFile = CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); + bool foundProjectAutoResponseFile = commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); if (foundProjectAutoResponseFile) { @@ -3018,59 +2524,6 @@ private static CommandLineSwitches CombineSwitchesRespectingPriority(CommandLine return commandLineSwitches; } - private static string GetProjectDirectory(string[] projectSwitchParameters) - { - string projectDirectory = "."; - ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); - - if (projectSwitchParameters.Length == 1) - { - var projectFile = FileUtilities.FixFilePath(projectSwitchParameters[0]); - - if (FileSystems.Default.DirectoryExists(projectFile)) - { - // the provided argument value is actually the directory - projectDirectory = projectFile; - } - else - { - InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); - projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); - } - } - - return projectDirectory; - } - - - /// - /// Identifies if there is rsp files near the project file - /// - /// true if there autoresponse file was found - private static bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) - { - bool found = false; - - var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); - - if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - // gather any switches from the first Directory.Build.rsp found in the project directory or above - string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); - - found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); - - // Don't look for more response files if it's only in the same place we already looked (next to the exe) - if (!string.Equals(projectDirectory, s_exePath, StringComparison.OrdinalIgnoreCase)) - { - // this combines any found, with higher precedence, with the switches from the original auto response file switches - found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); - } - } - - return found; - } - private static bool WarningsAsErrorsSwitchIsEmpty(CommandLineSwitches commandLineSwitches) { string val = commandLineSwitches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.WarningsAsErrors); @@ -3673,46 +3126,6 @@ private static void ValidateExtensions(string[] projectExtensionsToIgnore) } } - /// - /// Checks whether an argument given as a parameter starts with valid indicator, - ///
which means, whether switch begins with one of: "/", "-", "--" - ///
- /// Command line argument with beginning indicator (e.g. --help). - ///
This argument has to be unquoted, otherwise the first character will always be a quote character " - /// true if argument's beginning matches one of possible indicators - ///
false if argument's beginning doesn't match any of correct indicator - ///
- private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) - { - return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" - || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); - } - - /// - /// Gets the length of the switch indicator (- or / or --) - ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. - ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. - ///
- /// Unquoted argument with leading indicator and name - /// Correct length of used indicator - ///
0 if no leading sequence recognized as correct indicator
- /// Internal for testing purposes - internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) - { - if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) - { - return 2; - } - else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) - { - return 1; - } - else - { - return 0; - } - } - /// /// Figures out which targets are to be built. /// From de442659c5d70b90051fcc28d81968de1917e500 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 7 Jan 2026 14:45:51 +0100 Subject: [PATCH 07/10] Resolving comments --- src/MSBuild/AssemblyInfo.cs | 2 ++ src/MSBuild/XMake.cs | 11 ++++++++--- src/Shared/BuildEnvironmentHelper.cs | 7 ------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index 49249f7e410..c0407dd5a2d 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -10,6 +10,8 @@ [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] + +// Grant the dotnet CLI access to our command-line parsing logic, which it uses to parse MSBuild arguments. [assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 27ba886afba..468e3eefd04 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -239,9 +239,14 @@ private static void HandleConfigurationException(Exception ex) public static int Main(string[] args) { // When running on CoreCLR(.NET), insert the command executable path as the first element of the args array. - args = BuildEnvironmentHelper.IsRunningOnCoreClr ? - [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args] : - QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); + // This is needed because on .NET the first element of Environment.CommandLine is the dotnet executable path + // and not the msbuild executable path. CoreCLR version didn't support Environment.CommandLine initially, so + // workaround was needed. +#if NET + args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args]; +#else + args = QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); +#endif // Setup the console UI. using AutomaticEncodingRestorer _ = new(); diff --git a/src/Shared/BuildEnvironmentHelper.cs b/src/Shared/BuildEnvironmentHelper.cs index 6c9b8a0979a..3a0c945eb7d 100644 --- a/src/Shared/BuildEnvironmentHelper.cs +++ b/src/Shared/BuildEnvironmentHelper.cs @@ -38,13 +38,6 @@ internal sealed class BuildEnvironmentHelper ///
private static readonly string[] s_msBuildExeNames = { "MSBuild.exe", "MSBuild.dll" }; - public static bool IsRunningOnCoreClr { get; } = -#if NET - true; -#else - false; -#endif - /// /// Gets the cached Build Environment instance. /// From d6c1ca56c09024a7f2b4a9a282871b0b6cfd9e61 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Mon, 12 Jan 2026 15:41:06 +0100 Subject: [PATCH 08/10] Added nice API and merging switches to one instance --- .../CommandLineSwitches_Tests.cs | 23 +++ src/MSBuild/CommandLine/CommandLineParser.cs | 12 +- .../CommandLine/CommandLineSwitches.cs | 154 +++++++++++++++++- .../CommandLine/ICommandLineSwitches.cs | 86 ++++++++++ src/MSBuild/MSBuild.csproj | 1 + 5 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 src/MSBuild/CommandLine/ICommandLineSwitches.cs diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index 88c5d591c53..bae76a0a214 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -641,6 +641,29 @@ public void TargetsSwitchDoesNotSupportMultipleOccurrences() switches.HaveErrors().ShouldBeTrue(); } + [Fact] + public void ParseReturnsInstance() + { + CommandLineParser parser = new CommandLineParser(); + ICommandLineSwitches result = parser.Parse(["MSBuild.exe", "/targets:targets.txt"]); // first parameter must be the executable name + + result.ShouldNotBeNull(); + result.Targets.ShouldNotBeNull(); + result.Targets.ShouldBe(["targets.txt"]); + } + + [Fact] + public void ParseThrowsException() + { + CommandLineParser parser = new CommandLineParser(); + + Should.Throw(() => + { + // first parameter must be the executable name + parser.Parse(["MSBuild.exe", "tempproject.proj", "tempproject.proj"]); + }); + } + [Theory] [InlineData("isolate")] [InlineData("ISOLATE")] diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs index 291f58da04d..8a3f4fcff73 100644 --- a/src/MSBuild/CommandLine/CommandLineParser.cs +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -43,16 +43,22 @@ internal class CommandLineParser internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); - public (CommandLineSwitches commandLineSwitches, CommandLineSwitches responseFileSwitches) Parse(IEnumerable commandLineArgs) + public ICommandLineSwitches Parse(IEnumerable commandLineArgs) { GatherAllSwitches( commandLineArgs, out CommandLineSwitches responseFileSwitches, out CommandLineSwitches commandLineSwitches, - out _, + out string fullCommandLine, out _); - return (commandLineSwitches, responseFileSwitches); + CommandLineSwitches result = new CommandLineSwitches(); + result.Append(responseFileSwitches, fullCommandLine); // lowest precedence + result.Append(commandLineSwitches, fullCommandLine); + + result.ThrowErrors(); + + return result; } /// diff --git a/src/MSBuild/CommandLine/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs index 66b529eb62f..4c3f5947886 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -17,7 +17,7 @@ namespace Microsoft.Build.CommandLine /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter /// accumulation, and error generation. /// - internal sealed class CommandLineSwitches + internal sealed class CommandLineSwitches : ICommandLineSwitches { /// /// Enumeration of all recognized switches that do not take any parameters. @@ -423,6 +423,7 @@ private struct DetectedParameterlessSwitch /// /// This struct stores the details of a switch that takes parameters that is detected on the command line. /// + [DebuggerDisplay("{commandLineArg} | {parameters}")] private struct DetectedParameterizedSwitch { // the actual text of the switch @@ -432,8 +433,8 @@ private struct DetectedParameterizedSwitch internal ArrayList parameters; } - // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the - // command line + // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the command + // line private DetectedParameterlessSwitch[] _parameterlessSwitches; // for each recognized switch that takes parameters, this array indicates if the switch has been detected on the command // line, and it provides a store for the switch parameters @@ -442,6 +443,152 @@ private struct DetectedParameterizedSwitch internal static List<(string path, string contents)> SwitchesFromResponseFiles = new(); + #region ICommandLineSwitches Members + + // Parameterless switches + public bool? Help => IsParameterlessSwitchSet(ParameterlessSwitch.Help) ? this[ParameterlessSwitch.Help] : null; + + public bool? Version => IsParameterlessSwitchSet(ParameterlessSwitch.Version) ? this[ParameterlessSwitch.Version] : null; + + public bool? NoLogo => IsParameterlessSwitchSet(ParameterlessSwitch.NoLogo) ? this[ParameterlessSwitch.NoLogo] : null; + + public bool? NoAutoResponse => IsParameterlessSwitchSet(ParameterlessSwitch.NoAutoResponse) ? this[ParameterlessSwitch.NoAutoResponse] : null; + + public bool? NoConsoleLogger => IsParameterlessSwitchSet(ParameterlessSwitch.NoConsoleLogger) ? this[ParameterlessSwitch.NoConsoleLogger] : null; + + public bool? FileLogger => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger) ? this[ParameterlessSwitch.FileLogger] : null; + + public bool? FileLogger1 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger1) ? this[ParameterlessSwitch.FileLogger1] : null; + + public bool? FileLogger2 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger2) ? this[ParameterlessSwitch.FileLogger2] : null; + + public bool? FileLogger3 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger3) ? this[ParameterlessSwitch.FileLogger3] : null; + + public bool? FileLogger4 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger4) ? this[ParameterlessSwitch.FileLogger4] : null; + + public bool? FileLogger5 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger5) ? this[ParameterlessSwitch.FileLogger5] : null; + + public bool? FileLogger6 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger6) ? this[ParameterlessSwitch.FileLogger6] : null; + + public bool? FileLogger7 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger7) ? this[ParameterlessSwitch.FileLogger7] : null; + + public bool? FileLogger8 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger8) ? this[ParameterlessSwitch.FileLogger8] : null; + + public bool? FileLogger9 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger9) ? this[ParameterlessSwitch.FileLogger9] : null; + + public bool? DistributedFileLogger => IsParameterlessSwitchSet(ParameterlessSwitch.DistributedFileLogger) ? this[ParameterlessSwitch.DistributedFileLogger] : null; + +#if DEBUG + public bool? WaitForDebugger => IsParameterlessSwitchSet(ParameterlessSwitch.WaitForDebugger) ? this[ParameterlessSwitch.WaitForDebugger] : null; +#endif + + // Parameterized switches + public string[] Project => IsParameterizedSwitchSet(ParameterizedSwitch.Project) ? this[ParameterizedSwitch.Project] : null; + + public string[] Target => IsParameterizedSwitchSet(ParameterizedSwitch.Target) ? this[ParameterizedSwitch.Target] : null; + + public string[] Property => IsParameterizedSwitchSet(ParameterizedSwitch.Property) ? this[ParameterizedSwitch.Property] : null; + + public string[] Logger => IsParameterizedSwitchSet(ParameterizedSwitch.Logger) ? this[ParameterizedSwitch.Logger] : null; + + public string[] DistributedLogger => IsParameterizedSwitchSet(ParameterizedSwitch.DistributedLogger) ? this[ParameterizedSwitch.DistributedLogger] : null; + + public string[] Verbosity => IsParameterizedSwitchSet(ParameterizedSwitch.Verbosity) ? this[ParameterizedSwitch.Verbosity] : null; + +#if FEATURE_XML_SCHEMA_VALIDATION + public string[] Validate => IsParameterizedSwitchSet(ParameterizedSwitch.Validate) ? this[ParameterizedSwitch.Validate] : null; +#endif + + public string[] ConsoleLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.ConsoleLoggerParameters) ? this[ParameterizedSwitch.ConsoleLoggerParameters] : null; + + public string[] NodeMode => IsParameterizedSwitchSet(ParameterizedSwitch.NodeMode) ? this[ParameterizedSwitch.NodeMode] : null; + + public string[] MaxCpuCount => IsParameterizedSwitchSet(ParameterizedSwitch.MaxCPUCount) ? this[ParameterizedSwitch.MaxCPUCount] : null; + + public string[] IgnoreProjectExtensions => IsParameterizedSwitchSet(ParameterizedSwitch.IgnoreProjectExtensions) ? this[ParameterizedSwitch.IgnoreProjectExtensions] : null; + + public string[] ToolsVersion => IsParameterizedSwitchSet(ParameterizedSwitch.ToolsVersion) ? this[ParameterizedSwitch.ToolsVersion] : null; + + public string[] FileLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters) ? this[ParameterizedSwitch.FileLoggerParameters] : null; + + public string[] FileLoggerParameters1 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters1) ? this[ParameterizedSwitch.FileLoggerParameters1] : null; + + public string[] FileLoggerParameters2 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters2) ? this[ParameterizedSwitch.FileLoggerParameters2] : null; + + public string[] FileLoggerParameters3 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters3) ? this[ParameterizedSwitch.FileLoggerParameters3] : null; + + public string[] FileLoggerParameters4 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters4) ? this[ParameterizedSwitch.FileLoggerParameters4] : null; + + public string[] FileLoggerParameters5 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters5) ? this[ParameterizedSwitch.FileLoggerParameters5] : null; + + public string[] FileLoggerParameters6 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters6) ? this[ParameterizedSwitch.FileLoggerParameters6] : null; + + public string[] FileLoggerParameters7 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters7) ? this[ParameterizedSwitch.FileLoggerParameters7] : null; + + public string[] FileLoggerParameters8 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters8) ? this[ParameterizedSwitch.FileLoggerParameters8] : null; + + public string[] FileLoggerParameters9 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters9) ? this[ParameterizedSwitch.FileLoggerParameters9] : null; + + public string[] TerminalLogger => IsParameterizedSwitchSet(ParameterizedSwitch.TerminalLogger) ? this[ParameterizedSwitch.TerminalLogger] : null; + + public string[] TerminalLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.TerminalLoggerParameters) ? this[ParameterizedSwitch.TerminalLoggerParameters] : null; + + public string[] NodeReuse => IsParameterizedSwitchSet(ParameterizedSwitch.NodeReuse) ? this[ParameterizedSwitch.NodeReuse] : null; + + public string[] Preprocess => IsParameterizedSwitchSet(ParameterizedSwitch.Preprocess) ? this[ParameterizedSwitch.Preprocess] : null; + + public string[] Targets => IsParameterizedSwitchSet(ParameterizedSwitch.Targets) ? this[ParameterizedSwitch.Targets] : null; + + public string[] WarningsAsErrors => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsAsErrors) ? this[ParameterizedSwitch.WarningsAsErrors] : null; + + public string[] WarningsNotAsErrors => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsNotAsErrors) ? this[ParameterizedSwitch.WarningsNotAsErrors] : null; + + public string[] WarningsAsMessages => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsAsMessages) ? this[ParameterizedSwitch.WarningsAsMessages] : null; + + public string[] BinaryLogger => IsParameterizedSwitchSet(ParameterizedSwitch.BinaryLogger) ? this[ParameterizedSwitch.BinaryLogger] : null; + + public string[] Check => IsParameterizedSwitchSet(ParameterizedSwitch.Check) ? this[ParameterizedSwitch.Check] : null; + + public string[] Restore => IsParameterizedSwitchSet(ParameterizedSwitch.Restore) ? this[ParameterizedSwitch.Restore] : null; + + public string[] ProfileEvaluation => IsParameterizedSwitchSet(ParameterizedSwitch.ProfileEvaluation) ? this[ParameterizedSwitch.ProfileEvaluation] : null; + + public string[] RestoreProperty => IsParameterizedSwitchSet(ParameterizedSwitch.RestoreProperty) ? this[ParameterizedSwitch.RestoreProperty] : null; + + public string[] Interactive => IsParameterizedSwitchSet(ParameterizedSwitch.Interactive) ? this[ParameterizedSwitch.Interactive] : null; + + public string[] IsolateProjects => IsParameterizedSwitchSet(ParameterizedSwitch.IsolateProjects) ? this[ParameterizedSwitch.IsolateProjects] : null; + + public string[] GraphBuild => IsParameterizedSwitchSet(ParameterizedSwitch.GraphBuild) ? this[ParameterizedSwitch.GraphBuild] : null; + + public string[] InputResultsCaches => IsParameterizedSwitchSet(ParameterizedSwitch.InputResultsCaches) ? this[ParameterizedSwitch.InputResultsCaches] : null; + + public string[] OutputResultsCache => IsParameterizedSwitchSet(ParameterizedSwitch.OutputResultsCache) ? this[ParameterizedSwitch.OutputResultsCache] : null; + +#if FEATURE_REPORTFILEACCESSES + public string[] ReportFileAccesses => IsParameterizedSwitchSet(ParameterizedSwitch.ReportFileAccesses) ? this[ParameterizedSwitch.ReportFileAccesses] : null; +#endif + + public string[] LowPriority => IsParameterizedSwitchSet(ParameterizedSwitch.LowPriority) ? this[ParameterizedSwitch.LowPriority] : null; + + public string[] Question => IsParameterizedSwitchSet(ParameterizedSwitch.Question) ? this[ParameterizedSwitch.Question] : null; + + public string[] DetailedSummary => IsParameterizedSwitchSet(ParameterizedSwitch.DetailedSummary) ? this[ParameterizedSwitch.DetailedSummary] : null; + + public string[] GetProperty => IsParameterizedSwitchSet(ParameterizedSwitch.GetProperty) ? this[ParameterizedSwitch.GetProperty] : null; + + public string[] GetItem => IsParameterizedSwitchSet(ParameterizedSwitch.GetItem) ? this[ParameterizedSwitch.GetItem] : null; + + public string[] GetTargetResult => IsParameterizedSwitchSet(ParameterizedSwitch.GetTargetResult) ? this[ParameterizedSwitch.GetTargetResult] : null; + + public string[] GetResultOutputFile => IsParameterizedSwitchSet(ParameterizedSwitch.GetResultOutputFile) ? this[ParameterizedSwitch.GetResultOutputFile] : null; + + public string[] FeatureAvailability => IsParameterizedSwitchSet(ParameterizedSwitch.FeatureAvailability) ? this[ParameterizedSwitch.FeatureAvailability] : null; + + public string[] MultiThreaded => IsParameterizedSwitchSet(ParameterizedSwitch.MultiThreaded) ? this[ParameterizedSwitch.MultiThreaded] : null; + + #endregion + /// /// Default constructor. /// @@ -498,6 +645,7 @@ internal void SetParameterlessSwitch(ParameterlessSwitch parameterlessSwitch, st /// /// /// + /// /// true, if the given parameters were successfully stored internal bool SetParameterizedSwitch( ParameterizedSwitch parameterizedSwitch, diff --git a/src/MSBuild/CommandLine/ICommandLineSwitches.cs b/src/MSBuild/CommandLine/ICommandLineSwitches.cs new file mode 100644 index 00000000000..1122b316902 --- /dev/null +++ b/src/MSBuild/CommandLine/ICommandLineSwitches.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Build.CommandLine +{ + internal interface ICommandLineSwitches + { + // Parameterless switches + bool? Help { get; } + bool? Version { get; } + bool? NoLogo { get; } + bool? NoAutoResponse { get; } + bool? NoConsoleLogger { get; } + bool? FileLogger { get; } + bool? FileLogger1 { get; } + bool? FileLogger2 { get; } + bool? FileLogger3 { get; } + bool? FileLogger4 { get; } + bool? FileLogger5 { get; } + bool? FileLogger6 { get; } + bool? FileLogger7 { get; } + bool? FileLogger8 { get; } + bool? FileLogger9 { get; } + bool? DistributedFileLogger { get; } + +#if DEBUG + bool? WaitForDebugger { get; } +#endif + + // Parameterized switches + string[]? Project { get; } + string[]? Target { get; } + string[]? Property { get; } + string[]? Logger { get; } + string[]? DistributedLogger { get; } + string[]? Verbosity { get; } +#if FEATURE_XML_SCHEMA_VALIDATION + string[]? Validate { get; } +#endif + string[]? ConsoleLoggerParameters { get; } + string[]? NodeMode { get; } + string[]? MaxCpuCount { get; } + string[]? IgnoreProjectExtensions { get; } + string[]? ToolsVersion { get; } + string[]? FileLoggerParameters { get; } + string[]? FileLoggerParameters1 { get; } + string[]? FileLoggerParameters2 { get; } + string[]? FileLoggerParameters3 { get; } + string[]? FileLoggerParameters4 { get; } + string[]? FileLoggerParameters5 { get; } + string[]? FileLoggerParameters6 { get; } + string[]? FileLoggerParameters7 { get; } + string[]? FileLoggerParameters8 { get; } + string[]? FileLoggerParameters9 { get; } + string[]? TerminalLogger { get; } + string[]? TerminalLoggerParameters { get; } + string[]? NodeReuse { get; } + string[]? Preprocess { get; } + string[]? Targets { get; } + string[]? WarningsAsErrors { get; } + string[]? WarningsNotAsErrors { get; } + string[]? WarningsAsMessages { get; } + string[]? BinaryLogger { get; } + string[]? Check { get; } + string[]? Restore { get; } + string[]? ProfileEvaluation { get; } + string[]? RestoreProperty { get; } + string[]? Interactive { get; } + string[]? IsolateProjects { get; } + string[]? GraphBuild { get; } + string[]? InputResultsCaches { get; } + string[]? OutputResultsCache { get; } +#if FEATURE_REPORTFILEACCESSES + string[]? ReportFileAccesses { get; } +#endif + string[]? LowPriority { get; } + string[]? Question { get; } + string[]? DetailedSummary { get; } + string[]? GetProperty { get; } + string[]? GetItem { get; } + string[]? GetTargetResult { get; } + string[]? GetResultOutputFile { get; } + string[]? FeatureAvailability { get; } + string[]? MultiThreaded { get; } + } +} diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index d923d5bc84f..aa08cfd8016 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -135,6 +135,7 @@ + From c49ec2f03de69ae0b904d1eef0fe02ccd881ffcc Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 13 Jan 2026 12:54:31 +0100 Subject: [PATCH 09/10] Parse method is now documented and it injects executable path automatically. ICommandLineSwitechs was replaced by CommandLineSwitchesAccessor as a simple struct wrapper for CommandLineSwitches. --- .../CommandLineParserTests.cs | 38 ++++ .../CommandLineSwitches_Tests.cs | 23 --- src/MSBuild/CommandLine/CommandLineParser.cs | 22 ++- .../CommandLine/CommandLineSwitches.cs | 144 +--------------- .../CommandLineSwitchesAccessor.cs | 163 ++++++++++++++++++ .../CommandLine/ICommandLineSwitches.cs | 86 --------- src/MSBuild/MSBuild.csproj | 2 +- 7 files changed, 223 insertions(+), 255 deletions(-) create mode 100644 src/MSBuild.UnitTests/CommandLineParserTests.cs create mode 100644 src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs delete mode 100644 src/MSBuild/CommandLine/ICommandLineSwitches.cs diff --git a/src/MSBuild.UnitTests/CommandLineParserTests.cs b/src/MSBuild.UnitTests/CommandLineParserTests.cs new file mode 100644 index 00000000000..5c6007f86dc --- /dev/null +++ b/src/MSBuild.UnitTests/CommandLineParserTests.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.CommandLine.UnitTests +{ + public class CommandLineParserTests + { + [Fact] + public void ParseReturnsInstance() + { + CommandLineParser parser = new CommandLineParser(); + CommandLineSwitchesAccessor result = parser.Parse(["/targets:targets.txt"]); // first parameter must be the executable name + + result.Targets.ShouldNotBeNull(); + result.Targets.ShouldBe(["targets.txt"]); + } + + [Fact] + public void ParseThrowsException() + { + CommandLineParser parser = new CommandLineParser(); + + Should.Throw(() => + { + // first parameter must be the executable name + parser.Parse(["tempproject.proj", "tempproject.proj"]); + }); + } + } +} diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index bae76a0a214..88c5d591c53 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -641,29 +641,6 @@ public void TargetsSwitchDoesNotSupportMultipleOccurrences() switches.HaveErrors().ShouldBeTrue(); } - [Fact] - public void ParseReturnsInstance() - { - CommandLineParser parser = new CommandLineParser(); - ICommandLineSwitches result = parser.Parse(["MSBuild.exe", "/targets:targets.txt"]); // first parameter must be the executable name - - result.ShouldNotBeNull(); - result.Targets.ShouldNotBeNull(); - result.Targets.ShouldBe(["targets.txt"]); - } - - [Fact] - public void ParseThrowsException() - { - CommandLineParser parser = new CommandLineParser(); - - Should.Throw(() => - { - // first parameter must be the executable name - parser.Parse(["MSBuild.exe", "tempproject.proj", "tempproject.proj"]); - }); - } - [Theory] [InlineData("isolate")] [InlineData("ISOLATE")] diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs index 8a3f4fcff73..25e40f9800f 100644 --- a/src/MSBuild/CommandLine/CommandLineParser.cs +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -43,10 +43,26 @@ internal class CommandLineParser internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); - public ICommandLineSwitches Parse(IEnumerable commandLineArgs) + /// + /// Parses the provided command-line arguments into a . + /// + /// + /// The command-line arguments excluding the executable path. + /// + /// + /// A containing the effective set of switches after combining + /// switches from response files (including any auto-response file) with switches from the command line, + /// where command-line switches take precedence. + /// + /// + /// Thrown when invalid switch syntax or values are encountered while parsing the command line or response files. + /// + public CommandLineSwitchesAccessor Parse(IEnumerable commandLineArgs) { + List args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..commandLineArgs]; + GatherAllSwitches( - commandLineArgs, + args, out CommandLineSwitches responseFileSwitches, out CommandLineSwitches commandLineSwitches, out string fullCommandLine, @@ -58,7 +74,7 @@ public ICommandLineSwitches Parse(IEnumerable commandLineArgs) result.ThrowErrors(); - return result; + return new CommandLineSwitchesAccessor(result); } /// diff --git a/src/MSBuild/CommandLine/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs index 4c3f5947886..4b2943428f0 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -17,7 +17,7 @@ namespace Microsoft.Build.CommandLine /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter /// accumulation, and error generation. /// - internal sealed class CommandLineSwitches : ICommandLineSwitches + internal sealed class CommandLineSwitches { /// /// Enumeration of all recognized switches that do not take any parameters. @@ -445,147 +445,7 @@ private struct DetectedParameterizedSwitch #region ICommandLineSwitches Members - // Parameterless switches - public bool? Help => IsParameterlessSwitchSet(ParameterlessSwitch.Help) ? this[ParameterlessSwitch.Help] : null; - - public bool? Version => IsParameterlessSwitchSet(ParameterlessSwitch.Version) ? this[ParameterlessSwitch.Version] : null; - - public bool? NoLogo => IsParameterlessSwitchSet(ParameterlessSwitch.NoLogo) ? this[ParameterlessSwitch.NoLogo] : null; - - public bool? NoAutoResponse => IsParameterlessSwitchSet(ParameterlessSwitch.NoAutoResponse) ? this[ParameterlessSwitch.NoAutoResponse] : null; - - public bool? NoConsoleLogger => IsParameterlessSwitchSet(ParameterlessSwitch.NoConsoleLogger) ? this[ParameterlessSwitch.NoConsoleLogger] : null; - - public bool? FileLogger => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger) ? this[ParameterlessSwitch.FileLogger] : null; - - public bool? FileLogger1 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger1) ? this[ParameterlessSwitch.FileLogger1] : null; - - public bool? FileLogger2 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger2) ? this[ParameterlessSwitch.FileLogger2] : null; - - public bool? FileLogger3 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger3) ? this[ParameterlessSwitch.FileLogger3] : null; - - public bool? FileLogger4 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger4) ? this[ParameterlessSwitch.FileLogger4] : null; - - public bool? FileLogger5 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger5) ? this[ParameterlessSwitch.FileLogger5] : null; - - public bool? FileLogger6 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger6) ? this[ParameterlessSwitch.FileLogger6] : null; - - public bool? FileLogger7 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger7) ? this[ParameterlessSwitch.FileLogger7] : null; - - public bool? FileLogger8 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger8) ? this[ParameterlessSwitch.FileLogger8] : null; - - public bool? FileLogger9 => IsParameterlessSwitchSet(ParameterlessSwitch.FileLogger9) ? this[ParameterlessSwitch.FileLogger9] : null; - - public bool? DistributedFileLogger => IsParameterlessSwitchSet(ParameterlessSwitch.DistributedFileLogger) ? this[ParameterlessSwitch.DistributedFileLogger] : null; - -#if DEBUG - public bool? WaitForDebugger => IsParameterlessSwitchSet(ParameterlessSwitch.WaitForDebugger) ? this[ParameterlessSwitch.WaitForDebugger] : null; -#endif - - // Parameterized switches - public string[] Project => IsParameterizedSwitchSet(ParameterizedSwitch.Project) ? this[ParameterizedSwitch.Project] : null; - - public string[] Target => IsParameterizedSwitchSet(ParameterizedSwitch.Target) ? this[ParameterizedSwitch.Target] : null; - - public string[] Property => IsParameterizedSwitchSet(ParameterizedSwitch.Property) ? this[ParameterizedSwitch.Property] : null; - - public string[] Logger => IsParameterizedSwitchSet(ParameterizedSwitch.Logger) ? this[ParameterizedSwitch.Logger] : null; - - public string[] DistributedLogger => IsParameterizedSwitchSet(ParameterizedSwitch.DistributedLogger) ? this[ParameterizedSwitch.DistributedLogger] : null; - - public string[] Verbosity => IsParameterizedSwitchSet(ParameterizedSwitch.Verbosity) ? this[ParameterizedSwitch.Verbosity] : null; - -#if FEATURE_XML_SCHEMA_VALIDATION - public string[] Validate => IsParameterizedSwitchSet(ParameterizedSwitch.Validate) ? this[ParameterizedSwitch.Validate] : null; -#endif - - public string[] ConsoleLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.ConsoleLoggerParameters) ? this[ParameterizedSwitch.ConsoleLoggerParameters] : null; - - public string[] NodeMode => IsParameterizedSwitchSet(ParameterizedSwitch.NodeMode) ? this[ParameterizedSwitch.NodeMode] : null; - - public string[] MaxCpuCount => IsParameterizedSwitchSet(ParameterizedSwitch.MaxCPUCount) ? this[ParameterizedSwitch.MaxCPUCount] : null; - - public string[] IgnoreProjectExtensions => IsParameterizedSwitchSet(ParameterizedSwitch.IgnoreProjectExtensions) ? this[ParameterizedSwitch.IgnoreProjectExtensions] : null; - - public string[] ToolsVersion => IsParameterizedSwitchSet(ParameterizedSwitch.ToolsVersion) ? this[ParameterizedSwitch.ToolsVersion] : null; - - public string[] FileLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters) ? this[ParameterizedSwitch.FileLoggerParameters] : null; - - public string[] FileLoggerParameters1 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters1) ? this[ParameterizedSwitch.FileLoggerParameters1] : null; - - public string[] FileLoggerParameters2 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters2) ? this[ParameterizedSwitch.FileLoggerParameters2] : null; - - public string[] FileLoggerParameters3 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters3) ? this[ParameterizedSwitch.FileLoggerParameters3] : null; - - public string[] FileLoggerParameters4 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters4) ? this[ParameterizedSwitch.FileLoggerParameters4] : null; - - public string[] FileLoggerParameters5 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters5) ? this[ParameterizedSwitch.FileLoggerParameters5] : null; - - public string[] FileLoggerParameters6 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters6) ? this[ParameterizedSwitch.FileLoggerParameters6] : null; - - public string[] FileLoggerParameters7 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters7) ? this[ParameterizedSwitch.FileLoggerParameters7] : null; - - public string[] FileLoggerParameters8 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters8) ? this[ParameterizedSwitch.FileLoggerParameters8] : null; - - public string[] FileLoggerParameters9 => IsParameterizedSwitchSet(ParameterizedSwitch.FileLoggerParameters9) ? this[ParameterizedSwitch.FileLoggerParameters9] : null; - - public string[] TerminalLogger => IsParameterizedSwitchSet(ParameterizedSwitch.TerminalLogger) ? this[ParameterizedSwitch.TerminalLogger] : null; - - public string[] TerminalLoggerParameters => IsParameterizedSwitchSet(ParameterizedSwitch.TerminalLoggerParameters) ? this[ParameterizedSwitch.TerminalLoggerParameters] : null; - - public string[] NodeReuse => IsParameterizedSwitchSet(ParameterizedSwitch.NodeReuse) ? this[ParameterizedSwitch.NodeReuse] : null; - - public string[] Preprocess => IsParameterizedSwitchSet(ParameterizedSwitch.Preprocess) ? this[ParameterizedSwitch.Preprocess] : null; - - public string[] Targets => IsParameterizedSwitchSet(ParameterizedSwitch.Targets) ? this[ParameterizedSwitch.Targets] : null; - - public string[] WarningsAsErrors => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsAsErrors) ? this[ParameterizedSwitch.WarningsAsErrors] : null; - - public string[] WarningsNotAsErrors => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsNotAsErrors) ? this[ParameterizedSwitch.WarningsNotAsErrors] : null; - - public string[] WarningsAsMessages => IsParameterizedSwitchSet(ParameterizedSwitch.WarningsAsMessages) ? this[ParameterizedSwitch.WarningsAsMessages] : null; - - public string[] BinaryLogger => IsParameterizedSwitchSet(ParameterizedSwitch.BinaryLogger) ? this[ParameterizedSwitch.BinaryLogger] : null; - - public string[] Check => IsParameterizedSwitchSet(ParameterizedSwitch.Check) ? this[ParameterizedSwitch.Check] : null; - - public string[] Restore => IsParameterizedSwitchSet(ParameterizedSwitch.Restore) ? this[ParameterizedSwitch.Restore] : null; - - public string[] ProfileEvaluation => IsParameterizedSwitchSet(ParameterizedSwitch.ProfileEvaluation) ? this[ParameterizedSwitch.ProfileEvaluation] : null; - - public string[] RestoreProperty => IsParameterizedSwitchSet(ParameterizedSwitch.RestoreProperty) ? this[ParameterizedSwitch.RestoreProperty] : null; - - public string[] Interactive => IsParameterizedSwitchSet(ParameterizedSwitch.Interactive) ? this[ParameterizedSwitch.Interactive] : null; - - public string[] IsolateProjects => IsParameterizedSwitchSet(ParameterizedSwitch.IsolateProjects) ? this[ParameterizedSwitch.IsolateProjects] : null; - - public string[] GraphBuild => IsParameterizedSwitchSet(ParameterizedSwitch.GraphBuild) ? this[ParameterizedSwitch.GraphBuild] : null; - - public string[] InputResultsCaches => IsParameterizedSwitchSet(ParameterizedSwitch.InputResultsCaches) ? this[ParameterizedSwitch.InputResultsCaches] : null; - - public string[] OutputResultsCache => IsParameterizedSwitchSet(ParameterizedSwitch.OutputResultsCache) ? this[ParameterizedSwitch.OutputResultsCache] : null; - -#if FEATURE_REPORTFILEACCESSES - public string[] ReportFileAccesses => IsParameterizedSwitchSet(ParameterizedSwitch.ReportFileAccesses) ? this[ParameterizedSwitch.ReportFileAccesses] : null; -#endif - - public string[] LowPriority => IsParameterizedSwitchSet(ParameterizedSwitch.LowPriority) ? this[ParameterizedSwitch.LowPriority] : null; - - public string[] Question => IsParameterizedSwitchSet(ParameterizedSwitch.Question) ? this[ParameterizedSwitch.Question] : null; - - public string[] DetailedSummary => IsParameterizedSwitchSet(ParameterizedSwitch.DetailedSummary) ? this[ParameterizedSwitch.DetailedSummary] : null; - - public string[] GetProperty => IsParameterizedSwitchSet(ParameterizedSwitch.GetProperty) ? this[ParameterizedSwitch.GetProperty] : null; - - public string[] GetItem => IsParameterizedSwitchSet(ParameterizedSwitch.GetItem) ? this[ParameterizedSwitch.GetItem] : null; - - public string[] GetTargetResult => IsParameterizedSwitchSet(ParameterizedSwitch.GetTargetResult) ? this[ParameterizedSwitch.GetTargetResult] : null; - - public string[] GetResultOutputFile => IsParameterizedSwitchSet(ParameterizedSwitch.GetResultOutputFile) ? this[ParameterizedSwitch.GetResultOutputFile] : null; - - public string[] FeatureAvailability => IsParameterizedSwitchSet(ParameterizedSwitch.FeatureAvailability) ? this[ParameterizedSwitch.FeatureAvailability] : null; - - public string[] MultiThreaded => IsParameterizedSwitchSet(ParameterizedSwitch.MultiThreaded) ? this[ParameterizedSwitch.MultiThreaded] : null; + #endregion diff --git a/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs new file mode 100644 index 00000000000..b3e0081a591 --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.Build.CommandLine.CommandLineSwitches; + +namespace Microsoft.Build.CommandLine +{ + internal readonly struct CommandLineSwitchesAccessor + { + private readonly CommandLineSwitches switches; + + internal CommandLineSwitchesAccessor(CommandLineSwitches switches) + { + this.switches = switches; + } + + // Parameterless switches + public bool? Help => GetParameterlessSwitchValue(ParameterlessSwitch.Help); + + public bool? Version => GetParameterlessSwitchValue(ParameterlessSwitch.Version); + + public bool? NoLogo => GetParameterlessSwitchValue(ParameterlessSwitch.NoLogo); + + public bool? NoAutoResponse => GetParameterlessSwitchValue(ParameterlessSwitch.NoAutoResponse); + + public bool? NoConsoleLogger => GetParameterlessSwitchValue(ParameterlessSwitch.NoConsoleLogger); + + public bool? FileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger); + + public bool? FileLogger1 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger1); + + public bool? FileLogger2 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger2); + + public bool? FileLogger3 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger3); + + public bool? FileLogger4 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger4); + + public bool? FileLogger5 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger5); + + public bool? FileLogger6 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger6); + + public bool? FileLogger7 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger7); + + public bool? FileLogger8 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger8); + + public bool? FileLogger9 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger9); + + public bool? DistributedFileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.DistributedFileLogger); + +#if DEBUG + public bool? WaitForDebugger => GetParameterlessSwitchValue(ParameterlessSwitch.WaitForDebugger); +#endif + + // Parameterized switches + public string[]? Project => GetParameterizedSwitchValue(ParameterizedSwitch.Project); + + public string[]? Target => GetParameterizedSwitchValue(ParameterizedSwitch.Target); + + public string[]? Property => GetParameterizedSwitchValue(ParameterizedSwitch.Property); + + public string[]? Logger => GetParameterizedSwitchValue(ParameterizedSwitch.Logger); + + public string[]? DistributedLogger => GetParameterizedSwitchValue(ParameterizedSwitch.DistributedLogger); + + public string[]? Verbosity => GetParameterizedSwitchValue(ParameterizedSwitch.Verbosity); + +#if FEATURE_XML_SCHEMA_VALIDATION + public string[]? Validate => GetParameterizedSwitchValue(ParameterizedSwitch.Validate); +#endif + + public string[]? ConsoleLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.ConsoleLoggerParameters); + + public string[]? NodeMode => GetParameterizedSwitchValue(ParameterizedSwitch.NodeMode); + + public string[]? MaxCpuCount => GetParameterizedSwitchValue(ParameterizedSwitch.MaxCPUCount); + + public string[]? IgnoreProjectExtensions => GetParameterizedSwitchValue(ParameterizedSwitch.IgnoreProjectExtensions); + + public string[]? ToolsVersion => GetParameterizedSwitchValue(ParameterizedSwitch.ToolsVersion); + + public string[]? FileLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters); + + public string[]? FileLoggerParameters1 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters1); + + public string[]? FileLoggerParameters2 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters2); + + public string[]? FileLoggerParameters3 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters3); + + public string[]? FileLoggerParameters4 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters4); + + public string[]? FileLoggerParameters5 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters5); + + public string[]? FileLoggerParameters6 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters6); + + public string[]? FileLoggerParameters7 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters7); + + public string[]? FileLoggerParameters8 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters8); + + public string[]? FileLoggerParameters9 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters9); + + public string[]? TerminalLogger => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLogger); + + public string[]? TerminalLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLoggerParameters); + + public string[]? NodeReuse => GetParameterizedSwitchValue(ParameterizedSwitch.NodeReuse); + + public string[]? Preprocess => GetParameterizedSwitchValue(ParameterizedSwitch.Preprocess); + + public string[]? Targets => GetParameterizedSwitchValue(ParameterizedSwitch.Targets); + + public string[]? WarningsAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsErrors); + + public string[]? WarningsNotAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsNotAsErrors); + + public string[]? WarningsAsMessages => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsMessages); + + public string[]? BinaryLogger => GetParameterizedSwitchValue(ParameterizedSwitch.BinaryLogger); + + public string[]? Check => GetParameterizedSwitchValue(ParameterizedSwitch.Check); + + public string[]? Restore => GetParameterizedSwitchValue(ParameterizedSwitch.Restore); + + public string[]? ProfileEvaluation => GetParameterizedSwitchValue(ParameterizedSwitch.ProfileEvaluation); + + public string[]? RestoreProperty => GetParameterizedSwitchValue(ParameterizedSwitch.RestoreProperty); + + public string[]? Interactive => GetParameterizedSwitchValue(ParameterizedSwitch.Interactive); + + public string[]? IsolateProjects => GetParameterizedSwitchValue(ParameterizedSwitch.IsolateProjects); + + public string[]? GraphBuild => GetParameterizedSwitchValue(ParameterizedSwitch.GraphBuild); + + public string[]? InputResultsCaches => GetParameterizedSwitchValue(ParameterizedSwitch.InputResultsCaches); + + public string[]? OutputResultsCache => GetParameterizedSwitchValue(ParameterizedSwitch.OutputResultsCache); + +#if FEATURE_REPORTFILEACCESSES + public string[]? ReportFileAccesses => GetParameterizedSwitchValue(ParameterizedSwitch.ReportFileAccesses); +#endif + + public string[]? LowPriority => GetParameterizedSwitchValue(ParameterizedSwitch.LowPriority); + + public string[]? Question => GetParameterizedSwitchValue(ParameterizedSwitch.Question); + + public string[]? DetailedSummary => GetParameterizedSwitchValue(ParameterizedSwitch.DetailedSummary); + + public string[]? GetProperty => GetParameterizedSwitchValue(ParameterizedSwitch.GetProperty); + + public string[]? GetItem => GetParameterizedSwitchValue(ParameterizedSwitch.GetItem); + + public string[]? GetTargetResult => GetParameterizedSwitchValue(ParameterizedSwitch.GetTargetResult); + + public string[]? GetResultOutputFile => GetParameterizedSwitchValue(ParameterizedSwitch.GetResultOutputFile); + + public string[]? FeatureAvailability => GetParameterizedSwitchValue(ParameterizedSwitch.FeatureAvailability); + + public string[]? MultiThreaded => GetParameterizedSwitchValue(ParameterizedSwitch.MultiThreaded); + + private bool? GetParameterlessSwitchValue(ParameterlessSwitch switchType) => switches.IsParameterlessSwitchSet(switchType) ? switches[switchType] : null; + + private string[]? GetParameterizedSwitchValue(ParameterizedSwitch switchType) => switches.IsParameterizedSwitchSet(switchType) ? switches[switchType] : null; + } +} diff --git a/src/MSBuild/CommandLine/ICommandLineSwitches.cs b/src/MSBuild/CommandLine/ICommandLineSwitches.cs deleted file mode 100644 index 1122b316902..00000000000 --- a/src/MSBuild/CommandLine/ICommandLineSwitches.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.Build.CommandLine -{ - internal interface ICommandLineSwitches - { - // Parameterless switches - bool? Help { get; } - bool? Version { get; } - bool? NoLogo { get; } - bool? NoAutoResponse { get; } - bool? NoConsoleLogger { get; } - bool? FileLogger { get; } - bool? FileLogger1 { get; } - bool? FileLogger2 { get; } - bool? FileLogger3 { get; } - bool? FileLogger4 { get; } - bool? FileLogger5 { get; } - bool? FileLogger6 { get; } - bool? FileLogger7 { get; } - bool? FileLogger8 { get; } - bool? FileLogger9 { get; } - bool? DistributedFileLogger { get; } - -#if DEBUG - bool? WaitForDebugger { get; } -#endif - - // Parameterized switches - string[]? Project { get; } - string[]? Target { get; } - string[]? Property { get; } - string[]? Logger { get; } - string[]? DistributedLogger { get; } - string[]? Verbosity { get; } -#if FEATURE_XML_SCHEMA_VALIDATION - string[]? Validate { get; } -#endif - string[]? ConsoleLoggerParameters { get; } - string[]? NodeMode { get; } - string[]? MaxCpuCount { get; } - string[]? IgnoreProjectExtensions { get; } - string[]? ToolsVersion { get; } - string[]? FileLoggerParameters { get; } - string[]? FileLoggerParameters1 { get; } - string[]? FileLoggerParameters2 { get; } - string[]? FileLoggerParameters3 { get; } - string[]? FileLoggerParameters4 { get; } - string[]? FileLoggerParameters5 { get; } - string[]? FileLoggerParameters6 { get; } - string[]? FileLoggerParameters7 { get; } - string[]? FileLoggerParameters8 { get; } - string[]? FileLoggerParameters9 { get; } - string[]? TerminalLogger { get; } - string[]? TerminalLoggerParameters { get; } - string[]? NodeReuse { get; } - string[]? Preprocess { get; } - string[]? Targets { get; } - string[]? WarningsAsErrors { get; } - string[]? WarningsNotAsErrors { get; } - string[]? WarningsAsMessages { get; } - string[]? BinaryLogger { get; } - string[]? Check { get; } - string[]? Restore { get; } - string[]? ProfileEvaluation { get; } - string[]? RestoreProperty { get; } - string[]? Interactive { get; } - string[]? IsolateProjects { get; } - string[]? GraphBuild { get; } - string[]? InputResultsCaches { get; } - string[]? OutputResultsCache { get; } -#if FEATURE_REPORTFILEACCESSES - string[]? ReportFileAccesses { get; } -#endif - string[]? LowPriority { get; } - string[]? Question { get; } - string[]? DetailedSummary { get; } - string[]? GetProperty { get; } - string[]? GetItem { get; } - string[]? GetTargetResult { get; } - string[]? GetResultOutputFile { get; } - string[]? FeatureAvailability { get; } - string[]? MultiThreaded { get; } - } -} diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index aa08cfd8016..08dcad2207d 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -135,7 +135,7 @@ - + From a434b74cdcf4a0a56b1094d8a78be245958f13bf Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 13 Jan 2026 14:14:02 +0100 Subject: [PATCH 10/10] Parsing related types were moved to the Experimental namespace. Removed forgotten empty region in CommandLineSwitches. --- src/MSBuild.UnitTests/CommandLineParserTests.cs | 1 + src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs | 1 + src/MSBuild.UnitTests/XMake_Tests.cs | 1 + src/MSBuild/CommandLine/CommandLineParser.cs | 2 +- src/MSBuild/CommandLine/CommandLineSwitchException.cs | 2 +- src/MSBuild/CommandLine/CommandLineSwitches.cs | 8 +------- src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs | 4 ++-- src/MSBuild/XMake.cs | 7 ++++--- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/MSBuild.UnitTests/CommandLineParserTests.cs b/src/MSBuild.UnitTests/CommandLineParserTests.cs index 5c6007f86dc..cb5280d913c 100644 --- a/src/MSBuild.UnitTests/CommandLineParserTests.cs +++ b/src/MSBuild.UnitTests/CommandLineParserTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.Build.CommandLine.Experimental; using Shouldly; using Xunit; diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index 88c5d591c53..7cbf86ce493 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Resources; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Construction; using Microsoft.Build.Execution; using Microsoft.Build.Framework; diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 23c8ee799a7..0d5698317a3 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.Build.Logging; diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs index 25e40f9800f..ace7267233a 100644 --- a/src/MSBuild/CommandLine/CommandLineParser.cs +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -16,7 +16,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { internal class CommandLineParser { diff --git a/src/MSBuild/CommandLine/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs index e8ce5dd036d..54ec5e65ef2 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitchException.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchException.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This exception is used to flag (syntax) errors in command line switches passed to the application. diff --git a/src/MSBuild/CommandLine/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs index e50e79960f9..7c7add45c3e 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter @@ -446,12 +446,6 @@ private struct DetectedParameterizedSwitch internal static List<(string path, string contents)> SwitchesFromResponseFiles = new(); - #region ICommandLineSwitches Members - - - - #endregion - /// /// Default constructor. /// diff --git a/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs index b3e0081a591..66bf8c8582e 100644 --- a/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using static Microsoft.Build.CommandLine.CommandLineSwitches; +using static Microsoft.Build.CommandLine.Experimental.CommandLineSwitches; -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { internal readonly struct CommandLineSwitchesAccessor { diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index f8e7d4bd3c5..b14a753cc4b 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -36,6 +36,7 @@ using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; +using Microsoft.Build.CommandLine.Experimental; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; using FileLogger = Microsoft.Build.Logging.FileLogger; @@ -961,12 +962,12 @@ public static ExitType Execute(string[] commandLine) exitType = ExitType.InitializationError; } } -#pragma warning disable CS0618 // Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now - catch (Exception e) when (e is ProjectCacheException || e is Experimental.ProjectCache.ProjectCacheException) +#pragma warning disable CS0618 // Microsoft.Build.Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now + catch (Exception e) when (e is ProjectCacheException || e is Microsoft.Build.Experimental.ProjectCache.ProjectCacheException) { ProjectCacheException pce = e as ProjectCacheException; - Experimental.ProjectCache.ProjectCacheException exppce = e as Experimental.ProjectCache.ProjectCacheException; + Microsoft.Build.Experimental.ProjectCache.ProjectCacheException exppce = e as Microsoft.Build.Experimental.ProjectCache.ProjectCacheException; Console.WriteLine($"MSBUILD : error {pce?.ErrorCode ?? exppce?.ErrorCode}: {e.Message}");