diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index a6cc40c51b4..8f9ad13285d 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -37,6 +37,7 @@ Change wave checks around features will be removed in the release that accompani - [TaskHostTask forwards request-level global properties (e.g. MSBuildRestoreSessionId) to out-of-proc TaskHost in -mt mode](https://github.com/dotnet/msbuild/pull/13443) - [Fix ShouldTreatWarningAsError in OOP TaskHost checking wrong collection (WarningsAsMessages instead of WarningsAsErrors)](https://github.com/dotnet/msbuild/issues/11952) - [Fix ToolTask hang when tool spawns grandchild processes that inherit stdout/stderr pipe handles](https://github.com/dotnet/msbuild/issues/2981) +- [Unresolved project references in solution builds inherit parent Configuration and Platform instead of stripping them via GlobalPropertiesToRemove](https://github.com/dotnet/msbuild/issues/13453) ### 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/Tasks.UnitTests/AssignProjectConfigurationChangeWave_Tests.cs b/src/Tasks.UnitTests/AssignProjectConfigurationChangeWave_Tests.cs new file mode 100644 index 00000000000..5c8334a56a1 --- /dev/null +++ b/src/Tasks.UnitTests/AssignProjectConfigurationChangeWave_Tests.cs @@ -0,0 +1,467 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.Tasks; +using Microsoft.Build.Utilities; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +#nullable disable + +namespace Microsoft.Build.UnitTests +{ + /// + /// Tests for the ChangeWave 18.6 behavior change in AssignProjectConfiguration: + /// When project references are not found in the solution configuration blob, + /// they should inherit the parent's Configuration and Platform rather than + /// having them stripped via GlobalPropertiesToRemove. + /// + public sealed class AssignProjectConfigurationChangeWave_Tests + { + private readonly ITestOutputHelper _output; + + public AssignProjectConfigurationChangeWave_Tests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// Verifies that when ShouldUnsetParentConfigurationAndPlatform is true, + /// unresolved project references do NOT get GlobalPropertiesToRemove=Configuration;Platform + /// set on them. This ensures child projects inherit the parent's Configuration and Platform + /// rather than falling back to their defaults (which breaks non-standard configuration names + /// like "Debug Unicode"). + /// + [Fact] + public void UnresolvedReferencesDoNotStripConfigurationUnderChangeWave() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + var projectRefs = new ArrayList(); + // This reference is NOT in the solution config → will be unresolved + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + projectRefs.Add(unresolvedRef); + + // Solution config only contains a DIFFERENT project + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine engine = new MockEngine(_output); + AssignProjectConfiguration task = new AssignProjectConfiguration(); + task.BuildEngine = engine; + task.SolutionConfigurationContents = xmlString; + task.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + task.ShouldUnsetParentConfigurationAndPlatform = true; + + bool result = task.Execute(); + result.ShouldBeTrue(); + + // The reference should be unresolved (not in solution config) + task.UnassignedProjects.Length.ShouldBe(1); + task.AssignedProjects.Length.ShouldBe(0); + + // Under ChangeWave 18.6, GlobalPropertiesToRemove should NOT include Configuration;Platform + ITaskItem unresolved = task.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + globalPropertiesToRemove.ShouldNotContain("Configuration"); + globalPropertiesToRemove.ShouldNotContain("Platform"); + } + + /// + /// Verifies that when ChangeWave 18.6 is disabled (opted out), unresolved references + /// DO get GlobalPropertiesToRemove=Configuration;Platform set on them — the pre-18.6 behavior. + /// + [Fact] + public void UnresolvedReferencesStripConfigurationWhenChangeWaveDisabled() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + // Disable ChangeWave 18.6 to get the old behavior + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_6.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + var projectRefs = new ArrayList(); + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + projectRefs.Add(unresolvedRef); + + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine engine = new MockEngine(_output); + AssignProjectConfiguration task = new AssignProjectConfiguration(); + task.BuildEngine = engine; + task.SolutionConfigurationContents = xmlString; + task.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + task.ShouldUnsetParentConfigurationAndPlatform = true; + + bool result = task.Execute(); + result.ShouldBeTrue(); + + task.UnassignedProjects.Length.ShouldBe(1); + ITaskItem unresolved = task.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + + // With ChangeWave 18.6 disabled, the old behavior should apply: + // Configuration;Platform should be appended to GlobalPropertiesToRemove + globalPropertiesToRemove.ShouldContain("Configuration"); + globalPropertiesToRemove.ShouldContain("Platform"); + } + + /// + /// Verifies that when ShouldUnsetParentConfigurationAndPlatform is false, + /// unresolved references never had GlobalPropertiesToRemove set (this behavior + /// is unchanged by the ChangeWave). + /// + [Fact] + public void UnresolvedReferencesWithoutShouldUnsetDoNotStripConfiguration() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + var projectRefs = new ArrayList(); + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + projectRefs.Add(unresolvedRef); + + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine engine = new MockEngine(_output); + AssignProjectConfiguration task = new AssignProjectConfiguration(); + task.BuildEngine = engine; + task.SolutionConfigurationContents = xmlString; + task.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + task.ShouldUnsetParentConfigurationAndPlatform = false; // default for non-solution builds + + bool result = task.Execute(); + result.ShouldBeTrue(); + + task.UnassignedProjects.Length.ShouldBe(1); + ITaskItem unresolved = task.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + globalPropertiesToRemove.ShouldBeEmpty(); + } + + /// + /// Verifies that resolved references still get correct SetConfiguration and SetPlatform + /// metadata with configurations containing spaces. + /// + [Fact] + public void ResolvedReferencesPreserveSpacesInConfigurationName() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + var projectRefs = new ArrayList(); + var resolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Main.vcxproj", + "{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Main"); + projectRefs.Add(resolvedRef); + + // Solution config contains this project with a space in config name + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{CCCCCCCC-CCCC-CCCC-CCCC-CCCCCCCCCCCC}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine engine = new MockEngine(_output); + AssignProjectConfiguration task = new AssignProjectConfiguration(); + task.BuildEngine = engine; + task.SolutionConfigurationContents = xmlString; + task.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + task.ShouldUnsetParentConfigurationAndPlatform = true; + + bool result = task.Execute(); + result.ShouldBeTrue(); + + task.AssignedProjects.Length.ShouldBe(1); + task.UnassignedProjects.Length.ShouldBe(0); + + ITaskItem resolved = task.AssignedProjects[0]; + resolved.GetMetadata("SetConfiguration").ShouldBe("Configuration=Debug Unicode"); + resolved.GetMetadata("SetPlatform").ShouldBe("Platform=x64"); + resolved.GetMetadata("Configuration").ShouldBe("Debug Unicode"); + resolved.GetMetadata("Platform").ShouldBe("x64"); + } + + /// + /// Verifies that existing GlobalPropertiesToRemove metadata on an unresolved reference + /// is preserved (not appended to) under ChangeWave 18.6. + /// + [Fact] + public void ExistingGlobalPropertiesToRemovePreservedForUnresolvedReferences() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + var projectRefs = new ArrayList(); + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + // Set some pre-existing GlobalPropertiesToRemove + unresolvedRef.SetMetadata("GlobalPropertiesToRemove", "TargetFramework"); + projectRefs.Add(unresolvedRef); + + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine engine = new MockEngine(_output); + AssignProjectConfiguration task = new AssignProjectConfiguration(); + task.BuildEngine = engine; + task.SolutionConfigurationContents = xmlString; + task.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + task.ShouldUnsetParentConfigurationAndPlatform = true; + + bool result = task.Execute(); + result.ShouldBeTrue(); + + task.UnassignedProjects.Length.ShouldBe(1); + ITaskItem unresolved = task.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + + // The pre-existing TargetFramework should still be there + globalPropertiesToRemove.ShouldContain("TargetFramework"); + // But Configuration;Platform should NOT be appended + globalPropertiesToRemove.ShouldNotContain("Configuration"); + globalPropertiesToRemove.ShouldNotContain("Platform"); + } + + /// + /// End-to-end reproduction of issue #13453 (pre-18.6 behavior): + /// + /// In a solution build, Configuration flows as an inherited global property from the + /// command line (/p:Configuration="Debug Unicode") through the solution metaproject to + /// each project. For unresolved project references (not found in the .sln), the old + /// AssignProjectConfiguration behavior adds "Configuration;Platform" to + /// GlobalPropertiesToRemove. Microsoft.Common.CurrentVersion.targets then passes this + /// as RemoveProperties="%(GlobalPropertiesToRemove)" to the MSBuild task. The MSBuild + /// task strips Configuration from the child's inherited global properties, causing it + /// to fall back to its default ("Debug") instead of the parent's "Debug Unicode". + /// + /// This test demonstrates the full causal chain: + /// 1. AssignProjectConfiguration sets GlobalPropertiesToRemove=Configuration;Platform + /// 2. A parent project built with /p:Configuration="Debug Unicode" calls MSBuild on + /// a child project with RemoveProperties=Configuration;Platform + /// 3. The child project loses "Debug Unicode" and falls back to its default "Debug" + /// + [Fact] + public void Issue13453_OldBehavior_UnresolvedReference_ChildLosesSpacedConfiguration() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + // Disable ChangeWave 18.6 to reproduce the old (buggy) behavior + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_6.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); + + // --- Step 1: Run AssignProjectConfiguration with an unresolved reference --- + var projectRefs = new ArrayList(); + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + projectRefs.Add(unresolvedRef); + + // Solution config does NOT contain this project's GUID — it will be unresolved + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine assignEngine = new MockEngine(_output); + AssignProjectConfiguration assignTask = new AssignProjectConfiguration(); + assignTask.BuildEngine = assignEngine; + assignTask.SolutionConfigurationContents = xmlString; + assignTask.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + assignTask.ShouldUnsetParentConfigurationAndPlatform = true; + + bool assignResult = assignTask.Execute(); + assignResult.ShouldBeTrue(); + assignTask.UnassignedProjects.Length.ShouldBe(1); + + // The old behavior: GlobalPropertiesToRemove includes Configuration;Platform + ITaskItem unresolved = assignTask.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + globalPropertiesToRemove.ShouldContain("Configuration"); + globalPropertiesToRemove.ShouldContain("Platform"); + + // --- Step 2: Demonstrate the consequence of that metadata. + // A parent project built with Configuration=Debug Unicode calls MSBuild on + // itself (child target) with RemoveProperties=Configuration;Platform. + // This mirrors the solution build pipeline in + // Microsoft.Common.CurrentVersion.targets: + // + string projectFile = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + Debug + + + + + + + + + "); + + try + { + // Build with Configuration=Debug Unicode as a global property, + // simulating: msbuild /p:Configuration="Debug Unicode" /p:Platform=x64 + var globalProps = new Dictionary + { + { "Configuration", "Debug Unicode" }, + { "Platform", "x64" } + }; + + MockLogger logger = new MockLogger(_output); + bool buildResult = ObjectModelHelpers.BuildTempProjectFileWithTargets( + projectFile, new[] { "ParentBuild" }, globalProps, logger); + buildResult.ShouldBeTrue(); + + _output.WriteLine(logger.FullLog); + + // The child target received Configuration=Debug (the default) because + // RemoveProperties=Configuration;Platform stripped the inherited + // "Debug Unicode" global property. This is the bug: the user passed + // Configuration="Debug Unicode" but the child project sees "Debug". + logger.AssertLogContains("ChildConfiguration=[Debug]"); + logger.AssertLogDoesntContain("ChildConfiguration=[Debug Unicode]"); + } + finally + { + File.Delete(projectFile); + } + } + + /// + /// End-to-end verification that ChangeWave 18.6 fixes issue #13453: + /// + /// With the fix, AssignProjectConfiguration no longer adds "Configuration;Platform" + /// to GlobalPropertiesToRemove for unresolved references. Without RemoveProperties, + /// the MSBuild task preserves the parent's inherited Configuration="Debug Unicode" + /// when building the child project. + /// + /// This test demonstrates the fixed causal chain: + /// 1. AssignProjectConfiguration does NOT set GlobalPropertiesToRemove + /// 2. A parent project built with /p:Configuration="Debug Unicode" calls MSBuild on + /// a child project with RemoveProperties="" (empty) + /// 3. The child project correctly inherits "Debug Unicode" + /// + [Fact] + public void Issue13453_NewBehavior_UnresolvedReference_ChildInheritsSpacedConfiguration() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + // ChangeWave 18.6 is enabled by default — this is the fixed behavior + + // --- Step 1: Run AssignProjectConfiguration with an unresolved reference --- + var projectRefs = new ArrayList(); + var unresolvedRef = ResolveNonMSBuildProjectOutput_Tests.CreateReferenceItem( + "Utility.vcxproj", + "{AAAAAAAA-AAAA-AAAA-AAAA-AAAAAAAAAAAA}", + "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}", + "Utility"); + projectRefs.Add(unresolvedRef); + + // Solution config does NOT contain this project's GUID — it will be unresolved + var projectConfigurations = new Hashtable(); + projectConfigurations.Add("{BBBBBBBB-BBBB-BBBB-BBBB-BBBBBBBBBBBB}", @"Debug Unicode|x64"); + + string xmlString = ResolveNonMSBuildProjectOutput_Tests.CreatePregeneratedPathDoc(projectConfigurations); + + MockEngine assignEngine = new MockEngine(_output); + AssignProjectConfiguration assignTask = new AssignProjectConfiguration(); + assignTask.BuildEngine = assignEngine; + assignTask.SolutionConfigurationContents = xmlString; + assignTask.ProjectReferences = (ITaskItem[])projectRefs.ToArray(typeof(ITaskItem)); + assignTask.ShouldUnsetParentConfigurationAndPlatform = true; + + bool assignResult = assignTask.Execute(); + assignResult.ShouldBeTrue(); + assignTask.UnassignedProjects.Length.ShouldBe(1); + + // The fix: GlobalPropertiesToRemove does NOT include Configuration;Platform + ITaskItem unresolved = assignTask.UnassignedProjects[0]; + string globalPropertiesToRemove = unresolved.GetMetadata("GlobalPropertiesToRemove"); + globalPropertiesToRemove.ShouldNotContain("Configuration"); + globalPropertiesToRemove.ShouldNotContain("Platform"); + + // --- Step 2: Demonstrate that without RemoveProperties, the child keeps + // "Debug Unicode". Same project as the old-behavior test, but WITHOUT + // RemoveProperties on the MSBuild task call. --- + string projectFile = ObjectModelHelpers.CreateTempFileOnDisk(@" + + + Debug + + + + + + + + + "); + + try + { + // Build with Configuration=Debug Unicode as a global property + var globalProps = new Dictionary + { + { "Configuration", "Debug Unicode" }, + { "Platform", "x64" } + }; + + MockLogger logger = new MockLogger(_output); + bool buildResult = ObjectModelHelpers.BuildTempProjectFileWithTargets( + projectFile, new[] { "ParentBuild" }, globalProps, logger); + buildResult.ShouldBeTrue(); + + _output.WriteLine(logger.FullLog); + + // The child correctly inherits "Debug Unicode" from the parent. + // This is the fix for issue #13453. + logger.AssertLogContains("ChildConfiguration=[Debug Unicode]"); + } + finally + { + File.Delete(projectFile); + } + } + } +} diff --git a/src/Tasks/AssignProjectConfiguration.cs b/src/Tasks/AssignProjectConfiguration.cs index ba5062c1ce5..0f22e291956 100644 --- a/src/Tasks/AssignProjectConfiguration.cs +++ b/src/Tasks/AssignProjectConfiguration.cs @@ -205,10 +205,12 @@ public override bool Execute() } else { - // If the reference was unresolved, we want to undefine the Configuration and Platform - // global properties, so that the project will build using its default Configuration and - // Platform rather than that of its parent. - if (ShouldUnsetParentConfigurationAndPlatform) + // Pre-18.6 behavior: for unresolved references, undefine the Configuration and Platform + // global properties so that the referenced project builds using its own default + // Configuration and Platform rather than inheriting those from its parent. Under ChangeWave + // 18.6, this block is skipped and unresolved references keep the parent's Configuration/Platform. + if (ShouldUnsetParentConfigurationAndPlatform + && !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_6)) { string globalPropertiesToRemove = projectRef.GetMetadata("GlobalPropertiesToRemove");