Skip to content

Commit

Permalink
Handle solution traversal targets in graph builds (#9985)
Browse files Browse the repository at this point in the history
* Handle solution traversal targets in graph builds

* PR feedback
  • Loading branch information
dfederm committed Apr 19, 2024
1 parent 0c36724 commit c1c863e
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 25 deletions.
44 changes: 44 additions & 0 deletions src/Build.UnitTests/Construction/SolutionProjectGenerator_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,50 @@ public void CustomTargetNamesAreInInMetaproj()
Assert.Single(instances[0].Targets.Where(target => String.Equals(target.Value.Name, "Six", StringComparison.OrdinalIgnoreCase)));
}

/// <summary>
/// Verifies that disambiguated target names are used when a project name matches a standard solution entry point.
/// </summary>
[Fact]
public void DisambiguatedTargetNamesAreInInMetaproj()
{
foreach(string projectName in ProjectInSolution.projectNamesToDisambiguate)
{
SolutionFile solution = SolutionFile_Tests.ParseSolutionHelper(
$$"""
Microsoft Visual Studio Solution File, Format Version 14.00
# Visual Studio 2015
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "{{projectName}}", "{{projectName}}.csproj", "{6185CC21-BE89-448A-B3C0-D1C27112E595}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6185CC21-BE89-448A-B3C0-D1C27112E595}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
""");

ProjectInstance[] instances = SolutionProjectGenerator.Generate(solution, null, null, BuildEventContext.Invalid, CreateMockLoggingService(), null);

foreach (string targetName in ProjectInSolution.projectNamesToDisambiguate)
{
// The entry point still exists normally.
Assert.True(instances[0].Targets.ContainsKey(targetName));

// The traversal target should be disambiguated with a "Solution:" prefix.
// Note: The default targets are used instead of "Build".
string traversalTargetName = targetName.Equals("Build", StringComparison.OrdinalIgnoreCase)
? $"Solution:{projectName}"
: $"Solution:{projectName}:{targetName}";
Assert.True(instances[0].Targets.ContainsKey(traversalTargetName));
}
}
}

/// <summary>
/// Verifies that illegal user target names (the ones already used internally) don't crash the SolutionProjectGenerator
/// </summary>
Expand Down
113 changes: 113 additions & 0 deletions src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2808,6 +2808,119 @@ public void MultitargettingTargetsWithBuildProjectReferencesFalse()
}
}

