Skip to content

Commit c1c863e

Browse files
authored
Handle solution traversal targets in graph builds (#9985)
* Handle solution traversal targets in graph builds * PR feedback
1 parent 0c36724 commit c1c863e

File tree

5 files changed

+266
-25
lines changed

5 files changed

+266
-25
lines changed

src/Build.UnitTests/Construction/SolutionProjectGenerator_Tests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2299,6 +2299,50 @@ public void CustomTargetNamesAreInInMetaproj()
22992299
Assert.Single(instances[0].Targets.Where(target => String.Equals(target.Value.Name, "Six", StringComparison.OrdinalIgnoreCase)));
23002300
}
23012301

2302+
/// <summary>
2303+
/// Verifies that disambiguated target names are used when a project name matches a standard solution entry point.
2304+
/// </summary>
2305+
[Fact]
2306+
public void DisambiguatedTargetNamesAreInInMetaproj()
2307+
{
2308+
foreach(string projectName in ProjectInSolution.projectNamesToDisambiguate)
2309+
{
2310+
SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(
2311+
$$"""
2312+
Microsoft Visual Studio Solution File, Format Version 14.00
2313+
# Visual Studio 2015
2314+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "{{projectName}}", "{{projectName}}.csproj", "{6185CC21-BE89-448A-B3C0-D1C27112E595}"
2315+
EndProject
2316+
Global
2317+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2318+
Release|Any CPU = Release|Any CPU
2319+
EndGlobalSection
2320+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2321+
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2322+
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU
2323+
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU
2324+
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU
2325+
EndGlobalSection
2326+
EndGlobal
2327+
""");
2328+
2329+
ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, CreateMockLoggingService(), null);
2330+
2331+
foreach (string targetName in ProjectInSolution.projectNamesToDisambiguate)
2332+
{
2333+
// The entry point still exists normally.
2334+
Assert.True(instances[0].Targets.ContainsKey(targetName));
2335+
2336+
// The traversal target should be disambiguated with a "Solution:" prefix.
2337+
// Note: The default targets are used instead of "Build".
2338+
string traversalTargetName = targetName.Equals("Build", StringComparison.OrdinalIgnoreCase)
2339+
? $"Solution:{projectName}"
2340+
: $"Solution:{projectName}:{targetName}";
2341+
Assert.True(instances[0].Targets.ContainsKey(traversalTargetName));
2342+
}
2343+
}
2344+
}
2345+
23022346
/// <summary>
23032347
/// Verifies that illegal user target names (the ones already used internally) don't crash the SolutionProjectGenerator
23042348
/// </summary>

src/Build.UnitTests/Graph/ProjectGraph_Tests.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2808,6 +2808,119 @@ public void MultitargettingTargetsWithBuildProjectReferencesFalse()
28082808
}
28092809
}
28102810

