Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
66 changes: 66 additions & 0 deletions src/Build.UnitTests/BackEnd/BuildManager_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4496,6 +4496,72 @@ public void GraphBuildShouldBeAbleToConstructGraphButSkipBuild()
logger.FullLog.ShouldContain("3 nodes, 2 edges");
}

[Fact]
public void GraphBuildSolutionExecutesSyntheticSolutionNodeWithoutPublishingItInResults()
{
using TestEnvironment env = TestEnvironment.Create(_output);
using ProjectCollection projectCollection = env.CreateProjectCollection().Collection;

TransientTestFolder root = env.CreateFolder(createFolder: true);
TransientTestFolder projectFolder = env.CreateFolder(Path.Combine(root.Path, "SimpleProject"), createFolder: true);
env.CreateFile(projectFolder, "SimpleProject.csproj",
"""
<Project>
<Target Name="Build">
<Message Text="ProjectBuilt" Importance="High" />
</Target>
</Project>
""");

TransientTestFile solutionFile = env.CreateFile(root, "SimpleProject.sln",
"""
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29326.124
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleProject", "SimpleProject\SimpleProject.csproj", "{79B5EBA6-5D27-4976-BC31-14422245A59A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{79B5EBA6-5D27-4976-BC31-14422245A59A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79B5EBA6-5D27-4976-BC31-14422245A59A}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
""");

env.CreateFile(root, $"after.{Path.GetFileName(solutionFile.Path)}.targets",
"""
<Project>
<Target Name="AfterSolutionHook" AfterTargets="Build">
<Message Text="AfterSolutionHookRan" Importance="High" />
</Target>
</Project>
""");

ProjectGraph graph = new(new ProjectGraphEntryPoint(solutionFile.Path), projectCollection);
graph.EntryPointNodes.Count.ShouldBe(1);
graph.ProjectNodes.Contains(graph.EntryPointNodes.Single()).ShouldBeFalse();

GraphBuildRequestData request = new(graph, Array.Empty<string>(), projectCollection.HostServices);

GraphBuildResult result = _buildManager.Build(_parameters, request);
result.OverallResult.ShouldBe(BuildResultCode.Success);

result.ResultsByNode.Count.ShouldBe(graph.ProjectNodes.Count);
foreach (ProjectGraphNode graphNode in graph.ProjectNodes)
{
result.ResultsByNode.ContainsKey(graphNode).ShouldBeTrue();
}

_logger.AssertLogContains("AfterSolutionHookRan");
}
Comment on lines +4562 to +4563

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would catch my big problem . . .


/// <summary>
/// Helper task used by <see cref="TaskInputLoggingIsExposedToTasks"/> to verify <see cref="TaskLoggingHelper.IsTaskInputLoggingEnabled"/>.
/// </summary>
Expand Down
54 changes: 54 additions & 0 deletions src/Build.UnitTests/BinaryLogger_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,60 @@ public void BinaryLoggerShouldEmbedFilesWithRelativePathFromChildProjects()
generatedFileCount.ShouldBe(2, $"Embedded files: {string.Join(",", zipArchive.Entries)}");
}

[Fact]
public void BinaryLoggerGraphBuildSolutionEmbedsSolutionFile()
{
TransientTestFolder root = _env.CreateFolder(createFolder: true);
TransientTestFolder projectFolder = _env.CreateFolder(Path.Combine(root.Path, "SimpleProject"), createFolder: true);
_env.CreateFile(projectFolder, "SimpleProject.csproj",
"""
<Project>
<Target Name="Build">
<Message Text="ProjectBuilt" Importance="High" />
</Target>
</Project>
""");

TransientTestFile solutionFile = _env.CreateFile(root, "SimpleProject.sln",
"""
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29326.124
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleProject", "SimpleProject\SimpleProject.csproj", "{79B5EBA6-5D27-4976-BC31-14422245A59A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{79B5EBA6-5D27-4976-BC31-14422245A59A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{79B5EBA6-5D27-4976-BC31-14422245A59A}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
""");

RunnerUtilities.ExecMSBuild(
$"\"{solutionFile.Path}\" /graphBuild /bl:\"{_logFile};ProjectImports=ZipFile\"",
out bool success);
success.ShouldBeTrue();

string projectImportsZipPath = Path.ChangeExtension(_logFile, ".ProjectImports.zip");
using var fileStream = new FileStream(projectImportsZipPath, FileMode.Open);
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Read);