[Theory]
// Built-in targets
[InlineData(new string[0], new[] { "Project1Default" }, new[] { "Project2Default" })]
[InlineData(new[] { "Build" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
[InlineData(new[] { "Rebuild" }, new[] { "Rebuild" }, new[] { "Rebuild" })]
[InlineData(new[] { "Clean" }, new[] { "Clean" }, new[] { "Clean" })]
[InlineData(new[] { "Publish" }, new[] { "Publish" }, new[] { "Publish" })]
// Traversal targets
[InlineData(new[] { "Project1" }, new[] { "Project1Default" }, new string[0])]
[InlineData(new[] { "Project2" }, new string[0], new[] { "Project2Default" })]
[InlineData(new[] { "Project1", "Project2" }, new[] { "Project1Default" }, new[] { "Project2Default" })]
[InlineData(new[] { "Project1:Rebuild" }, new[] { "Rebuild" }, new string[0])]
[InlineData(new[] { "Project2:Rebuild" }, new string[0], new[] { "Rebuild" })]
[InlineData(new[] { "Project1:Rebuild", "Project2:Clean" }, new[] { "Rebuild" }, new[] { "Clean" })]
[InlineData(new[] { "CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
[InlineData(new[] { "Project1:CustomTarget" }, new[] { "CustomTarget" }, new string[0])]
[InlineData(new[] { "Project2:CustomTarget" }, new string[0], new[] { "CustomTarget" })]
[InlineData(new[] { "Project1:CustomTarget", "Project2:CustomTarget" }, new[] { "CustomTarget" }, new[] { "CustomTarget" })]
public void GetTargetListsWithSolution(string[] entryTargets, string[] expectedProject1Targets, string[] expectedProject2Targets)
{
using (var env = TestEnvironment.Create())
{
const string ExtraContent = """
<Target Name="CustomTarget" />
""";
TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1, defaultTargets: "Project1Default", extraContent: ExtraContent);
TransientTestFile project2File = CreateProjectFile(env: env, projectNumber: 2, defaultTargets: "Project2Default", extraContent: ExtraContent);

string solutionFileContents = $$"""
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 17.0.31903.59
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project2", "{{project2File.Path}}", "{2022C11A-1405-4983-BEC2-3A8B0233108F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.ActiveCfg = Debug|x64
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Debug|x64.Build.0 = Debug|x64
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.ActiveCfg = Release|x64
{2022C11A-1405-4983-BEC2-3A8B0233108F}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
""";
TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);

ProjectGraph projectGraph = new(slnFile.Path);
ProjectGraphNode project1Node = GetFirstNodeWithProjectNumber(projectGraph, 1);
ProjectGraphNode project2Node = GetFirstNodeWithProjectNumber(projectGraph, 2);

IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetLists = projectGraph.GetTargetLists(entryTargets);
targetLists.Count.ShouldBe(projectGraph.ProjectNodes.Count);
targetLists[project1Node].ShouldBe(expectedProject1Targets);
targetLists[project2Node].ShouldBe(expectedProject2Targets);
}
}

[Theory]
[InlineData("Project1:Build")]
[InlineData("Project1:")]
public void GetTargetListsWithSolutionInvalidTargets(string entryTarget)
{
using (var env = TestEnvironment.Create())
{
TransientTestFile project1File = CreateProjectFile(env: env, projectNumber: 1);
string solutionFileContents = $$"""
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 17.0.31903.59
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project1", "{{project1File.Path}}", "{8761499A-7280-43C4-A32F-7F41C47CA6DF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.ActiveCfg = Debug|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Debug|x64.Build.0 = Debug|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.ActiveCfg = Release|x64
{8761499A-7280-43C4-A32F-7F41C47CA6DF}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
""";
TransientTestFile slnFile = env.CreateFile(@"Solution.sln", solutionFileContents);
SolutionFile solutionFile = SolutionFile.Parse(slnFile.Path);

ProjectGraph projectGraph = new(slnFile.Path);

var getTargetListsFunc = (() => projectGraph.GetTargetLists([entryTarget]));
InvalidProjectFileException exception = getTargetListsFunc.ShouldThrow<InvalidProjectFileException>();
exception.Message.ShouldContain($"The target \"{entryTarget}\" does not exist in the project.");
}
}

public void Dispose()
{
_env.Dispose();
Expand Down
5 changes: 2 additions & 3 deletions src/Build/BackEnd/BuildManager/BuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1965,11 +1965,10 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)

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

resultsPerNode = BuildGraph(projectGraph, targetsPerNode, submission.BuildRequestData);
Expand Down
32 changes: 17 additions & 15 deletions src/Build/Graph/GraphBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ internal class GraphBuilder

public GraphEdges Edges { get; private set; }

public SolutionFile Solution { get; private set; }

private readonly List<ConfigurationMetadata> _entryPointConfigurationMetadata;

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

var solution = SolutionFile.Parse(solutionEntryPoint.ProjectFile);
Solution = SolutionFile.Parse(solutionEntryPoint.ProjectFile);

if (solution.SolutionParserWarnings.Count != 0 || solution.SolutionParserErrorCodes.Count != 0)
if (Solution.SolutionParserWarnings.Count != 0 || Solution.SolutionParserErrorCodes.Count != 0)
{
throw new InvalidProjectFileException(
ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword(
"StaticGraphSolutionLoaderEncounteredSolutionWarningsAndErrors",
solutionEntryPoint.ProjectFile,
string.Join(";", solution.SolutionParserWarnings),
string.Join(";", solution.SolutionParserErrorCodes)));
string.Join(";", Solution.SolutionParserWarnings),
string.Join(";", Solution.SolutionParserErrorCodes)));
}

// Mimic behavior of SolutionProjectGenerator
SolutionConfigurationInSolution currentSolutionConfiguration = SelectSolutionConfiguration(solution, solutionEntryPoint.GlobalProperties);
SolutionConfigurationInSolution currentSolutionConfiguration = SelectSolutionConfiguration(Solution, solutionEntryPoint.GlobalProperties);
solutionGlobalPropertiesBuilder["Configuration"] = currentSolutionConfiguration.ConfigurationName;
solutionGlobalPropertiesBuilder["Platform"] = currentSolutionConfiguration.PlatformName;

string solutionConfigurationXml = SolutionProjectGenerator.GetSolutionConfiguration(solution, currentSolutionConfiguration);
string solutionConfigurationXml = SolutionProjectGenerator.GetSolutionConfiguration(Solution, currentSolutionConfiguration);
solutionGlobalPropertiesBuilder["CurrentSolutionConfigurationContents"] = solutionConfigurationXml;
solutionGlobalPropertiesBuilder["BuildingSolutionFile"] = "true";

string solutionDirectoryName = solution.SolutionFileDirectory;
string solutionDirectoryName = Solution.SolutionFileDirectory;
if (!solutionDirectoryName.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
solutionDirectoryName += Path.DirectorySeparatorChar;
}

solutionGlobalPropertiesBuilder["SolutionDir"] = EscapingUtilities.Escape(solutionDirectoryName);
solutionGlobalPropertiesBuilder["SolutionExt"] = EscapingUtilities.Escape(Path.GetExtension(solution.FullPath));
solutionGlobalPropertiesBuilder["SolutionFileName"] = EscapingUtilities.Escape(Path.GetFileName(solution.FullPath));
solutionGlobalPropertiesBuilder["SolutionName"] = EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(solution.FullPath));
solutionGlobalPropertiesBuilder[SolutionProjectGenerator.SolutionPathPropertyName] = EscapingUtilities.Escape(Path.Combine(solution.SolutionFileDirectory, Path.GetFileName(solution.FullPath)));
solutionGlobalPropertiesBuilder["SolutionExt"] = EscapingUtilities.Escape(Path.GetExtension(Solution.FullPath));
solutionGlobalPropertiesBuilder["SolutionFileName"] = EscapingUtilities.Escape(Path.GetFileName(Solution.FullPath));
solutionGlobalPropertiesBuilder["SolutionName"] = EscapingUtilities.Escape(Path.GetFileNameWithoutExtension(Solution.FullPath));
solutionGlobalPropertiesBuilder[SolutionProjectGenerator.SolutionPathPropertyName] = EscapingUtilities.Escape(Path.Combine(Solution.SolutionFileDirectory, Path.GetFileName(Solution.FullPath)));

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

IReadOnlyList<ProjectInSolution> projectsInSolution = solution.ProjectsInOrder;
IReadOnlyList<ProjectInSolution> projectsInSolution = Solution.ProjectsInOrder;
List<ProjectGraphEntryPoint> newEntryPoints = new(projectsInSolution.Count);
Dictionary<string, IReadOnlyCollection<string>> solutionDependencies = new();

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

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

if (!SolutionProjectGenerator.WouldProjectBuild(solution, currentSolutionConfiguration.FullName, project, projectConfiguration))
if (!SolutionProjectGenerator.WouldProjectBuild(Solution, currentSolutionConfiguration.FullName, project, projectConfiguration))
{
continue;
}
Expand All @@ -341,11 +343,11 @@ private static void AddEdgesFromSolution(IReadOnlyDictionary<ConfigurationMetada
List<string> solutionDependenciesForProject = new(project.Dependencies.Count);
foreach (string dependencyProjectGuid in project.Dependencies)
{
if (!solution.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out ProjectInSolution dependencyProject))
if (!Solution.ProjectsByGuid.TryGetValue(dependencyProjectGuid, out ProjectInSolution dependencyProject))
{
ProjectFileErrorUtilities.ThrowInvalidProjectFile(
"SubCategoryForSolutionParsingErrors",
new BuildEventFileInfo(solution.FullPath),
new BuildEventFileInfo(Solution.FullPath),
"SolutionParseProjectDepNotFoundError",
project.ProjectGuid,
dependencyProjectGuid);
Expand Down
Loading

0 comments on commit c1c863e

Please sign in to comment.