diff --git a/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs b/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs
index fa121ef..9c1a95d 100644
--- a/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs
+++ b/src/BuildPrediction/Predictors/GetCopyToOutputDirectoryItemsGraphPredictor.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Execution;
using Microsoft.Build.Graph;
@@ -15,9 +16,21 @@ public sealed class GetCopyToOutputDirectoryItemsGraphPredictor : IProjectGraphP
{
internal const string UseCommonOutputDirectoryPropertyName = "UseCommonOutputDirectory";
internal const string OutDirPropertyName = "OutDir";
+ internal const string MSBuildCopyContentTransitivelyPropertyName = "MSBuildCopyContentTransitively";
///
public void PredictInputsAndOutputs(ProjectGraphNode projectGraphNode, ProjectPredictionReporter predictionReporter)
+ {
+ string outDir = projectGraphNode.ProjectInstance.GetPropertyValue(OutDirPropertyName);
+ HashSet visitedNodes = new();
+ PredictInputsAndOutputs(projectGraphNode, outDir, predictionReporter, visitedNodes);
+ }
+
+ private static void PredictInputsAndOutputs(
+ ProjectGraphNode projectGraphNode,
+ string outDir,
+ ProjectPredictionReporter predictionReporter,
+ HashSet visitedNodes)
{
ProjectInstance projectInstance = projectGraphNode.ProjectInstance;
@@ -25,13 +38,22 @@ public void PredictInputsAndOutputs(ProjectGraphNode projectGraphNode, ProjectPr
var useCommonOutputDirectory = projectInstance.GetPropertyValue(UseCommonOutputDirectoryPropertyName);
if (!useCommonOutputDirectory.Equals("true", StringComparison.OrdinalIgnoreCase))
{
- string outDir = projectInstance.GetPropertyValue(OutDirPropertyName);
+ bool copyContentTransitively = projectInstance.GetPropertyValue(MSBuildCopyContentTransitivelyPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase);
- // Note that GetCopyToOutputDirectoryItems effectively only is able to go one project reference deep despite being recursive as
- // it uses @(_MSBuildProjectReferenceExistent) to recurse, which is not set in the recursive calls.
- // See: https://github.com/microsoft/msbuild/blob/master/src/Tasks/Microsoft.Common.CurrentVersion.targets
foreach (ProjectGraphNode dependency in projectGraphNode.ProjectReferences)
{
+ if (!visitedNodes.Add(dependency))
+ {
+ // Avoid duplicate predictions
+ continue;
+ }
+
+ // If transitive, recurse
+ if (copyContentTransitively)
+ {
+ PredictInputsAndOutputs(dependency, outDir, predictionReporter, visitedNodes);
+ }
+
// Process each item type considered in GetCopyToOutputDirectoryItems. Yes, Compile is considered.
ReportCopyToOutputDirectoryItemsAsInputs(dependency.ProjectInstance, ContentItemsPredictor.ContentItemName, outDir, predictionReporter);
ReportCopyToOutputDirectoryItemsAsInputs(dependency.ProjectInstance, EmbeddedResourceItemsPredictor.EmbeddedResourceItemName, outDir, predictionReporter);
diff --git a/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs b/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs
index 0c9019e..607690f 100644
--- a/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs
+++ b/src/BuildPredictionTests/Predictors/GetCopyToOutputDirectoryItemsGraphPredictorTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Construction;
using Microsoft.Build.Prediction.Predictors;
@@ -77,8 +78,10 @@ public void UseCommonOutputDirectory()
.AssertNoPredictions();
}
- [Fact]
- public void WithCopy()
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void WithCopy(bool copyContentTransitively)
{
string projectFile = Path.Combine(_rootDir, @"src\project.csproj");
ProjectRootElement projectRootElement = ProjectRootElement.Create(projectFile);
@@ -89,7 +92,12 @@ public void WithCopy()
ProjectRootElement dep2 = CreateDependencyProject("dep2", shouldCopy);
ProjectRootElement dep3 = CreateDependencyProject("dep3", shouldCopy);
- // The main project depends on 1 and 2; 2 depends on 3; 3 depends on 1. Note that this should *not* be transitive
+ projectRootElement.AddProperty(GetCopyToOutputDirectoryItemsGraphPredictor.MSBuildCopyContentTransitivelyPropertyName, copyContentTransitively.ToString());
+ dep1.AddProperty(GetCopyToOutputDirectoryItemsGraphPredictor.MSBuildCopyContentTransitivelyPropertyName, copyContentTransitively.ToString());
+ dep2.AddProperty(GetCopyToOutputDirectoryItemsGraphPredictor.MSBuildCopyContentTransitivelyPropertyName, copyContentTransitively.ToString());
+ dep3.AddProperty(GetCopyToOutputDirectoryItemsGraphPredictor.MSBuildCopyContentTransitivelyPropertyName, copyContentTransitively.ToString());
+
+ // The main project depends on 1 and 2; 2 depends on 3; 3 depends on 1.
projectRootElement.AddItem("ProjectReference", @"..\dep1\dep1.proj");
projectRootElement.AddItem("ProjectReference", @"..\dep2\dep2.proj");
dep2.AddItem("ProjectReference", @"..\dep3\dep3.proj");
@@ -100,8 +108,8 @@ public void WithCopy()
dep2.Save();
dep3.Save();
- var expectedInputFiles = new[]
- {
+ List expectedInputFiles =
+ [
new PredictedItem(@"dep1\dep1.xml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"dep1\dep1.resx", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"dep1\dep1.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
@@ -112,10 +120,10 @@ public void WithCopy()
new PredictedItem(@"dep2\dep2.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"dep2\dep2.txt", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"dep2\dep2.xaml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
- };
+ ];
- var expectedOutputFiles = new[]
- {
+ List expectedOutputFiles =
+ [
new PredictedItem(@"src\bin\dep1.xml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"src\bin\dep1.resx", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"src\bin\dep1.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
@@ -126,7 +134,28 @@ public void WithCopy()
new PredictedItem(@"src\bin\dep2.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"src\bin\dep2.txt", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
new PredictedItem(@"src\bin\dep2.xaml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
- };
+ ];
+
+ if (copyContentTransitively)
+ {
+ expectedInputFiles.AddRange(
+ [
+ new PredictedItem(@"dep3\dep3.xml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"dep3\dep3.resx", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"dep3\dep3.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"dep3\dep3.txt", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"dep3\dep3.xaml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ ]);
+
+ expectedOutputFiles.AddRange(
+ [
+ new PredictedItem(@"src\bin\dep3.xml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"src\bin\dep3.resx", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"src\bin\dep3.cs", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"src\bin\dep3.txt", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ new PredictedItem(@"src\bin\dep3.xaml", nameof(GetCopyToOutputDirectoryItemsGraphPredictor)),
+ ]);
+ }
new GetCopyToOutputDirectoryItemsGraphPredictor()
.GetProjectPredictions(projectFile)