From 76455b8bfb860ed1467c62a883938a2195a4c62b Mon Sep 17 00:00:00 2001 From: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Date: Fri, 9 Jan 2026 09:45:38 +0100 Subject: [PATCH 01/12] Add documentation for enabling binlog collection via env var (#12805) Document the process for enabling binary logging in CI/CD pipelines using environment variables, including supported arguments, argument processing order, and implementation flow. Fixes https://github.com/dotnet/msbuild/issues/12804 --- .../enable-binlog-collection-by-env-var.md | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 documentation/specs/enable-binlog-collection-by-env-var.md diff --git a/documentation/specs/enable-binlog-collection-by-env-var.md b/documentation/specs/enable-binlog-collection-by-env-var.md new file mode 100644 index 00000000000..9db31a50f7b --- /dev/null +++ b/documentation/specs/enable-binlog-collection-by-env-var.md @@ -0,0 +1,158 @@ +# Enable Binary Log Collection via Environment Variable + +## Purpose + +Enable binary logging in CI/CD pipelines without modifying artifacts on disk. + +**Proposed solution:** An environment variable that enables diagnostic logging without touching any files on disk-no response file creation, no project file modifications, no build script changes. + +**Important for company-wide deployment:** When enabling this feature organization-wide (e.g., via CI/CD pipeline configuration), the team setting the environment variable may not be the team that owns individual codebases. Ensure stakeholders understand that builds with `/warnaserror` may be affected and be ready to mitigate this. + +### Demoting Warnings to Messages + +For scenarios where warnings would break builds (e.g., `/warnaserror` is enabled), set: + +```bash +set MSBUILD_LOGGING_ARGS_LEVEL=message +``` + +| Value | Behavior | +|-------|----------| +| `warning` (default) | Issues logged as warnings; may fail `/warnaserror` builds | +| `message` | Issues logged as low-importance messages; never fails builds | + +**Problem scenarios addressed:** + +- `-noAutoResponse` blocks response files entirely +- Creating `Directory.Build.rsp` requires writing new files to the source tree +- Modifying existing RSP files risks merge conflicts or unintended side effects +- Some build environments restrict write access to source directories + +### Why Not MSBUILDDEBUGENGINE? + +The existing `MSBUILDDEBUGENGINE=1` + `MSBUILDDEBUGPATH` mechanism works but has limitations for the desired CI/CD scenarios: + +- **Excessive logging:** Captures *all* MSBuild invocations including design-time builds, generating many files +- **No filename control:** Auto-generates filenames; cannot specify output path with `{}` placeholder for unique names +- **Debug overhead:** Enables additional debugging infrastructure beyond just binary logging + +## Supported Arguments + +- `-bl` / `/bl` / `-binarylogger` / `/binarylogger` (with optional parameters) +- `-check` / `/check` (with optional parameters) + +> **Note:** The `deferred` mode for `-check` is not currently supported. Enabling this feature requires changes to the MSBuild codebase. See section "Build Check (-check) Handling" below. + +> **Recommendation:** For CI/CD use, specify an **absolute path** with the `{}` placeholder (e.g., `-bl:C:\BuildLogs\build{}.binlog` or `-bl:/var/log/builds/build{}.binlog`) to generate unique filenames in a known location, avoiding CWD-relative paths that vary by build. + +**All other switches are blocked** to maintain diagnosability. + +### Rationale + +Environment variables that unexpectedly affect build behavior are notoriously difficult to diagnose (e.g., `Platform` is a known source of build issues). By restricting this environment variable to logging/diagnostic switches only, we ensure it cannot accidentally change build outcomes-only what gets recorded about the build. + +## Argument Processing Order + +1. **MSBuild.rsp** (next to MSBuild.exe) - skipped if `-noAutoResponse` present +2. **Directory.Build.rsp** (next to project) - skipped if `-noAutoResponse` present +3. **MSBUILD_LOGGING_ARGS** - always processed, regardless of `-noAutoResponse` +4. **Command-line arguments** + +### Why Precedence Doesn't Matter Here + +Since `MSBUILD_LOGGING_ARGS` only allows logging switches (`-bl` and `-check`), traditional precedence concerns don't apply: + +- **`-bl` is additive:** Each `-bl` argument creates a separate binlog file (requires [#12706](https://github.com/dotnet/msbuild/pull/12706)). Multiple sources specifying `-bl` simply result in multiple binlog files-there's no conflict to resolve. + +## Implementation Flow + +1. `MSBuildApp.Execute()` called +2. Check for `-noAutoResponse` in command line +3. Process response files (if no `-noAutoResponse`) +4. Read `MSBUILD_LOGGING_ARGS` environment variable +5. Validate and filter arguments +6. Prepend valid arguments to command line +7. Parse combined command line (merging happens here) +8. Execute build + +## Scope and Limitations + +### Supported Entry Points + +This environment variable only affects builds that go through MSBuild's `Main()` entry point: + +| Entry Point | Supported | Notes | +|-------------|-----------|-------| +| `MSBuild.exe` | ✅ Yes | | +| `dotnet build` | ✅ Yes | | +| `dotnet msbuild` | ✅ Yes | | +| Visual Studio (IDE builds) | ❌ No | Uses MSBuild API directly | +| `devenv.exe /build` | ❌ No | Uses MSBuild API directly | +| MSBuildWorkspace (Roslyn) | ❌ No | Uses MSBuild API directly | +| Custom build drivers via API | ❌ No | Any direct `Microsoft.Build` API usage | + +### API-Driven Builds + +For builds that use the MSBuild API directly (including Visual Studio and `devenv.exe /build`), this environment variable has no effect. + +**Alternative:** Use `MSBUILDDEBUGENGINE` to inject binlog collection into API-driven builds. This existing mechanism is already used for debugging Visual Studio builds and works across all MSBuild entry points. +```bash +# For API-driven builds (VS, devenv.exe /build, etc.) +set MSBUILDDEBUGENGINE=1 + +# For command-line builds (MSBuild.exe, dotnet build) +set MSBUILD_LOGGING_ARGS=-bl:build{}.binlog +``` + +## Warning Messages + +Issues are logged as **warnings** by default. Note that users with `/warnaserror` enabled will see these as errors-by opting into this environment variable, users also opt into these diagnostics. + +### Messages + +- **Informational:** "Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0}" - build continues with arguments applied +- **Unsupported argument:** "MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed." - the specific invalid argument is skipped, other valid arguments in the same env var are still processed (e.g., `-bl:a.binlog -maxcpucount:4` → `-bl:a.binlog` is applied, `-maxcpucount:4` is ignored with warning) +- **Malformed input:** "Error processing MSBUILD_LOGGING_ARGS environment variable: {0}" - the entire environment variable is skipped to avoid partial/unpredictable behavior, build proceeds as if the env var was not set + +## Build Check (-check) Handling + +### Deferred Analysis Mode + +`-check:deferred` enables binlog replay analysis with reduced build-time overhead: + +- **During build:** Flag recorded in binlog along with additional data needed for checks; BuildCheck NOT activated +- **During replay:** Binlog reader activates BuildCheck for analysis + +**Rationale:** BuildCheck analysis can be expensive and checks can fail the build. The environment variable is for diagnostics that can be analyzed later, allowing teams to record data with minimal impact to the build itself. + +### Example Workflow +```bash +# 1. Configure environment +set MSBUILD_LOGGING_ARGS=-bl:build{}.binlog -check:deferred + +# 2. Run build (reduced overhead, no BuildCheck analysis during build) +msbuild solution.sln + +# 3. Later: Replay binlog (BuildCheck analyzes recorded events) +msbuild build{}.binlog +``` + +## CI/CD Integration + + +### Environment Variable + +- Set `MSBUILD_LOGGING_ARGS=-bl:build{}.binlog` +- No file creation needed +- The `{}` placeholder generates unique filenames for each build invocation + +### Combining Both Approaches +```bash +# Environment provides base logging +set MSBUILD_LOGGING_ARGS=-bl:base{}.binlog -check:deferred + +# Command line adds specific logging +msbuild solution.sln -bl:detailed.binlog + +# Result: Two binlog files created (base{...}.binlog + detailed.binlog) +``` \ No newline at end of file From d9c31f155478b766376cc29d0434126c0e9eea9a Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 9 Jan 2026 14:18:27 +0100 Subject: [PATCH 02/12] add MSBUILD_LOGGING_ARGS support --- .mcp.json | 7 + src/Framework/Traits.cs | 20 ++ .../XMake_BinlogSwitch_Tests.cs | 217 ++++++++++++++++++ src/MSBuild/Resources/Strings.resx | 16 +- src/MSBuild/Resources/xlf/Strings.cs.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.de.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.es.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.fr.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.it.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.ja.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.ko.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.pl.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.ru.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.tr.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 15 ++ src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 15 ++ src/MSBuild/XMake.cs | 112 ++++++++- 18 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 .mcp.json create mode 100644 src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000000..e50bd76f3d6 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,7 @@ +{ + "servers": { + "codebase-insights": { + "url": "http://98.71.32.114:8000/sse" + } + } +} \ No newline at end of file diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 8cbf21feef1..70ee1d3653b 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -135,6 +135,26 @@ public Traits() /// public const string UseMSBuildServerEnvVarName = "MSBUILDUSESERVER"; + /// + /// Name of environment variable for logging arguments (e.g., -bl, -check). + /// + public const string MSBuildLoggingArgsEnvVarName = "MSBUILD_LOGGING_ARGS"; + + /// + /// Name of environment variable for logging level (warning or message). + /// + public const string MSBuildLoggingArgsLevelEnvVarName = "MSBUILD_LOGGING_ARGS_LEVEL"; + + /// + /// Value of the MSBUILD_LOGGING_ARGS environment variable. + /// + public static string? MSBuildLoggingArgs => Environment.GetEnvironmentVariable(MSBuildLoggingArgsEnvVarName); + + /// + /// Value of the MSBUILD_LOGGING_ARGS_LEVEL environment variable. + /// + public static string? MSBuildLoggingArgsLevel => Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName); + public readonly bool DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); public readonly bool DebugScheduler; public readonly bool DebugNodeCommunication; diff --git a/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs new file mode 100644 index 00000000000..0f1ac735563 --- /dev/null +++ b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs @@ -0,0 +1,217 @@ +// 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.IO; +using Microsoft.Build.CommandLine; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for MSBUILD_LOGGING_ARGS environment variable functionality. + /// + public class XMakeBinlogSwitchTests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestEnvironment _env; + + public XMakeBinlogSwitchTests(ITestOutputHelper output) + { + _output = output; + _env = TestEnvironment.Create(output); + } + + public void Dispose() => _env.Dispose(); + + /// + /// Test that MSBUILD_LOGGING_ARGS with -bl creates a binary log. + /// + [Fact] + public void LoggingArgsEnvVarWithBinaryLogger() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS ignores unsupported arguments and continues with valid ones. + /// + [Fact] + public void LoggingArgsEnvVarIgnoresUnsupportedArguments() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + // Set env var with mixed valid and invalid arguments + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath} -maxcpucount:4 -verbosity:detailed"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Binary log should still be created (valid argument) + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created at {binlogPath}"); + + // Warning should appear for invalid arguments + output.ShouldContain("MSB1070"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS works with /noautoresponse. + /// + [Fact] + public void LoggingArgsEnvVarWorksWithNoAutoResponse() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + string binlogPath = Path.Combine(directory.Path, "test.binlog"); + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", $"-bl:{binlogPath}"); + + // Use /noautoresponse - MSBUILD_LOGGING_ARGS should still work + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\" /noautoresponse", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + File.Exists(binlogPath).ShouldBeTrue($"Binary log should have been created even with /noautoresponse"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS_LEVEL=message emits diagnostics as messages instead of warnings. + /// + [Fact] + public void LoggingArgsEnvVarLevelMessageSuppressesWarnings() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4"); + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS_LEVEL", "message"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldNotContain("MSB1070"); + } + + /// + /// Test that MSBUILD_LOGGING_ARGS emits warnings by default when MSBUILD_LOGGING_ARGS_LEVEL is not set. + /// + [Fact] + public void LoggingArgsEnvVarDefaultLevelEmitsWarnings() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + // Set env var with invalid argument, but do NOT set MSBUILD_LOGGING_ARGS_LEVEL + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-maxcpucount:4"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + // Warning SHOULD appear when level is not set (default behavior) + output.ShouldContain("MSB1070"); + } + + /// + /// Test that empty or whitespace MSBUILD_LOGGING_ARGS is ignored. + /// + [Fact] + public void LoggingArgsEnvVarEmptyIsIgnored() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", " "); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + } + + /// + /// Test that -check switch is allowed in MSBUILD_LOGGING_ARGS. + /// + [Fact] + public void LoggingArgsEnvVarAllowsCheckSwitch() + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", "-check"); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldNotContain("MSB1070"); + } + + /// + /// Test that only logging-related switches are allowed. + /// + [Theory] + [InlineData("-bl")] + [InlineData("-bl:test.binlog")] + [InlineData("-binarylogger")] + [InlineData("-binarylogger:test.binlog")] + [InlineData("/bl")] + [InlineData("/bl:test.binlog")] + [InlineData("--bl")] + [InlineData("-check")] + [InlineData("/check")] + public void LoggingArgsEnvVarAllowedSwitches(string switchArg) + { + CommandLineSwitches switches = new(); + _ = _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg); + + MSBuildApp.GatherLoggingArgsEnvironmentVariableSwitches(ref switches, "test"); + + switches.HaveErrors().ShouldBeFalse($"Switch {switchArg} should be allowed"); + } + + /// + /// Test that non-logging switches are rejected. + /// + [Theory] + [InlineData("-property:A=1")] + [InlineData("-target:Build")] + [InlineData("-verbosity:detailed")] + [InlineData("-maxcpucount:4")] + [InlineData("/p:A=1")] + [InlineData("-restore")] + [InlineData("-nologo")] + public void LoggingArgsEnvVarDisallowedSwitches(string switchArg) + { + var directory = _env.CreateFolder(); + string content = ObjectModelHelpers.CleanupFileContents(""); + var projectPath = directory.CreateFile("my.proj", content).Path; + + _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg); + + string output = RunnerUtilities.ExecMSBuild($"\"{projectPath}\"", out var successfulExit, _output); + successfulExit.ShouldBeTrue(output); + + output.ShouldContain("MSB1070"); + } + } +} diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 701bb681d2c..b31ae5b57a3 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1700,10 +1700,24 @@ 0: turned off + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index c050d60e03e..043bb4285cb 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -330,6 +330,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Na základě klíče registru Windows LongPathsEnabled má funkce LongPaths hodnotu {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 30794b9ef29..0ca20ee273a 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -330,6 +330,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Basierend auf dem Windows-Registrierungsschlüssel LongPathsEnabled ist das Feature LongPaths {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index a96c9ca8c11..14c1d520ed2 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -329,6 +329,21 @@ Esta marca es experimental y puede que no funcione según lo previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. De acuerdo con la clave del Registro de Windows LongPathsEnabled, la característica LongPaths está {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index cca6ab9830e..0cf4b46b440 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -330,6 +330,21 @@ futures LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. D’après la clé de Registre Windows LongPathsEnabled, la fonctionnalité LongPaths est {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 26f3d724edb..dc8bcc42eea 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -330,6 +330,21 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. In base alla chiave del Registro di sistema di Windows LongPathsEnabled, la funzionalità LongPaths è {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 5d1f7dacd88..afc9f92f6c0 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -330,6 +330,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows レジストリ キー LongPathsEnabled に基づいて、LongPaths 機能は{0}です。 diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index b5691e61719..e1789812bf8 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -331,6 +331,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows 레지스트리 키 LongPathsEnabled에 따라 LongPaths 기능이 {0}입니다. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 03b0e01f39c..cdfce437cf4 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -329,6 +329,21 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Na podstawie klucza rejestru systemu Windows LongPathsEnabled funkcja LongPaths jest {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 76cbbebd996..da97d604989 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -329,6 +329,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Com base na chave do Registro do Windows LongPathsEnabled, o recurso LongPaths é {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index cec4de53558..52f74c58443 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -329,6 +329,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. На основе раздела реестра Windows LongPathsEnabled функция LongPaths имеет значение {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 940f30c6aa8..fd6e7660279 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -329,6 +329,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. Windows kayıt defteri anahtarı LongPathsEnabled ayarına bağlı olarak LongPaths özelliği {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index f9cd58c592f..ddd18359d43 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -330,6 +330,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. 基于 Windows 注册表项 LongPathsEnabled,LongPaths 功能为 {0}。 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 3bb145115b7..328376fbc4e 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -330,6 +330,21 @@ LOCALIZATION: The prefix "MSBUILD : error MSBxxxx:" should not be localized. + + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + + + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + + + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. 根據 Windows 登錄機碼 LongPathsEnabled,LongPaths 功能為 {0}。 diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 0bc9bd7aafd..f64219c51c1 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -53,6 +53,7 @@ using File = Microsoft.IO.File; using FileInfo = Microsoft.IO.FileInfo; using Path = Microsoft.IO.Path; +using System.Linq.Expressions; #endif #nullable disable @@ -1915,6 +1916,66 @@ private static void VerifyThrowSupportedOS() } } + /// + /// Gathers and validates logging switches from the MSBUILD_LOGGING_ARGS environment variable. + /// Only -bl and -check switches are allowed. All other switches are logged as warnings and ignored. + /// + internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLineSwitches switches, string commandLine) + { + if (string.IsNullOrWhiteSpace(Traits.MSBuildLoggingArgs)) + { + return; + } + + // Determine if warnings should be emitted as messages (for builds with /warnaserror) + bool emitAsMessage = string.Equals(Traits.MSBuildLoggingArgsLevel, BinaryLogRecordKind.Message.ToString(), StringComparison.OrdinalIgnoreCase); + + try + { + List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); + + List validArgs = new(envVarArgs.Count); + List invalidArgs = null; + + foreach (string arg in envVarArgs) + { + string unquotedArg = QuotingUtilities.Unquote(arg); + if (string.IsNullOrWhiteSpace(unquotedArg)) + { + continue; + } + + if (IsAllowedLoggingArg(unquotedArg)) + { + validArgs.Add(arg); + } + else + { + invalidArgs ??= []; + invalidArgs.Add(unquotedArg); + } + } + + if (invalidArgs != null) + { + foreach (string invalidArg in invalidArgs) + { + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarUnsupportedArgument", invalidArg), emitAsMessage); + } + } + + if (validArgs.Count > 0) + { + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), emitAsMessage: true); + GatherCommandLineSwitches(validArgs, switches, commandLine); + } + } + catch (Exception ex) + { + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarError", ex.Message), emitAsMessage); + } + } + /// /// MSBuild.exe need to fallback to English if it cannot print Japanese (or other language) characters /// @@ -1999,7 +2060,9 @@ private static void GatherAllSwitches( #else string[] commandLine, #endif - out CommandLineSwitches switchesFromAutoResponseFile, out CommandLineSwitches switchesNotFromAutoResponseFile, out string fullCommandLine) + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine) { ResetGatheringSwitchesState(); @@ -2044,6 +2107,53 @@ private static void GatherAllSwitches( { GatherAutoResponseFileSwitches(s_exePath, switchesFromAutoResponseFile, fullCommandLine); } + + CommandLineSwitches switchesFromEnvironmentVariable = new(); + GatherLoggingArgsEnvironmentVariableSwitches(ref switchesFromEnvironmentVariable, fullCommandLine); + switchesNotFromAutoResponseFile.Append(switchesFromEnvironmentVariable, fullCommandLine); + } + + /// + /// Checks if the argument is an allowed logging argument (-bl or -check). + /// + /// The unquoted argument to check. + /// True if the argument is allowed, false otherwise. + private static bool IsAllowedLoggingArg(string arg) + { + if (!ValidateSwitchIndicatorInUnquotedArgument(arg)) + { + return false; + } + + string switchPart = arg.Substring(GetLengthOfSwitchIndicator(arg)); + + // Extract switch name (before any ':' parameter indicator) + int colonIndex = switchPart.IndexOf(':'); + string switchName = colonIndex >= 0 ? switchPart.Substring(0, colonIndex) : switchPart; + + return IsBinaryLoggerSwitch(switchName) || string.Equals(switchName, "check", StringComparison.OrdinalIgnoreCase); + } + + private static bool IsBinaryLoggerSwitch(string switchName) => string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase); + + /// + /// Logs a message from MSBUILD_LOGGING_ARGS processing. Messages are either emitted as warnings + /// to the console or queued as low-importance build messages. + /// + /// The message to log. + /// If true, emit as low-importance message; if false, emit as warning to console. + private static void LogLoggingArgsMessage(string message, bool emitAsMessage) + { + if (emitAsMessage) + { + // Queue as a low-importance message to be logged when the build starts + s_globalMessagesToLogInBuildLoggers.Add(new BuildManager.DeferredBuildMessage(message, MessageImportance.Low)); + } + else + { + // Emit as warning to console immediately + Console.WriteLine(message); + } } /// From 7ebdc9ce88b3de14ef231a53cda67bd3a91490b1 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 9 Jan 2026 14:35:06 +0100 Subject: [PATCH 03/12] remove extra file --- .mcp.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index e50bd76f3d6..00000000000 --- a/.mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "servers": { - "codebase-insights": { - "url": "http://98.71.32.114:8000/sse" - } - } -} \ No newline at end of file From 0826586d68e95bf0259c74e3e124f7cc9447116f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 07:32:02 -0600 Subject: [PATCH 04/12] Support multiple binary logs from command line arguments (#12706) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Context Today it's possible to provide the `-bl` flag multiple times, but the behavior of the engine is that the last-provided flag wins. This is confusing, and in extreme cases can mean that tools that need binlogs and provide them via things like response files can be overridden by a user's invocation. Tools like CodeQL have a harder time adopting binlog usage for C# code analysis because they can't control where the binlog is generated. ### Changes Made Implemented support for writing multiple binlogs when supplied via command line arguments. The implementation intelligently handles two scenarios: 1. **Same Configuration (Optimized)**: When all `-bl` flags have the same configuration (only file paths differ), a single BinaryLogger writes to one location and copies to additional destinations at build completion for consistency and performance. 2. **Different Configurations**: When `-bl` flags have different configurations (e.g., `ProjectImports=None` vs `ProjectImports=Embed`), separate BinaryLogger instances are created to respect each configuration. **Key changes:** - Added `AdditionalFilePaths` property to BinaryLogger with `init` accessor (documented as internal-use only) - Added `BinaryLogger.ProcessParameters()` static method to process multiple parameter sets and return distinct paths with configuration info - Added `ProcessedBinaryLoggerParameters` readonly struct to encapsulate the processing results - Updated `Shutdown()` method to copy binlog to additional paths after stream close - Uses `LogMessage` to log copy destinations before stream close - Uses `Console.Error` to log copy errors (won't fail build on copy failures) - Updated `XMake.ProcessBinaryLogger()` to use the new BinaryLogger.ProcessParameters() method with object initializer syntax for `init` properties - Added error message resource for copy failures - Replaced hardcoded string literals with constants (BinlogFileExtension, LogFileParameterPrefix) - All new methods have XML documentation - Added `DuplicateFilePaths` property to track and report duplicate paths - Added message to inform users when duplicate binary log paths are filtered out - Refactored `TryInterpretPathParameter` methods to eliminate code duplication via shared core method - Optimized `ProcessParameters` to avoid redundant path extraction calls ### Testing - Added unit tests for multiple binlog files with same configuration - Added unit tests for multiple binlog files with different configurations - Added unit test for duplicate path deduplication - Added dedicated unit tests for `ExtractFilePathFromParameters`, `ExtractNonPathParameters`, and `ProcessParameters` - Manual verification confirms all specified binlog files are created with identical content - All tests passing with `init` accessor implementation ### Notes - The `AdditionalFilePaths` property uses `init` accessor to enforce immutability after object initialization, signaling it should only be set during construction - External code should create multiple logger instances instead of using AdditionalFilePaths - When MSBUILDDEBUGENGINE is set, the engine creates a separate BinaryLogger instance which operates independently (functionally correct behavior) - Duplicate paths specified via multiple `-bl` flags are automatically deduplicated - only the first occurrence is kept - Users are now informed when duplicate paths are detected and filtered out - Copy errors are logged to stderr but don't fail the build, ensuring builds succeed even if binlog copies fail
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > MSBuild.exe should support writing multiple binlogs if supplied as part of command line arguments > ### Summary > > Today it's possible to provide the `-bl` flag multiple times, but the behavior of the engine is that the last-provided flag wins: > > ``` > > dotnet build -bl:1.binlog -bl:2.binlog > Restore complete (0.1s) > info NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy > lib2 net10.0 succeeded (0.1s) → D:\Code\scratch\bleh\lib2\bin\Debug\net10.0\lib2.dll > bleh net10.0 succeeded (0.1s) → bin\Debug\net10.0\bleh.dll > > Build succeeded in 0.5s > > ls *.binlog > > Directory: D:\Code\scratch\bleh\lib > > Mode LastWriteTime Length Name > ---- ------------- ------ ---- > -a--- 10/29/2025 11:14 AM 454756 2.binlog > ``` > This is confusing, and in extreme cases can mean that tools that need binlogs and provide them via things like response files can be overridden by a users invocation. > > ### Background and Motivation > > The inability to write more than one binlog file means that tools like codeql have a harder time adopting binlog usage for C# code analysis, because they can't control where the binlog is generated. > > ### Proposed Feature > > The command-line handling should accept _multiple_ bl flags, resolve them into their distinct binlog filenames, ensure that set is distinct, and then the binary logger should be able to write binary logs to all of those specified files. > > This file writing could be done in two ways (at least) > * multiple independent writers, each writing to a separate file > * one writer, writing the binlog file to a temp location and then atomically copying that file to the various destinations at build-finish > > We should use the second option for consistency. > > > ### Alternative Designs > > _No response_ > > ## Comments on the Issue (you are @copilot in this section) > > > >
- Fixes dotnet/msbuild#12705 --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baronfel <573979+baronfel@users.noreply.github.com> Co-authored-by: YuliiaKovalova <95473390+YuliiaKovalova@users.noreply.github.com> Co-authored-by: YuliiaKovalova --- src/Build.UnitTests/BinaryLogger_Tests.cs | 176 +++++++++++ .../Logging/BinaryLogger/BinaryLogger.cs | 273 +++++++++++++++++- src/Build/Resources/Strings.resx | 6 +- src/Build/Resources/xlf/Strings.cs.xlf | 5 + src/Build/Resources/xlf/Strings.de.xlf | 5 + src/Build/Resources/xlf/Strings.es.xlf | 5 + src/Build/Resources/xlf/Strings.fr.xlf | 5 + src/Build/Resources/xlf/Strings.it.xlf | 5 + src/Build/Resources/xlf/Strings.ja.xlf | 5 + src/Build/Resources/xlf/Strings.ko.xlf | 5 + src/Build/Resources/xlf/Strings.pl.xlf | 5 + src/Build/Resources/xlf/Strings.pt-BR.xlf | 5 + src/Build/Resources/xlf/Strings.ru.xlf | 5 + src/Build/Resources/xlf/Strings.tr.xlf | 5 + src/Build/Resources/xlf/Strings.zh-Hans.xlf | 5 + src/Build/Resources/xlf/Strings.zh-Hant.xlf | 5 + src/MSBuild.UnitTests/XMake_Tests.cs | 110 +++++++ src/MSBuild/Resources/Strings.resx | 4 + src/MSBuild/Resources/xlf/Strings.cs.xlf | 5 + src/MSBuild/Resources/xlf/Strings.de.xlf | 5 + src/MSBuild/Resources/xlf/Strings.es.xlf | 5 + src/MSBuild/Resources/xlf/Strings.fr.xlf | 5 + src/MSBuild/Resources/xlf/Strings.it.xlf | 5 + src/MSBuild/Resources/xlf/Strings.ja.xlf | 5 + src/MSBuild/Resources/xlf/Strings.ko.xlf | 5 + src/MSBuild/Resources/xlf/Strings.pl.xlf | 5 + src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 5 + src/MSBuild/Resources/xlf/Strings.ru.xlf | 5 + src/MSBuild/Resources/xlf/Strings.tr.xlf | 5 + src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 5 + src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 5 + src/MSBuild/XMake.cs | 34 ++- 32 files changed, 722 insertions(+), 11 deletions(-) diff --git a/src/Build.UnitTests/BinaryLogger_Tests.cs b/src/Build.UnitTests/BinaryLogger_Tests.cs index 7156a81eb58..55884569139 100644 --- a/src/Build.UnitTests/BinaryLogger_Tests.cs +++ b/src/Build.UnitTests/BinaryLogger_Tests.cs @@ -748,6 +748,182 @@ public void ParseParameters_InvalidParameter_ThrowsLoggerException(string parame File.Create(_logFile).Dispose(); } + #region ExtractFilePathFromParameters Tests + + [Theory] + [InlineData(null, "msbuild.binlog")] + [InlineData("", "msbuild.binlog")] + [InlineData("output.binlog", "output.binlog")] + [InlineData("LogFile=output.binlog", "output.binlog")] + [InlineData("output.binlog;ProjectImports=None", "output.binlog")] + [InlineData("ProjectImports=None;output.binlog", "output.binlog")] + [InlineData("ProjectImports=None;LogFile=output.binlog;OmitInitialInfo", "output.binlog")] + [InlineData("ProjectImports=None", "msbuild.binlog")] // No path specified + [InlineData("OmitInitialInfo", "msbuild.binlog")] // No path specified + public void ExtractFilePathFromParameters_ReturnsExpectedPath(string parameters, string expectedFileName) + { + string result = BinaryLogger.ExtractFilePathFromParameters(parameters); + + // The method returns full paths, so check just the filename + Path.GetFileName(result).ShouldBe(expectedFileName); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ExtractFilePathFromParameters_ReturnsFullPath() + { + string result = BinaryLogger.ExtractFilePathFromParameters("mylog.binlog"); + + // Should be a full path, not relative + Path.IsPathRooted(result).ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ExtractNonPathParameters Tests + + [Theory] + [InlineData(null, "")] + [InlineData("", "")] + [InlineData("output.binlog", "")] // Path only, no config + [InlineData("LogFile=output.binlog", "")] // Path only, no config + [InlineData("ProjectImports=None", "ProjectImports=None")] + [InlineData("OmitInitialInfo", "OmitInitialInfo")] + [InlineData("output.binlog;ProjectImports=None", "ProjectImports=None")] + [InlineData("output.binlog;ProjectImports=None;OmitInitialInfo", "OmitInitialInfo;ProjectImports=None")] // Sorted + [InlineData("OmitInitialInfo;output.binlog;ProjectImports=None", "OmitInitialInfo;ProjectImports=None")] // Sorted + public void ExtractNonPathParameters_ReturnsExpectedConfig(string parameters, string expectedConfig) + { + string result = BinaryLogger.ExtractNonPathParameters(parameters); + result.ShouldBe(expectedConfig); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + + #region ProcessParameters Tests + + [Fact] + public void ProcessParameters_NullArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(null); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_EmptyArray_ReturnsEmptyResult() + { + var result = BinaryLogger.ProcessParameters(Array.Empty()); + + result.DistinctParameterSets.ShouldBeEmpty(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_SingleParameter_ReturnsOneParameterSet() + { + var result = BinaryLogger.ProcessParameters(new[] { "output.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(1); + result.DistinctParameterSets[0].ShouldBe("output.binlog"); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + result.AllConfigurationsIdentical.ShouldBeTrue(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MultipleIdenticalConfigs_OptimizesWithAdditionalPaths() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog", "3.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(3); + result.AllConfigurationsIdentical.ShouldBeTrue(); + result.AdditionalFilePaths.Count.ShouldBe(2); // 2.binlog and 3.binlog + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DifferentConfigs_NoOptimization() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "2.binlog;ProjectImports=None" }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.AdditionalFilePaths.ShouldBeEmpty(); + result.DuplicateFilePaths.ShouldBeEmpty(); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_FilteredOut() + { + var result = BinaryLogger.ProcessParameters(new[] { "1.binlog", "1.binlog", "2.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // 1.binlog and 2.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate of 1.binlog + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_DuplicatePaths_CaseInsensitive() + { + var result = BinaryLogger.ProcessParameters(new[] { "Output.binlog", "output.BINLOG", "other.binlog" }); + + result.DistinctParameterSets.Count.ShouldBe(2); // Output.binlog and other.binlog + result.DuplicateFilePaths.Count.ShouldBe(1); // One duplicate + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + [Fact] + public void ProcessParameters_MixedConfigsWithDuplicates_HandledCorrectly() + { + var result = BinaryLogger.ProcessParameters(new[] { + "1.binlog", + "2.binlog;ProjectImports=None", + "1.binlog;ProjectImports=None" // Different config but same path - filtered as duplicate + }); + + result.DistinctParameterSets.Count.ShouldBe(2); + result.AllConfigurationsIdentical.ShouldBeFalse(); + result.DuplicateFilePaths.Count.ShouldBe(1); + + // Create the expected log file to satisfy test environment expectations + File.Create(_logFile).Dispose(); + } + + #endregion + public void Dispose() { _env.Dispose(); diff --git a/src/Build/Logging/BinaryLogger/BinaryLogger.cs b/src/Build/Logging/BinaryLogger/BinaryLogger.cs index 3d40ddeb7af..73031a02798 100644 --- a/src/Build/Logging/BinaryLogger/BinaryLogger.cs +++ b/src/Build/Logging/BinaryLogger/BinaryLogger.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; using Microsoft.Build.Experimental.BuildCheck.Infrastructure.EditorConfig; @@ -296,6 +297,17 @@ private static bool TryParsePathParameter(string parameter, out string filePath) internal string FilePath { get; private set; } + /// + /// Gets or sets additional output file paths. When set, the binlog will be copied to all these paths + /// after the build completes. The primary FilePath will be used as the temporary write location. + /// + /// + /// This property is intended for internal use by MSBuild command-line processing. + /// It should not be set by external code or logger implementations. + /// Use multiple logger instances with different Parameters instead. + /// + public IReadOnlyList AdditionalFilePaths { get; init; } + /// Gets or sets the verbosity level. /// /// The binary logger Verbosity is always maximum (Diagnostic). It tries to capture as much @@ -505,6 +517,15 @@ public void Shutdown() } + // Log additional file paths before closing stream (so they're recorded in the binlog) + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0 && stream != null) + { + foreach (var additionalPath in AdditionalFilePaths) + { + LogMessage("BinLogCopyDestination=" + additionalPath); + } + } + if (stream != null) { // It's hard to determine whether we're at the end of decoding GZipStream @@ -514,6 +535,37 @@ public void Shutdown() stream.Dispose(); stream = null; } + + // Copy the binlog file to additional destinations if specified + if (AdditionalFilePaths != null && AdditionalFilePaths.Count > 0) + { + foreach (var additionalPath in AdditionalFilePaths) + { + try + { + string directory = Path.GetDirectoryName(additionalPath); + if (!string.IsNullOrEmpty(directory)) + { + Directory.CreateDirectory(directory); + } + File.Copy(FilePath, additionalPath, overwrite: true); + } + catch (Exception ex) + { + // Log the error but don't fail the build + // Note: We can't use LogMessage here since the stream is already closed + string message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword( + out _, + out _, + "ErrorCopyingBinaryLog", + FilePath, + additionalPath, + ex.Message); + + Console.Error.WriteLine(message); + } + } + } } private void RawEvents_LogDataSliceReceived(BinaryLogRecordKind recordKind, Stream stream) @@ -635,6 +687,78 @@ private void ProcessParameters(out bool omitInitialInfo) } private bool TryInterpretPathParameter(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, GetUniqueStamp, out filePath); + } + + private string GetUniqueStamp() + => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + + private static string ExpandPathParameter(string parameters) + => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + + /// + /// Extracts the file path from binary logger parameters string. + /// This is a helper method for processing multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog" or "output.binlog;ProjectImports=None") + /// The resolved file path, or "msbuild.binlog" if no path is specified + public static string ExtractFilePathFromParameters(string parameters) + { + const string DefaultBinlogFileName = "msbuild" + BinlogFileExtension; + + if (string.IsNullOrEmpty(parameters)) + { + return Path.GetFullPath(DefaultBinlogFileName); + } + + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + string filePath = null; + + foreach (var parameter in paramParts) + { + if (TryInterpretPathParameterStatic(parameter, out string extractedPath)) + { + filePath = extractedPath; + break; + } + } + + if (filePath == null) + { + filePath = DefaultBinlogFileName; + } + + try + { + return Path.GetFullPath(filePath); + } + catch + { + // If path resolution fails, return the original path + return filePath; + } + } + + /// + /// Attempts to interpret a parameter string as a file path. + /// + /// The parameter to interpret (e.g., "LogFile=output.binlog" or "output.binlog") + /// The extracted file path if the parameter is a path, otherwise the original parameter + /// True if the parameter is a valid file path (ends with .binlog or contains wildcards), false otherwise + private static bool TryInterpretPathParameterStatic(string parameter, out string filePath) + { + return TryInterpretPathParameterCore(parameter, () => ExpandPathParameter(string.Empty), out filePath); + } + + /// + /// Core logic for interpreting a parameter string as a file path. + /// + /// The parameter to interpret + /// Function to expand wildcard placeholders + /// The extracted file path + /// True if the parameter is a valid file path + private static bool TryInterpretPathParameterCore(string parameter, Func wildcardExpander, out string filePath) { bool hasPathPrefix = parameter.StartsWith(LogFileParameterPrefix, StringComparison.OrdinalIgnoreCase); @@ -654,7 +778,7 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return hasProperExtension; } - filePath = parameter.Replace("{}", GetUniqueStamp(), StringComparison.Ordinal); + filePath = parameter.Replace("{}", wildcardExpander(), StringComparison.Ordinal); if (!hasProperExtension) { @@ -663,10 +787,149 @@ private bool TryInterpretPathParameter(string parameter, out string filePath) return true; } - private string GetUniqueStamp() - => (PathParameterExpander ?? ExpandPathParameter)(string.Empty); + /// + /// Extracts the non-file-path parameters from binary logger parameters string. + /// This is used to compare configurations between multiple binlog parameters. + /// + /// The parameters string (e.g., "output.binlog;ProjectImports=None") + /// A normalized string of non-path parameters, or empty string if only path parameters + public static string ExtractNonPathParameters(string parameters) + { + if (string.IsNullOrEmpty(parameters)) + { + return string.Empty; + } - private static string ExpandPathParameter(string parameters) - => $"{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss")}--{EnvironmentUtilities.CurrentProcessId}--{StringUtils.GenerateRandomString(6)}"; + var paramParts = parameters.Split(MSBuildConstants.SemicolonChar, StringSplitOptions.RemoveEmptyEntries); + var nonPathParams = new List(); + + foreach (var parameter in paramParts) + { + // Skip file path parameters + if (TryInterpretPathParameterStatic(parameter, out _)) + { + continue; + } + + // This is a configuration parameter (like ProjectImports=None, OmitInitialInfo, etc.) + nonPathParams.Add(parameter); + } + + // Sort for consistent comparison + nonPathParams.Sort(StringComparer.OrdinalIgnoreCase); + return string.Join(";", nonPathParams); + } + + /// + /// Result of processing multiple binary logger parameter sets. + /// + public readonly struct ProcessedBinaryLoggerParameters + { + /// + /// List of distinct parameter sets that need separate logger instances. + /// + public IReadOnlyList DistinctParameterSets { get; } + + /// + /// If true, all parameter sets have identical configurations (only file paths differ), + /// so a single logger can be used with file copying for additional paths. + /// + public bool AllConfigurationsIdentical { get; } + + /// + /// Additional file paths to copy the binlog to (only valid when AllConfigurationsIdentical is true). + /// + public IReadOnlyList AdditionalFilePaths { get; } + + /// + /// List of duplicate file paths that were filtered out. + /// + public IReadOnlyList DuplicateFilePaths { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// List of distinct parameter sets that need separate logger instances. + /// Whether all parameter sets have identical configurations. + /// Additional file paths to copy the binlog to. + /// List of duplicate file paths that were filtered out. + public ProcessedBinaryLoggerParameters( + IReadOnlyList distinctParameterSets, + bool allConfigurationsIdentical, + IReadOnlyList additionalFilePaths, + IReadOnlyList duplicateFilePaths) + { + DistinctParameterSets = distinctParameterSets; + AllConfigurationsIdentical = allConfigurationsIdentical; + AdditionalFilePaths = additionalFilePaths; + DuplicateFilePaths = duplicateFilePaths; + } + } + + /// + /// Processes multiple binary logger parameter sets and returns distinct paths and configuration info. + /// + /// Array of parameter strings from command line + /// Processed result with distinct parameter sets and configuration info + public static ProcessedBinaryLoggerParameters ProcessParameters(string[] binaryLoggerParameters) + { + var distinctParameterSets = new List(); + var additionalFilePaths = new List(); + var duplicateFilePaths = new List(); + bool allConfigurationsIdentical = true; + + if (binaryLoggerParameters == null || binaryLoggerParameters.Length == 0) + { + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + if (binaryLoggerParameters.Length == 1) + { + distinctParameterSets.Add(binaryLoggerParameters[0]); + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } + + string primaryArguments = binaryLoggerParameters[0]; + string primaryNonPathParams = ExtractNonPathParameters(primaryArguments); + + var distinctFilePaths = new HashSet(StringComparer.OrdinalIgnoreCase); + var extractedFilePaths = new List(); + + // Check if all parameter sets have the same non-path configuration + for (int i = 0; i < binaryLoggerParameters.Length; i++) + { + string currentParams = binaryLoggerParameters[i]; + string currentNonPathParams = ExtractNonPathParameters(currentParams); + string currentFilePath = ExtractFilePathFromParameters(currentParams); + + // Check if this is a duplicate file path + if (distinctFilePaths.Add(currentFilePath)) + { + if (!string.Equals(primaryNonPathParams, currentNonPathParams, StringComparison.OrdinalIgnoreCase)) + { + allConfigurationsIdentical = false; + } + distinctParameterSets.Add(currentParams); + extractedFilePaths.Add(currentFilePath); + } + else + { + // Track duplicate paths for logging + duplicateFilePaths.Add(currentFilePath); + } + } + + // If all configurations are identical, compute additional file paths for copying + // Use the pre-extracted paths to avoid redundant ExtractFilePathFromParameters calls + if (allConfigurationsIdentical && distinctParameterSets.Count > 1) + { + for (int i = 1; i < extractedFilePaths.Count; i++) + { + additionalFilePaths.Add(extractedFilePaths[i]); + } + } + + return new ProcessedBinaryLoggerParameters(distinctParameterSets, allConfigurationsIdentical, additionalFilePaths, duplicateFilePaths); + } } } diff --git a/src/Build/Resources/Strings.resx b/src/Build/Resources/Strings.resx index 95da5690fa1..5006689ecc3 100644 --- a/src/Build/Resources/Strings.resx +++ b/src/Build/Resources/Strings.resx @@ -2447,6 +2447,10 @@ Utilization: {0} Average Utilization: {1:###.0} The directory does not exist: {0}. .NET Runtime Task Host could not be instantiated. See https://aka.ms/nettaskhost for details on how to resolve this error. + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + The custom task '{0}' required a fallback to out-of-process execution because the UsingTask definition does not specify the correct Runtime and Architecture. This reduces build performance. Update the UsingTask element to explicitly specify Runtime and Architecture attributes (e.g., Runtime="CLR4" Architecture="x64") or use TaskFactory="TaskHostFactory". @@ -2457,7 +2461,7 @@ Utilization: {0} Average Utilization: {1:###.0} diff --git a/src/Build/Resources/xlf/Strings.cs.xlf b/src/Build/Resources/xlf/Strings.cs.xlf index a5773bf2038..c46fbabc608 100644 --- a/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/Build/Resources/xlf/Strings.cs.xlf @@ -489,6 +489,11 @@ Číst proměnnou prostředí {0} + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Při čtení vstupních souborů mezipaměti pro výsledky z cesty {0} byla zjištěna chyba: {1} diff --git a/src/Build/Resources/xlf/Strings.de.xlf b/src/Build/Resources/xlf/Strings.de.xlf index 29625306e1d..dc832057f19 100644 --- a/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/Build/Resources/xlf/Strings.de.xlf @@ -489,6 +489,11 @@ Umgebungsvariable "{0}" lesen + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Beim Lesen der Cachedateien für Eingabeergebnisse aus dem Pfad "{0}" wurde ein Fehler festgestellt: {1} diff --git a/src/Build/Resources/xlf/Strings.es.xlf b/src/Build/Resources/xlf/Strings.es.xlf index fbad7f6fbd9..5b1e78c6d01 100644 --- a/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/Build/Resources/xlf/Strings.es.xlf @@ -489,6 +489,11 @@ Leer la variable de entorno "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Error al leer los archivos de caché de resultados de entrada en la ruta de acceso "{0}": {1} diff --git a/src/Build/Resources/xlf/Strings.fr.xlf b/src/Build/Resources/xlf/Strings.fr.xlf index e36c0d13f30..6f23768cbb7 100644 --- a/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/Build/Resources/xlf/Strings.fr.xlf @@ -489,6 +489,11 @@ Lire la variable d'environnement "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: La lecture des fichiers cache des résultats d'entrée à partir du chemin "{0}" a rencontré une erreur : {1} diff --git a/src/Build/Resources/xlf/Strings.it.xlf b/src/Build/Resources/xlf/Strings.it.xlf index 35520f4ead6..4059064d652 100644 --- a/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/Build/Resources/xlf/Strings.it.xlf @@ -489,6 +489,11 @@ Legge la variabile di ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: durante la lettura dei file della cache dei risultati di input dal percorso "{0}" è stato rilevato un errore: {1} diff --git a/src/Build/Resources/xlf/Strings.ja.xlf b/src/Build/Resources/xlf/Strings.ja.xlf index ff6592d2d65..00d184725e2 100644 --- a/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/Build/Resources/xlf/Strings.ja.xlf @@ -489,6 +489,11 @@ 環境変数 "{0}" の読み取り + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: パス "{0}" から入力結果キャッシュ ファイルを読み取る処理でエラーが発生しました: {1} diff --git a/src/Build/Resources/xlf/Strings.ko.xlf b/src/Build/Resources/xlf/Strings.ko.xlf index 1f8febc1325..9fb51a6afd8 100644 --- a/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/Build/Resources/xlf/Strings.ko.xlf @@ -489,6 +489,11 @@ 환경 변수 "{0}" 읽기 + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" 경로에서 입력 결과 캐시 파일을 읽는 중 오류가 발생했습니다. {1} diff --git a/src/Build/Resources/xlf/Strings.pl.xlf b/src/Build/Resources/xlf/Strings.pl.xlf index 48fcf71d76d..e26c3ac3326 100644 --- a/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/Build/Resources/xlf/Strings.pl.xlf @@ -489,6 +489,11 @@ Odczytaj zmienną środowiskową „{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: Podczas odczytywania plików wejściowej pamięci podręcznej wyników ze ścieżki „{0}” wystąpił błąd: {1} diff --git a/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/Build/Resources/xlf/Strings.pt-BR.xlf index 78e5e61e1f4..7640d2c5e30 100644 --- a/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -489,6 +489,11 @@ Ler a variável de ambiente "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: a leitura dos arquivos de cache do resultado de entrada do caminho "{0}" encontrou um erro: {1} diff --git a/src/Build/Resources/xlf/Strings.ru.xlf b/src/Build/Resources/xlf/Strings.ru.xlf index 50ad0335993..1d790cf2f02 100644 --- a/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/Build/Resources/xlf/Strings.ru.xlf @@ -489,6 +489,11 @@ Чтение переменной среды "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: произошла ошибка при чтении входных файлов кэша результатов из пути "{0}": {1} diff --git a/src/Build/Resources/xlf/Strings.tr.xlf b/src/Build/Resources/xlf/Strings.tr.xlf index 2299d8178e6..42c1ff06b9d 100644 --- a/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/Build/Resources/xlf/Strings.tr.xlf @@ -489,6 +489,11 @@ "{0}" ortam değişkenini oku + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: "{0}" yolundan giriş sonucu önbellek dosyaları okunurken bir hatayla karşılaşıldı: {1} diff --git a/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/Build/Resources/xlf/Strings.zh-Hans.xlf index 9f1610a48a2..8ba4b6104e7 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -489,6 +489,11 @@ 读取环境变量“{0}” + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 从路径“{0}”读取输入结果缓存文件时遇到错误: {1} diff --git a/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/Build/Resources/xlf/Strings.zh-Hant.xlf index 0c718642f76..584bed61d4a 100644 --- a/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -489,6 +489,11 @@ 讀取環境變數 "{0}" + + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + MSB4279: Failed to copy binary log from "{0}" to "{1}". {2} + {StrBegin="MSB4279: "}UE: This is shown when the Binary Logger fails to copy the log file to one of the specified output locations. + MSB4256: Reading input result cache files from path "{0}" encountered an error: {1} MSB4256: 從路徑 "{0}" 讀取輸入結果快取檔案發生錯誤: {1} diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 3ebfcae6615..9ed990fc17c 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -2714,6 +2714,116 @@ public void BinaryLogContainsImportedFiles() archive.Entries.ShouldContain(e => e.FullName.EndsWith(".proj", StringComparison.OrdinalIgnoreCase), 2); } + [Fact] + public void MultipleBinaryLogsCreatesMultipleFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + string binLog3 = Path.Combine(binLogLocation, "3.binlog"); + + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog3}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify all three binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + File.Exists(binLog3).ShouldBeTrue("Third binlog file should exist"); + + // Verify all files have content (are not empty) + new FileInfo(binLog1).Length.ShouldBeGreaterThan(0, "First binlog should not be empty"); + new FileInfo(binLog2).Length.ShouldBeGreaterThan(0, "Second binlog should not be empty"); + new FileInfo(binLog3).Length.ShouldBeGreaterThan(0, "Third binlog should not be empty"); + + // Verify all files are identical (have the same content) + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + byte[] file3Bytes = File.ReadAllBytes(binLog3); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("First and second binlog should be identical"); + file1Bytes.SequenceEqual(file3Bytes).ShouldBeTrue("First and third binlog should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDuplicatesCreateDistinctFiles() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "1.binlog"); + string binLog2 = Path.Combine(binLogLocation, "2.binlog"); + + // Specify binLog1 twice - should only create two distinct files + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2}\" \"/bl:{binLog1}\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify both files are identical + byte[] file1Bytes = File.ReadAllBytes(binLog1); + byte[] file2Bytes = File.ReadAllBytes(binLog2); + + file1Bytes.SequenceEqual(file2Bytes).ShouldBeTrue("Binlog files should be identical"); + } + + [Fact] + public void MultipleBinaryLogsWithDifferentConfigurationsCreatesSeparateLoggers() + { + var testProject = _env.CreateFile("TestProject.proj", @" + + + + + + + "); + + _env.CreateFile("Imported.proj", @" + + + Value + + + "); + + string binLogLocation = _env.DefaultTestDirectory.Path; + string binLog1 = Path.Combine(binLogLocation, "with-imports.binlog"); + string binLog2 = Path.Combine(binLogLocation, "no-imports.binlog"); + + // One with default imports, one with ProjectImports=None + string output = RunnerUtilities.ExecMSBuild($"\"{testProject.Path}\" \"/bl:{binLog1}\" \"/bl:{binLog2};ProjectImports=None\"", out var success, _output); + + success.ShouldBeTrue(output); + + // Verify both binlog files exist + File.Exists(binLog1).ShouldBeTrue("First binlog file should exist"); + File.Exists(binLog2).ShouldBeTrue("Second binlog file should exist"); + + // Verify files are different sizes (one has imports embedded, one doesn't) + long size1 = new FileInfo(binLog1).Length; + long size2 = new FileInfo(binLog2).Length; + + size1.ShouldBeGreaterThan(size2, "Binlog with imports should be larger than one without"); + } + [Theory] [InlineData("-warnaserror", "", "", false)] [InlineData("-warnaserror -warnnotaserror:FOR123", "", "", true)] diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 701bb681d2c..0c9087351cb 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1700,6 +1700,10 @@ 0: turned off + + Duplicate binary log path(s) specified and ignored: {0} + {0} is the list of duplicate paths that were filtered out + Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index b21e77a1cc7..f70bc3cf2db 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -348,7 +348,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 2d1e406e1ba..c2cd937d34c 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -348,7 +348,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index 5dfd685cb4f..b452e2d83da 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -347,7 +347,7 @@ Esta marca es experimental y puede que no funcione según lo previsto. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 8473f011959..a240cb9cc17 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -348,7 +348,7 @@ futures Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 4f2040c0b34..4ebfef7a0ee 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -348,7 +348,7 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 459466b340d..ccdaa7dd386 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -348,7 +348,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index f6fc3105e29..15b5952483c 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -349,7 +349,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 74bf9859e73..710e3001d42 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -347,7 +347,7 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 21bb11618ff..336e2e69048 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -347,7 +347,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 5857f6745e4..ead571a1cf4 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -347,7 +347,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index f6b0c8d58dc..8a299d18422 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -347,7 +347,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 7526b2dcee9..4712ae67562 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -348,7 +348,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 0cfd7a5e16c..db2a84f698d 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -348,7 +348,7 @@ Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} - LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the arguments from the environment variable. + LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. Based on the Windows registry key LongPathsEnabled, the LongPaths feature is {0}. diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index c66ff4d3e53..a23802b41d9 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -43,6 +43,7 @@ using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; using SimpleErrorLogger = Microsoft.Build.Logging.SimpleErrorLogger.SimpleErrorLogger; using TerminalLogger = Microsoft.Build.Logging.TerminalLogger; +using static Microsoft.Build.CommandLine.CommandLineSwitches; #if NETFRAMEWORK // Use I/O operations from Microsoft.IO.Redist which is generally higher perf @@ -1927,9 +1928,6 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin return; } - // Determine if warnings should be emitted as messages (for builds with /warnaserror) - bool emitAsMessage = string.Equals(Traits.MSBuildLoggingArgsLevel, BinaryLogRecordKind.Message.ToString(), StringComparison.OrdinalIgnoreCase); - try { List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); @@ -1960,7 +1958,7 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin { foreach (string invalidArg in invalidArgs) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarUnsupportedArgument", invalidArg), emitAsMessage); + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarUnsupportedArgument", invalidArg), Traits.EmitAsLogsAsMessage); } } @@ -1972,7 +1970,7 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin } catch (Exception ex) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarError", ex.Message), emitAsMessage); + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarError", ex.ToString()), Traits.EmitAsLogsAsMessage); } } @@ -2125,17 +2123,23 @@ private static bool IsAllowedLoggingArg(string arg) return false; } - string switchPart = arg.Substring(GetLengthOfSwitchIndicator(arg)); + ReadOnlySpan switchPart = arg.AsSpan(GetLengthOfSwitchIndicator(arg)); // Extract switch name (before any ':' parameter indicator) int colonIndex = switchPart.IndexOf(':'); - string switchName = colonIndex >= 0 ? switchPart.Substring(0, colonIndex) : switchPart; - - return IsBinaryLoggerSwitch(switchName) || string.Equals(switchName, "check", StringComparison.OrdinalIgnoreCase); + ReadOnlySpan switchNameSpan = colonIndex >= 0 ? switchPart.Slice(0, colonIndex) : switchPart; + string switchName = switchNameSpan.ToString(); + + return IsParameterizedSwitch( + switchName, + out ParameterizedSwitch paramSwitch, + out _, + out _, + out _, + out _, + out _) && (paramSwitch == ParameterizedSwitch.BinaryLogger || paramSwitch == ParameterizedSwitch.Check); } - private static bool IsBinaryLoggerSwitch(string switchName) => string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase); - /// /// Logs a message from MSBUILD_LOGGING_ARGS processing. Messages are either emitted as warnings /// to the console or queued as low-importance build messages. From 5936c8c0226f075091aca34649d0876babcc2527 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 12 Jan 2026 11:40:58 +0100 Subject: [PATCH 08/12] fix using --- src/MSBuild/XMake.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index a23802b41d9..a4fdb135224 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -43,6 +43,7 @@ using LoggerDescription = Microsoft.Build.Logging.LoggerDescription; using SimpleErrorLogger = Microsoft.Build.Logging.SimpleErrorLogger.SimpleErrorLogger; using TerminalLogger = Microsoft.Build.Logging.TerminalLogger; + using static Microsoft.Build.CommandLine.CommandLineSwitches; #if NETFRAMEWORK From ad1eb72bd6afda1a56dd45da3a3494e3f2fda33c Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Mon, 12 Jan 2026 12:03:59 +0100 Subject: [PATCH 09/12] fix review comments --- src/MSBuild/XMake.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index a4fdb135224..4e00cef4025 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -36,6 +36,9 @@ using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; + +using static Microsoft.Build.CommandLine.CommandLineSwitches; + using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; using FileLogger = Microsoft.Build.Logging.FileLogger; @@ -44,8 +47,6 @@ using SimpleErrorLogger = Microsoft.Build.Logging.SimpleErrorLogger.SimpleErrorLogger; using TerminalLogger = Microsoft.Build.Logging.TerminalLogger; -using static Microsoft.Build.CommandLine.CommandLineSwitches; - #if NETFRAMEWORK // Use I/O operations from Microsoft.IO.Redist which is generally higher perf // and also works around https://github.com/dotnet/msbuild/issues/10540. From 3545027f30e9fe2eb9f8730471e9bd563a38ccc2 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Wed, 14 Jan 2026 13:33:20 +0100 Subject: [PATCH 10/12] fix review comments --- .../BackEnd/BuildManager/BuildManager.cs | 43 ++++++++++++++++++- src/Framework/Traits.cs | 9 +--- src/MSBuild/Resources/Strings.resx | 8 ++-- src/MSBuild/Resources/xlf/Strings.cs.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.de.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.es.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.fr.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.it.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.ja.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.ko.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.pl.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.ru.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.tr.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 12 +++--- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 12 +++--- src/MSBuild/XMake.cs | 28 +++++------- 17 files changed, 136 insertions(+), 108 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 7a70791f17b..6371a812af9 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -406,11 +406,23 @@ public readonly struct DeferredBuildMessage public string? FilePath { get; } + /// + /// If true, log as a warning; if false, log as a message. + /// + public bool IsWarning { get; } + + /// + /// Warning code (e.g., "MSB1070") if this is a warning. + /// + public string? WarningCode { get; } + public DeferredBuildMessage(string text, MessageImportance importance) { Importance = importance; Text = text; FilePath = null; + IsWarning = false; + WarningCode = null; } public DeferredBuildMessage(string text, MessageImportance importance, string filePath) @@ -418,6 +430,22 @@ public DeferredBuildMessage(string text, MessageImportance importance, string fi Importance = importance; Text = text; FilePath = filePath; + IsWarning = false; + WarningCode = null; + } + + /// + /// Creates a deferred warning message. + /// + /// The warning message text. + /// The warning code (e.g., "MSB1070"). + public DeferredBuildMessage(string text, string warningCode) + { + Importance = MessageImportance.Normal; + Text = text; + FilePath = null; + IsWarning = true; + WarningCode = warningCode; } } @@ -3166,7 +3194,20 @@ private static void LogDeferredMessages(ILoggingService loggingService, IEnumera foreach (var message in deferredBuildMessages) { - loggingService.LogCommentFromText(BuildEventContext.Invalid, message.Importance, message.Text); + if (message.IsWarning) + { + loggingService.LogWarningFromText( + BuildEventContext.Invalid, + subcategoryResourceName: null, + warningCode: message.WarningCode, + helpKeyword: null, + file: BuildEventFileInfo.Empty, + message: message.Text); + } + else + { + loggingService.LogCommentFromText(BuildEventContext.Invalid, message.Importance, message.Text); + } // If message includes a file path, include that file if (message.FilePath is not null) diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index c211f8c1607..91a83847fc9 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -155,14 +155,7 @@ public Traits() /// /// Gets if the logging level for MSBUILD_LOGGING_ARGS diagnostic is message. /// - public static bool EmitAsLogsAsMessage - { - get - { - string? level = Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName); - return string.Equals(level, "message", StringComparison.OrdinalIgnoreCase); - } - } + public readonly bool EmitAsLogsAsMessage = string.Equals(Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName), "message", StringComparison.OrdinalIgnoreCase); public readonly bool DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); public readonly bool DebugScheduler; diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index fdabad55b8f..6e75cc0db7e 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1710,12 +1710,12 @@ LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is a string with the command-line arguments from the environment variable. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index f70bc3cf2db..4c5a6b3efa8 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -336,14 +336,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index c2cd937d34c..cadcfd4dd48 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -336,14 +336,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index b452e2d83da..cf14afb978a 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -335,14 +335,14 @@ Esta marca es experimental y puede que no funcione según lo previsto. - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index a240cb9cc17..96f4aa0130a 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -336,14 +336,14 @@ futures - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 4ebfef7a0ee..0c7056c3663 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -336,14 +336,14 @@ Questo flag è sperimentale e potrebbe non funzionare come previsto. - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index ccdaa7dd386..3c818d6fbb2 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -336,14 +336,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 15b5952483c..411c1d95294 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -337,14 +337,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 710e3001d42..5ce6bbfc336 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -335,14 +335,14 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 336e2e69048..1bcf3e21a22 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -335,14 +335,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index ead571a1cf4..4d1ae14ff24 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -335,14 +335,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 8a299d18422..3b8cdeb76ff 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -335,14 +335,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 4712ae67562..11864fab049 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -336,14 +336,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index db2a84f698d..3148a590c4d 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -336,14 +336,14 @@ - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - MSBUILD : error MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} - {StrBegin="MSBUILD : error MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + MSB1071: Error processing MSBUILD_LOGGING_ARGS environment variable: {0} + {StrBegin="MSB1071: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the error message. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - MSBUILD : warning MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. - {StrBegin="MSBUILD : warning MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + MSB1070: MSBUILD_LOGGING_ARGS: Ignoring unsupported argument '{0}'. Only -bl and -check arguments are allowed. + {StrBegin="MSB1070: "}LOCALIZATION: "MSBUILD_LOGGING_ARGS" should not be localized. {0} is the unsupported argument. Using arguments from MSBUILD_LOGGING_ARGS environment variable: {0} diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 4e00cef4025..81382c549ca 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1960,19 +1960,19 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin { foreach (string invalidArg in invalidArgs) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarUnsupportedArgument", invalidArg), Traits.EmitAsLogsAsMessage); + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg), warningCode, Traits.Instance.EmitAsLogsAsMessage); } } if (validArgs.Count > 0) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), emitAsMessage: true); + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), warningCode: string.Empty, emitAsMessage: true); GatherCommandLineSwitches(validArgs, switches, commandLine); } } catch (Exception ex) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("LoggingArgsEnvVarError", ex.ToString()), Traits.EmitAsLogsAsMessage); + LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()), errorCode, Traits.Instance.EmitAsLogsAsMessage); } } @@ -2143,23 +2143,17 @@ private static bool IsAllowedLoggingArg(string arg) } /// - /// Logs a message from MSBUILD_LOGGING_ARGS processing. Messages are either emitted as warnings - /// to the console or queued as low-importance build messages. + /// Logs a message from MSBUILD_LOGGING_ARGS processing. Messages are either emitted as deferred warnings + /// or queued as low-importance build messages. /// /// The message to log. - /// If true, emit as low-importance message; if false, emit as warning to console. - private static void LogLoggingArgsMessage(string message, bool emitAsMessage) + /// The warning code when emitting as a warning. + /// If true, emit as low-importance message; if false, emit as warning. + private static void LogLoggingArgsMessage(string message, string warningCode, bool emitAsMessage) { - if (emitAsMessage) - { - // Queue as a low-importance message to be logged when the build starts - s_globalMessagesToLogInBuildLoggers.Add(new BuildManager.DeferredBuildMessage(message, MessageImportance.Low)); - } - else - { - // Emit as warning to console immediately - Console.WriteLine(message); - } + s_globalMessagesToLogInBuildLoggers.Add(emitAsMessage + ? new BuildManager.DeferredBuildMessage(message, MessageImportance.Low) + : new BuildManager.DeferredBuildMessage(message, warningCode)); } /// From 2a866d2d0b24f230ec894f285aa540b7c17ce471 Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Thu, 15 Jan 2026 16:55:54 +0100 Subject: [PATCH 11/12] fix review comments --- .../BackEnd/BuildManager/BuildManager.cs | 38 +++--- src/MSBuild/XMake.cs | 127 ++++++------------ 2 files changed, 66 insertions(+), 99 deletions(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 05d4aaa829f..c909be660fc 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -396,6 +396,16 @@ public static BuildManager DefaultBuildManager /// LegacyThreadingData IBuildComponentHost.LegacyThreadingData => _legacyThreadingData; + /// + /// Enumeration describing the severity of a deferred build message. + /// + public enum DeferredBuildMessageSeverity + { + Message = 1, + Warning, + Error + } + /// /// /// @@ -407,23 +417,19 @@ public readonly struct DeferredBuildMessage public string? FilePath { get; } - /// - /// If true, log as a warning; if false, log as a message. - /// - public bool IsWarning { get; } + public DeferredBuildMessageSeverity MessageSeverity { get; } /// - /// Warning code (e.g., "MSB1070") if this is a warning. + /// Build event code (e.g., "MSB1070"). /// - public string? WarningCode { get; } + public string? Code { get; } public DeferredBuildMessage(string text, MessageImportance importance) { Importance = importance; Text = text; FilePath = null; - IsWarning = false; - WarningCode = null; + Code = null; } public DeferredBuildMessage(string text, MessageImportance importance, string filePath) @@ -431,22 +437,22 @@ public DeferredBuildMessage(string text, MessageImportance importance, string fi Importance = importance; Text = text; FilePath = filePath; - IsWarning = false; - WarningCode = null; + Code = null; } /// /// Creates a deferred warning message. /// /// The warning message text. - /// The warning code (e.g., "MSB1070"). - public DeferredBuildMessage(string text, string warningCode) + /// The build message code (e.g., "MSB1070"). + /// The severity of the deferred build message. + public DeferredBuildMessage(string text, string code, DeferredBuildMessageSeverity messageSeverity) { Importance = MessageImportance.Normal; Text = text; FilePath = null; - IsWarning = true; - WarningCode = warningCode; + Code = code; + MessageSeverity = messageSeverity; } } @@ -3182,12 +3188,12 @@ private static void LogDeferredMessages(ILoggingService loggingService, IEnumera foreach (var message in deferredBuildMessages) { - if (message.IsWarning) + if (message.MessageSeverity is DeferredBuildMessageSeverity.Warning) { loggingService.LogWarningFromText( BuildEventContext.Invalid, subcategoryResourceName: null, - warningCode: message.WarningCode, + warningCode: message.Code, helpKeyword: null, file: BuildEventFileInfo.Empty, message: message.Text); diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 97c062c432f..12cc6acb393 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -26,19 +26,18 @@ using Microsoft.Build.Execution; using Microsoft.Build.Experimental; using Microsoft.Build.Experimental.BuildCheck; -using Microsoft.Build.ProjectCache; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Logging; +using Microsoft.Build.ProjectCache; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; - using static Microsoft.Build.CommandLine.CommandLineSwitches; - +using static Microsoft.Build.Execution.BuildManager; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; using FileLogger = Microsoft.Build.Logging.FileLogger; @@ -48,6 +47,7 @@ using TerminalLogger = Microsoft.Build.Logging.TerminalLogger; #if NETFRAMEWORK +using System.Linq.Expressions; // Use I/O operations from Microsoft.IO.Redist which is generally higher perf // and also works around https://github.com/dotnet/msbuild/issues/10540. // Unnecessary on .NET 6+ because the perf improvements are in-box there. @@ -56,7 +56,6 @@ using File = Microsoft.IO.File; using FileInfo = Microsoft.IO.FileInfo; using Path = Microsoft.IO.Path; -using System.Linq.Expressions; #endif #nullable disable @@ -1256,7 +1255,7 @@ private static void ResetGatheringSwitchesState() /// /// List of messages to be sent to the logger when it is attached /// - private static readonly List s_globalMessagesToLogInBuildLoggers = new(); + private static readonly List s_globalMessagesToLogInBuildLoggers = new(); /// /// The original console output mode if we changed it as part of initialization. @@ -1550,7 +1549,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) parameters.EnableRarNode = true; } - List messagesToLogInBuildLoggers = new(); + List messagesToLogInBuildLoggers = new(); BuildManager buildManager = BuildManager.DefaultBuildManager; @@ -1571,7 +1570,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) foreach (var responseFilePath in s_includedResponseFiles) { messagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage( + new DeferredBuildMessage( ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "PickedUpSwitchesFromAutoResponse", responseFilePath), @@ -1759,72 +1758,44 @@ private static bool PrintTargets(string projectFile, string toolsVersion, Dictio } } - private static List GetMessagesToLogInBuildLoggers(string commandLineString) + private static List GetMessagesToLogInBuildLoggers(string commandLineString) { - List messages = new(s_globalMessagesToLogInBuildLoggers) - { - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "Process", - EnvironmentUtilities.ProcessPath ?? string.Empty), - MessageImportance.Low), - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "MSBExePath", - BuildEnvironmentHelper.Instance.CurrentMSBuildExePath), - MessageImportance.Low), - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "CommandLine", - commandLineString), - MessageImportance.Low), - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "CurrentDirectory", - Environment.CurrentDirectory), - MessageImportance.Low), - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "MSBVersion", - ProjectCollection.DisplayVersion), - MessageImportance.Low), - }; + var messages = new List(s_globalMessagesToLogInBuildLoggers); + + AddMessage(messages, "Process", EnvironmentUtilities.ProcessPath ?? string.Empty); + AddMessage(messages, "MSBExePath", BuildEnvironmentHelper.Instance.CurrentMSBuildExePath); + AddMessage(messages, "CommandLine", commandLineString); + AddMessage(messages, "CurrentDirectory", Environment.CurrentDirectory); + AddMessage(messages, "MSBVersion", ProjectCollection.DisplayVersion); - NativeMethodsShared.LongPathsStatus longPaths = NativeMethodsShared.IsLongPathsEnabled(); - if (longPaths != NativeMethodsShared.LongPathsStatus.NotApplicable) + var longPathsStatus = NativeMethodsShared.IsLongPathsEnabled(); + if (longPathsStatus != NativeMethodsShared.LongPathsStatus.NotApplicable) { - messages.Add( - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "LongPaths", - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - $"LongPaths_{longPaths}")), - MessageImportance.Low)); + string statusResource = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword($"LongPaths_{longPathsStatus}"); + AddMessage(messages, "LongPaths", statusResource); } - NativeMethodsShared.SAC_State SAC_State = NativeMethodsShared.GetSACState(); - if (SAC_State != NativeMethodsShared.SAC_State.NotApplicable && SAC_State != NativeMethodsShared.SAC_State.Missing) + var sacState = NativeMethodsShared.GetSACState(); + if (sacState is not (NativeMethodsShared.SAC_State.NotApplicable or NativeMethodsShared.SAC_State.Missing)) { - messages.Add( - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "SAC", - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - $"SAC_{SAC_State}")), - MessageImportance.Low)); + string stateResource = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword($"SAC_{sacState}"); + AddMessage(messages, "SAC", stateResource); } if (Traits.Instance.DebugEngine) { - messages.Add( - new BuildManager.DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( - "MSBuildDebugPath", - DebugUtils.DebugPath), - MessageImportance.High)); + AddMessage(messages, "MSBuildDebugPath", DebugUtils.DebugPath, MessageImportance.High); } return messages; + + static void AddMessage( + List list, + string resourceName, + string arg, + MessageImportance importance = MessageImportance.Low) => list.Add(new DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, arg), + importance)); } private static BuildResult ExecuteBuild(BuildManager buildManager, BuildRequestData request) @@ -1930,6 +1901,8 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin return; } + DeferredBuildMessageSeverity messageSeverity = Traits.Instance.EmitAsLogsAsMessage ? DeferredBuildMessageSeverity.Message : DeferredBuildMessageSeverity.Warning; + try { List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); @@ -1960,19 +1933,21 @@ internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLin { foreach (string invalidArg in invalidArgs) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg), warningCode, Traits.Instance.EmitAsLogsAsMessage); + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg); + s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(message, warningCode, messageSeverity)); } } if (validArgs.Count > 0) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), warningCode: string.Empty, emitAsMessage: true); + s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), MessageImportance.Low)); GatherCommandLineSwitches(validArgs, switches, commandLine); } } catch (Exception ex) { - LogLoggingArgsMessage(ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()), errorCode, Traits.Instance.EmitAsLogsAsMessage); + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()); + s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(message, errorCode, messageSeverity)); } } @@ -2145,20 +2120,6 @@ private static bool IsAllowedLoggingArg(string arg) out _) && (paramSwitch == ParameterizedSwitch.BinaryLogger || paramSwitch == ParameterizedSwitch.Check); } - /// - /// Logs a message from MSBUILD_LOGGING_ARGS processing. Messages are either emitted as deferred warnings - /// or queued as low-importance build messages. - /// - /// The message to log. - /// The warning code when emitting as a warning. - /// If true, emit as low-importance message; if false, emit as warning. - private static void LogLoggingArgsMessage(string message, string warningCode, bool emitAsMessage) - { - s_globalMessagesToLogInBuildLoggers.Add(emitAsMessage - ? new BuildManager.DeferredBuildMessage(message, MessageImportance.Low) - : new BuildManager.DeferredBuildMessage(message, warningCode)); - } - /// /// 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. @@ -2978,7 +2939,7 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (IsAutomatedEnvironment()) { s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedAutomated"), MessageImportance.Low)); + new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedAutomated"), MessageImportance.Low)); return false; } @@ -2987,7 +2948,7 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (!outputIsScreen) { s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedRedirected"), MessageImportance.Low)); + new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedRedirected"), MessageImportance.Low)); return false; } @@ -2995,14 +2956,14 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (!acceptAnsiColorCodes) { s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedNotSupported"), MessageImportance.Low)); + new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedNotSupported"), MessageImportance.Low)); return false; } if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) { s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedDisabled"), MessageImportance.Low)); + new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedDisabled"), MessageImportance.Low)); return false; } @@ -3077,7 +3038,7 @@ bool TryFromEnvironmentVariables() if (!string.IsNullOrEmpty(terminalLoggerArg)) { s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage($"The environment variable MSBUILDTERMINALLOGGER was set to {terminalLoggerArg}.", MessageImportance.Low)); + new DeferredBuildMessage($"The environment variable MSBUILDTERMINALLOGGER was set to {terminalLoggerArg}.", MessageImportance.Low)); KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntent = terminalLoggerArg; KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntentSource = "MSBUILDTERMINALLOGGER"; @@ -3086,7 +3047,7 @@ bool TryFromEnvironmentVariables() { terminalLoggerArg = liveLoggerArg; s_globalMessagesToLogInBuildLoggers.Add( - new BuildManager.DeferredBuildMessage($"The environment variable MSBUILDLIVELOGGER was set to {liveLoggerArg}.", MessageImportance.Low)); + new DeferredBuildMessage($"The environment variable MSBUILDLIVELOGGER was set to {liveLoggerArg}.", MessageImportance.Low)); KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntent = terminalLoggerArg; KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntentSource = "MSBUILDLIVELOGGER"; From 5c273d7111d4a4eae67578a7f90458dd491e691e Mon Sep 17 00:00:00 2001 From: YuliiaKovalova Date: Fri, 16 Jan 2026 16:37:13 +0100 Subject: [PATCH 12/12] fix review comments --- src/Build.UnitTests/Utilities_Tests.cs | 10 +- .../BackEnd/BuildManager/BuildManager.cs | 2 +- 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 - src/Framework/Traits.cs | 2 +- .../CommandLineParserTests.cs | 39 + .../CommandLineSwitches_Tests.cs | 76 +- .../ProjectSchemaValidationHandler_Tests.cs | 10 +- .../XMake_BinlogSwitch_Tests.cs | 11 +- src/MSBuild.UnitTests/XMake_Tests.cs | 166 ++-- src/MSBuild/AssemblyInfo.cs | 3 + src/MSBuild/CommandLine/CommandLineParser.cs | 741 ++++++++++++++ .../CommandLineSwitchException.cs | 2 +- .../{ => CommandLine}/CommandLineSwitches.cs | 8 +- .../CommandLineSwitchesAccessor.cs | 163 +++ src/MSBuild/MSBuild.csproj | 8 +- src/MSBuild/MSBuildClientApp.cs | 25 +- src/MSBuild/XMake.cs | 935 ++---------------- 21 files changed, 1271 insertions(+), 1016 deletions(-) create mode 100644 src/MSBuild.UnitTests/CommandLineParserTests.cs create mode 100644 src/MSBuild/CommandLine/CommandLineParser.cs rename src/MSBuild/{ => CommandLine}/CommandLineSwitchException.cs (99%) rename src/MSBuild/{ => CommandLine}/CommandLineSwitches.cs (99%) create mode 100644 src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs diff --git a/src/Build.UnitTests/Utilities_Tests.cs b/src/Build.UnitTests/Utilities_Tests.cs index a29466e852c..880fbe49e59 100644 --- a/src/Build.UnitTests/Utilities_Tests.cs +++ b/src/Build.UnitTests/Utilities_Tests.cs @@ -80,17 +80,9 @@ public void CommentsInPreprocessing() env.SetEnvironmentVariable("MSBUILDLOADALLFILESASWRITEABLE", "1"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe """ + inputFile.Path + - (NativeMethodsShared.IsUnixLike ? @""" -pp:""" : @""" /pp:""") + outputFile.Path + @"""") - .ShouldBe(MSBuildApp.ExitType.Success); -#else Assert.Equal( MSBuildApp.ExitType.Success, - MSBuildApp.Execute( - new[] { @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', - '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'})); -#endif + MSBuildApp.Execute([ @"c:\bin\msbuild.exe", '"' + inputFile.Path + '"', '"' + (NativeMethodsShared.IsUnixLike ? "-pp:" : "/pp:") + outputFile.Path + '"'])); bool foundDoNotModify = false; foreach (string line in File.ReadLines(outputFile.Path)) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c909be660fc..02b2799a972 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -417,7 +417,7 @@ public readonly struct DeferredBuildMessage public string? FilePath { get; } - public DeferredBuildMessageSeverity MessageSeverity { get; } + public DeferredBuildMessageSeverity MessageSeverity { get; } = DeferredBuildMessageSeverity.Message; /// /// Build event code (e.g., "MSB1070"). diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 9bd05788271..e1df9945e91 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -48,11 +48,7 @@ public sealed class MSBuildClient /// The command line to process. /// The first argument on the command line is assumed to be the name/path of the executable, and is ignored. /// -#if FEATURE_GET_COMMANDLINE - private readonly string _commandLine; -#else private readonly string[] _commandLine; -#endif /// /// The MSBuild client execution result. @@ -112,13 +108,7 @@ public sealed class MSBuildClient /// on the command line is assumed to be the name/path of the executable, and is ignored /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. - public MSBuildClient( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation) + public MSBuildClient(string[] commandLine, string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); @@ -162,12 +152,7 @@ private void CreateNodePipeStream() public MSBuildClientExitResult Execute(CancellationToken cancellationToken) { // Command line in one string used only in human readable content. - string descriptiveCommandLine = -#if FEATURE_GET_COMMANDLINE - _commandLine; -#else - string.Join(" ", _commandLine); -#endif + string descriptiveCommandLine = string.Join(" ", _commandLine); CommunicationsUtilities.Trace("Executing build with command line '{0}'", descriptiveCommandLine); diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index e5b1e76f412..49ed1d610dd 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -25,12 +25,7 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// /// A callback used to execute command line build. /// - public delegate (int exitCode, string exitType) BuildCallback( -#if FEATURE_GET_COMMANDLINE - string commandLine); -#else - string[] commandLine); -#endif + public delegate (int exitCode, string exitType) BuildCallback(string[] commandLine); private readonly BuildCallback _buildFunction; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index fc5bca7e920..ab067c7d4ad 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -14,11 +14,7 @@ namespace Microsoft.Build.BackEnd /// internal sealed class ServerNodeBuildCommand : INodePacket { -#if FEATURE_GET_COMMANDLINE - private string _commandLine = default!; -#else private string[] _commandLine = default!; -#endif private string _startupDirectory = default!; private Dictionary _buildProcessEnvironment = default!; private CultureInfo _culture = default!; @@ -34,11 +30,7 @@ internal sealed class ServerNodeBuildCommand : INodePacket /// /// Command line including arguments /// -#if FEATURE_GET_COMMANDLINE - public string CommandLine => _commandLine; -#else public string[] CommandLine => _commandLine; -#endif /// /// The startup directory @@ -79,11 +71,7 @@ private ServerNodeBuildCommand() } public ServerNodeBuildCommand( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else string[] commandLine, -#endif string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, diff --git a/src/Build/CompatibilitySuppressions.xml b/src/Build/CompatibilitySuppressions.xml index 0497d618a92..e1a53b3ea2e 100644 --- a/src/Build/CompatibilitySuppressions.xml +++ b/src/Build/CompatibilitySuppressions.xml @@ -1,3 +1,48 @@  - \ No newline at end of file + + + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + lib/net472/Microsoft.Build.dll + lib/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.MSBuildClient.#ctor(System.String,System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.BeginInvoke(System.String,System.AsyncCallback,System.Object) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + + CP0002 + M:Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback.Invoke(System.String) + ref/net472/Microsoft.Build.dll + ref/net472/Microsoft.Build.dll + true + + diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 73b91fe6c96..db68730447e 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -31,7 +31,6 @@ $(DefineConstants);FEATURE_ENVIRONMENT_SYSTEMDIRECTORY $(DefineConstants);FEATURE_FILE_TRACKER $(DefineConstants);FEATURE_GAC - $(DefineConstants);FEATURE_GET_COMMANDLINE $(DefineConstants);FEATURE_HANDLEPROCESSCORRUPTEDSTATEEXCEPTIONS $(DefineConstants);FEATURE_HTTP_LISTENER $(DefineConstants);FEATURE_INSTALLED_MSBUILD diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 24b80e25dcc..f14208e145a 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -155,7 +155,7 @@ public Traits() /// /// Gets if the logging level for MSBUILD_LOGGING_ARGS diagnostic is message. /// - public readonly bool EmitAsLogsAsMessage = string.Equals(Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName), "message", StringComparison.OrdinalIgnoreCase); + public readonly bool EmitLogsAsMessage = string.Equals(Environment.GetEnvironmentVariable(MSBuildLoggingArgsLevelEnvVarName), "message", StringComparison.OrdinalIgnoreCase); public readonly bool DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); public readonly bool DebugScheduler; diff --git a/src/MSBuild.UnitTests/CommandLineParserTests.cs b/src/MSBuild.UnitTests/CommandLineParserTests.cs new file mode 100644 index 00000000000..cb5280d913c --- /dev/null +++ b/src/MSBuild.UnitTests/CommandLineParserTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.CommandLine.Experimental; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.CommandLine.UnitTests +{ + public class CommandLineParserTests + { + [Fact] + public void ParseReturnsInstance() + { + CommandLineParser parser = new CommandLineParser(); + CommandLineSwitchesAccessor result = parser.Parse(["/targets:targets.txt"]); // first parameter must be the executable name + + result.Targets.ShouldNotBeNull(); + result.Targets.ShouldBe(["targets.txt"]); + } + + [Fact] + public void ParseThrowsException() + { + CommandLineParser parser = new CommandLineParser(); + + Should.Throw(() => + { + // first parameter must be the executable name + parser.Parse(["tempproject.proj", "tempproject.proj"]); + }); + } + } +} diff --git a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs index a2de7a8fb1c..7cbf86ce493 100644 --- a/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs +++ b/src/MSBuild.UnitTests/CommandLineSwitches_Tests.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Resources; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Construction; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -622,7 +623,9 @@ public void FeatureAvailibilitySwitchIdentificationTest(string switchName) public void TargetsSwitchParameter() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets:targets.txt" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets:targets.txt"], switches); switches.HaveErrors().ShouldBeFalse(); switches[CommandLineSwitches.ParameterizedSwitch.Targets].ShouldBe(new[] { "targets.txt" }); @@ -632,7 +635,9 @@ public void TargetsSwitchParameter() public void TargetsSwitchDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/targets /targets" }, switches); + CommandLineParser parser = new CommandLineParser(); + + parser.GatherCommandLineSwitches(["/targets /targets"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -709,8 +714,9 @@ public void LowPrioritySwitchIdentificationTests(string lowpriority) public void GraphBuildSwitchCanHaveParameters() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph", "/graph:true; NoBuild ;; ;", "/graph:foo" }, switches); + parser.GatherCommandLineSwitches(["/graph", "/graph:true; NoBuild ;; ;", "/graph:foo"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(new[] { "true", " NoBuild ", " ", "foo" }); @@ -721,8 +727,9 @@ public void GraphBuildSwitchCanHaveParameters() public void GraphBuildSwitchCanBeParameterless() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List { "/graph" }, switches); + parser.GatherCommandLineSwitches(["/graph"], switches); switches[CommandLineSwitches.ParameterizedSwitch.GraphBuild].ShouldBe(Array.Empty()); @@ -733,8 +740,9 @@ public void GraphBuildSwitchCanBeParameterless() public void InputResultsCachesSupportsMultipleOccurrence() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/irc", "/irc:a;b", "/irc:c;d" }, switches); + parser.GatherCommandLineSwitches(["/irc", "/irc:a;b", "/irc:c;d"], switches); switches[CommandLineSwitches.ParameterizedSwitch.InputResultsCaches].ShouldBe(new[] { null, "a", "b", "c", "d" }); @@ -745,8 +753,9 @@ public void InputResultsCachesSupportsMultipleOccurrence() public void OutputResultsCache() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a" }, switches); + parser.GatherCommandLineSwitches(["/orc:a"], switches); switches[CommandLineSwitches.ParameterizedSwitch.OutputResultsCache].ShouldBe(new[] { "a" }); @@ -757,8 +766,9 @@ public void OutputResultsCache() public void OutputResultsCachesDoesNotSupportMultipleOccurrences() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List() { "/orc:a", "/orc:b" }, switches); + parser.GatherCommandLineSwitches(["/orc:a", "/orc:b"], switches); switches.HaveErrors().ShouldBeTrue(); } @@ -1288,8 +1298,9 @@ public void ExtractAnyLoggerParameterPickLast() public void ProcessWarnAsErrorSwitchNotSpecified() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "" }), commandLineSwitches); + parser.GatherCommandLineSwitches([""], commandLineSwitches); Assert.Null(MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches)); } @@ -1303,16 +1314,17 @@ public void ProcessWarnAsErrorSwitchWithCodes() ISet expectedWarningsAsErrors = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnaserror: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnaserror:A,b,C", // Repeats of different case "\"/warnaserror:, ,,\"", // Empty items "/err:D,d;E,e", // A different source with new items and uses the short form "/warnaserror:a", // A different source with a single duplicate "/warnaserror:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1328,12 +1340,13 @@ public void ProcessWarnAsErrorSwitchWithCodes() public void ProcessWarnAsErrorSwitchEmptySwitchClearsSet() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1351,13 +1364,14 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() ISet expectedWarningsAsErors = new HashSet(StringComparer.OrdinalIgnoreCase) { "e", "f", "g" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "/warnaserror:a;b;c", "/warnaserror", "/warnaserror:e;f;g", - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1373,8 +1387,9 @@ public void ProcessWarnAsErrorSwitchValuesAfterEmptyAddOn() public void ProcessWarnAsErrorSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnaserror" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/warnaserror"], commandLineSwitches); ISet actualWarningsAsErrors = MSBuildApp.ProcessWarnAsErrorSwitch(commandLineSwitches); @@ -1390,10 +1405,11 @@ public void ProcessWarnAsErrorSwitchEmpty() public void ProcessWarnAsMessageSwitchEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); // Set "expanded" content to match the placeholder so the verify can use the exact resource string as "expected." string command = "{0}"; - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/warnasmessage" }), commandLineSwitches, command); + parser.GatherCommandLineSwitches(["/warnasmessage"], commandLineSwitches, command); VerifySwitchError(commandLineSwitches, "/warnasmessage", AssemblyResources.GetString("MissingWarnAsMessageParameterError")); } @@ -1410,13 +1426,15 @@ public void ProcessEnvironmentVariableSwitch() env.SetEnvironmentVariable("ENVIRONMENTVARIABLE", string.Empty); CommandLineSwitches commandLineSwitches = new(); + CommandLineParser parser = new CommandLineParser(); + string fullCommandLine = "msbuild validProject.csproj %ENVIRONMENTVARIABLE%"; - MSBuildApp.GatherCommandLineSwitches(new List() { "validProject.csproj", "%ENVIRONMENTVARIABLE%" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["validProject.csproj", "%ENVIRONMENTVARIABLE%"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); commandLineSwitches = new(); fullCommandLine = "msbuild %ENVIRONMENTVARIABLE% validProject.csproj"; - MSBuildApp.GatherCommandLineSwitches(new List() { "%ENVIRONMENTVARIABLE%", "validProject.csproj" }, commandLineSwitches, fullCommandLine); + parser.GatherCommandLineSwitches(["%ENVIRONMENTVARIABLE%", "validProject.csproj"], commandLineSwitches, fullCommandLine); VerifySwitchError(commandLineSwitches, "%ENVIRONMENTVARIABLE%", String.Format(AssemblyResources.GetString("EnvironmentVariableAsSwitch"), fullCommandLine)); } } @@ -1430,16 +1448,17 @@ public void ProcessWarnAsMessageSwitchWithCodes() ISet expectedWarningsAsMessages = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "B", "c", "D", "e" }; CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] - { + parser.GatherCommandLineSwitches( + [ "\"/warnasmessage: a,B ; c \"", // Leading, trailing, leading and trailing whitespace "/warnasmessage:A,b,C", // Repeats of different case "\"/warnasmessage:, ,,\"", // Empty items "/nowarn:D,d;E,e", // A different source with new items and uses the short form "/warnasmessage:a", // A different source with a single duplicate "/warnasmessage:a,b", // A different source with multiple duplicates - }), commandLineSwitches); + ], commandLineSwitches); ISet actualWarningsAsMessages = MSBuildApp.ProcessWarnAsMessageSwitch(commandLineSwitches); @@ -1455,8 +1474,9 @@ public void ProcessWarnAsMessageSwitchWithCodes() public void ProcessProfileEvaluationEmpty() { CommandLineSwitches commandLineSwitches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - MSBuildApp.GatherCommandLineSwitches(new List(new[] { "/profileevaluation" }), commandLineSwitches); + parser.GatherCommandLineSwitches(["/profileevaluation"], commandLineSwitches); commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ProfileEvaluation][0].ShouldBe("no-file"); } @@ -1548,11 +1568,7 @@ public void ProcessInvalidTargetSwitch() using TestEnvironment testEnvironment = TestEnvironment.Create(); string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"msbuild.exe " + project + " /t:foo.bar").ShouldBe(MSBuildApp.ExitType.SwitchError); -#else - MSBuildApp.Execute(new[] { @"msbuild.exe", project, "/t:foo.bar" }).ShouldBe(MSBuildApp.ExitType.SwitchError); -#endif + MSBuildApp.Execute([@"msbuild.exe", project, "/t:foo.bar"]).ShouldBe(MSBuildApp.ExitType.SwitchError); } /// diff --git a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs index 7a224860a2f..82fa56a588c 100644 --- a/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs +++ b/src/MSBuild.UnitTests/ProjectSchemaValidationHandler_Tests.cs @@ -52,7 +52,7 @@ public void VerifyInvalidProjectSchema() "); string quotedProjectFilename = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFilename + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFilename, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); } finally { @@ -95,7 +95,7 @@ public void VerifyInvalidSchemaItself1() "); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -155,7 +155,7 @@ public void VerifyInvalidSchemaItself2() string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + invalidSchemaFile + "\"")); + Assert.Equal(MSBuildApp.ExitType.InitializationError, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{invalidSchemaFile}\""])); } finally { @@ -203,7 +203,7 @@ public void VerifyValidProjectSchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( @@ -256,7 +256,7 @@ public void VerifyInvalidImportNotCaughtBySchema() msbuildTempXsdFilenames = PrepareSchemaFiles(); string quotedProjectFile = "\"" + projectFilename + "\""; - Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute(@"c:\foo\msbuild.exe " + quotedProjectFile + " /validate:\"" + msbuildTempXsdFilenames[0] + "\"")); + Assert.Equal(MSBuildApp.ExitType.Success, MSBuildApp.Execute([@"c:\foo\msbuild.exe", quotedProjectFile, $"/validate:\"{msbuildTempXsdFilenames[0]}\""])); // ProjectSchemaValidationHandler.VerifyProjectSchema // ( diff --git a/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs index 35e72e578e2..af764f80e36 100644 --- a/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_BinlogSwitch_Tests.cs @@ -2,9 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; -using Microsoft.Build.CommandLine; -using Microsoft.Build.Shared; +using Microsoft.Build.CommandLine.Experimental; +using Microsoft.Build.Execution; using Microsoft.Build.UnitTests.Shared; using Shouldly; using Xunit; @@ -255,10 +256,12 @@ public void LoggingArgsEnvVarAllowsCheckSwitch() [InlineData("/check")] public void LoggingArgsEnvVarAllowedSwitches(string switchArg) { - CommandLineSwitches switches = new(); + CommandLineParser parser = new(); _ = _env.SetEnvironmentVariable("MSBUILD_LOGGING_ARGS", switchArg); - MSBuildApp.GatherLoggingArgsEnvironmentVariableSwitches(ref switches, "test"); + CommandLineSwitches switches = new(); + List deferredBuildMessages = new(); + parser.GatherLoggingArgsEnvironmentVariableSwitches(ref switches, deferredBuildMessages, "test"); switches.HaveErrors().ShouldBeFalse($"Switch {switchArg} should be allowed"); } diff --git a/src/MSBuild.UnitTests/XMake_Tests.cs b/src/MSBuild.UnitTests/XMake_Tests.cs index 9ed990fc17c..8cdd6c467a9 100644 --- a/src/MSBuild.UnitTests/XMake_Tests.cs +++ b/src/MSBuild.UnitTests/XMake_Tests.cs @@ -14,6 +14,7 @@ using System.Threading; using System.Xml.Linq; using Microsoft.Build.CommandLine; +using Microsoft.Build.CommandLine.Experimental; using Microsoft.Build.Evaluation; using Microsoft.Build.Framework; using Microsoft.Build.Logging; @@ -96,11 +97,9 @@ public XMakeAppTests(ITestOutputHelper output) public void GatherCommandLineSwitchesTwoProperties() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/p:a=b", "/p:c=d" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/p:a=b", "/p:c=d"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -111,13 +110,9 @@ public void GatherCommandLineSwitchesTwoProperties() public void GatherCommandLineSwitchesAnyDash() { var switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List { - "-p:a=b", - "--p:maxcpucount=8" - }; - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["-p:a=b", "--p:maxcpucount=8"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.Property]; parameters[0].ShouldBe("a=b"); @@ -128,11 +123,9 @@ public void GatherCommandLineSwitchesAnyDash() public void GatherCommandLineSwitchesMaxCpuCountWithArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:2" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:2"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[0].ShouldBe("2"); @@ -145,11 +138,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:3", "/m" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:3", "/m"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters[1].ShouldBe(Convert.ToString(NativeMethodsShared.GetLogicalCoreCount())); @@ -165,11 +156,9 @@ public void GatherCommandLineSwitchesMaxCpuCountWithoutArgument() public void GatherCommandLineSwitchesMaxCpuCountWithoutArgumentButWithColon() { CommandLineSwitches switches = new CommandLineSwitches(); + CommandLineParser parser = new CommandLineParser(); - var arguments = new List(); - arguments.AddRange(new[] { "/m:" }); - - MSBuildApp.GatherCommandLineSwitches(arguments, switches); + parser.GatherCommandLineSwitches(["/m:"], switches); string[] parameters = switches[CommandLineSwitches.ParameterizedSwitch.MaxCPUCount]; parameters.Length.ShouldBe(0); @@ -459,44 +448,44 @@ public void ExtractSwitchParametersTest() { string commandLineArg = "\"/p:foo=\"bar"; string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "/p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); // this test is totally unreal -- we'd never attempt to extract switch parameters if the leading character is not a // switch indicator (either '-' or '/') -- here the leading character is a double-quote commandLineArg = "\"\"\"/p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "/p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"/pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "/p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 1).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -505,37 +494,37 @@ public void ExtractSwitchParametersTestDoubleDash() { var commandLineArg = "\"--p:foo=\"bar"; var unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo=\"bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(2); commandLineArg = "--p:foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(0); commandLineArg = "\"\"--p:foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar\""); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"operty\":foo=bar"; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(3); commandLineArg = "\"--pr\"op\"\"erty\":foo=bar\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "property", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":foo=bar"); doubleQuotesRemovedFromArg.ShouldBe(6); commandLineArg = "--p:\"foo foo\"=\"bar bar\";\"baz=onga\""; unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out doubleQuotesRemovedFromArg); - MSBuildApp.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); + CommandLineParser.ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, "p", unquotedCommandLineArg.IndexOf(':'), 2).ShouldBe(":\"foo foo\"=\"bar bar\";\"baz=onga\""); doubleQuotesRemovedFromArg.ShouldBe(6); } @@ -548,11 +537,11 @@ public void GetLengthOfSwitchIndicatorTest() var commandLineSwitchWithNoneOrIncorrectIndicator = "zSwitch"; - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSlash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithSingleDash).ShouldBe(1); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithDoubleDash).ShouldBe(2); - MSBuildApp.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); + CommandLineParser.GetLengthOfSwitchIndicator(commandLineSwitchWithNoneOrIncorrectIndicator).ShouldBe(0); } [Theory] @@ -562,12 +551,7 @@ public void GetLengthOfSwitchIndicatorTest() [InlineData(@"/h")] public void Help(string indicator) { - MSBuildApp.Execute( -#if FEATURE_GET_COMMANDLINE - @$"c:\bin\msbuild.exe {indicator} ") -#else - new[] { @"c:\bin\msbuild.exe", indicator }) -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", indicator]) .ShouldBe(MSBuildApp.ExitType.Success); } @@ -660,19 +644,11 @@ public void VersionSwitchDisableChangeWave() public void ErrorCommandLine() { string oldValueForMSBuildLoadMicrosoftTargetsReadOnly = Environment.GetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly"); -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe -junk").ShouldBe(MSBuildApp.ExitType.SwitchError); - MSBuildApp.Execute(@"msbuild.exe -t").ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(@"msbuild.exe @bogus.rsp").ShouldBe(MSBuildApp.ExitType.InitializationError); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", "-junk" }).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"c:\bin\msbuild.exe", "-junk"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "-t"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + MSBuildApp.Execute([@"msbuild.exe", "@bogus.rsp"]).ShouldBe(MSBuildApp.ExitType.InitializationError); - MSBuildApp.Execute(new[] { @"msbuild.exe", "-t" }).ShouldBe(MSBuildApp.ExitType.SwitchError); - - MSBuildApp.Execute(new[] { @"msbuild.exe", "@bogus.rsp" }).ShouldBe(MSBuildApp.ExitType.InitializationError); -#endif Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", oldValueForMSBuildLoadMicrosoftTargetsReadOnly); } @@ -1185,11 +1161,7 @@ public void TestEnvironmentTest() sw.WriteLine(projectString); } // Should pass -#if FEATURE_GET_COMMANDLINE - MSBuildApp.Execute(@"c:\bin\msbuild.exe " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); -#else - MSBuildApp.Execute(new[] { @"c:\bin\msbuild.exe", quotedProjectFileName }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + MSBuildApp.Execute([@"c:\bin\msbuild.exe", quotedProjectFileName]).ShouldBe(MSBuildApp.ExitType.Success); } finally { @@ -1222,21 +1194,15 @@ public void MSBuildEngineLogger() { sw.WriteLine(projectString); } -#if FEATURE_GET_COMMANDLINE // Should pass - MSBuildApp.Execute(@$"c:\bin\msbuild.exe /logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}"" /verbosity:detailed " + quotedProjectFileName).ShouldBe(MSBuildApp.ExitType.Success); - -#else - // Should pass - MSBuildApp.Execute( - new[] - { + MSBuildApp + .Execute([ NativeMethodsShared.IsWindows ? @"c:\bin\msbuild.exe" : "/msbuild.exe", @$"/logger:FileLogger,""Microsoft.Build, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"";""LogFile={logFile}""", "/verbosity:detailed", - quotedProjectFileName - }).ShouldBe(MSBuildApp.ExitType.Success); -#endif + quotedProjectFileName]) + .ShouldBe(MSBuildApp.ExitType.Success); + File.Exists(logFile).ShouldBeTrue(); var logFileContents = File.ReadAllText(logFile); @@ -2294,6 +2260,44 @@ public void TestProcessFileLoggerSwitch5() distributedLoggerRecords.Count.ShouldBe(0); // "Expected no distributed loggers to be attached" loggers.Count.ShouldBe(0); // "Expected no central loggers to be attached" } + + /// + /// Verify that DistributedLoggerRecords with null CentralLogger don't cause exceptions when creating ProjectCollection + /// This is a regression test for the issue where -dfl flag caused MSB1025 error due to null logger not being filtered. + /// + [Fact] + public void TestNullCentralLoggerInDistributedLoggerRecord() + { + // Simulate the scenario when using -dfl flag + // ProcessDistributedFileLogger creates a DistributedLoggerRecord with null CentralLogger + var distributedLoggerRecords = new List(); + bool distributedFileLogger = true; + string[] fileLoggerParameters = null; + + MSBuildApp.ProcessDistributedFileLogger( + distributedFileLogger, + fileLoggerParameters, + distributedLoggerRecords); + + // Verify that we have a distributed logger record with null central logger + distributedLoggerRecords.Count.ShouldBe(1); + distributedLoggerRecords[0].CentralLogger.ShouldBeNull(); + + // This should not throw ArgumentNullException when creating ProjectCollection + // The fix filters out null central loggers from the evaluationLoggers array + var loggers = Array.Empty(); + Should.NotThrow(() => + { + using var projectCollection = new ProjectCollection( + new Dictionary(), + loggers: [.. loggers, .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not null)], + remoteLoggers: null, + toolsetDefinitionLocations: ToolsetDefinitionLocations.Default, + maxNodeCount: 1, + onlyLogCriticalEvents: false, + loadProjectsReadOnly: true); + }); + } #endregion #region ProcessConsoleLoggerSwitches @@ -3077,6 +3081,20 @@ public void TasksGetAssemblyLoadContexts() #endif + [Fact] + public void ThrowsWhenMaxCpuCountTooLargeForMultiThreadedAndForceAllTasksOutOfProc() + { + string projectContent = """ + + + """; + using TestEnvironment testEnvironment = TestEnvironment.Create(); + testEnvironment.SetEnvironmentVariable("MSBUILDFORCEALLTASKSOUTOFPROC", "1"); + string project = testEnvironment.CreateTestProjectWithFiles("project.proj", projectContent).ProjectFile; + + MSBuildApp.Execute([@"c:\bin\msbuild.exe", project, "/m:257 /mt"]).ShouldBe(MSBuildApp.ExitType.SwitchError); + } + private string CopyMSBuild() { string dest = null; diff --git a/src/MSBuild/AssemblyInfo.cs b/src/MSBuild/AssemblyInfo.cs index f93e8a6db00..c0407dd5a2d 100644 --- a/src/MSBuild/AssemblyInfo.cs +++ b/src/MSBuild/AssemblyInfo.cs @@ -11,6 +11,9 @@ [assembly: InternalsVisibleTo("Microsoft.Build.CommandLine.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] [assembly: InternalsVisibleTo("Microsoft.Build.Utilities.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010015c01ae1f50e8cc09ba9eac9147cf8fd9fce2cfe9f8dce4f7301c4132ca9fb50ce8cbf1df4dc18dd4d210e4345c744ecb3365ed327efdbc52603faa5e21daa11234c8c4a73e51f03bf192544581ebe107adee3a34928e39d04e524a9ce729d5090bfd7dad9d10c722c0def9ccc08ff0a03790e48bcd1f9b6c476063e1966a1c4")] +// Grant the dotnet CLI access to our command-line parsing logic, which it uses to parse MSBuild arguments. +[assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] + // This will enable passing the SafeDirectories flag to any P/Invoke calls/implementations within the assembly, // so that we don't run into known security issues with loading libraries from unsafe locations [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)] diff --git a/src/MSBuild/CommandLine/CommandLineParser.cs b/src/MSBuild/CommandLine/CommandLineParser.cs new file mode 100644 index 00000000000..913e2dff2f6 --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineParser.cs @@ -0,0 +1,741 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime; +using System.Security; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using static Microsoft.Build.CommandLine.Experimental.CommandLineSwitches; +using static Microsoft.Build.Execution.BuildManager; + +#nullable disable + +namespace Microsoft.Build.CommandLine.Experimental +{ + internal class CommandLineParser + { + /// + /// String replacement pattern to support paths in response files. + /// + private const string responseFilePathReplacement = "%MSBuildThisFileDirectory%"; + + /// + /// The name of an auto-response file to search for in the project directory and above. + /// + private const string directoryResponseFileName = "Directory.Build.rsp"; + + /// + /// The name of the auto-response file. + /// + private const string autoResponseFileName = "MSBuild.rsp"; + + /// + /// Used to keep track of response files to prevent them from + /// being included multiple times (or even recursively). + /// + private List includedResponseFiles; + + internal IReadOnlyList IncludedResponseFiles => includedResponseFiles ?? (IReadOnlyList)Array.Empty(); + + /// + /// Parses the provided command-line arguments into a . + /// + /// + /// The command-line arguments excluding the executable path. + /// + /// + /// A containing the effective set of switches after combining + /// switches from response files (including any auto-response file) with switches from the command line, + /// where command-line switches take precedence. + /// + /// + /// Thrown when invalid switch syntax or values are encountered while parsing the command line or response files. + /// + public CommandLineSwitchesAccessor Parse(IEnumerable commandLineArgs) + { + List args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, ..commandLineArgs]; + List deferredBuildMessages = []; + + GatherAllSwitches( + args, + deferredBuildMessages, + out CommandLineSwitches responseFileSwitches, + out CommandLineSwitches commandLineSwitches, + out string fullCommandLine, + out _); + + CommandLineSwitches result = new(); + result.Append(responseFileSwitches, fullCommandLine); // lowest precedence + result.Append(commandLineSwitches, fullCommandLine); + + result.ThrowErrors(); + + return new CommandLineSwitchesAccessor(result); + } + + /// + /// Gets all specified switches, from the command line, as well as all + /// response files, including the auto-response file. + /// + /// + /// + /// + /// + /// + /// Combined bag of switches. + internal void GatherAllSwitches( + IEnumerable commandLineArgs, + List deferredBuildMessages, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine, + out string exeName) + { + ResetGatheringSwitchesState(); + + // discard the first piece, because that's the path to the executable -- the rest are args + commandLineArgs = commandLineArgs.Skip(1); + + exeName = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + +#if USE_MSBUILD_DLL_EXTN + var msbuildExtn = ".dll"; +#else + var msbuildExtn = ".exe"; +#endif + if (!exeName.EndsWith(msbuildExtn, StringComparison.OrdinalIgnoreCase)) + { + exeName += msbuildExtn; + } + + fullCommandLine = $"'{string.Join(" ", commandLineArgs)}'"; + + // parse the command line, and flag syntax errors and obvious switch errors + switchesNotFromAutoResponseFile = new CommandLineSwitches(); + GatherCommandLineSwitches(commandLineArgs, switchesNotFromAutoResponseFile, fullCommandLine); + + // parse the auto-response file (if "/noautoresponse" is not specified), and combine those switches with the + // switches on the command line + switchesFromAutoResponseFile = new CommandLineSwitches(); + if (!switchesNotFromAutoResponseFile[ParameterlessSwitch.NoAutoResponse]) + { + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + GatherAutoResponseFileSwitches(exePath, switchesFromAutoResponseFile, fullCommandLine); + } + + CommandLineSwitches switchesFromEnvironmentVariable = new(); + GatherLoggingArgsEnvironmentVariableSwitches(ref switchesFromEnvironmentVariable, deferredBuildMessages, fullCommandLine); + switchesNotFromAutoResponseFile.Append(switchesFromEnvironmentVariable, fullCommandLine); + } + + /// + /// Gathers and validates logging switches from the MSBUILD_LOGGING_ARGS environment variable. + /// Only -bl and -check switches are allowed. All other switches are logged as warnings and ignored. + /// + internal void GatherLoggingArgsEnvironmentVariableSwitches( + ref CommandLineSwitches switches, + List deferredBuildMessages, + string commandLine) + { + if (string.IsNullOrWhiteSpace(Traits.MSBuildLoggingArgs)) + { + return; + } + + DeferredBuildMessageSeverity messageSeverity = Traits.Instance.EmitLogsAsMessage ? DeferredBuildMessageSeverity.Message : DeferredBuildMessageSeverity.Warning; + + try + { + List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); + + List validArgs = new(envVarArgs.Count); + List invalidArgs = null; + + foreach (string arg in envVarArgs) + { + string unquotedArg = QuotingUtilities.Unquote(arg); + if (string.IsNullOrWhiteSpace(unquotedArg)) + { + continue; + } + + if (IsAllowedLoggingArg(unquotedArg)) + { + validArgs.Add(arg); + } + else + { + invalidArgs ??= []; + invalidArgs.Add(unquotedArg); + } + } + + if (invalidArgs != null) + { + foreach (string invalidArg in invalidArgs) + { + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg); + deferredBuildMessages.Add(new DeferredBuildMessage(message, warningCode, messageSeverity)); + } + } + + if (validArgs.Count > 0) + { + deferredBuildMessages.Add(new DeferredBuildMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), MessageImportance.Low)); + GatherCommandLineSwitches(validArgs, switches, commandLine); + } + } + catch (Exception ex) + { + var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()); + deferredBuildMessages.Add(new DeferredBuildMessage(message, errorCode, messageSeverity)); + } + } + + /// + /// Checks if the argument is an allowed logging argument (-bl or -check). + /// + /// The unquoted argument to check. + /// True if the argument is allowed, false otherwise. + private bool IsAllowedLoggingArg(string arg) + { + if (!ValidateSwitchIndicatorInUnquotedArgument(arg)) + { + return false; + } + + ReadOnlySpan switchPart = arg.AsSpan(GetLengthOfSwitchIndicator(arg)); + + // Extract switch name (before any ':' parameter indicator) + int colonIndex = switchPart.IndexOf(':'); + ReadOnlySpan switchNameSpan = colonIndex >= 0 ? switchPart.Slice(0, colonIndex) : switchPart; + string switchName = switchNameSpan.ToString(); + + return IsParameterizedSwitch( + switchName, + out ParameterizedSwitch paramSwitch, + out _, + out _, + out _, + out _, + out _) && (paramSwitch == ParameterizedSwitch.BinaryLogger || paramSwitch == ParameterizedSwitch.Check); + } + + /// + /// Coordinates the parsing of the command line. It detects switches on the command line, gathers their parameters, and + /// flags syntax errors, and other obvious switch errors. + /// + /// + /// Internal for unit testing only. + /// + internal void GatherCommandLineSwitches(IEnumerable commandLineArgs, CommandLineSwitches commandLineSwitches, string commandLine = "") + { + foreach (string commandLineArg in commandLineArgs) + { + string unquotedCommandLineArg = QuotingUtilities.Unquote(commandLineArg, out var doubleQuotesRemovedFromArg); + + if (unquotedCommandLineArg.Length > 0) + { + // response file switch starts with @ + if (unquotedCommandLineArg.StartsWith("@", StringComparison.Ordinal)) + { + GatherResponseFileSwitch(unquotedCommandLineArg, commandLineSwitches, commandLine); + } + else + { + string switchName; + string switchParameters; + + // all switches should start with - or / or -- unless a project is being specified + if (!ValidateSwitchIndicatorInUnquotedArgument(unquotedCommandLineArg) || FileUtilities.LooksLikeUnixFilePath(unquotedCommandLineArg)) + { + switchName = null; + // add a (fake) parameter indicator for later parsing + switchParameters = $":{commandLineArg}"; + } + else + { + // check if switch has parameters (look for the : parameter indicator) + int switchParameterIndicator = unquotedCommandLineArg.IndexOf(':'); + + // get the length of the beginning sequence considered as a switch indicator (- or / or --) + int switchIndicatorsLength = GetLengthOfSwitchIndicator(unquotedCommandLineArg); + + // extract the switch name and parameters -- the name is sandwiched between the switch indicator (the + // leading - or / or --) and the parameter indicator (if the switch has parameters); the parameters (if any) + // follow the parameter indicator + if (switchParameterIndicator == -1) + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength); + switchParameters = string.Empty; + } + else + { + switchName = unquotedCommandLineArg.Substring(switchIndicatorsLength, switchParameterIndicator - switchIndicatorsLength); + switchParameters = ExtractSwitchParameters(commandLineArg, unquotedCommandLineArg, doubleQuotesRemovedFromArg, switchName, switchParameterIndicator, switchIndicatorsLength); + } + } + + // Special case: for the switches "/m" (or "/maxCpuCount") and "/bl" (or "/binarylogger") we wish to pretend we saw a default argument + // This allows a subsequent /m:n on the command line to override it. + // We could create a new kind of switch with optional parameters, but it's a great deal of churn for this single case. + // Note that if no "/m" or "/maxCpuCount" switch -- either with or without parameters -- is present, then we still default to 1 cpu + // for backwards compatibility. + if (string.IsNullOrEmpty(switchParameters)) + { + if (string.Equals(switchName, "m", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "maxcpucount", StringComparison.OrdinalIgnoreCase)) + { + int numberOfCpus = NativeMethodsShared.GetLogicalCoreCount(); + switchParameters = $":{numberOfCpus}"; + } + else if (string.Equals(switchName, "bl", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "binarylogger", StringComparison.OrdinalIgnoreCase)) + { + // we have to specify at least one parameter otherwise it's impossible to distinguish the situation + // where /bl is not specified at all vs. where /bl is specified without the file name. + switchParameters = ":msbuild.binlog"; + } + else if (string.Equals(switchName, "prof", StringComparison.OrdinalIgnoreCase) || + string.Equals(switchName, "profileevaluation", StringComparison.OrdinalIgnoreCase)) + { + switchParameters = ":no-file"; + } + } + + if (CommandLineSwitches.IsParameterlessSwitch(switchName, out var parameterlessSwitch, out var duplicateSwitchErrorMessage)) + { + GatherParameterlessCommandLineSwitch(commandLineSwitches, parameterlessSwitch, switchParameters, duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + else if (CommandLineSwitches.IsParameterizedSwitch(switchName, out var parameterizedSwitch, out duplicateSwitchErrorMessage, out var multipleParametersAllowed, out var missingParametersErrorMessage, out var unquoteParameters, out var allowEmptyParameters)) + { + GatherParameterizedCommandLineSwitch(commandLineSwitches, parameterizedSwitch, switchParameters, duplicateSwitchErrorMessage, multipleParametersAllowed, missingParametersErrorMessage, unquoteParameters, unquotedCommandLineArg, allowEmptyParameters, commandLine); + } + else + { + commandLineSwitches.SetUnknownSwitchError(unquotedCommandLineArg, commandLine); + } + } + } + } + } + + /// + /// Called when a response file switch is detected on the command line. It loads the specified response file, and parses + /// each line in it like a command line. It also prevents multiple (or recursive) inclusions of the same response file. + /// + /// + /// + private void GatherResponseFileSwitch(string unquotedCommandLineArg, CommandLineSwitches commandLineSwitches, string commandLine) + { + try + { + string responseFile = FileUtilities.FixFilePath(unquotedCommandLineArg.Substring(1)); + + if (responseFile.Length == 0) + { + commandLineSwitches.SetSwitchError("MissingResponseFileError", unquotedCommandLineArg, commandLine); + } + else if (!FileSystems.Default.FileExists(responseFile)) + { + commandLineSwitches.SetParameterError("ResponseFileNotFoundError", unquotedCommandLineArg, commandLine); + } + else + { + // normalize the response file path to help catch multiple (or recursive) inclusions + responseFile = Path.GetFullPath(responseFile); + // NOTE: for network paths or mapped paths, normalization is not guaranteed to work + + bool isRepeatedResponseFile = false; + + foreach (string includedResponseFile in includedResponseFiles) + { + if (string.Equals(responseFile, includedResponseFile, StringComparison.OrdinalIgnoreCase)) + { + commandLineSwitches.SetParameterError("RepeatedResponseFileError", unquotedCommandLineArg, commandLine); + isRepeatedResponseFile = true; + break; + } + } + + if (!isRepeatedResponseFile) + { + var responseFileDirectory = FileUtilities.EnsureTrailingSlash(Path.GetDirectoryName(responseFile)); + includedResponseFiles.Add(responseFile); + + List argsFromResponseFile; + +#if FEATURE_ENCODING_DEFAULT + using (StreamReader responseFileContents = new StreamReader(responseFile, Encoding.Default)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#else + using (StreamReader responseFileContents = FileUtilities.OpenRead(responseFile)) // HIGHCHAR: If response files have no byte-order marks, then assume ANSI rather than ASCII. +#endif + { + argsFromResponseFile = new List(); + + while (responseFileContents.Peek() != -1) + { + // ignore leading whitespace on each line + string responseFileLine = responseFileContents.ReadLine().TrimStart(); + + // skip comment lines beginning with # + if (!responseFileLine.StartsWith("#", StringComparison.Ordinal)) + { + // Allow special case to support a path relative to the .rsp file being processed. + responseFileLine = Regex.Replace(responseFileLine, responseFilePathReplacement, + responseFileDirectory, RegexOptions.IgnoreCase); + + // treat each line of the response file like a command line i.e. args separated by whitespace + argsFromResponseFile.AddRange(QuotingUtilities.SplitUnquoted(Environment.ExpandEnvironmentVariables(responseFileLine))); + } + } + } + + CommandLineSwitches.SwitchesFromResponseFiles.Add((responseFile, string.Join(" ", argsFromResponseFile))); + + GatherCommandLineSwitches(argsFromResponseFile, commandLineSwitches, commandLine); + } + } + } + catch (NotSupportedException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (SecurityException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (UnauthorizedAccessException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + catch (IOException e) + { + commandLineSwitches.SetParameterError("ReadResponseFileError", unquotedCommandLineArg, e, commandLine); + } + } + + /// + /// Called when a switch that doesn't take parameters is detected on the command line. + /// + /// + /// + /// + /// + /// + private static void GatherParameterlessCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterlessSwitch parameterlessSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + string unquotedCommandLineArg, + string commandLine) + { + // switch should not have any parameters + if (switchParameters.Length == 0) + { + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterlessSwitchSet(parameterlessSwitch) || + (duplicateSwitchErrorMessage == null)) + { + commandLineSwitches.SetParameterlessSwitch(parameterlessSwitch, unquotedCommandLineArg); + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetUnexpectedParametersError(unquotedCommandLineArg, commandLine); + } + } + + /// + /// Called when a switch that takes parameters is detected on the command line. This method flags errors and stores the + /// switch parameters. + /// + /// + /// + /// + /// + /// + /// + /// + /// + private static void GatherParameterizedCommandLineSwitch( + CommandLineSwitches commandLineSwitches, + CommandLineSwitches.ParameterizedSwitch parameterizedSwitch, + string switchParameters, + string duplicateSwitchErrorMessage, + bool multipleParametersAllowed, + string missingParametersErrorMessage, + bool unquoteParameters, + string unquotedCommandLineArg, + bool allowEmptyParameters, + string commandLine) + { + if (// switch must have parameters + (switchParameters.Length > 1) || + // unless the parameters are optional + (missingParametersErrorMessage == null)) + { + // skip the parameter indicator (if any) + if (switchParameters.Length > 0) + { + switchParameters = switchParameters.Substring(1); + } + + if (parameterizedSwitch == CommandLineSwitches.ParameterizedSwitch.Project && IsEnvironmentVariable(switchParameters)) + { + commandLineSwitches.SetSwitchError("EnvironmentVariableAsSwitch", unquotedCommandLineArg, commandLine); + } + + // check if switch is duplicated, and if that's allowed + if (!commandLineSwitches.IsParameterizedSwitchSet(parameterizedSwitch) || + (duplicateSwitchErrorMessage == null)) + { + // save the parameters after unquoting and splitting them if necessary + if (!commandLineSwitches.SetParameterizedSwitch(parameterizedSwitch, unquotedCommandLineArg, switchParameters, multipleParametersAllowed, unquoteParameters, allowEmptyParameters)) + { + // if parsing revealed there were no real parameters, flag an error, unless the parameters are optional + if (missingParametersErrorMessage != null) + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + } + else + { + commandLineSwitches.SetSwitchError(duplicateSwitchErrorMessage, unquotedCommandLineArg, commandLine); + } + } + else + { + commandLineSwitches.SetSwitchError(missingParametersErrorMessage, unquotedCommandLineArg, commandLine); + } + } + + /// + /// Identifies if there is rsp files near the project file + /// + /// true if there autoresponse file was found + internal bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) + { + bool found = false; + + var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); + + if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + // gather any switches from the first Directory.Build.rsp found in the project directory or above + string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); + + found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); + + // Don't look for more response files if it's only in the same place we already looked (next to the exe) + string exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); // Copied from XMake + if (!string.Equals(projectDirectory, exePath, StringComparison.OrdinalIgnoreCase)) + { + // this combines any found, with higher precedence, with the switches from the original auto response file switches + found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); + } + } + + return found; + } + + private static string GetProjectDirectory(string[] projectSwitchParameters) + { + string projectDirectory = "."; + ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); + + if (projectSwitchParameters.Length == 1) + { + var projectFile = FileUtilities.FixFilePath(projectSwitchParameters[0]); + + if (FileSystems.Default.DirectoryExists(projectFile)) + { + // the provided argument value is actually the directory + projectDirectory = projectFile; + } + else + { + InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); + projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); + } + } + + return projectDirectory; + } + + /// + /// Extracts a switch's parameters after processing all quoting around the switch. + /// + /// + /// This method is marked "internal" for unit-testing purposes only -- ideally it should be "private". + /// + /// + /// + /// + /// + /// + /// + /// The given switch's parameters (with interesting quoting preserved). + internal static string ExtractSwitchParameters( + string commandLineArg, + string unquotedCommandLineArg, + int doubleQuotesRemovedFromArg, + string switchName, + int switchParameterIndicator, + int switchIndicatorsLength) + { + + // find the parameter indicator again using the quoted arg + // NOTE: since the parameter indicator cannot be part of a switch name, quoting around it is not relevant, because a + // parameter indicator cannot be escaped or made into a literal + int quotedSwitchParameterIndicator = commandLineArg.IndexOf(':'); + + // check if there is any quoting in the name portion of the switch + string unquotedSwitchIndicatorAndName = QuotingUtilities.Unquote(commandLineArg.Substring(0, quotedSwitchParameterIndicator), out var doubleQuotesRemovedFromSwitchIndicatorAndName); + + ErrorUtilities.VerifyThrow(switchName == unquotedSwitchIndicatorAndName.Substring(switchIndicatorsLength), + "The switch name extracted from either the partially or completely unquoted arg should be the same."); + + ErrorUtilities.VerifyThrow(doubleQuotesRemovedFromArg >= doubleQuotesRemovedFromSwitchIndicatorAndName, + "The name portion of the switch cannot contain more quoting than the arg itself."); + + string switchParameters; + // if quoting in the name portion of the switch was terminated + if ((doubleQuotesRemovedFromSwitchIndicatorAndName % 2) == 0) + { + // get the parameters exactly as specified on the command line i.e. including quoting + switchParameters = commandLineArg.Substring(quotedSwitchParameterIndicator); + } + else + { + // if quoting was not terminated in the name portion of the switch, and the terminal double-quote (if any) + // terminates the switch parameters + int terminalDoubleQuote = commandLineArg.IndexOf('"', quotedSwitchParameterIndicator + 1); + if (((doubleQuotesRemovedFromArg - doubleQuotesRemovedFromSwitchIndicatorAndName) <= 1) && + ((terminalDoubleQuote == -1) || (terminalDoubleQuote == (commandLineArg.Length - 1)))) + { + // then the parameters are not quoted in any interesting way, so use the unquoted parameters + switchParameters = unquotedCommandLineArg.Substring(switchParameterIndicator); + } + else + { + // otherwise, use the quoted parameters, after compensating for the quoting that was started in the name + // portion of the switch + switchParameters = $":\"{commandLineArg.Substring(quotedSwitchParameterIndicator + 1)}"; + } + } + + ErrorUtilities.VerifyThrow(switchParameters != null, "We must be able to extract the switch parameters."); + + return switchParameters; + } + + /// + /// Checks whether envVar is an environment variable. MSBuild uses + /// Environment.ExpandEnvironmentVariables(string), which only + /// considers %-delimited variables. + /// + /// A possible environment variable + /// Whether envVar is an environment variable + private static bool IsEnvironmentVariable(string envVar) + { + return envVar.StartsWith("%") && envVar.EndsWith("%") && envVar.Length > 1; + } + + /// + /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the + /// switches from the auto-response file with the switches passed in. + /// Returns true if the response file was found. + /// + private bool GatherAutoResponseFileSwitches(string path, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + string autoResponseFile = Path.Combine(path, autoResponseFileName); + return GatherAutoResponseFileSwitchesFromFullPath(autoResponseFile, switchesFromAutoResponseFile, commandLine); + } + + private bool GatherAutoResponseFileSwitchesFromFullPath(string autoResponseFile, CommandLineSwitches switchesFromAutoResponseFile, string commandLine) + { + bool found = false; + + // if the auto-response file does not exist, only use the switches on the command line + if (FileSystems.Default.FileExists(autoResponseFile)) + { + found = true; + GatherResponseFileSwitch($"@{autoResponseFile}", switchesFromAutoResponseFile, commandLine); + + // if the "/noautoresponse" switch was set in the auto-response file, flag an error + if (switchesFromAutoResponseFile[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) + { + switchesFromAutoResponseFile.SetSwitchError("CannotAutoDisableAutoResponseFile", + switchesFromAutoResponseFile.GetParameterlessSwitchCommandLineArg(CommandLineSwitches.ParameterlessSwitch.NoAutoResponse), commandLine); + } + + // Throw errors found in the response file + switchesFromAutoResponseFile.ThrowErrors(); + } + + return found; + } + + /// + /// Checks whether an argument given as a parameter starts with valid indicator, + ///
which means, whether switch begins with one of: "/", "-", "--" + ///
+ /// Command line argument with beginning indicator (e.g. --help). + ///
This argument has to be unquoted, otherwise the first character will always be a quote character " + /// true if argument's beginning matches one of possible indicators + ///
false if argument's beginning doesn't match any of correct indicator + ///
+ private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) + { + return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" + || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); + } + + /// + /// Gets the length of the switch indicator (- or / or --) + ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. + ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. + ///
+ /// Unquoted argument with leading indicator and name + /// Correct length of used indicator + ///
0 if no leading sequence recognized as correct indicator
+ /// Internal for testing purposes + internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) + { + if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) + { + return 2; + } + else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) + { + return 1; + } + else + { + return 0; + } + } + + public void ResetGatheringSwitchesState() + { + includedResponseFiles = new List(); + CommandLineSwitches.SwitchesFromResponseFiles = new(); + } + } +} diff --git a/src/MSBuild/CommandLineSwitchException.cs b/src/MSBuild/CommandLine/CommandLineSwitchException.cs similarity index 99% rename from src/MSBuild/CommandLineSwitchException.cs rename to src/MSBuild/CommandLine/CommandLineSwitchException.cs index e8ce5dd036d..54ec5e65ef2 100644 --- a/src/MSBuild/CommandLineSwitchException.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitchException.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This exception is used to flag (syntax) errors in command line switches passed to the application. diff --git a/src/MSBuild/CommandLineSwitches.cs b/src/MSBuild/CommandLine/CommandLineSwitches.cs similarity index 99% rename from src/MSBuild/CommandLineSwitches.cs rename to src/MSBuild/CommandLine/CommandLineSwitches.cs index 30490337ab0..7c7add45c3e 100644 --- a/src/MSBuild/CommandLineSwitches.cs +++ b/src/MSBuild/CommandLine/CommandLineSwitches.cs @@ -11,7 +11,7 @@ #nullable disable -namespace Microsoft.Build.CommandLine +namespace Microsoft.Build.CommandLine.Experimental { /// /// This class encapsulates the switches gathered from the application command line. It helps with switch detection, parameter @@ -426,6 +426,7 @@ private struct DetectedParameterlessSwitch /// /// This struct stores the details of a switch that takes parameters that is detected on the command line. /// + [DebuggerDisplay("{commandLineArg} | {parameters}")] private struct DetectedParameterizedSwitch { // the actual text of the switch @@ -435,8 +436,8 @@ private struct DetectedParameterizedSwitch internal ArrayList parameters; } - // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the - // command line + // for each recognized switch that doesn't take parameters, this array indicates if the switch has been detected on the command + // line private DetectedParameterlessSwitch[] _parameterlessSwitches; // for each recognized switch that takes parameters, this array indicates if the switch has been detected on the command // line, and it provides a store for the switch parameters @@ -504,6 +505,7 @@ internal void SetParameterlessSwitch(ParameterlessSwitch parameterlessSwitch, st /// /// /// + /// /// true, if the given parameters were successfully stored internal bool SetParameterizedSwitch( ParameterizedSwitch parameterizedSwitch, diff --git a/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs new file mode 100644 index 00000000000..66bf8c8582e --- /dev/null +++ b/src/MSBuild/CommandLine/CommandLineSwitchesAccessor.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using static Microsoft.Build.CommandLine.Experimental.CommandLineSwitches; + +namespace Microsoft.Build.CommandLine.Experimental +{ + internal readonly struct CommandLineSwitchesAccessor + { + private readonly CommandLineSwitches switches; + + internal CommandLineSwitchesAccessor(CommandLineSwitches switches) + { + this.switches = switches; + } + + // Parameterless switches + public bool? Help => GetParameterlessSwitchValue(ParameterlessSwitch.Help); + + public bool? Version => GetParameterlessSwitchValue(ParameterlessSwitch.Version); + + public bool? NoLogo => GetParameterlessSwitchValue(ParameterlessSwitch.NoLogo); + + public bool? NoAutoResponse => GetParameterlessSwitchValue(ParameterlessSwitch.NoAutoResponse); + + public bool? NoConsoleLogger => GetParameterlessSwitchValue(ParameterlessSwitch.NoConsoleLogger); + + public bool? FileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger); + + public bool? FileLogger1 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger1); + + public bool? FileLogger2 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger2); + + public bool? FileLogger3 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger3); + + public bool? FileLogger4 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger4); + + public bool? FileLogger5 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger5); + + public bool? FileLogger6 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger6); + + public bool? FileLogger7 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger7); + + public bool? FileLogger8 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger8); + + public bool? FileLogger9 => GetParameterlessSwitchValue(ParameterlessSwitch.FileLogger9); + + public bool? DistributedFileLogger => GetParameterlessSwitchValue(ParameterlessSwitch.DistributedFileLogger); + +#if DEBUG + public bool? WaitForDebugger => GetParameterlessSwitchValue(ParameterlessSwitch.WaitForDebugger); +#endif + + // Parameterized switches + public string[]? Project => GetParameterizedSwitchValue(ParameterizedSwitch.Project); + + public string[]? Target => GetParameterizedSwitchValue(ParameterizedSwitch.Target); + + public string[]? Property => GetParameterizedSwitchValue(ParameterizedSwitch.Property); + + public string[]? Logger => GetParameterizedSwitchValue(ParameterizedSwitch.Logger); + + public string[]? DistributedLogger => GetParameterizedSwitchValue(ParameterizedSwitch.DistributedLogger); + + public string[]? Verbosity => GetParameterizedSwitchValue(ParameterizedSwitch.Verbosity); + +#if FEATURE_XML_SCHEMA_VALIDATION + public string[]? Validate => GetParameterizedSwitchValue(ParameterizedSwitch.Validate); +#endif + + public string[]? ConsoleLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.ConsoleLoggerParameters); + + public string[]? NodeMode => GetParameterizedSwitchValue(ParameterizedSwitch.NodeMode); + + public string[]? MaxCpuCount => GetParameterizedSwitchValue(ParameterizedSwitch.MaxCPUCount); + + public string[]? IgnoreProjectExtensions => GetParameterizedSwitchValue(ParameterizedSwitch.IgnoreProjectExtensions); + + public string[]? ToolsVersion => GetParameterizedSwitchValue(ParameterizedSwitch.ToolsVersion); + + public string[]? FileLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters); + + public string[]? FileLoggerParameters1 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters1); + + public string[]? FileLoggerParameters2 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters2); + + public string[]? FileLoggerParameters3 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters3); + + public string[]? FileLoggerParameters4 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters4); + + public string[]? FileLoggerParameters5 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters5); + + public string[]? FileLoggerParameters6 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters6); + + public string[]? FileLoggerParameters7 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters7); + + public string[]? FileLoggerParameters8 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters8); + + public string[]? FileLoggerParameters9 => GetParameterizedSwitchValue(ParameterizedSwitch.FileLoggerParameters9); + + public string[]? TerminalLogger => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLogger); + + public string[]? TerminalLoggerParameters => GetParameterizedSwitchValue(ParameterizedSwitch.TerminalLoggerParameters); + + public string[]? NodeReuse => GetParameterizedSwitchValue(ParameterizedSwitch.NodeReuse); + + public string[]? Preprocess => GetParameterizedSwitchValue(ParameterizedSwitch.Preprocess); + + public string[]? Targets => GetParameterizedSwitchValue(ParameterizedSwitch.Targets); + + public string[]? WarningsAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsErrors); + + public string[]? WarningsNotAsErrors => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsNotAsErrors); + + public string[]? WarningsAsMessages => GetParameterizedSwitchValue(ParameterizedSwitch.WarningsAsMessages); + + public string[]? BinaryLogger => GetParameterizedSwitchValue(ParameterizedSwitch.BinaryLogger); + + public string[]? Check => GetParameterizedSwitchValue(ParameterizedSwitch.Check); + + public string[]? Restore => GetParameterizedSwitchValue(ParameterizedSwitch.Restore); + + public string[]? ProfileEvaluation => GetParameterizedSwitchValue(ParameterizedSwitch.ProfileEvaluation); + + public string[]? RestoreProperty => GetParameterizedSwitchValue(ParameterizedSwitch.RestoreProperty); + + public string[]? Interactive => GetParameterizedSwitchValue(ParameterizedSwitch.Interactive); + + public string[]? IsolateProjects => GetParameterizedSwitchValue(ParameterizedSwitch.IsolateProjects); + + public string[]? GraphBuild => GetParameterizedSwitchValue(ParameterizedSwitch.GraphBuild); + + public string[]? InputResultsCaches => GetParameterizedSwitchValue(ParameterizedSwitch.InputResultsCaches); + + public string[]? OutputResultsCache => GetParameterizedSwitchValue(ParameterizedSwitch.OutputResultsCache); + +#if FEATURE_REPORTFILEACCESSES + public string[]? ReportFileAccesses => GetParameterizedSwitchValue(ParameterizedSwitch.ReportFileAccesses); +#endif + + public string[]? LowPriority => GetParameterizedSwitchValue(ParameterizedSwitch.LowPriority); + + public string[]? Question => GetParameterizedSwitchValue(ParameterizedSwitch.Question); + + public string[]? DetailedSummary => GetParameterizedSwitchValue(ParameterizedSwitch.DetailedSummary); + + public string[]? GetProperty => GetParameterizedSwitchValue(ParameterizedSwitch.GetProperty); + + public string[]? GetItem => GetParameterizedSwitchValue(ParameterizedSwitch.GetItem); + + public string[]? GetTargetResult => GetParameterizedSwitchValue(ParameterizedSwitch.GetTargetResult); + + public string[]? GetResultOutputFile => GetParameterizedSwitchValue(ParameterizedSwitch.GetResultOutputFile); + + public string[]? FeatureAvailability => GetParameterizedSwitchValue(ParameterizedSwitch.FeatureAvailability); + + public string[]? MultiThreaded => GetParameterizedSwitchValue(ParameterizedSwitch.MultiThreaded); + + private bool? GetParameterlessSwitchValue(ParameterlessSwitch switchType) => switches.IsParameterlessSwitchSet(switchType) ? switches[switchType] : null; + + private string[]? GetParameterizedSwitchValue(ParameterizedSwitch switchType) => switches.IsParameterizedSwitchSet(switchType) ? switches[switchType] : null; + } +} diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index d8cd63d2a5c..a9c57846e05 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -134,9 +134,11 @@ - - + + + + @@ -210,7 +212,7 @@ - + diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index 3eeb975bc40..33100583fe2 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -34,18 +34,12 @@ internal static class MSBuildClientApp /// /// The locations of msbuild exe/dll and dotnet.exe would be automatically detected if called from dotnet or msbuild cli. Calling this function from other executables might not work. /// - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, CancellationToken cancellationToken) { string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; return Execute( - commandLine, + commandLineArgs, msbuildLocation, cancellationToken); } @@ -53,7 +47,7 @@ public static MSBuildApp.ExitType Execute( /// /// This is the entry point for the MSBuild client. /// - /// The command line to process. The first argument + /// The command line to process. The first argument /// on the command line is assumed to be the name/path of the executable, and /// is ignored. /// Full path to current MSBuild.exe if executable is MSBuild.exe, @@ -61,16 +55,9 @@ public static MSBuildApp.ExitType Execute( /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. - public static MSBuildApp.ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine, -#else - string[] commandLine, -#endif - string msbuildLocation, - CancellationToken cancellationToken) + public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbuildLocation, CancellationToken cancellationToken) { - MSBuildClient msbuildClient = new MSBuildClient(commandLine, msbuildLocation); + MSBuildClient msbuildClient = new MSBuildClient(commandLineArgs, msbuildLocation); MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || @@ -84,7 +71,7 @@ public static MSBuildApp.ExitType Execute( } // Server is busy, fallback to old behavior. - return MSBuildApp.Execute(commandLine); + return MSBuildApp.Execute(commandLineArgs); } if (exitResult.MSBuildClientExitType == MSBuildClientExitType.Success && diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 12cc6acb393..f73781e4bf6 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -26,18 +26,17 @@ using Microsoft.Build.Execution; using Microsoft.Build.Experimental; using Microsoft.Build.Experimental.BuildCheck; +using Microsoft.Build.ProjectCache; using Microsoft.Build.Framework; using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Logging; -using Microsoft.Build.ProjectCache; using Microsoft.Build.Shared; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Shared.FileSystem; using Microsoft.Build.Tasks.AssemblyDependency; -using static Microsoft.Build.CommandLine.CommandLineSwitches; -using static Microsoft.Build.Execution.BuildManager; +using Microsoft.Build.CommandLine.Experimental; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using ConsoleLogger = Microsoft.Build.Logging.ConsoleLogger; using FileLogger = Microsoft.Build.Logging.FileLogger; @@ -47,7 +46,6 @@ using TerminalLogger = Microsoft.Build.Logging.TerminalLogger; #if NETFRAMEWORK -using System.Linq.Expressions; // Use I/O operations from Microsoft.IO.Redist which is generally higher perf // and also works around https://github.com/dotnet/msbuild/issues/10540. // Unnecessary on .NET 6+ because the perf improvements are in-box there. @@ -146,6 +144,8 @@ public enum ExitType private static readonly char[] s_commaSemicolon = { ',', ';' }; + private static CommandLineParser commandLineParser; + /// /// Static constructor /// @@ -161,6 +161,7 @@ static MSBuildApp() // any configuration file exceptions can be caught here. // //////////////////////////////////////////////////////////////////////////////// s_exePath = Path.GetDirectoryName(FileUtilities.ExecutingAssemblyPath); + commandLineParser = new CommandLineParser(); s_initialized = true; } @@ -236,14 +237,18 @@ private static void HandleConfigurationException(Exception ex) #if FEATURE_APPDOMAIN [LoaderOptimization(LoaderOptimization.MultiDomain)] #endif -#pragma warning disable SA1111, SA1009 // Closing parenthesis should be on line of last parameter - public static int Main( -#if !FEATURE_GET_COMMANDLINE - string[] args -#endif - ) -#pragma warning restore SA1111, SA1009 // Closing parenthesis should be on line of last parameter + public static int Main(string[] args) { + // When running on CoreCLR(.NET), insert the command executable path as the first element of the args array. + // This is needed because on .NET the first element of Environment.CommandLine is the dotnet executable path + // and not the msbuild executable path. CoreCLR version didn't support Environment.CommandLine initially, so + // workaround was needed. +#if NET + args = [BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, .. args]; +#else + args = QuotingUtilities.SplitUnquoted(Environment.CommandLine).ToArray(); +#endif + // Setup the console UI. using AutomaticEncodingRestorer _ = new(); SetConsoleUI(); @@ -266,35 +271,18 @@ string[] args if ( Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1" && !Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout && - CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine)) -#else - ConstructArrayArg(args))) -#endif + CanRunServerBasedOnCommandLineSwitches(args)) { Console.CancelKeyPress += Console_CancelKeyPress; // Use the client app to execute build in msbuild server. Opt-in feature. - exitCode = ((s_initialized && MSBuildClientApp.Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine, -#else - ConstructArrayArg(args), -#endif - s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && MSBuildClientApp.Execute(args, s_buildCancellationSource.Token) == ExitType.Success) ? 0 : 1); } else { // return 0 on success, non-zero on failure - exitCode = ((s_initialized && Execute( -#if FEATURE_GET_COMMANDLINE - Environment.CommandLine) -#else - ConstructArrayArg(args)) -#endif - == ExitType.Success) ? 0 : 1); + exitCode = ((s_initialized && Execute(args) == ExitType.Success) ? 0 : 1); } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") @@ -313,19 +301,21 @@ string[] args /// /// Will not throw. If arguments processing fails, we will not run it on server - no reason as it will not run any build anyway. /// - private static bool CanRunServerBasedOnCommandLineSwitches( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + private static bool CanRunServerBasedOnCommandLineSwitches(string[] commandLine) { bool canRunServer = true; try { - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out string fullCommandLine); + commandLineParser.GatherAllSwitches( + commandLine, + s_globalMessagesToLogInBuildLoggers, + out CommandLineSwitches switchesFromAutoResponseFile, + out CommandLineSwitches switchesNotFromAutoResponseFile, + out string fullCommandLine, + out s_exeName); + CommandLineSwitches commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); - if (CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) + if (commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, false, fullCommandLine)) { commandLineSwitches = CombineSwitchesRespectingPriority(switchesFromAutoResponseFile, switchesNotFromAutoResponseFile, fullCommandLine); } @@ -356,23 +346,6 @@ private static bool CanRunServerBasedOnCommandLineSwitches( return canRunServer; } -#if !FEATURE_GET_COMMANDLINE - /// - /// Insert the command executable path as the first element of the args array. - /// - /// - /// - private static string[] ConstructArrayArg(string[] args) - { - string[] newArgArray = new string[args.Length + 1]; - - newArgArray[0] = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - Array.Copy(args, 0, newArgArray, 1, args.Length); - - return newArgArray; - } -#endif // !FEATURE_GET_COMMANDLINE - /// /// Append output file with elapsedTime /// @@ -626,12 +599,7 @@ private static void DebuggerLaunchCheck() /// is ignored. /// A value of type ExitType that indicates whether the build succeeded, /// or the manner in which it failed. - public static ExitType Execute( -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else - string[] commandLine) -#endif + public static ExitType Execute(string[] commandLine) { DebuggerLaunchCheck(); @@ -648,9 +616,7 @@ public static ExitType Execute( // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); -#if FEATURE_GET_COMMANDLINE ErrorUtilities.VerifyThrowArgumentLength(commandLine); -#endif AppDomain.CurrentDomain.UnhandledException += ExceptionHandling.UnhandledExceptionHandler; @@ -662,14 +628,11 @@ public static ExitType Execute( TextWriter targetsWriter = null; try { -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStart(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStart(string.Join(" ", commandLine)); } -#endif + Console.CancelKeyPress += cancelHandler; // check the operating system the code is running on @@ -725,7 +688,7 @@ public static ExitType Execute( bool reportFileAccesses = false; #endif - GatherAllSwitches(commandLine, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _); + commandLineParser.GatherAllSwitches(commandLine, s_globalMessagesToLogInBuildLoggers, out var switchesFromAutoResponseFile, out var switchesNotFromAutoResponseFile, out _, out s_exeName); bool buildCanBeInvoked = ProcessCommandLineSwitches( switchesFromAutoResponseFile, @@ -772,11 +735,7 @@ public static ExitType Execute( ref getTargetResult, ref getResultOutputFile, recursing: false, -#if FEATURE_GET_COMMANDLINE - commandLine); -#else - string.Join(' ', commandLine)); -#endif + string.Join(" ", commandLine)); CommandLineSwitches.SwitchesFromResponseFiles = null; @@ -1010,12 +969,12 @@ public static ExitType Execute( exitType = ExitType.InitializationError; } } -#pragma warning disable CS0618 // Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now - catch (Exception e) when (e is ProjectCacheException || e is Experimental.ProjectCache.ProjectCacheException) +#pragma warning disable CS0618 // Microsoft.Build.Experimental.ProjectCache.ProjectCacheException is obsolete, but we need to support both namespaces for now + catch (Exception e) when (e is ProjectCacheException || e is Microsoft.Build.Experimental.ProjectCache.ProjectCacheException) { ProjectCacheException pce = e as ProjectCacheException; - Experimental.ProjectCache.ProjectCacheException exppce = e as Experimental.ProjectCache.ProjectCacheException; + Microsoft.Build.Experimental.ProjectCache.ProjectCacheException exppce = e as Microsoft.Build.Experimental.ProjectCache.ProjectCacheException; Console.WriteLine($"MSBUILD : error {pce?.ErrorCode ?? exppce?.ErrorCode}: {e.Message}"); @@ -1076,14 +1035,10 @@ public static ExitType Execute( preprocessWriter?.Dispose(); targetsWriter?.Dispose(); -#if FEATURE_GET_COMMANDLINE - MSBuildEventSource.Log.MSBuildExeStop(commandLine); -#else if (MSBuildEventSource.Log.IsEnabled()) { MSBuildEventSource.Log.MSBuildExeStop(string.Join(" ", commandLine)); } -#endif } /********************************************************************************************************************** * WARNING: Do NOT add any more catch blocks above! @@ -1224,14 +1179,7 @@ private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs /// private static void ResetBuildState() { - ResetGatheringSwitchesState(); - } - - private static void ResetGatheringSwitchesState() - { - s_includedResponseFiles = new List(); - usingSwitchesFromAutoResponseFile = false; - CommandLineSwitches.SwitchesFromResponseFiles = new(); + commandLineParser.ResetGatheringSwitchesState(); } /// @@ -1255,7 +1203,7 @@ private static void ResetGatheringSwitchesState() /// /// List of messages to be sent to the logger when it is attached /// - private static readonly List s_globalMessagesToLogInBuildLoggers = new(); + private static readonly List s_globalMessagesToLogInBuildLoggers = new(); /// /// The original console output mode if we changed it as part of initialization. @@ -1306,11 +1254,7 @@ internal static bool BuildProject( #if FEATURE_REPORTFILEACCESSES bool reportFileAccesses, #endif -#if FEATURE_GET_COMMANDLINE - string commandLine) -#else string[] commandLine) -#endif { if (FileUtilities.IsVCProjFilename(projectFile) || FileUtilities.IsDspFilename(projectFile)) { @@ -1395,8 +1339,8 @@ internal static bool BuildProject( // all of the loggers that are single-node only .. loggers, // all of the central loggers for multi-node systems. These need to be resilient to multiple calls - // to Initialize - .. distributedLoggerRecords.Select(d => d.CentralLogger) + // to Initialize. Filter out null loggers (e.g., DistributedFileLogger uses null central logger). + .. distributedLoggerRecords.Select(d => d.CentralLogger).Where(l => l is not null) ]; projectCollection = new ProjectCollection( @@ -1549,7 +1493,7 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) parameters.EnableRarNode = true; } - List messagesToLogInBuildLoggers = new(); + List messagesToLogInBuildLoggers = new(); BuildManager buildManager = BuildManager.DefaultBuildManager; @@ -1559,18 +1503,15 @@ .. distributedLoggerRecords.Select(d => d.CentralLogger) if (!Traits.Instance.EscapeHatches.DoNotSendDeferredMessagesToBuildManager) { var commandLineString = -#if FEATURE_GET_COMMANDLINE - commandLine; -#else string.Join(" ", commandLine); -#endif + messagesToLogInBuildLoggers.AddRange(GetMessagesToLogInBuildLoggers(commandLineString)); // Log a message for every response file and include it in log - foreach (var responseFilePath in s_includedResponseFiles) + foreach (var responseFilePath in commandLineParser.IncludedResponseFiles) { messagesToLogInBuildLoggers.Add( - new DeferredBuildMessage( + new BuildManager.DeferredBuildMessage( ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( "PickedUpSwitchesFromAutoResponse", responseFilePath), @@ -1758,44 +1699,72 @@ private static bool PrintTargets(string projectFile, string toolsVersion, Dictio } } - private static List GetMessagesToLogInBuildLoggers(string commandLineString) + private static List GetMessagesToLogInBuildLoggers(string commandLineString) { - var messages = new List(s_globalMessagesToLogInBuildLoggers); - - AddMessage(messages, "Process", EnvironmentUtilities.ProcessPath ?? string.Empty); - AddMessage(messages, "MSBExePath", BuildEnvironmentHelper.Instance.CurrentMSBuildExePath); - AddMessage(messages, "CommandLine", commandLineString); - AddMessage(messages, "CurrentDirectory", Environment.CurrentDirectory); - AddMessage(messages, "MSBVersion", ProjectCollection.DisplayVersion); + List messages = new(s_globalMessagesToLogInBuildLoggers) + { + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "Process", + EnvironmentUtilities.ProcessPath ?? string.Empty), + MessageImportance.Low), + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "MSBExePath", + BuildEnvironmentHelper.Instance.CurrentMSBuildExePath), + MessageImportance.Low), + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "CommandLine", + commandLineString), + MessageImportance.Low), + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "CurrentDirectory", + Environment.CurrentDirectory), + MessageImportance.Low), + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "MSBVersion", + ProjectCollection.DisplayVersion), + MessageImportance.Low), + }; - var longPathsStatus = NativeMethodsShared.IsLongPathsEnabled(); - if (longPathsStatus != NativeMethodsShared.LongPathsStatus.NotApplicable) + NativeMethodsShared.LongPathsStatus longPaths = NativeMethodsShared.IsLongPathsEnabled(); + if (longPaths != NativeMethodsShared.LongPathsStatus.NotApplicable) { - string statusResource = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword($"LongPaths_{longPathsStatus}"); - AddMessage(messages, "LongPaths", statusResource); + messages.Add( + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "LongPaths", + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + $"LongPaths_{longPaths}")), + MessageImportance.Low)); } - var sacState = NativeMethodsShared.GetSACState(); - if (sacState is not (NativeMethodsShared.SAC_State.NotApplicable or NativeMethodsShared.SAC_State.Missing)) + NativeMethodsShared.SAC_State SAC_State = NativeMethodsShared.GetSACState(); + if (SAC_State != NativeMethodsShared.SAC_State.NotApplicable && SAC_State != NativeMethodsShared.SAC_State.Missing) { - string stateResource = ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword($"SAC_{sacState}"); - AddMessage(messages, "SAC", stateResource); + messages.Add( + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "SAC", + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + $"SAC_{SAC_State}")), + MessageImportance.Low)); } if (Traits.Instance.DebugEngine) { - AddMessage(messages, "MSBuildDebugPath", DebugUtils.DebugPath, MessageImportance.High); + messages.Add( + new BuildManager.DeferredBuildMessage( + ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword( + "MSBuildDebugPath", + DebugUtils.DebugPath), + MessageImportance.High)); } return messages; - - static void AddMessage( - List list, - string resourceName, - string arg, - MessageImportance importance = MessageImportance.Low) => list.Add(new DeferredBuildMessage( - ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(resourceName, arg), - importance)); } private static BuildResult ExecuteBuild(BuildManager buildManager, BuildRequestData request) @@ -1890,67 +1859,6 @@ private static void VerifyThrowSupportedOS() } } - /// - /// Gathers and validates logging switches from the MSBUILD_LOGGING_ARGS environment variable. - /// Only -bl and -check switches are allowed. All other switches are logged as warnings and ignored. - /// - internal static void GatherLoggingArgsEnvironmentVariableSwitches(ref CommandLineSwitches switches, string commandLine) - { - if (string.IsNullOrWhiteSpace(Traits.MSBuildLoggingArgs)) - { - return; - } - - DeferredBuildMessageSeverity messageSeverity = Traits.Instance.EmitAsLogsAsMessage ? DeferredBuildMessageSeverity.Message : DeferredBuildMessageSeverity.Warning; - - try - { - List envVarArgs = QuotingUtilities.SplitUnquoted(Traits.MSBuildLoggingArgs); - - List validArgs = new(envVarArgs.Count); - List invalidArgs = null; - - foreach (string arg in envVarArgs) - { - string unquotedArg = QuotingUtilities.Unquote(arg); - if (string.IsNullOrWhiteSpace(unquotedArg)) - { - continue; - } - - if (IsAllowedLoggingArg(unquotedArg)) - { - validArgs.Add(arg); - } - else - { - invalidArgs ??= []; - invalidArgs.Add(unquotedArg); - } - } - - if (invalidArgs != null) - { - foreach (string invalidArg in invalidArgs) - { - var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string warningCode, out _, "LoggingArgsEnvVarUnsupportedArgument", invalidArg); - s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(message, warningCode, messageSeverity)); - } - } - - if (validArgs.Count > 0) - { - s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("LoggingArgsEnvVarUsing", string.Join(" ", validArgs)), MessageImportance.Low)); - GatherCommandLineSwitches(validArgs, switches, commandLine); - } - } - catch (Exception ex) - { - var message = ResourceUtilities.FormatResourceStringStripCodeAndKeyword(out string errorCode, out _, "LoggingArgsEnvVarError", ex.ToString()); - s_globalMessagesToLogInBuildLoggers.Add(new DeferredBuildMessage(message, errorCode, messageSeverity)); - } - } - /// /// MSBuild.exe need to fallback to English if it cannot print Japanese (or other language) characters /// @@ -2023,549 +1931,11 @@ internal static void SetConsoleUI() #endif } - /// - /// Gets all specified switches, from the command line, as well as all - /// response files, including the auto-response file. - /// - /// - /// - /// - /// - /// Combined bag of switches. - private static void GatherAllSwitches( -#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"; -#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); - -#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(); - 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); - } - - CommandLineSwitches switchesFromEnvironmentVariable = new(); - GatherLoggingArgsEnvironmentVariableSwitches(ref switchesFromEnvironmentVariable, fullCommandLine); - switchesNotFromAutoResponseFile.Append(switchesFromEnvironmentVariable, fullCommandLine); - } - - /// - /// Checks if the argument is an allowed logging argument (-bl or -check). - /// - /// The unquoted argument to check. - /// True if the argument is allowed, false otherwise. - private static bool IsAllowedLoggingArg(string arg) - { - if (!ValidateSwitchIndicatorInUnquotedArgument(arg)) - { - return false; - } - - ReadOnlySpan switchPart = arg.AsSpan(GetLengthOfSwitchIndicator(arg)); - - // Extract switch name (before any ':' parameter indicator) - int colonIndex = switchPart.IndexOf(':'); - ReadOnlySpan switchNameSpan = colonIndex >= 0 ? switchPart.Slice(0, colonIndex) : switchPart; - string switchName = switchNameSpan.ToString(); - - return IsParameterizedSwitch( - switchName, - out ParameterizedSwitch paramSwitch, - out _, - out _, - out _, - out _, - out _) && (paramSwitch == ParameterizedSwitch.BinaryLogger || paramSwitch == ParameterizedSwitch.Check); - } - - /// - /// 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. @@ -2641,8 +2011,8 @@ private static bool ProcessCommandLineSwitches( bool useTerminalLogger = ProcessTerminalLoggerConfiguration(commandLineSwitches, out string aggregatedTerminalLoggerParameters); // This is temporary until we can remove the need for the environment variable. - // DO NOT use this environment variable for any new features as it will be removed without further notice. - Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); + // DO NOT use this environment variable for any new features as it will be removed without further notice. + Environment.SetEnvironmentVariable("_MSBUILDTLENABLED", useTerminalLogger ? "1" : "0"); DisplayVersionMessageIfNeeded(recursing, useTerminalLogger, commandLineSwitches); @@ -2703,7 +2073,7 @@ private static bool ProcessCommandLineSwitches( } else { - bool foundProjectAutoResponseFile = CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); + bool foundProjectAutoResponseFile = commandLineParser.CheckAndGatherProjectAutoResponseFile(switchesFromAutoResponseFile, commandLineSwitches, recursing, commandLine); if (foundProjectAutoResponseFile) { @@ -2939,7 +2309,7 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (IsAutomatedEnvironment()) { s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedAutomated"), MessageImportance.Low)); + new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedAutomated"), MessageImportance.Low)); return false; } @@ -2948,7 +2318,7 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (!outputIsScreen) { s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedRedirected"), MessageImportance.Low)); + new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedRedirected"), MessageImportance.Low)); return false; } @@ -2956,14 +2326,14 @@ static bool CheckIfTerminalIsSupportedAndTryEnableAnsiColorCodes() if (!acceptAnsiColorCodes) { s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedNotSupported"), MessageImportance.Low)); + new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedNotSupported"), MessageImportance.Low)); return false; } if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) { s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedDisabled"), MessageImportance.Low)); + new BuildManager.DeferredBuildMessage(ResourceUtilities.GetResourceString("TerminalLoggerNotUsedDisabled"), MessageImportance.Low)); return false; } @@ -3038,7 +2408,7 @@ bool TryFromEnvironmentVariables() if (!string.IsNullOrEmpty(terminalLoggerArg)) { s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage($"The environment variable MSBUILDTERMINALLOGGER was set to {terminalLoggerArg}.", MessageImportance.Low)); + new BuildManager.DeferredBuildMessage($"The environment variable MSBUILDTERMINALLOGGER was set to {terminalLoggerArg}.", MessageImportance.Low)); KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntent = terminalLoggerArg; KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntentSource = "MSBUILDTERMINALLOGGER"; @@ -3047,7 +2417,7 @@ bool TryFromEnvironmentVariables() { terminalLoggerArg = liveLoggerArg; s_globalMessagesToLogInBuildLoggers.Add( - new DeferredBuildMessage($"The environment variable MSBUILDLIVELOGGER was set to {liveLoggerArg}.", MessageImportance.Low)); + new BuildManager.DeferredBuildMessage($"The environment variable MSBUILDLIVELOGGER was set to {liveLoggerArg}.", MessageImportance.Low)); KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntent = terminalLoggerArg; KnownTelemetry.LoggingConfigurationTelemetry.TerminalLoggerUserIntentSource = "MSBUILDLIVELOGGER"; @@ -3162,59 +2532,6 @@ private static CommandLineSwitches CombineSwitchesRespectingPriority(CommandLine return commandLineSwitches; } - private static string GetProjectDirectory(string[] projectSwitchParameters) - { - string projectDirectory = "."; - ErrorUtilities.VerifyThrow(projectSwitchParameters.Length <= 1, "Expect exactly one project at a time."); - - if (projectSwitchParameters.Length == 1) - { - var projectFile = FileUtilities.FixFilePath(projectSwitchParameters[0]); - - if (FileSystems.Default.DirectoryExists(projectFile)) - { - // the provided argument value is actually the directory - projectDirectory = projectFile; - } - else - { - InitializationException.VerifyThrow(FileSystems.Default.FileExists(projectFile), "ProjectNotFoundError", projectFile); - projectDirectory = Path.GetDirectoryName(Path.GetFullPath(projectFile)); - } - } - - return projectDirectory; - } - - - /// - /// Identifies if there is rsp files near the project file - /// - /// true if there autoresponse file was found - private static bool CheckAndGatherProjectAutoResponseFile(CommandLineSwitches switchesFromAutoResponseFile, CommandLineSwitches commandLineSwitches, bool recursing, string commandLine) - { - bool found = false; - - var projectDirectory = GetProjectDirectory(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.Project]); - - if (!recursing && !commandLineSwitches[CommandLineSwitches.ParameterlessSwitch.NoAutoResponse]) - { - // gather any switches from the first Directory.Build.rsp found in the project directory or above - string directoryResponseFile = FileUtilities.GetPathOfFileAbove(directoryResponseFileName, projectDirectory); - - found = !string.IsNullOrWhiteSpace(directoryResponseFile) && GatherAutoResponseFileSwitchesFromFullPath(directoryResponseFile, switchesFromAutoResponseFile, commandLine); - - // Don't look for more response files if it's only in the same place we already looked (next to the exe) - if (!string.Equals(projectDirectory, s_exePath, StringComparison.OrdinalIgnoreCase)) - { - // this combines any found, with higher precedence, with the switches from the original auto response file switches - found |= GatherAutoResponseFileSwitches(projectDirectory, switchesFromAutoResponseFile, commandLine); - } - } - - return found; - } - private static bool WarningsAsErrorsSwitchIsEmpty(CommandLineSwitches commandLineSwitches) { string val = commandLineSwitches.GetParameterizedSwitchCommandLineArg(CommandLineSwitches.ParameterizedSwitch.WarningsAsErrors); @@ -3849,46 +3166,6 @@ private static void ValidateExtensions(string[] projectExtensionsToIgnore) } } - /// - /// Checks whether an argument given as a parameter starts with valid indicator, - ///
which means, whether switch begins with one of: "/", "-", "--" - ///
- /// Command line argument with beginning indicator (e.g. --help). - ///
This argument has to be unquoted, otherwise the first character will always be a quote character " - /// true if argument's beginning matches one of possible indicators - ///
false if argument's beginning doesn't match any of correct indicator - ///
- private static bool ValidateSwitchIndicatorInUnquotedArgument(string unquotedCommandLineArgument) - { - return unquotedCommandLineArgument.StartsWith("-", StringComparison.Ordinal) // superset of "--" - || unquotedCommandLineArgument.StartsWith("/", StringComparison.Ordinal); - } - - /// - /// Gets the length of the switch indicator (- or / or --) - ///
The length returned from this method is deduced from the beginning sequence of unquoted argument. - ///
This way it will "assume" that there's no further error (e.g. // or ---) which would also be considered as a correct indicator. - ///
- /// Unquoted argument with leading indicator and name - /// Correct length of used indicator - ///
0 if no leading sequence recognized as correct indicator
- /// Internal for testing purposes - internal static int GetLengthOfSwitchIndicator(string unquotedSwitch) - { - if (unquotedSwitch.StartsWith("--", StringComparison.Ordinal)) - { - return 2; - } - else if (unquotedSwitch.StartsWith("-", StringComparison.Ordinal) || unquotedSwitch.StartsWith("/", StringComparison.Ordinal)) - { - return 1; - } - else - { - return 0; - } - } - /// /// Figures out which targets are to be built. ///