2811+
[Theory]
2812+
// Built-in targets
2813+
[InlineData(new string[0], new[] { "Project1Default" }, new[] { "Project2Default" })]
2814+
[InlineData(new[] { "Build" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
2815+
[InlineData(new[] { "Rebuild" }, new[] { "Rebuild" }, new[] { "Rebuild" })]
2816+
[InlineData(new[] { "Clean" }, new[] { "Clean" }, new[] { "Clean" })]
2817+
[InlineData(new[] { "Publish" }, new[] { "Publish" }, new[] { "Publish" })]
2818+
// Traversal targets
2819+
[InlineData(new[] { "Project1" }, new[] { "Project1Default" }, new string[0])]
2820+
[InlineData(new[] { "Project2" }, new string[0], new[] { "Project2Default" })]
2821+
[InlineData(new[] { "Project1", "Project2" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
2822+
[InlineData(new[] { "Project1:Rebuild" }, new[] { "Rebuild" }, new string[0])]
2823+
[InlineData(new[] { "Project2:Rebuild" }, new string[0], new[] { "Rebuild" })]
2824+
[InlineData(new[] { "Project1:Rebuild", "Project2:Clean" }, new[] { "Rebuild" }, new[] { "Clean" })]
2825+
[InlineData(new[] { "CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
2826+
[InlineData(new[] { "Project1:CustomTarget" }, new[] { "CustomTarget" }, new string[0])]
2827+
[InlineData(new[] { "Project2:CustomTarget" }, new string[0], new[] { "CustomTarget" })]
2828+
[InlineData(new[] { "Project1:CustomTarget", "Project2:CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
2829+
public void GetTargetListsWithSolution(string[] entryTargets, string[] expectedProject1Targets, string[] expectedProject2Targets)
2830+
{
2831+
using (var env = TestEnvironment.Create())
2832+
{
2833+
const string ExtraContent = """
2834+
<Target Name="CustomTarget" />
2835+
""";
2836+
TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1, defaultTargets: "Project1Default", extraContent: ExtraContent);
2837+
TransientTestFile project2File = CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "Project2Default", extraContent: ExtraContent);
2838+
2839+
string solutionFileContents = $$"""
2840+
Microsoft Visual Studio Solution File, Format Version 12.00
2841+
# Visual Studio Version 17
2842+
VisualStudioVersion = 17.0.31903.59
2843+
MinimumVisualStudioVersion = 17.0.31903.59
2844+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
2845+
EndProject
2846+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project2", "{{project2File.Path}}", "{2022C11A-1405-4983-BEC2-3A8B0233108F}"
2847+
EndProject
2848+
Global
2849+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2850+
Debug|x64 = Debug|x64
2851+
Release|x64 = Release|x64
2852+
EndGlobalSection
2853+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2854+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
2855+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
2856+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
2857+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
2858+
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.ActiveCfg = Debug|x64
2859+
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.Build.0 = Debug|x64
2860+
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.ActiveCfg = Release|x64
2861+
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.Build.0 = Release|x64
2862+
EndGlobalSection
2863+
GlobalSection(SolutionProperties) = preSolution
2864+
HideSolutionNode = FALSE
2865+
EndGlobalSection
2866+
EndGlobal
2867+
""";
2868+
TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
2869+
SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
2870+
2871+
ProjectGraph projectGraph = new(slnFile.Path);
2872+
ProjectGraphNode project1Node = GetFirstNodeWithProjectNumber(projectGraph, 1);
2873+
ProjectGraphNode project2Node = GetFirstNodeWithProjectNumber(projectGraph, 2);
2874+
2875+
IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(entryTargets);
2876+
targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
2877+
targetLists[project1Node].ShouldBe(expectedProject1Targets);
2878+
targetLists[project2Node].ShouldBe(expectedProject2Targets);
2879+
}
2880+
}
2881+
2882+
[Theory]
2883+
[InlineData("Project1:Build")]
2884+
[InlineData("Project1:")]
2885+
public void GetTargetListsWithSolutionInvalidTargets(string entryTarget)
2886+
{
2887+
using (var env = TestEnvironment.Create())
2888+
{
2889+
TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1);
2890+
string solutionFileContents = $$"""
2891+
Microsoft Visual Studio Solution File, Format Version 12.00
2892+
# Visual Studio Version 17
2893+
VisualStudioVersion = 17.0.31903.59
2894+
MinimumVisualStudioVersion = 17.0.31903.59
2895+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
2896+
EndProject
2897+
Global
2898+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2899+
Debug|x64 = Debug|x64
2900+
Release|x64 = Release|x64
2901+
EndGlobalSection
2902+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2903+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
2904+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
2905+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
2906+
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
2907+
EndGlobalSection
2908+
GlobalSection(SolutionProperties) = preSolution
2909+
HideSolutionNode = FALSE
2910+
EndGlobalSection
2911+
EndGlobal
2912+
""";
2913+
TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
2914+
SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);
2915+
2916+
ProjectGraph projectGraph = new(slnFile.Path);
2917+
2918+
var getTargetListsFunc = (() => projectGraph.GetTargetLists([entryTarget]));
2919+
InvalidProjectFileException exception = getTargetListsFunc.ShouldThrow<InvalidProjectFileException>();
2920+
exception.Message.ShouldContain($"The target \"{entryTarget}\" does not exist in the project.");
2921+
}
2922+
}
2923+
28112924
public void Dispose()
28122925
{
28132926
_env.Dispose();

src/Build/BackEnd/BuildManager/BuildManager.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,11 +1965,10 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)
19651965

19661966
// Non-graph builds verify this in RequestBuilder, but for graph builds we need to disambiguate
19671967
// between entry nodes and other nodes in the graph since only entry nodes should error. Just do
1968-
// the verification expicitly before the build even starts.
1968+
// the verification explicitly before the build even starts.
19691969
foreach (ProjectGraphNode entryPointNode in projectGraph.EntryPointNodes)
19701970
{
1971-
ImmutableList<string> targetList = targetsPerNode[entryPointNode];
1972-
ProjectErrorUtilities.VerifyThrowInvalidProject(targetList.Count > 0, entryPointNode.ProjectInstance.ProjectFileLocation, "NoTargetSpecified");
1971+
ProjectErrorUtilities.VerifyThrowInvalidProject(entryPointNode.ProjectInstance.Targets.Count > 0, entryPointNode.ProjectInstance.ProjectFileLocation, "NoTargetSpecified");
19731972
}
19741973

19751974
resultsPerNode = BuildGraph(projectGraph, targetsPerNode, submission.BuildRequestData);

src/Build/Graph/GraphBuilder.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ internal class GraphBuilder
3838

3939
public GraphEdges Edges { get; private set; }
4040

41+
public SolutionFile Solution { get; private set; }
42+
4143
private readonly List<ConfigurationMetadata> _entryPointConfigurationMetadata;
4244

4345
private readonly ParallelWorkSet<ConfigurationMetadata, ParsedProject> _graphWorkSet;
@@ -269,43 +271,43 @@ private static void AddEdgesFromSolution(IReadOnlyDictionary<ConfigurationMetada
269271
solutionGlobalPropertiesBuilder.AddRange(solutionEntryPoint.GlobalProperties);
270272
}
271273

272-
var solution = SolutionFile.Parse(solutionEntryPoint.ProjectFile);
274+
Solution = SolutionFile.Parse(solutionEntryPoint.ProjectFile);
273275

274-
if (solution.SolutionParserWarnings.Count != 0 || solution.SolutionParserErrorCodes.Count != 0)
276+
if (Solution.SolutionParserWarnings.Count != 0 || Solution.SolutionParserErrorCodes.Count != 0)
275277
{
276278
throw new InvalidProjectFileException(
277279
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
278280
"StaticGraphSolutionLoaderEncounteredSolutionWarningsAndErrors",
279281
solutionEntryPoint.ProjectFile,
280-
string.Join(";", solution.SolutionParserWarnings),
281-
string.Join(";", solution.SolutionParserErrorCodes)));
282+
string.Join(";", Solution.SolutionParserWarnings),
283+
string.Join(";", Solution.SolutionParserErrorCodes)));
282284
}
283285

284286
// Mimic behavior of SolutionProjectGenerator
285-
SolutionConfigurationInSolution currentSolutionConfiguration = SelectSolutionConfiguration(solution, solutionEntryPoint.GlobalProperties);
287+
SolutionConfigurationInSolution currentSolutionConfiguration = SelectSolutionConfiguration(Solution, solutionEntryPoint.GlobalProperties);
286288
solutionGlobalPropertiesBuilder["Configuration"] = currentSolutionConfiguration.ConfigurationName;
287289
solutionGlobalPropertiesBuilder["Platform"] = currentSolutionConfiguration.PlatformName;
288290

289-
string solutionConfigurationXml = SolutionProjectGenerator.GetSolutionConfiguration(solution, currentSolutionConfiguration);
291+
string solutionConfigurationXml = SolutionProjectGenerator.GetSolutionConfiguration(Solution, currentSolutionConfiguration);
290292
solutionGlobalPropertiesBuilder["CurrentSolutionConfigurationContents"] = solutionConfigurationXml;
291293
solutionGlobalPropertiesBuilder["BuildingSolutionFile"] = "true";
292294

293-
string solutionDirectoryName = solution.SolutionFileDirectory;
295+
string solutionDirectoryName = Solution.SolutionFileDirectory;
294296
if (!solutionDirectoryName.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
295297
{
296298
solutionDirectoryName += Path.DirectorySeparatorChar;
297299
}
298300

299301
solutionGlobalPropertiesBuilder["SolutionDir"] = EscapingUtilities.Escape(solutionDirectoryName);
300-
solutionGlobalPropertiesBuilder["SolutionExt"] = EscapingUtilities.Escape(Path.GetExtension(solution.FullPath));
301-
solutionGlobalPropertiesBuilder["SolutionFileName"] = EscapingUtilities.Escape(Path.GetFileName(solution.FullPath));
302-
solutionGlobalPropertiesBuilder["SolutionName"] = EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(solution.FullPath));
303-
solutionGlobalPropertiesBuilder[SolutionProjectGenerator.SolutionPathPropertyName] = EscapingUtilities.Escape(Path.Combine(solution.SolutionFileDirectory, Path.GetFileName(solution.FullPath)));
302+
solutionGlobalPropertiesBuilder["SolutionExt"] = EscapingUtilities.Escape(Path.GetExtension(Solution.FullPath));
303+
solutionGlobalPropertiesBuilder["SolutionFileName"] = EscapingUtilities.Escape(Path.GetFileName(Solution.FullPath));
304+
solutionGlobalPropertiesBuilder["SolutionName"] = EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(Solution.FullPath));
305+
solutionGlobalPropertiesBuilder[SolutionProjectGenerator.SolutionPathPropertyName] = EscapingUtilities.Escape(Path.Combine(Solution.SolutionFileDirectory, Path.GetFileName(Solution.FullPath)));
304306

305307
// Project configurations are reused heavily, so cache the global properties for each
306308
Dictionary<string, ImmutableDictionary<string, string>> globalPropertiesForProjectConfiguration = new(StringComparer.OrdinalIgnoreCase);
307309

308-
IReadOnlyList<ProjectInSolution> projectsInSolution = solution.ProjectsInOrder;
310+
IReadOnlyList<ProjectInSolution> projectsInSolution = Solution.ProjectsInOrder;
309311
List<ProjectGraphEntryPoint> newEntryPoints = new(projectsInSolution.Count);
310312
Dictionary<string, IReadOnlyCollection<string>> solutionDependencies = new();
311313

@@ -318,7 +320,7 @@ private static void AddEdgesFromSolution(IReadOnlyDictionary<ConfigurationMetada
318320

319321
ProjectConfigurationInSolution projectConfiguration = SelectProjectConfiguration(currentSolutionConfiguration, project.ProjectConfigurations);
320322

321-
if (!SolutionProjectGenerator.WouldProjectBuild(solution, currentSolutionConfiguration.FullName, project, projectConfiguration))
323+
if (!SolutionProjectGenerator.WouldProjectBuild(Solution, currentSolutionConfiguration.FullName, project, projectConfiguration))
322324
{
323325
continue;
324326
}
@@ -341,11 +343,11 @@ private static void AddEdgesFromSolution(IReadOnlyDictionary<ConfigurationMetada
341343
List<string> solutionDependenciesForProject = new(project.Dependencies.Count);
342344
foreach (string dependencyProjectGuid in project.Dependencies)
343345
{
344-
if (!solution.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out ProjectInSolution dependencyProject))
346+
if (!Solution.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out ProjectInSolution dependencyProject))
345347
{
346348
ProjectFileErrorUtilities.ThrowInvalidProjectFile(
347349
"SubCategoryForSolutionParsingErrors",
348-
new BuildEventFileInfo(solution.FullPath),
350+
new BuildEventFileInfo(Solution.FullPath),
349351
"SolutionParseProjectDepNotFoundError",
350352
project.ProjectGuid,
351353
dependencyProjectGuid);

0 commit comments

Comments
 (0)