From 26934775650dda0b69bc28ca5604633e68069a6b Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Mar 2026 10:18:11 +0100 Subject: [PATCH 1/5] Fix TaskHostTask to pass request-level global properties to TaskHost When -mt mode routes tasks to an out-of-process TaskHost, the TaskHostTask was sending BuildParameters.GlobalProperties (build-level) to the TaskHostConfiguration. These build-level properties do not include per-request properties like MSBuildRestoreSessionId that are added by ExecuteRestore(). This caused NuGet's RestoreTaskEx (which lacks MSBuildMultiThreadableTaskAttribute and thus gets routed to TaskHost in -mt mode) to receive empty MSBuildRestoreSessionId via IBuildEngine6.GetGlobalProperties(). The NuGet static graph restore then excluded conditional ProjectReference items that depend on MSBuildRestoreSessionId, breaking restore for projects like externals.csproj in dotnet/runtime. The fix uses BuildEngine.GetGlobalProperties() (which returns the correct request-level properties from RequestConfiguration) instead of BuildParameters.GlobalProperties, with a fallback to the old behavior if IBuildEngine6 is not available. Fixes #13153 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Instance/TaskFactories/TaskHostTask.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 269362b1c7a..717640c8836 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -344,7 +344,7 @@ public bool Execute() _projectFile, _buildComponentHost.BuildParameters.LogTaskInputs, _setParameters, - new Dictionary(_buildComponentHost.BuildParameters.GlobalProperties), + GetGlobalPropertiesForTaskHost(), _taskLoggingContext.GetWarningsAsErrors(), _taskLoggingContext.GetWarningsNotAsErrors(), _taskLoggingContext.GetWarningsAsMessages()); @@ -412,6 +412,31 @@ public bool Execute() return _taskExecutionSucceeded; } + /// + /// Gets the global properties to send to the out-of-proc TaskHost. + /// Uses request-level properties from BuildEngine (which is a + /// implementing ) because those include per-request properties + /// like MSBuildRestoreSessionId that are added by ExecuteRestore() but are not present + /// in the build-level BuildParameters.GlobalProperties. + /// Falls back to build-level properties if the BuildEngine does not support IBuildEngine6. + /// + private Dictionary GetGlobalPropertiesForTaskHost() + { + if (BuildEngine is IBuildEngine6 buildEngine6) + { + IReadOnlyDictionary requestProperties = buildEngine6.GetGlobalProperties(); + var result = new Dictionary(requestProperties.Count, StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair kvp in requestProperties) + { + result[kvp.Key] = kvp.Value; + } + + return result; + } + + return new Dictionary(_buildComponentHost.BuildParameters.GlobalProperties); + } + /// /// Registers the specified handler for a particular packet type. /// From 20c08c1739b323ff94e5d13b0bba52426b53b556 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Mar 2026 12:19:32 +0100 Subject: [PATCH 2/5] Address review: use ChangeWave 18.6, add regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate the fix behind ChangeWave 18.6 so users can opt out via MSBUILDDISABLEFEATURESFROMVERSION=18.6 to get the old behavior (build-level BuildParameters.GlobalProperties) exactly as before - Remove dead IBuildEngine6 fallback path — BuildEngine is always a TaskHost implementing IBuildEngine6 in this context - Add GetGlobalPropertiesTask helper and two regression tests: - GlobalProperties_ForwardedToAutoEjectedTaskInMultiThreadedMode verifies request-level properties reach auto-ejected tasks (#13153) - GlobalProperties_UseBuildLevelWhenChangeWaveDisabled verifies the old behavior is preserved when the wave is opted out - Document the change in ChangeWaves.md under 18.6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/wiki/ChangeWaves.md | 1 + .../BackEnd/GetGlobalPropertiesTask.cs | 43 +++++++ .../BackEnd/TaskHostCallback_Tests.cs | 111 ++++++++++++++++++ .../Instance/TaskFactories/TaskHostTask.cs | 11 +- 4 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 68cae7eb843..0613f762892 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -31,6 +31,7 @@ Change wave checks around features will be removed in the release that accompani ### 18.6 - [AbsolutePath.GetCanonicalForm optimization - avoid expensive Path.GetFullPath calls when paths don't need canonicalization](https://github.com/dotnet/msbuild/pull/13369) +- [TaskHostTask forwards request-level global properties (e.g. MSBuildRestoreSessionId) to out-of-proc TaskHost in -mt mode](https://github.com/dotnet/msbuild/pull/13443) ### 18.5 - [FindUnderPath and AssignTargetPath tasks no longer throw on invalid path characters when using TaskEnvironment.GetAbsolutePath](https://github.com/dotnet/msbuild/pull/13069) diff --git a/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs b/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs new file mode 100644 index 00000000000..eb7c1cff600 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// A simple task that queries global properties from the build engine via IBuildEngine6. + /// Used by TaskHostCallback_Tests to verify that request-level global properties + /// (not just build-level properties) are forwarded through TaskHostTask to the out-of-proc TaskHost. + /// + public class GetGlobalPropertiesTask : Task + { + [Output] + public int GlobalPropertyCount { get; set; } + + [Output] + public string GlobalPropertyLog { get; set; } = string.Empty; + + public override bool Execute() + { + if (BuildEngine is IBuildEngine6 engine6) + { + IReadOnlyDictionary globalProperties = engine6.GetGlobalProperties(); + GlobalPropertyCount = globalProperties.Count; + + foreach (KeyValuePair kvp in globalProperties) + { + Log.LogMessage(MessageImportance.High, $"GlobalProperty: {kvp.Key}={kvp.Value}"); + } + + Log.LogMessage(MessageImportance.High, $"GlobalPropertyCount = {GlobalPropertyCount}"); + return true; + } + + Log.LogError("BuildEngine does not implement IBuildEngine6"); + return false; + } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index ab3cd481f87..f54cde2470e 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -5,6 +5,7 @@ using System.IO; using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; +using Microsoft.Build.Framework; using Microsoft.Build.UnitTests.Shared; using Shouldly; using Xunit; @@ -247,6 +248,116 @@ public void RequestCores_WorksWhenAutoEjectedInMultiThreadedMode() logger.FullLog.ShouldContain("RequestCores(1) ="); } + /// + /// Regression test for https://github.com/dotnet/msbuild/issues/13153 + /// Verifies that request-level global properties (passed via BuildRequestData, not just + /// BuildParameters.GlobalProperties) are forwarded through TaskHostTask to the out-of-proc + /// TaskHost when a task is auto-ejected in multithreaded mode. + /// + /// Before the fix, TaskHostTask.Execute() used BuildParameters.GlobalProperties (build-level), + /// which did not include per-request properties like MSBuildRestoreSessionId. This caused + /// NuGet static graph restore to fail for conditional ProjectReference items. + /// + [Fact] + public void GlobalProperties_ForwardedToAutoEjectedTaskInMultiThreadedMode() + { + using TestEnvironment env = TestEnvironment.Create(_output); + string testDir = env.CreateFolder().Path; + + // GetGlobalPropertiesTask lacks MSBuildMultiThreadableTask attribute, + // so it's auto-ejected to TaskHost in MT mode + string projectContents = $@" + + + + <{nameof(GetGlobalPropertiesTask)}> + + + +"; + + string projectFile = Path.Combine(testDir, "Test.proj"); + File.WriteAllText(projectFile, projectContents); + + // Pass request-level global properties via BuildRequestData (simulates what + // ExecuteRestore() does when adding MSBuildRestoreSessionId) + var requestGlobalProperties = new Dictionary + { + ["TestRequestProperty"] = "RequestValue", + ["AnotherRequestProp"] = "AnotherValue", + }; + + var logger = new MockLogger(_output); + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters + { + MultiThreaded = true, + MaxNodeCount = 4, + Loggers = [logger], + EnableNodeReuse = false, + }, + new BuildRequestData(projectFile, requestGlobalProperties, null, ["Test"], null)); + + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // Verify task was ejected to TaskHost + logger.FullLog.ShouldContain("external task host"); + + // Verify request-level global properties were forwarded to the TaskHost + logger.FullLog.ShouldContain("GlobalProperty: TestRequestProperty=RequestValue"); + logger.FullLog.ShouldContain("GlobalProperty: AnotherRequestProp=AnotherValue"); + } + + /// + /// Verifies that when ChangeWave 18.6 is disabled, the old behavior is preserved: + /// TaskHostTask sends build-level properties (BuildParameters.GlobalProperties) instead + /// of request-level properties. This is the opt-out for the fix in #13153. + /// + [Fact] + public void GlobalProperties_UseBuildLevelWhenChangeWaveDisabled() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_6.ToString()); + string testDir = env.CreateFolder().Path; + + string projectContents = $@" + + + + <{nameof(GetGlobalPropertiesTask)}> + + + +"; + + string projectFile = Path.Combine(testDir, "Test.proj"); + File.WriteAllText(projectFile, projectContents); + + // These request-level properties should NOT be forwarded when the wave is disabled + var requestGlobalProperties = new Dictionary + { + ["TestRequestProperty"] = "RequestValue", + }; + + var logger = new MockLogger(_output); + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters + { + MultiThreaded = true, + MaxNodeCount = 4, + Loggers = [logger], + EnableNodeReuse = false, + }, + new BuildRequestData(projectFile, requestGlobalProperties, null, ["Test"], null)); + + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // With wave disabled, build-level properties are used (empty in this test), + // so request-level properties should NOT appear + logger.FullLog.ShouldNotContain("GlobalProperty: TestRequestProperty=RequestValue"); + logger.FullLog.ShouldContain("GlobalPropertyCount = 0"); + } + /// /// Regression test for https://github.com/dotnet/msbuild/issues/13333 /// When callbacks are not supported (cross-version OOP TaskHost), RequestCores must diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 717640c8836..4d4bbd5d3e9 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -414,15 +414,14 @@ public bool Execute() /// /// Gets the global properties to send to the out-of-proc TaskHost. - /// Uses request-level properties from BuildEngine (which is a - /// implementing ) because those include per-request properties - /// like MSBuildRestoreSessionId that are added by ExecuteRestore() but are not present - /// in the build-level BuildParameters.GlobalProperties. - /// Falls back to build-level properties if the BuildEngine does not support IBuildEngine6. + /// Under ChangeWave 18.6, uses request-level properties from BuildEngine via + /// because those include per-request + /// properties like MSBuildRestoreSessionId that are added by ExecuteRestore() but are + /// not present in the build-level BuildParameters.GlobalProperties. /// private Dictionary GetGlobalPropertiesForTaskHost() { - if (BuildEngine is IBuildEngine6 buildEngine6) + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6) && BuildEngine is IBuildEngine6 buildEngine6) { IReadOnlyDictionary requestProperties = buildEngine6.GetGlobalProperties(); var result = new Dictionary(requestProperties.Count, StringComparer.OrdinalIgnoreCase); From 120070c7970c7c2651779e302a29ceef84760f7c Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Mar 2026 12:19:32 +0100 Subject: [PATCH 3/5] Address review: use ChangeWave 18.6, add regression test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gate the fix behind ChangeWave 18.6 so users can opt out via MSBUILDDISABLEFEATURESFROMVERSION=18.6 to get the old behavior (build-level BuildParameters.GlobalProperties) exactly as before - Remove dead IBuildEngine6 fallback path — BuildEngine is always a TaskHost implementing IBuildEngine6 in this context - Add GetGlobalPropertiesTask helper and two regression tests: - GlobalProperties_ForwardedToAutoEjectedTaskInMultiThreadedMode verifies request-level properties reach auto-ejected tasks (#13153) - GlobalProperties_UseBuildLevelWhenChangeWaveDisabled verifies the old behavior is preserved when the wave is opted out - Document the change in ChangeWaves.md under 18.6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build/Instance/TaskFactories/TaskHostTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 4d4bbd5d3e9..04bc5679d8a 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -424,7 +424,7 @@ private Dictionary GetGlobalPropertiesForTaskHost() if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6) && BuildEngine is IBuildEngine6 buildEngine6) { IReadOnlyDictionary requestProperties = buildEngine6.GetGlobalProperties(); - var result = new Dictionary(requestProperties.Count, StringComparer.OrdinalIgnoreCase); + var result = new Dictionary(requestProperties.Count); foreach (KeyValuePair kvp in requestProperties) { result[kvp.Key] = kvp.Value; From e3ab1b451ef7e09cd50cb93dc1febe8590d4b8e0 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Mar 2026 18:10:19 +0100 Subject: [PATCH 4/5] Remove runtime from -mt exclusion list in VMR validation pipeline With the TaskHostTask fix, runtime builds successfully with -mt mode. Remove it from MSBuildMTExcludedReposOverride so VMR validation exercises runtime with -mt enabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines/vmr-sb-validation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines/vmr-sb-validation.yml b/azure-pipelines/vmr-sb-validation.yml index 305dea8e8b5..97995be9161 100644 --- a/azure-pipelines/vmr-sb-validation.yml +++ b/azure-pipelines/vmr-sb-validation.yml @@ -13,7 +13,7 @@ parameters: - name: extraPropertiesStage2 displayName: 'Extra MSBuild properties for stage 2 (e.g., /p:DotNetBuildMT=true)' type: string - default: '/p:DotNetBuildMT=true /p:MSBuildMTExcludedReposOverride=runtime%3Broslyn%3Baspnetcore%3Bsdk%3Befcore%3Bwinforms%3Bwpf%3Brazor%3Bsource-build-reference-packages' + default: '/p:DotNetBuildMT=true /p:MSBuildMTExcludedReposOverride=roslyn%3Baspnetcore%3Bsdk%3Befcore%3Bwinforms%3Bwpf%3Brazor%3Bsource-build-reference-packages' variables: - template: /eng/common/templates/variables/pool-providers.yml@self From 1de0a8511f4ea25511e468a2b1723e625a3d7f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Wed, 25 Mar 2026 11:05:08 +0100 Subject: [PATCH 5/5] Apply suggestion from @JanProvaznik --- src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs b/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs index eb7c1cff600..910a6f59ee5 100644 --- a/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs +++ b/src/Build.UnitTests/BackEnd/GetGlobalPropertiesTask.cs @@ -17,9 +17,6 @@ public class GetGlobalPropertiesTask : Task [Output] public int GlobalPropertyCount { get; set; } - [Output] - public string GlobalPropertyLog { get; set; } = string.Empty; - public override bool Execute() { if (BuildEngine is IBuildEngine6 engine6)