string solutionFileName = Path.GetFileName(solutionFile.Path);
ZipArchiveEntry embeddedSolutionEntry = zipArchive.Entries
.FirstOrDefault(entry => entry.Name.Equals(solutionFileName, StringComparison.OrdinalIgnoreCase));

embeddedSolutionEntry.ShouldNotBeNull(
$"Expected solution file '{solutionFileName}' in project imports archive. Embedded files: {string.Join(",", zipArchive.Entries.Select(e => e.FullName))}");
embeddedSolutionEntry.Length.ShouldBeGreaterThan(0);
}

[RequiresSymbolicLinksFact]
public void BinaryLoggerShouldEmbedSymlinkFilesViaTaskOutput()
{
Expand Down
10 changes: 4 additions & 6 deletions src/Build.UnitTests/Graph/GraphLoadedFromSolution_tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -687,15 +687,13 @@ private void AssertSolutionBasedGraph(
// Exactly 1 node per project
graph.ProjectNodes.Count.ShouldBe(graph.ProjectNodes.Select(GetProjectPath).Distinct().Count());

// in the solution, all nodes are entry points
graphFromSolution.EntryPointNodes.Select(GetProjectPath)
.ShouldBeSetEquivalentTo(graph.ProjectNodes.Select(GetProjectPath));
graphFromSolution.EntryPointNodes.Count.ShouldBe(1);
graphFromSolution.EntryPointNodes.First().ProjectInstance.FullPath.ShouldBe(solutionPath);
graphFromSolution.GraphRoots.Count.ShouldBe(1);
graphFromSolution.GraphRoots.First().ProjectInstance.FullPath.ShouldBe(solutionPath);

if (projectConfigurations == null || graphFromSolution.ProjectNodes.All(n => n.ProjectReferences.Count == 0))
{
graphFromSolution.GraphRoots.Select(GetProjectPath)
.ShouldBeSameIgnoringOrder(graph.GraphRoots.Select(GetProjectPath));

graphFromSolution.ProjectNodes.Select(GetProjectPath)
.ShouldBeSameIgnoringOrder(graph.ProjectNodes.Select(GetProjectPath));
}
Expand Down
8 changes: 4 additions & 4 deletions src/Build.UnitTests/Graph/ProjectGraph_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -860,10 +860,10 @@ public void ConstructGraphWithSolution()
project8Xml.Save(project8Path);

var projectGraph = new ProjectGraph(slnFile.Path);
projectGraph.EntryPointNodes.Count.ShouldBe(5);
projectGraph.EntryPointNodes.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project1Path, project2Path, project3Path, project6Path, project8Path }, ignoreOrder: true);
projectGraph.GraphRoots.Count.ShouldBe(2);
projectGraph.GraphRoots.Select(node => node.ProjectInstance.FullPath).ShouldBe(new[] { project1Path, project6Path }, ignoreOrder: true);
projectGraph.EntryPointNodes.Count.ShouldBe(1);
projectGraph.EntryPointNodes.Single().ProjectInstance.FullPath.ShouldBe(slnFile.Path);
projectGraph.GraphRoots.Count.ShouldBe(1);
projectGraph.GraphRoots.Single().ProjectInstance.FullPath.ShouldBe(slnFile.Path);
projectGraph.ProjectNodes.Count.ShouldBe(7);

ProjectGraphNode project1Node = projectGraph.ProjectNodes.Single(node => node.ProjectInstance.FullPath == project1Path);
Expand Down
94 changes: 78 additions & 16 deletions src/Build/BackEnd/BuildManager/BuildManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2279,12 +2279,14 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)
projectGraph.ConstructionMetrics.EdgeCount));

