From 32f2c1ccb8970a425ae7d1828e5c025d009a8084 Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Mon, 26 Oct 2026 21:11:52 +0100 Subject: [PATCH] fix global property inheritance and duplicate project loads restoring builds should inherit the creator's properties, and not-restoring builds should reuse compatible evaluations --- .../BuildTests.cs | 110 ++++++++++++++++++ .../ProjectCreator.Build.cs | 11 +- 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.Build.Utilities.ProjectCreation.UnitTests/BuildTests.cs b/src/Microsoft.Build.Utilities.ProjectCreation.UnitTests/BuildTests.cs index 9c61af4..11b1028 100644 --- a/src/Microsoft.Build.Utilities.ProjectCreation.UnitTests/BuildTests.cs +++ b/src/Microsoft.Build.Utilities.ProjectCreation.UnitTests/BuildTests.cs @@ -205,6 +205,116 @@ public void BuildWithGlobalProperties() buildOutputWithGlobalProperties.MessageEvents.High.ShouldHaveSingleItem(buildOutputWithGlobalProperties.GetConsoleLog()).Message.ShouldBe("Value = D7BBABDFB2D142D3A75E0C1A33E33780", buildOutputWithGlobalProperties.GetConsoleLog()); } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void BuildInheritsGlobalProperties(bool restore, bool useCollectionProperties) + { + Dictionary globalProperties = new() + { + ["Property1"] = "inherited value", + }; + using ProjectCollection projectCollection = new(useCollectionProperties ? globalProperties : new Dictionary()); + + ProjectCreator.Create( + path: GetTempProjectPath(), + projectCollection: projectCollection, + globalProperties: useCollectionProperties ? null : globalProperties) + .Property("Property1", "project value") + .Target("Restore") + .TaskMessage("Restore value = $(Property1)", MessageImportance.High) + .Target("Build") + .TaskMessage("Build value = $(Property1)", MessageImportance.High) + .Save() + .TryBuild(restore: restore, target: "Build", out bool result, out BuildOutput buildOutput); + + result.ShouldBeTrue(buildOutput.GetConsoleLog()); + buildOutput.MessageEvents.High.ShouldContain(message => message.Message == "Build value = inherited value", buildOutput.GetConsoleLog()); + if (restore) + { + buildOutput.MessageEvents.High.ShouldContain(message => message.Message == "Restore value = inherited value", buildOutput.GetConsoleLog()); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void BuildTimeGlobalPropertiesDoNotChangeCreatorDefaults(bool restore, bool useEmptyProperties) + { + Dictionary creatorProperties = new() + { + ["Property1"] = "creator value", + }; + Dictionary buildProperties = new(); + if (!useEmptyProperties) + { + buildProperties["Property1"] = "build value"; + } + + using ProjectCollection projectCollection = new(); + ProjectCreator creator = ProjectCreator.Create( + path: GetTempProjectPath(), + projectCollection: projectCollection, + globalProperties: creatorProperties) + .Property("Property1", "project value") + .Target("Restore") + .TaskMessage("Restore value = $(Property1)", MessageImportance.High) + .Target("Build") + .TaskMessage("Build value = $(Property1)", MessageImportance.High) + .Save() + .TryBuild(restore: restore, target: "Build", globalProperties: buildProperties, out bool overrideResult, out BuildOutput overrideOutput) + .TryGetPropertyValue("Property1", out string propertyAfterOverride) + .TryBuild(restore: restore, target: "Build", out bool defaultResult, out BuildOutput defaultOutput); + + string expectedOverride = useEmptyProperties ? "project value" : "build value"; + overrideResult.ShouldBeTrue(overrideOutput.GetConsoleLog()); + overrideOutput.MessageEvents.High.ShouldContain(message => message.Message == $"Build value = {expectedOverride}", overrideOutput.GetConsoleLog()); + if (restore) + { + overrideOutput.MessageEvents.High.ShouldContain(message => message.Message == $"Restore value = {expectedOverride}", overrideOutput.GetConsoleLog()); + } + + propertyAfterOverride.ShouldBe("creator value"); + defaultResult.ShouldBeTrue(defaultOutput.GetConsoleLog()); + defaultOutput.MessageEvents.High.ShouldContain(message => message.Message == "Build value = creator value", defaultOutput.GetConsoleLog()); + creator.Project.GlobalProperties.ShouldBe(creatorProperties); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void BuildWithGlobalPropertiesCanBeRepeated(bool evaluateBeforeBuild) + { + Dictionary globalProperties = new() + { + ["Property1"] = "collection value", + }; + using ProjectCollection projectCollection = new(globalProperties); + string projectPath = GetTempProjectPath(); + ProjectCreator creator = ProjectCreator.Create(path: projectPath, projectCollection: projectCollection) + .Target("Build") + .TaskMessage("Value = $(Property1)", MessageImportance.High) + .Save(); + if (evaluateBeforeBuild) + { + creator.Project.GetPropertyValue("Property1").ShouldBe("collection value"); + } + + creator + .TryBuild("Build", globalProperties, out bool firstResult, out BuildOutput firstOutput) + .TryGetPropertyValue("Property1", out string propertyAfterBuild) + .TryBuild("Build", globalProperties, out bool secondResult, out BuildOutput secondOutput); + + firstResult.ShouldBeTrue(firstOutput.GetConsoleLog()); + secondResult.ShouldBeTrue(secondOutput.GetConsoleLog()); + propertyAfterBuild.ShouldBe("collection value"); + projectCollection.GetLoadedProjects(projectPath).ShouldHaveSingleItem(); + } + [Fact] public void CanBuildLotsOfProjects() { diff --git a/src/Microsoft.Build.Utilities.ProjectCreation/ProjectCreator.Build.cs b/src/Microsoft.Build.Utilities.ProjectCreation/ProjectCreator.Build.cs index f5229fe..62e3fa8 100644 --- a/src/Microsoft.Build.Utilities.ProjectCreation/ProjectCreator.Build.cs +++ b/src/Microsoft.Build.Utilities.ProjectCreation/ProjectCreator.Build.cs @@ -5,6 +5,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Execution; using Microsoft.Build.Framework; +using System; using System.Collections.Generic; using System.Linq; @@ -498,6 +499,8 @@ private bool Build(bool restore, string[]? targets, IDictionary? { if (restore) { + globalProperties ??= _globalProperties ?? ProjectCollection.GlobalProperties; + if (!Restore(globalProperties, buildOutput, out targetOutputs)) { return false; @@ -514,9 +517,11 @@ private bool Build(bool restore, string[]? targets, IDictionary? if (globalProperties != null) { - TryGetProject(out Project project, globalProperties); - - projectInstance = project.CreateProjectInstance(); + projectInstance = new ProjectInstance( + RootElement, + globalProperties, + string.IsNullOrEmpty(RootElement.ToolsVersion) ? null : RootElement.ToolsVersion, + ProjectCollection); } else {