From 5caa20081402e37e12056c53dbe7a28f0174e3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 19 Nov 2025 14:26:10 +0100 Subject: [PATCH 1/2] Revert "CmdLine parsing was extracted from XMake and the implementation is visible to dotnet (#12704)" This reverts commit a73379bc4af3599eb79b899a7c4c56a6c0dff9ff. --- .../CommandLineSwitches_Tests.cs | 51 +- src/MSBuild.UnitTests/XMake_Tests.cs | 53 +- src/MSBuild/AssemblyInfo.cs | 1 - src/MSBuild/CommandLine/CommandLineParser.cs | 632 ------------------ .../CommandLineSwitchException.cs | 0 .../{CommandLine => }/CommandLineSwitches.cs | 0 src/MSBuild/MSBuild.csproj | 7 +- src/MSBuild/XMake.cs | 603 ++++++++++++++++- 8 files changed, 638 insertions(+), 709 deletions(-) delete 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 f958cdfd11e..e339d2c9dd7 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -622,8 +622,7 @@ public void FeatureAvailibilitySwitchIdentificationTest(string switchName) public void TargetsSwitchParameter() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List() { "/targets:targets.txt" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List() { "/targets:targets.txt" }, switches); switches.HaveErrors().ShouldBeFalse(); switches[CommandLineSwitches.ParameterizedSwitch.Targets].ShouldBe(new[] { "targets.txt" }); @@ -633,8 +632,7 @@ public void TargetsSwitchParameter() public void TargetsSwitchDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List() { "/targets /targets" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List() { "/targets /targets" }, switches); switches.HaveErrors().ShouldBeTrue(); } @@ -711,9 +709,8 @@ public void LowPrioritySwitchIdentificationTests(string lowpriority) public void GraphBuildSwitchCanHaveParameters() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List { "/graph", "/graph:true; NoBuild ;; ;", "/graph:foo" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List { "/graph", "/graph:true; NoBuild ;; ;", "/graph:foo" }, switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(new[] { "true", " NoBuild ", " ", "foo" }); @@ -724,9 +721,8 @@ public void GraphBuildSwitchCanHaveParameters() public void GraphBuildSwitchCanBeParameterless() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List { "/graph" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List { "/graph" }, switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(Array.Empty()); @@ -737,9 +733,8 @@ public void GraphBuildSwitchCanBeParameterless() public void InputResultsCachesSupportsMultipleOccurrence() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List() { "/irc", "/irc:a;b", "/irc:c;d" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List() { "/irc", "/irc:a;b", "/irc:c;d" }, switches); switches[CommandLineSwitches.ParameterizedSwitch.InputResultsCaches].ShouldBe(new[] { null, "a", "b", "c", "d" }); @@ -750,9 +745,8 @@ public void InputResultsCachesSupportsMultipleOccurrence() public void OutputResultsCache() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List() { "/orc:a" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a" }, switches); switches[CommandLineSwitches.ParameterizedSwitch.OutputResultsCache].ShouldBe(new[] { "a" }); @@ -763,9 +757,8 @@ public void OutputResultsCache() public void OutputResultsCachesDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List() { "/orc:a", "/orc:b" }, switches); + MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a", "/orc:b" }, switches); switches.HaveErrors().ShouldBeTrue(); } @@ -1295,9 +1288,8 @@ public void ExtractAnyLoggerParameterPickLast() public void ProcessWarnAsErrorSwitchNotSpecified() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] { "" }), commandLineSwitches); + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "" }), commandLineSwitches); Assert.Null(MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches)); } @@ -1311,9 +1303,8 @@ public void ProcessWarnAsErrorSwitchWithCodes() ISet expectedWarningsAsErrors = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "\"/warnaserror: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnaserror:A,b,C", // Repeats of different case @@ -1337,9 +1328,8 @@ public void ProcessWarnAsErrorSwitchWithCodes() public void ProcessWarnAsErrorSwitchEmptySwitchClearsSet() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror:a;b;c", "/warnaserror", @@ -1361,9 +1351,8 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() ISet expectedWarningsAsErors = new HashSet(StringComparer.OrdinalIgnoreCase) { "e", "f", "g" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror:a;b;c", "/warnaserror", @@ -1384,9 +1373,8 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() public void ProcessWarnAsErrorSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] { "/warnaserror" }), commandLineSwitches); + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror" }), commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1402,11 +1390,10 @@ 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}"; - parser.GatherCommandLineSwitches(new List(new[] { "/warnasmessage" }), commandLineSwitches, command); + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnasmessage" }), commandLineSwitches, command); VerifySwitchError(commandLineSwitches, "/warnasmessage", AssemblyResources.GetString("MissingWarnAsMessageParameterError")); } @@ -1423,15 +1410,13 @@ public void ProcessEnvironmentVariableSwitch() env.SetEnvironmentVariable("ENVIRONMENTVARIABLE", string.Empty); CommandLineSwitches commandLineSwitches = new(); - CommandLineParser parser = new CommandLineParser(); - string fullCommandLine = "msbuild validProject.csproj %ENVIRONMENTVARIABLE%"; - parser.GatherCommandLineSwitches(new List() { "validProject.csproj", "%ENVIRONMENTVARIABLE%" }, commandLineSwitches, fullCommandLine); + MSBuildApp.GatherCommandLineSwitches(new List() { "validProject.csproj", "%ENVIRONMENTVARIABLE%" }, commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); commandLineSwitches = new(); fullCommandLine = "msbuild %ENVIRONMENTVARIABLE% validProject.csproj"; - parser.GatherCommandLineSwitches(new List() { "%ENVIRONMENTVARIABLE%", "validProject.csproj" }, commandLineSwitches, fullCommandLine); + MSBuildApp.GatherCommandLineSwitches(new List() { "%ENVIRONMENTVARIABLE%", "validProject.csproj" }, commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); } } @@ -1445,9 +1430,8 @@ public void ProcessWarnAsMessageSwitchWithCodes() ISet expectedWarningsAsMessages = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "\"/warnasmessage: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnasmessage:A,b,C", // Repeats of different case @@ -1471,9 +1455,8 @@ public void ProcessWarnAsMessageSwitchWithCodes() public void ProcessProfileEvaluationEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); - parser.GatherCommandLineSwitches(new List(new[] { "/profileevaluation" }), commandLineSwitches); + MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/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 8185cde4b89..f5de4715ccb 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -96,12 +96,11 @@ 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" }); - parser.GatherCommandLineSwitches(arguments, switches); + MSBuildApp.GatherCommandLineSwitches(arguments, switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -112,14 +111,13 @@ public void GatherCommandLineSwitchesTwoProperties() public void GatherCommandLineSwitchesAnyDash() { var switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); var arguments = new List { "-p:a=b", "--p:maxcpucount=8" }; - parser.GatherCommandLineSwitches(arguments, switches); + MSBuildApp.GatherCommandLineSwitches(arguments, switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -130,12 +128,11 @@ public void GatherCommandLineSwitchesAnyDash() public void GatherCommandLineSwitchesMaxCpuCountWithArgument() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); var arguments = new List(); arguments.AddRange(new[] { "/m:2" }); - parser.GatherCommandLineSwitches(arguments, switches); + MSBuildApp.GatherCommandLineSwitches(arguments, switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[0].ShouldBe("2"); @@ -148,12 +145,11 @@ public void GatherCommandLineSwitchesMaxCpuCountWithArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); var arguments = new List(); arguments.AddRange(new[] { "/m:3", "/m" }); - parser.GatherCommandLineSwitches(arguments, switches); + MSBuildApp.GatherCommandLineSwitches(arguments, switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[1].ShouldBe(Convert.ToString(NativeMethodsShared.GetLogicalCoreCount())); @@ -169,12 +165,11 @@ public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgumentButWithColon() { CommandLineSwitches switches = new CommandLineSwitches(); - CommandLineParser parser = new CommandLineParser(); var arguments = new List(); arguments.AddRange(new[] { "/m:" }); - parser.GatherCommandLineSwitches(arguments, switches); + MSBuildApp.GatherCommandLineSwitches(arguments, switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters.Length.ShouldBe(0); @@ -464,44 +459,44 @@ public void ExtractSwitchParametersTest() { string commandLineArg = "\"/p:foo=\"bar"; string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "/p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -510,37 +505,37 @@ public void ExtractSwitchParametersTestDoubleDash() { var commandLineArg = "\"--p:foo=\"bar"; var unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "--p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + MSBuildApp.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); - CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -553,11 +548,11 @@ public void GetLengthOfSwitchIndicatorTest() var commandLineSwitchWithNoneOrIncorrectIndicator = "zSwitch"; - CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); - CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); - CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); + MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); + MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); + MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); - CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); + MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); } [Theory] diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index 49249f7e410..f93e8a6db00 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -10,7 +10,6 @@ [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 deleted file mode 100644 index 09d7f5d7f8a..00000000000 --- a/src/MSBuild/CommandLine/CommandLineParser.cs +++ /dev/null @@ -1,632 +0,0 @@ -// 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 Microsoft.Build.Internal; -using Microsoft.Build.Shared; -using Microsoft.Build.Shared.FileSystem; - -#nullable disable - -namespace Microsoft.Build.CommandLine -{ - internal class CommandLineParser - { - /// - /// Used to keep track of response files to prevent them from - /// being included multiple times (or even recursively). - /// - private List includedResponseFiles; - - /// - /// 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%"; - - internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); - - public (CommandLineSwitches commandLineSwitches, CommandLineSwitches responseFileSwitches) Parse(IEnumerable commandLineArgs) - { - GatherAllSwitches( - commandLineArgs, - $"'{string.Join(" ", commandLineArgs)}'", - out CommandLineSwitches responseFileSwitches, - out CommandLineSwitches commandLineSwitches); - - return (commandLineSwitches, responseFileSwitches); - } - - internal void GatherAllSwitches( - string commandLine, - out CommandLineSwitches switchesFromAutoResponseFile, - out CommandLineSwitches switchesNotFromAutoResponseFile, - out string fullCommandLine, - out string exeName) - { - // split the command line on (unquoted) whitespace - List commandLineArgs = QuotingUtilities.SplitUnquoted(commandLine); - - CommunicationsUtilities.Trace($"Command line parameters: {string.Join(" ", commandLineArgs)}"); - - exeName = FileUtilities.FixFilePath(QuotingUtilities.Unquote(commandLineArgs[0])); - -#if USE_MSBUILD_DLL_EXTN - var msbuildExtn = ".dll"; -#else - var msbuildExtn = ".exe"; -#endif - if (!exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) - { - exeName += msbuildExtn; - } - - fullCommandLine = $"'{commandLine}'"; - - GatherAllSwitches( - commandLineArgs, - fullCommandLine, - out switchesFromAutoResponseFile, - out switchesNotFromAutoResponseFile); - } - - /// - /// Gets all specified switches, from the command line, as well as all - /// response files, including the auto-response file. - /// - /// - /// - /// - /// - /// Combined bag of switches. - private void GatherAllSwitches( - IEnumerable commandLineArgs, - string fullCommandLine, - out CommandLineSwitches switchesFromAutoResponseFile, - out CommandLineSwitches switchesNotFromAutoResponseFile) - { - ResetGatheringSwitchesState(); - - // discard the first piece, because that's the path to the executable -- the rest are args - commandLineArgs = commandLineArgs.Skip(1); - - // 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); - } - } - } - } - } - - /// - /// 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; - } - - /// - /// 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); - } - } - - /// - /// 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; - } - } - - internal void ResetGatheringSwitchesState() - { - includedResponseFiles = new List(); - CommandLineSwitches.SwitchesFromResponseFiles = new(); - } - } -} diff --git a/src/MSBuild/CommandLine/CommandLineSwitchException.cs b/src/MSBuild/CommandLineSwitchException.cs similarity index 100% rename from src/MSBuild/CommandLine/CommandLineSwitchException.cs rename to src/MSBuild/CommandLineSwitchException.cs diff --git a/src/MSBuild/CommandLine/CommandLineSwitches.cs b/src/MSBuild/CommandLineSwitches.cs similarity index 100% rename from src/MSBuild/CommandLine/CommandLineSwitches.cs rename to src/MSBuild/CommandLineSwitches.cs diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 696dc3d652e..e43039e8e6c 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -131,10 +131,9 @@ - - + + - @@ -208,7 +207,7 @@ - + diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 623b6eabd70..4ae2363f9e9 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -143,8 +143,6 @@ public enum ExitType private static readonly char[] s_commaSemicolon = { ',', ';' }; - private static CommandLineParser commandLineParser; - /// /// Static constructor /// @@ -160,7 +158,6 @@ static MSBuildApp() // any configuration file exceptions can be caught here. // //////////////////////////////////////////////////////////////////////////////// s_exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); - commandLineParser = new CommandLineParser(); s_initialized = true; } @@ -295,9 +292,9 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string commandLine) bool canRunServer = true; try { - commandLineParser.GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine, out s_exeName); + GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine); CommandLineSwitches commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); - if (commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) + if (CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) { commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } @@ -667,7 +664,7 @@ public static ExitType Execute(string commandLine) bool reportFileAccesses = false; #endif - commandLineParser.GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _, out s_exeName); + GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _); bool buildCanBeInvoked = ProcessCommandLineSwitches( switchesFromAutoResponseFile, @@ -1155,7 +1152,14 @@ private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs /// private static void ResetBuildState() { - commandLineParser.ResetGatheringSwitchesState(); + ResetGatheringSwitchesState(); + } + + private static void ResetGatheringSwitchesState() + { + s_includedResponseFiles = new List(); + usingSwitchesFromAutoResponseFile = false; + CommandLineSwitches.SwitchesFromResponseFiles = new(); } /// @@ -1489,7 +1493,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLine)); // Log a message for every response file and include it in log - foreach (var responseFilePath in commandLineParser.IncludedResponseFiles) + foreach (var responseFilePath in s_includedResponseFiles) { messagesToLogInBuildLoggers.Add( new BuildManager.DeferredBuildMessage( @@ -1909,11 +1913,499 @@ 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(); + + // split the command line on (unquoted) whitespace + var commandLineArgs = QuotingUtilities.SplitUnquoted(commandLine); + + s_exeName = FileUtilities.FixFilePath(QuotingUtilities.Unquote(commandLineArgs[0])); + +#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 = $"'{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. @@ -2051,7 +2543,7 @@ private static bool ProcessCommandLineSwitches( } else { - bool foundProjectAutoResponseFile = commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); + bool foundProjectAutoResponseFile = CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); if (foundProjectAutoResponseFile) { @@ -2510,6 +3002,59 @@ 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); @@ -3112,6 +3657,46 @@ private static void ValidateExtensions(string[] projectExtensionsToIgnore) } } + /// + /// Checks whether an argument given as a parameter starts with valid indicator, + ///
which means, whether switch begins with one of: "/", "-", "--" + ///
+ /// Command line argument with beginning indicator (e.g. --help). + ///
This argument has to be unquoted, otherwise the first character will always be a quote character " + /// true if argument's beginning matches one of possible indicators + ///
false if argument's beginning doesn't match any of correct indicator + ///
+ private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) + { + return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" + || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); + } + + /// + /// Gets the length of the switch indicator (- or / or --) + ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. + ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. + ///
+ /// Unquoted argument with leading indicator and name + /// Correct length of used indicator + ///
0 if no leading sequence recognized as correct indicator
+ /// Internal for testing purposes + internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) + { + if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) + { + return 2; + } + else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) + { + return 1; + } + else + { + return 0; + } + } + /// /// Figures out which targets are to be built. /// From 1d29c2aa21b3bf8cf96241386bae73e335e064fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 19 Nov 2025 14:26:59 +0100 Subject: [PATCH 2/2] Revert "Removing FEATURE_GET_COMMANDLINE constant (#12693)" This reverts commit 60fc61c383f640985fc62f05bf9cfa01c4159a6b. --- src/Build.UnitTests/Utilities_Tests.cs | 8 ++ 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 | 4 + src/MSBuild.UnitTests/XMake_Tests.cs | 35 +++++- src/MSBuild/MSBuildClientApp.cs | 12 +- src/MSBuild/XMake.cs | 112 ++++++++++++++++-- 10 files changed, 196 insertions(+), 61 deletions(-) diff --git a/src/Build.UnitTests/Utilities_Tests.cs b/src/Build.UnitTests/Utilities_Tests.cs index 9218a7dd7ba..a29466e852c 100644 --- a/src/Build.UnitTests/Utilities_Tests.cs +++ b/src/Build.UnitTests/Utilities_Tests.cs @@ -80,9 +80,17 @@ 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 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 96dc183c6b0..9bd05788271 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -48,7 +48,11 @@ 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. @@ -108,7 +112,13 @@ 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(string commandLine, string msbuildLocation) + public MSBuildClient( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); @@ -152,7 +162,12 @@ private void CreateNodePipeStream() public MSBuildClientExitResult Execute(CancellationToken cancellationToken) { // Command line in one string used only in human readable content. - string descriptiveCommandLine = _commandLine; + string descriptiveCommandLine = +#if FEATURE_GET_COMMANDLINE + _commandLine; +#else + string.Join(" ", _commandLine); +#endif 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 a987544f670..e5b1e76f412 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -25,7 +25,12 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// /// A callback used to execute command line build. /// - public delegate (int exitCode, string exitType) BuildCallback(string commandLine); + public delegate (int exitCode, string exitType) BuildCallback( +#if FEATURE_GET_COMMANDLINE + string commandLine); +#else + string[] commandLine); +#endif private readonly BuildCallback _buildFunction; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index 0cb76428a2b..fc5bca7e920 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -14,7 +14,11 @@ 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!; @@ -30,7 +34,11 @@ 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 @@ -71,7 +79,11 @@ 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 4edb31735b6..0497d618a92 100644 --- a/src/Build/CompatibilitySuppressions.xml +++ b/src/Build/CompatibilitySuppressions.xml @@ -1,48 +1,3 @@  - - - - - CP0002 - M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String[],System.String) - lib/net10.0/Microsoft.Build.dll - lib/net10.0/Microsoft.Build.dll - true - - - CP0002 - M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String[],System.AsyncCallback,System.Object) - lib/net10.0/Microsoft.Build.dll - lib/net10.0/Microsoft.Build.dll - true - - - CP0002 - M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String[]) - lib/net10.0/Microsoft.Build.dll - lib/net10.0/Microsoft.Build.dll - true - - - CP0002 - M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String[],System.String) - ref/net10.0/Microsoft.Build.dll - ref/net10.0/Microsoft.Build.dll - true - - - CP0002 - M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String[],System.AsyncCallback,System.Object) - ref/net10.0/Microsoft.Build.dll - ref/net10.0/Microsoft.Build.dll - true - - - CP0002 - M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String[]) - ref/net10.0/Microsoft.Build.dll - ref/net10.0/Microsoft.Build.dll - true - - + \ No newline at end of file diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index db68730447e..73b91fe6c96 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -31,6 +31,7 @@ $(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 e339d2c9dd7..a2de7a8fb1c 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -1548,7 +1548,11 @@ 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 } /// diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index f5de4715ccb..4173c747739 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -562,7 +562,12 @@ public void GetLengthOfSwitchIndicatorTest() [InlineData(@"/h")] public void Help(string indicator) { - MSBuildApp.Execute(@$"c:\bin\msbuild.exe {indicator} ") + MSBuildApp.Execute( +#if FEATURE_GET_COMMANDLINE + @$"c:\bin\msbuild.exe {indicator} ") +#else + new[] { @"c:\bin\msbuild.exe", indicator }) +#endif .ShouldBe(MSBuildApp.ExitType.Success); } @@ -655,13 +660,19 @@ 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(new[] { @"msbuild.exe", "@bogus.rsp" }).ShouldBe(MSBuildApp.ExitType.InitializationError); +#endif Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", oldValueForMSBuildLoadMicrosoftTargetsReadOnly); } @@ -1142,7 +1153,11 @@ 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 } finally { @@ -1175,9 +1190,21 @@ 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[] + { + 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 File.Exists(logFile).ShouldBeTrue(); var logFileContents = File.ReadAllText(logFile); @@ -2919,7 +2946,11 @@ 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 } private string CopyMSBuild() diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index 643c1b6a4f5..3eeb975bc40 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -34,7 +34,13 @@ 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(string commandLine, CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + CancellationToken cancellationToken) { string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; @@ -56,7 +62,11 @@ public static MSBuildApp.ExitType Execute(string commandLine, CancellationToken /// 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) { diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 4ae2363f9e9..f6bb284bbe3 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -233,7 +233,13 @@ private static void HandleConfigurationException(Exception ex) #if FEATURE_APPDOMAIN [LoaderOptimization(LoaderOptimization.MultiDomain)] #endif - public static int Main(string[] args) +#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 { // Setup the console UI. using AutomaticEncodingRestorer _ = new(); @@ -257,18 +263,35 @@ public static int Main(string[] args) if ( Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches(Environment.CommandLine)) + CanRunServerBasedOnCommandLineSwitches( +#if FEATURE_GET_COMMANDLINE + Environment.CommandLine)) +#else + ConstructArrayArg(args))) +#endif { Console.CancelKeyPress += Console_CancelKeyPress; // Use the client app to execute build in msbuild server. Opt-in feature. - exitCode = (s_initialized && MSBuildClientApp.Execute(Environment.CommandLine, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1; + exitCode = ((s_initialized && MSBuildClientApp.Execute( +#if FEATURE_GET_COMMANDLINE + Environment.CommandLine, +#else + ConstructArrayArg(args), +#endif + s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { // return 0 on success, non-zero on failure - exitCode = (s_initialized && Execute(Environment.CommandLine) == ExitType.Success) ? 0 : 1; + exitCode = ((s_initialized && Execute( +#if FEATURE_GET_COMMANDLINE + Environment.CommandLine) +#else + ConstructArrayArg(args)) +#endif + == ExitType.Success) ? 0 : 1); } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") @@ -287,7 +310,12 @@ public static int Main(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(string commandLine) + private static bool CanRunServerBasedOnCommandLineSwitches( +#if FEATURE_GET_COMMANDLINE + string commandLine) +#else + string[] commandLine) +#endif { bool canRunServer = true; try @@ -325,6 +353,23 @@ private static bool CanRunServerBasedOnCommandLineSwitches(string commandLine) 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 /// @@ -578,7 +623,12 @@ 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(string commandLine) + public static ExitType Execute( +#if FEATURE_GET_COMMANDLINE + string commandLine) +#else + string[] commandLine) +#endif { DebuggerLaunchCheck(); @@ -595,7 +645,9 @@ public static ExitType Execute(string commandLine) // 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; @@ -607,8 +659,14 @@ public static ExitType Execute(string commandLine) 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 @@ -711,7 +769,11 @@ public static ExitType Execute(string commandLine) ref getTargetResult, ref getResultOutputFile, recursing: false, +#if FEATURE_GET_COMMANDLINE commandLine); +#else + string.Join(' ', commandLine)); +#endif CommandLineSwitches.SwitchesFromResponseFiles = null; @@ -1011,7 +1073,14 @@ public static ExitType Execute(string commandLine) 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! @@ -1236,7 +1305,11 @@ 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) @@ -1490,7 +1563,13 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) if (!Traits.Instance.EscapeHatches.DoNotSendDeferredMessagesToBuildManager) { - messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLine)); + 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 foreach (var responseFilePath in s_includedResponseFiles) @@ -1922,15 +2001,26 @@ internal static void SetConsoleUI() /// /// /// Combined bag of switches. - private static void GatherAllSwitches(string commandLine, + private static void GatherAllSwitches( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif 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"; @@ -1945,7 +2035,11 @@ private static void GatherAllSwitches(string commandLine, // 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 // parse the command line, and flag syntax errors and obvious switch errors switchesNotFromAutoResponseFile = new CommandLineSwitches();