Skip to content

Commit 07e2947

Browse files
authored
Emulate AssignProjectConfiguration behavior in graph construction (#8625)
Summary The for sln-based builds, the AssignProjectConfiguration task ends up using the Configuration and Platform defined in the sln rather than passing through the global properties from the referencing project or attempting to do dynamic platform negotiation. This change adds equivalent functionality to graph construction. A concrete scenario this fixes for graph-based builds using an sln file is that most csproj define the "x86" platform while most vcxproj define "Win32". Previously for a graph build, if the csproj referenced the vcxproj, the platform passed to vcxproj would be x86, not Win32. Even worse, the vcxproj would be an entry point anyway, so it would double-build with both x86 AND Win32, which leads to race conditions. Customer Impact Microsoft-internal customer using sln-based builds will be able to opt-into graph builds Regression? No Testing Manual validation in the customer repo, as well as added unit tests Risk Low. Graph builds are a less-used feature, and this adds parity to what non-graph builds and VS-based builds do. It's unlikely that any behavioral change would be impactful due to those other scenarios presumably working for customers who may be using graph builds.
1 parent f6feb85 commit 07e2947

File tree

8 files changed

+262
-64
lines changed

8 files changed

+262
-64
lines changed

eng/Versions.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the MIT license. See License.txt in the project root for full license information. -->
33
<Project>
44
<PropertyGroup>
5-
<VersionPrefix>17.6.2</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
5+
<VersionPrefix>17.6.3</VersionPrefix><DotNetFinalVersionKind>release</DotNetFinalVersionKind>
66
<PackageValidationBaselineVersion>17.5.0</PackageValidationBaselineVersion>
77
<AssemblyVersion>15.1.0.0</AssemblyVersion>
88
<PreReleaseVersionLabel>preview</PreReleaseVersionLabel>

src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs

+15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using Microsoft.Build.BackEnd;
8+
using Microsoft.Build.Collections;
89
using Microsoft.Build.Construction;
910
using Microsoft.Build.Engine.UnitTests;
1011
using Microsoft.Build.Exceptions;
@@ -523,6 +524,20 @@ public void SolutionsCanInjectEdgesIntoTheProjectGraph(Dictionary<int, int[]> ed
523524

524525
var graphFromSolutionEdges = graphFromSolution.TestOnly_Edges.TestOnly_AsConfigurationMetadata();
525526

527+
// Solutions add the CurrentSolutionConfigurationContents global property for platform resolution
528+
foreach ((ConfigurationMetadata, ConfigurationMetadata) graphFromSolutionEdge in graphFromSolutionEdges.Keys)
529+
{
530+
graphFromSolutionEdge.Item1.GlobalProperties.ShouldContainKey("CurrentSolutionConfigurationContents");
531+
graphFromSolutionEdge.Item2.GlobalProperties.ShouldContainKey("CurrentSolutionConfigurationContents");
532+
}
533+
534+
// Remove CurrentSolutionConfigurationContents for comparison purposes. This is done as a separate pass since some edges may be sharing an instance.
535+
foreach ((ConfigurationMetadata, ConfigurationMetadata) graphFromSolutionEdge in graphFromSolutionEdges.Keys)
536+
{
537+
graphFromSolutionEdge.Item1.GlobalProperties.Remove("CurrentSolutionConfigurationContents");
538+
graphFromSolutionEdge.Item2.GlobalProperties.Remove("CurrentSolutionConfigurationContents");
539+
}
540+
526541
// Original edges get preserved.
527542
foreach (var graphEdge in graphEdges)
528543
{

src/Build.UnitTests/Graph/ProjectGraph_Tests.cs

+125
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.IO;
88
using System.Linq;
99
using System.Text.RegularExpressions;
10+
using Microsoft.Build.Construction;
1011
using Microsoft.Build.Evaluation;
1112
using Microsoft.Build.Exceptions;
1213
using Microsoft.Build.Execution;
@@ -674,6 +675,130 @@ public void ConstructGraphWithDifferentEntryPointsAndGraphRoots()
674675
}
675676
}
676677

678+
[Fact]
679+
public void ConstructGraphWithSolution()
680+
{
681+
// This test exercises two key features of solution-based builds from AssignProjectConfiguration:
682+
// 1. Adding synthetic project references
683+
// 2. Resolving project configuration based on the sln
684+
using (var env = TestEnvironment.Create())
685+
{
686+
const string SolutionFileContents = """
687+
Microsoft Visual Studio Solution File, Format Version 12.00
688+
# Visual Studio Version 17
689+
VisualStudioVersion = 17.0.31903.59
690+
MinimumVisualStudioVersion = 17.0.31903.59
691+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "Project1.csproj", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
692+
ProjectSection(ProjectDependencies) = postProject
693+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98} = {52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}
694+
EndProjectSection
695+
EndProject
696+
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Project2", "Project2.vcxproj", "{D638A8EF-3A48-45F2-913C-88B29FED03CB}"
697+
EndProject
698+
Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Project3", "Project3.vcxproj", "{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}"
699+
EndProject
700+
Global
701+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
702+
Debug|Win32 = Debug|Win32
703+
Debug|x64 = Debug|x64
704+
Debug|x86 = Debug|x86
705+
Release|Win32 = Release|Win32
706+
Release|x64 = Release|x64
707+
Release|x86 = Release|x86
708+
EndGlobalSection
709+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
710+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|Win32.ActiveCfg = Debug|x86
711+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|Win32.Build.0 = Debug|x86
712+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
713+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
714+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x86.ActiveCfg = Debug|x86
715+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x86.Build.0 = Debug|x86
716+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|Win32.ActiveCfg = Release|x86
717+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|Win32.Build.0 = Release|x86
718+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
719+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
720+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x86.ActiveCfg = Release|x86
721+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x86.Build.0 = Release|x86
722+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|Win32.ActiveCfg = Debug|Win32
723+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|Win32.Build.0 = Debug|Win32
724+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|x64.ActiveCfg = Debug|x64
725+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Debug|x64.Build.0 = Debug|x64
726+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|Win32.ActiveCfg = Release|Win32
727+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|Win32.Build.0 = Release|Win32
728+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|x64.ActiveCfg = Release|x64
729+
{D638A8EF-3A48-45F2-913C-88B29FED03CB}.Release|x64.Build.0 = Release|x64
730+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.ActiveCfg = Debug|Win32
731+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.Build.0 = Debug|Win32
732+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.ActiveCfg = Debug|x64
733+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.Build.0 = Debug|x64
734+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.ActiveCfg = Debug|Win32
735+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.Build.0 = Debug|Win32
736+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.ActiveCfg = Release|Win32
737+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.Build.0 = Release|Win32
738+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.ActiveCfg = Release|x64
739+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.Build.0 = Release|x64
740+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.ActiveCfg = Release|Win32
741+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.Build.0 = Release|Win32
742+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.ActiveCfg = Debug|Win32
743+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|Win32.Build.0 = Debug|Win32
744+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.ActiveCfg = Debug|x64
745+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x64.Build.0 = Debug|x64
746+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.ActiveCfg = Debug|Win32
747+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Debug|x86.Build.0 = Debug|Win32
748+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.ActiveCfg = Release|Win32
749+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|Win32.Build.0 = Release|Win32
750+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.ActiveCfg = Release|x64
751+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x64.Build.0 = Release|x64
752+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.ActiveCfg = Release|Win32
753+
{52B2ED64-1CFC-401B-8C5B-6D1E1DEADF98}.Release|x86.Build.0 = Release|Win32
754+
EndGlobalSection
755+
GlobalSection(SolutionProperties) = preSolution
756+
HideSolutionNode = FALSE
757+
EndGlobalSection
758+
EndGlobal
759+
""";
760+
TransientTestFile slnFile = env.CreateFile(@"Solution.sln", SolutionFileContents);
761+
SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
762+
763+
ProjectRootElement project1Xml = ProjectRootElement.Create();
764+
765+
// Project 1 depends on Project 2 using ProjectReference but there is a sln-based dependency defined on Project 3 as well.
766+
project1Xml.AddItem("ProjectReference", "Project2.vcxproj");
767+
768+
ProjectRootElement project2Xml = ProjectRootElement.Create();
769+
ProjectRootElement project3Xml = ProjectRootElement.Create();
770+
771+
string project1Path = Path.Combine(env.DefaultTestDirectory.Path, "Project1.csproj");
772+
string project2Path = Path.Combine(env.DefaultTestDirectory.Path, "Project2.vcxproj");
773+
string project3Path = Path.Combine(env.DefaultTestDirectory.Path, "Project3.vcxproj");
774+
775+
project1Xml.Save(project1Path);
776+
project2Xml.Save(project2Path);
777+
project3Xml.Save(project3Path);
778+
779+
var projectGraph = new ProjectGraph(slnFile.Path);
780+
projectGraph.EntryPointNodes.Count.ShouldBe(3);
781+
projectGraph.GraphRoots.Count.ShouldBe(1);
782+
projectGraph.GraphRoots.First().ProjectInstance.FullPath.ShouldBe(project1Path);
783+
projectGraph.ProjectNodes.Count.ShouldBe(3);
784+
785+
ProjectGraphNode project1Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project1Path);
786+
project1Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
787+
project1Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("x86");
788+
project1Node.ProjectReferences.Count.ShouldBe(2);
789+
790+
ProjectGraphNode project2Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project2Path);
791+
project2Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
792+
project2Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("Win32");
793+
project2Node.ProjectReferences.Count.ShouldBe(0);
794+
795+
ProjectGraphNode project3Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project3Path);
796+
project3Node.ProjectInstance.GlobalProperties["Configuration"].ShouldBe("Debug");
797+
project3Node.ProjectInstance.GlobalProperties["Platform"].ShouldBe("Win32");
798+
project3Node.ProjectReferences.Count.ShouldBe(0);
799+
}
800+
}
801+
677802
[Fact]
678803
public void GetTargetListsAggregatesFromMultipleEdges()
679804
{

src/Build/Construction/Solution/SolutionProjectGenerator.cs

+19-13
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ internal static void AddPropertyGroupForSolutionConfiguration(ProjectRootElement
239239
msbuildProject.AppendChild(solutionConfigurationProperties);
240240
solutionConfigurationProperties.Condition = GetConditionStringForConfiguration(solutionConfiguration);
241241

242+
string escapedSolutionConfigurationContents = GetSolutionConfiguration(solutionFile, solutionConfiguration);
243+
244+
solutionConfigurationProperties.AddProperty("CurrentSolutionConfigurationContents", escapedSolutionConfigurationContents);
245+
246+
msbuildProject.AddItem(
247+
"SolutionConfiguration",
248+
solutionConfiguration.FullName,
249+
new Dictionary<string, string>
250+
{
251+
{ "Configuration", solutionConfiguration.ConfigurationName },
252+
{ "Platform", solutionConfiguration.PlatformName },
253+
{ "Content", escapedSolutionConfigurationContents },
254+
});
255+
}
256+
257+
internal static string GetSolutionConfiguration(SolutionFile solutionFile, SolutionConfigurationInSolution solutionConfiguration)
258+
{
242259
var solutionConfigurationContents = new StringBuilder(1024);
243260
var settings = new XmlWriterSettings
244261
{
@@ -292,19 +309,8 @@ internal static void AddPropertyGroupForSolutionConfiguration(ProjectRootElement
292309
xw.WriteEndElement(); // </SolutionConfiguration>
293310
}
294311

295-
var escapedSolutionConfigurationContents = EscapingUtilities.Escape(solutionConfigurationContents.ToString());
296-
297-
solutionConfigurationProperties.AddProperty("CurrentSolutionConfigurationContents", escapedSolutionConfigurationContents);
298-
299-
msbuildProject.AddItem(
300-
"SolutionConfiguration",
301-
solutionConfiguration.FullName,
302-
new Dictionary<string, string>
303-
{
304-
{ "Configuration", solutionConfiguration.ConfigurationName },
305-
{ "Platform", solutionConfiguration.PlatformName },
306-
{ "Content", escapedSolutionConfigurationContents },
307-
});
312+
string escapedSolutionConfigurationContents = EscapingUtilities.Escape(solutionConfigurationContents.ToString());
313+
return escapedSolutionConfigurationContents;
308314
}
309315

310316
/// <summary>

0 commit comments

Comments
 (0)