Skip to content
Merged
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
LinkItemsPredictor: Add predictions for AdditionalDependencies and Ad…
…ditionalLibraryDirectories
  • Loading branch information
dfederm committed Oct 1, 2024
commit 47993540e4df6435e95830e6b9fd165777f75eed
80 changes: 65 additions & 15 deletions src/BuildPrediction/Predictors/LinkItemsPredictor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Predicts inputs based on Link, Lib, and ImpLib items.
/// </summary>
public sealed class LinkItemsPredictor : IProjectPredictor
{
/// <summary>
/// Finds ClInclude items, typically header files used during compilation.
/// </summary>
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 = [';'];

/// <inheritdoc />
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<string> reportedFiles = new(StringComparer.OrdinalIgnoreCase);
HashSet<string> 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<string> reportedFiles,
HashSet<string> reportedDirectories)
{
internal const string LinkItemName = "Link";
ICollection<ProjectItemInstance> items = projectInstance.GetItems(itemType);
if (items.Count == 0)
{
return;
}

/// <inheritdoc/>
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);
}
}
}
}
Expand Down
208 changes: 164 additions & 44 deletions src/BuildPredictionTests/Predictors/LinkItemsPredictorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}