diff --git a/src/BuildPrediction/Predictors/LinkItemsPredictor.cs b/src/BuildPrediction/Predictors/LinkItemsPredictor.cs index 9991568..60803ab 100644 --- a/src/BuildPrediction/Predictors/LinkItemsPredictor.cs +++ b/src/BuildPrediction/Predictors/LinkItemsPredictor.cs @@ -2,31 +2,81 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using Microsoft.Build.Execution; -namespace Microsoft.Build.Prediction.Predictors +namespace Microsoft.Build.Prediction.Predictors; + +/// +/// Predicts inputs based on Link, Lib, and ImpLib items. +/// +public sealed class LinkItemsPredictor : IProjectPredictor { - /// - /// Finds ClInclude items, typically header files used during compilation. - /// - public sealed class LinkItemsPredictor : IProjectPredictor + // See Microsoft.CppCommon.targets for items which use AdditionalDependencies. + internal const string LinkItemName = "Link"; + internal const string LibItemName = "Lib"; + internal const string ImpLibItemName = "ImpLib"; + + internal const string AdditionalDependenciesMetadata = "AdditionalDependencies"; + internal const string AdditionalLibraryDirectoriesMetadata = "AdditionalLibraryDirectories"; + + private static readonly char[] IncludePathsSeparator = [';']; + + /// + public void PredictInputsAndOutputs(ProjectInstance projectInstance, ProjectPredictionReporter reporter) + { + // This predictor only applies to vcxproj files + if (!projectInstance.FullPath.EndsWith(".vcxproj", StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // These are commonly added via an ItemDefinitionGroup, so use a HashSet to dedupe the repeats. + HashSet reportedFiles = new(StringComparer.OrdinalIgnoreCase); + HashSet reportedDirectories = new(StringComparer.OrdinalIgnoreCase); + + ReportInputsForItemType(reporter, projectInstance, LinkItemName, reportedFiles, reportedDirectories); + ReportInputsForItemType(reporter, projectInstance, LibItemName, reportedFiles, reportedDirectories); + ReportInputsForItemType(reporter, projectInstance, ImpLibItemName, reportedFiles, reportedDirectories); + } + + private void ReportInputsForItemType( + ProjectPredictionReporter reporter, + ProjectInstance projectInstance, + string itemType, + HashSet reportedFiles, + HashSet reportedDirectories) { - internal const string LinkItemName = "Link"; + ICollection items = projectInstance.GetItems(itemType); + if (items.Count == 0) + { + return; + } - /// - public void PredictInputsAndOutputs( - ProjectInstance projectInstance, - ProjectPredictionReporter predictionReporter) + foreach (ProjectItemInstance item in items) { - // This predictor only applies to vcxproj files - if (!projectInstance.FullPath.EndsWith(".vcxproj", StringComparison.OrdinalIgnoreCase)) + reporter.ReportInputFile(item.EvaluatedInclude); + + string[] additionalDependencies = item.GetMetadataValue(AdditionalDependenciesMetadata) + .Split(IncludePathsSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (string dependency in additionalDependencies) { - return; + string trimmedDependency = dependency.Trim(); + if (!string.IsNullOrEmpty(trimmedDependency) && reportedFiles.Add(trimmedDependency)) + { + reporter.ReportInputFile(trimmedDependency); + } } - foreach (ProjectItemInstance item in projectInstance.GetItems(LinkItemName)) + string[] additionalLibraryDirectories = item.GetMetadataValue(AdditionalLibraryDirectoriesMetadata) + .Split(IncludePathsSeparator, StringSplitOptions.RemoveEmptyEntries); + foreach (string directory in additionalLibraryDirectories) { - predictionReporter.ReportInputFile(item.EvaluatedInclude); + string trimmedDirectory = directory.Trim(); + if (!string.IsNullOrEmpty(trimmedDirectory) && reportedDirectories.Add(trimmedDirectory)) + { + reporter.ReportInputDirectory(trimmedDirectory); + } } } } diff --git a/src/BuildPredictionTests/Predictors/LinkItemsPredictorTests.cs b/src/BuildPredictionTests/Predictors/LinkItemsPredictorTests.cs index f49285c..141e922 100644 --- a/src/BuildPredictionTests/Predictors/LinkItemsPredictorTests.cs +++ b/src/BuildPredictionTests/Predictors/LinkItemsPredictorTests.cs @@ -8,58 +8,178 @@ using Microsoft.Build.Prediction.Predictors; using Xunit; -namespace Microsoft.Build.Prediction.Tests.Predictors +namespace Microsoft.Build.Prediction.Tests.Predictors; + +public sealed class LinkItemsPredictorTests { - public class LinkItemsPredictorTests + [Theory] + [InlineData(LinkItemsPredictor.LinkItemName)] + [InlineData(LinkItemsPredictor.LibItemName)] + [InlineData(LinkItemsPredictor.ImpLibItemName)] + public void ItemDefinitionGroup(string itemType) { - private readonly string _rootDir; + ProjectRootElement projectRootElement = ProjectRootElement.Create(@"src\project.vcxproj"); + + ProjectItemDefinitionElement itemDefinition = projectRootElement.AddItemDefinitionGroup().AddItemDefinition(itemType); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AdditionalDependency.lib;%(AdditionalDependencies)"); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); + + projectRootElement.AddItem(itemType, @"..\someLib.lib"); - public LinkItemsPredictorTests() + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] + { + new PredictedItem("someLib.lib", nameof(LinkItemsPredictor)), + new PredictedItem("AdditionalDependency.lib", nameof(LinkItemsPredictor)), + }; + var expectedInputDirectories = new[] { - // Isolate each test into its own folder - _rootDir = Path.Combine(Directory.GetCurrentDirectory(), nameof(LinkItemsPredictorTests), Guid.NewGuid().ToString()); - Directory.CreateDirectory(_rootDir); - } + new PredictedItem("AdditionalLibraryDirectory", nameof(LinkItemsPredictor)), + }; + new LinkItemsPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(Directory.GetCurrentDirectory()), + expectedInputDirectories.MakeAbsolute(Directory.GetCurrentDirectory()), + null, + null); + } + + [Theory] + [InlineData(LinkItemsPredictor.LinkItemName)] + [InlineData(LinkItemsPredictor.LibItemName)] + [InlineData(LinkItemsPredictor.ImpLibItemName)] + public void OverrideMetadata(string itemType) + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(@"src\project.vcxproj"); + + ProjectItemDefinitionElement itemDefinition = projectRootElement.AddItemDefinitionGroup().AddItemDefinition(itemType); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AdditionalDependency.lib;%(AdditionalDependencies)"); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); - [Fact] - public void FindsItems() + ProjectItemElement item = projectRootElement.AddItem(itemType, @"..\someLib.lib"); + item.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\ReplacedAdditionalDependency.lib"); + item.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\ReplacedAdditionalLibraryDirectory"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] { - ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"project.vcxproj")); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "foo.lib"); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "bar.lib"); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "baz.lib"); - - ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); - - var expectedInputFiles = new[] - { - new PredictedItem("foo.lib", nameof(LinkItemsPredictor)), - new PredictedItem("bar.lib", nameof(LinkItemsPredictor)), - new PredictedItem("baz.lib", nameof(LinkItemsPredictor)), - }; - new LinkItemsPredictor() - .GetProjectPredictions(projectInstance) - .AssertPredictions( - projectInstance, - expectedInputFiles, - null, - null, - null); - } - - [Fact] - public void SkipOtherProjectTypes() + new PredictedItem("someLib.lib", nameof(LinkItemsPredictor)), + new PredictedItem("ReplacedAdditionalDependency.lib", nameof(LinkItemsPredictor)), + }; + var expectedInputDirectories = new[] { - ProjectRootElement projectRootElement = ProjectRootElement.Create(Path.Combine(_rootDir, @"project.csproj")); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "foo.lib"); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "bar.lib"); - projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, "baz.lib"); + new PredictedItem("ReplacedAdditionalLibraryDirectory", nameof(LinkItemsPredictor)), + }; + new LinkItemsPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(Directory.GetCurrentDirectory()), + expectedInputDirectories.MakeAbsolute(Directory.GetCurrentDirectory()), + null, + null); + } + + [Theory] + [InlineData(LinkItemsPredictor.LinkItemName)] + [InlineData(LinkItemsPredictor.LibItemName)] + [InlineData(LinkItemsPredictor.ImpLibItemName)] + public void AppendMetadata(string itemType) + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(@"src\project.vcxproj"); + + ProjectItemDefinitionElement itemDefinition = projectRootElement.AddItemDefinitionGroup().AddItemDefinition(itemType); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AdditionalDependency.lib;%(AdditionalDependencies)"); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); + + ProjectItemElement item = projectRootElement.AddItem(itemType, @"..\someLib.lib"); + item.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AnotherAdditionalDependency.lib;%(AdditionalDependencies)"); + item.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AnotherAdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] + { + new PredictedItem("someLib.lib", nameof(LinkItemsPredictor)), + new PredictedItem("AdditionalDependency.lib", nameof(LinkItemsPredictor)), + new PredictedItem("AnotherAdditionalDependency.lib", nameof(LinkItemsPredictor)), + }; + var expectedInputDirectories = new[] + { + new PredictedItem("AdditionalLibraryDirectory", nameof(LinkItemsPredictor)), + new PredictedItem("AnotherAdditionalLibraryDirectory", nameof(LinkItemsPredictor)), + }; + new LinkItemsPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(Directory.GetCurrentDirectory()), + expectedInputDirectories.MakeAbsolute(Directory.GetCurrentDirectory()), + null, + null); + } + + [Theory] + [InlineData(LinkItemsPredictor.LinkItemName)] + [InlineData(LinkItemsPredictor.LibItemName)] + [InlineData(LinkItemsPredictor.ImpLibItemName)] + public void DuplicatesAndSpaces(string itemType) + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(@"src\project.vcxproj"); + + ProjectItemDefinitionElement itemDefinition = projectRootElement.AddItemDefinitionGroup().AddItemDefinition(itemType); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AdditionalDependency.lib;%(AdditionalDependencies)"); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); + + ProjectItemElement item = projectRootElement.AddItem(itemType, @"..\someLib.lib"); + item.AddMetadata( + LinkItemsPredictor.AdditionalDependenciesMetadata, + $@"%(AdditionalDependencies); ;;%(AdditionalDependencies);..\AnotherAdditionalDependency.lib;{Environment.NewLine}..\AnotherAdditionalDependency.lib;%(AdditionalDependencies)"); + item.AddMetadata( + LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, + $@"%(AdditionalLibraryDirectories); ;;%(AdditionalLibraryDirectories);..\AnotherAdditionalLibraryDirectories;{Environment.NewLine}..\AnotherAdditionalLibraryDirectories;%(AdditionalLibraryDirectories)"); + + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + + var expectedInputFiles = new[] + { + new PredictedItem("someLib.lib", nameof(LinkItemsPredictor)), + new PredictedItem("AdditionalDependency.lib", nameof(LinkItemsPredictor)), + new PredictedItem("AnotherAdditionalDependency.lib", nameof(LinkItemsPredictor)), + }; + var expectedInputDirectories = new[] + { + new PredictedItem("AdditionalLibraryDirectory", nameof(LinkItemsPredictor)), + new PredictedItem("AnotherAdditionalLibraryDirectories", nameof(LinkItemsPredictor)), + }; + new LinkItemsPredictor() + .GetProjectPredictions(projectInstance) + .AssertPredictions( + projectInstance, + expectedInputFiles.MakeAbsolute(Directory.GetCurrentDirectory()), + expectedInputDirectories.MakeAbsolute(Directory.GetCurrentDirectory()), + null, + null); + } + + [Fact] + public void SkipOtherProjectTypes() + { + ProjectRootElement projectRootElement = ProjectRootElement.Create(@"src\project.csproj"); + + ProjectItemDefinitionElement itemDefinition = projectRootElement.AddItemDefinitionGroup().AddItemDefinition(LinkItemsPredictor.LinkItemName); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalDependenciesMetadata, @"..\AdditionalDependency.lib;%(AdditionalDependencies)"); + itemDefinition.AddMetadata(LinkItemsPredictor.AdditionalLibraryDirectoriesMetadata, @"..\AdditionalLibraryDirectory;%(AdditionalLibraryDirectories)"); - ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + projectRootElement.AddItem(LinkItemsPredictor.LinkItemName, @"..\someLib.lib"); - new AdditionalIncludeDirectoriesPredictor() - .GetProjectPredictions(projectInstance) - .AssertNoPredictions(); - } + ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement); + new LinkItemsPredictor() + .GetProjectPredictions(projectInstance) + .AssertNoPredictions(); } } \ No newline at end of file