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)