Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1962,11 +1962,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