Dictionary<ProjectGraphNode, BuildResult>? resultsPerNode = null;
BuildResult? syntheticSolutionNodeResult = null;

if (submission.BuildRequestData.GraphBuildOptions.Build)
{
_projectCacheService!.InitializePluginsForGraph(projectGraph, submission.BuildRequestData.TargetNames, _executionCancellationTokenSource!.Token);

IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetsPerNode = projectGraph.GetTargetLists(submission.BuildRequestData.TargetNames);
ProjectGraphNode? syntheticSolutionNode = TryGetSyntheticSolutionEntryPointNode(projectGraph);

DumpGraph(projectGraph, targetsPerNode);

Expand All @@ -2293,10 +2295,15 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)
// the verification explicitly before the build even starts.
foreach (ProjectGraphNode entryPointNode in projectGraph.EntryPointNodes)
{
if (ReferenceEquals(entryPointNode, syntheticSolutionNode))
{
continue;
}

ProjectErrorUtilities.VerifyThrowInvalidProject(entryPointNode.ProjectInstance.Targets.Count > 0, entryPointNode.ProjectInstance.ProjectFileLocation, "NoTargetSpecified");
}

resultsPerNode = BuildGraph(projectGraph, targetsPerNode, submission.BuildRequestData);
(resultsPerNode, syntheticSolutionNodeResult) = BuildGraph(projectGraph, targetsPerNode, submission.BuildRequestData, syntheticSolutionNode);
}
else
{
Expand All @@ -2305,11 +2312,18 @@ private void ExecuteGraphBuildScheduler(GraphBuildSubmission submission)

Assumed.Null(submission.BuildResult?.Exception, "Exceptions only get set when the graph submission gets completed with an exception in OnThreadException. That should not happen during graph builds.");

var graphBuildResult = new GraphBuildResult(
submission.SubmissionId,
new ReadOnlyDictionary<ProjectGraphNode, BuildResult>(resultsPerNode ?? new Dictionary<ProjectGraphNode, BuildResult>()));

if (syntheticSolutionNodeResult?.OverallResult == BuildResultCode.Failure)
{
graphBuildResult.Exception = syntheticSolutionNodeResult.Exception
?? new InvalidOperationException("Synthetic solution graph node execution failed.");
}
Comment on lines +2319 to +2323

// The overall submission is complete, so report it as complete
ReportResultsToSubmission<GraphBuildRequestData, GraphBuildResult>(
new GraphBuildResult(
submission.SubmissionId,
new ReadOnlyDictionary<ProjectGraphNode, BuildResult>(resultsPerNode ?? new Dictionary<ProjectGraphNode, BuildResult>())));
ReportResultsToSubmission<GraphBuildRequestData, GraphBuildResult>(graphBuildResult);

static void DumpGraph(ProjectGraph graph, IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>>? targetList = null)
{
Expand All @@ -2324,11 +2338,25 @@ static void DumpGraph(ProjectGraph graph, IReadOnlyDictionary<ProjectGraphNode,
}
}

private static ProjectGraphNode? TryGetSyntheticSolutionEntryPointNode(ProjectGraph projectGraph)
{
if (projectGraph.Solution is null || projectGraph.EntryPointNodes.Count != 1)
{
return null;
}

ProjectGraphNode entryPointNode = projectGraph.EntryPointNodes.First();
return projectGraph.ProjectNodes.Contains(entryPointNode)
? null
: entryPointNode;
}

[RequiresUnreferencedCode("Initializes loggers and project cache plugins by reflecting over assemblies discovered at runtime, which is incompatible with trimming.")]
private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
private (Dictionary<ProjectGraphNode, BuildResult> ResultsPerNode, BuildResult? SyntheticSolutionNodeResult) BuildGraph(
ProjectGraph projectGraph,
IReadOnlyDictionary<ProjectGraphNode, ImmutableList<string>> targetsPerNode,
GraphBuildRequestData graphBuildRequestData)
GraphBuildRequestData graphBuildRequestData,
ProjectGraphNode? syntheticSolutionNode = null)
{
// The handle is used within captured async scope. If error occurs during the build
// and we return from the function before async call signals - it causes unhandled ObjectDisposedException
Expand All @@ -2339,10 +2367,18 @@ private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
var graphBuildStateLock = new object();

var blockedNodes = new HashSet<ProjectGraphNode>(projectGraph.ProjectNodes);
var finishedNodes = new HashSet<ProjectGraphNode>(projectGraph.ProjectNodes.Count);
if (syntheticSolutionNode is not null)
{
blockedNodes.Add(syntheticSolutionNode);
}

var finishedNodes = new HashSet<ProjectGraphNode>(blockedNodes.Count);
var buildingNodes = new Dictionary<BuildSubmissionBase, ProjectGraphNode>();
var resultsPerNode = new Dictionary<ProjectGraphNode, BuildResult>(projectGraph.ProjectNodes.Count);
BuildResult? syntheticSolutionNodeResult = null;
ExceptionDispatchInfo? submissionException = null;
int finishedProjectNodesCount = 0;
int projectNodesCount = projectGraph.ProjectNodes.Count;

while (blockedNodes.Count > 0 || buildingNodes.Count > 0)
{
Expand All @@ -2358,7 +2394,9 @@ private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
lock (graphBuildStateLock)
{
var unblockedNodes = blockedNodes
.Where(node => node.ProjectReferences.All(projectReference => finishedNodes.Contains(projectReference)))
.Where(node =>
node.ProjectReferences.All(projectReference => finishedNodes.Contains(projectReference))
&& (!IsSyntheticSolutionNode(node) || finishedProjectNodesCount == projectNodesCount))
.ToList();
foreach (var node in unblockedNodes)
{
Expand All @@ -2367,18 +2405,31 @@ private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
{
// An empty target list here means "no targets" instead of "default targets", so don't even build it.
finishedNodes.Add(node);
if (!IsSyntheticSolutionNode(node))
{
finishedProjectNodesCount++;
}

blockedNodes.Remove(node);

waitHandle.Set();

continue;
}

var request = new BuildRequestData(
node.ProjectInstance,
targetList.ToArray(),
graphBuildRequestData.HostServices,
graphBuildRequestData.Flags);
BuildRequestData request = IsSyntheticSolutionNode(node)
? new BuildRequestData(
node.ProjectInstance.FullPath,
node.ProjectInstance.GlobalProperties.ToDictionary(kvp => kvp.Key, kvp => (string?)kvp.Value),
toolsVersion: null,
targetList.ToArray(),
graphBuildRequestData.HostServices,
graphBuildRequestData.Flags)
: new BuildRequestData(
node.ProjectInstance,
targetList.ToArray(),
graphBuildRequestData.HostServices,
graphBuildRequestData.Flags);

// TODO Tack onto the existing submission instead of pending a whole new submission for every node
// Among other things, this makes BuildParameters.DetailedSummary produce a summary for each node, which is not desirable.
Expand All @@ -2402,7 +2453,15 @@ private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
finishedNodes.Add(finishedNode);
buildingNodes.Remove(finishedBuildSubmission);

resultsPerNode.Add(finishedNode, finishedBuildSubmission.BuildResult!);
if (IsSyntheticSolutionNode(finishedNode))
{
syntheticSolutionNodeResult = finishedBuildSubmission.BuildResult;
}
else
{
finishedProjectNodesCount++;
resultsPerNode.Add(finishedNode, finishedBuildSubmission.BuildResult!);
}
}

waitHandle.Set();
Expand All @@ -2411,7 +2470,10 @@ private Dictionary<ProjectGraphNode, BuildResult> BuildGraph(
}
}

return resultsPerNode;
return (resultsPerNode, syntheticSolutionNodeResult);

bool IsSyntheticSolutionNode(ProjectGraphNode node) =>
syntheticSolutionNode is not null && ReferenceEquals(node, syntheticSolutionNode);
}

/// <summary>
Expand Down
Loading
Loading