From 78278012511926aa159d891d7a95cb8924b285d2 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Wed, 26 Nov 2025 15:09:09 +0100 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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. ///