Skip to content

Commit

Permalink
Add predictions for deps.json and runtimeconfig.json files (#121)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfederm authored Oct 25, 2024
1 parent 0b2744b commit 58b6802
Show file tree
Hide file tree
Showing 11 changed files with 691 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.Build.Execution;

namespace Microsoft.Build.Prediction.Predictors;

/// <summary>
/// Makes predictions based on the GenerateBuildDependencyFile target.
/// </summary>
public sealed class GenerateBuildDependencyFilePredictor : IProjectPredictor
{
internal const string GenerateDependencyFilePropertyName = "GenerateDependencyFile";
internal const string ProjectAssetsFilePropertyName = "ProjectAssetsFile";
internal const string ProjectDepsFilePathPropertyName = "ProjectDepsFilePath";

/// <inheritdoc/>
public void PredictInputsAndOutputs(
ProjectInstance projectInstance,
ProjectPredictionReporter predictionReporter)
{
if (!projectInstance.GetPropertyValue(GenerateDependencyFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
return;
}

predictionReporter.ReportInputFile(projectInstance.GetPropertyValue(ProjectAssetsFilePropertyName));
predictionReporter.ReportOutputFile(projectInstance.GetPropertyValue(ProjectDepsFilePathPropertyName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using Microsoft.Build.Execution;

namespace Microsoft.Build.Prediction.Predictors;

/// <summary>
/// Makes predictions based on the GeneratePublishDependencyFile target.
/// </summary>
public sealed class GeneratePublishDependencyFilePredictor : IProjectPredictor
{
internal const string PublishAotPropertyName = "PublishAot";
internal const string PublishDirPropertyName = "PublishDir";
internal const string PublishSingleFilePropertyName = "PublishSingleFile";
internal const string SelfContainedPropertyName = "SelfContained";
internal const string PreserveStoreLayoutPropertyName = "PreserveStoreLayout";
internal const string PublishTrimmedPropertyName = "PublishTrimmed";
internal const string RuntimeStorePackagesItemName = "RuntimeStorePackages";
internal const string PackageReferenceItemName = "PackageReference";
internal const string PrivateAssetsMetadataName = "PrivateAssets";
internal const string PublishMetadataName = "Publish";
internal const string PublishDepsFilePathPropertyName = "PublishDepsFilePath";
internal const string ProjectDepsFileNamePropertyName = "ProjectDepsFileName";
internal const string IntermediateOutputPathPropertyName = "IntermediateOutputPath";

/// <inheritdoc/>
public void PredictInputsAndOutputs(
ProjectInstance projectInstance,
ProjectPredictionReporter predictionReporter)
{
if (!projectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.GenerateDependencyFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)
|| ShouldUseBuildDependencyFile(projectInstance)
|| projectInstance.GetPropertyValue(PublishAotPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
return;
}

predictionReporter.ReportInputFile(projectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.ProjectAssetsFilePropertyName));

string publishDepsFilePath = GetEffectivePublishDepsFilePath(projectInstance);
string intermediateDepsFilePath = publishDepsFilePath is not null
? publishDepsFilePath
: projectInstance.GetPropertyValue(IntermediateOutputPathPropertyName) + projectInstance.GetPropertyValue(ProjectDepsFileNamePropertyName);
predictionReporter.ReportOutputFile(intermediateDepsFilePath);

// Note: GetCopyToPublishDirectoryItemsGraphPredictor will predict the final (published) location for the publish deps file since that's the target which does that copy.
}

/// <summary>
/// Determines the value of _UseBuildDependencyFile by emulating the behavior from the _ComputeUseBuildDependencyFile target (and the _ComputePackageReferencePublish target).
/// </remarks>
internal static bool ShouldUseBuildDependencyFile(ProjectInstance projectInstance)
{
bool hasExcludeFromPublishPackageReference = false;
foreach (ProjectItemInstance packageReference in projectInstance.GetItems(PackageReferenceItemName))
{
string packageReferencePublishMetadata = packageReference.GetMetadataValue(PublishMetadataName);
if (packageReferencePublishMetadata.Equals("false", StringComparison.OrdinalIgnoreCase))
{
hasExcludeFromPublishPackageReference = true;
break;
}

if (string.IsNullOrEmpty(packageReferencePublishMetadata)
&& packageReference.GetMetadataValue(PrivateAssetsMetadataName).Equals("All", StringComparison.OrdinalIgnoreCase))
{
hasExcludeFromPublishPackageReference = true;
break;
}
}

bool trimRuntimeAssets = projectInstance.GetPropertyValue(PublishSingleFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)
&& projectInstance.GetPropertyValue(SelfContainedPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase);
return !hasExcludeFromPublishPackageReference
&& projectInstance.GetItems(RuntimeStorePackagesItemName).Count == 0
&& !projectInstance.GetPropertyValue(PreserveStoreLayoutPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)
&& !projectInstance.GetPropertyValue(PublishTrimmedPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase)
&& !trimRuntimeAssets;
}

/// <summary>
/// Calculates the effective value of $(PublishDepsFilePath). In unspecified, the default value is calculated inside the GeneratePublishDependencyFile target.
/// </summary>
/// <remarks>
/// This can return null in the case of PublishSingleFile since the deps.json file is embedded within the single-file bundle.
/// </remarks>
internal static string GetEffectivePublishDepsFilePath(ProjectInstance projectInstance)
{
string publishDepsFilePath = projectInstance.GetPropertyValue(PublishDepsFilePathPropertyName);
if (!string.IsNullOrEmpty(publishDepsFilePath))
{
return publishDepsFilePath;
}

if (!projectInstance.GetPropertyValue(PublishSingleFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
return projectInstance.GetPropertyValue(PublishDirPropertyName) + projectInstance.GetPropertyValue(ProjectDepsFileNamePropertyName);
}

return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using Microsoft.Build.Execution;

namespace Microsoft.Build.Prediction.Predictors;

/// <summary>
/// Makes predictions based on the GenerateRuntimeConfigurationFiles target.
/// </summary>
public sealed class GenerateRuntimeConfigurationFilesPredictor : IProjectPredictor
{
internal const string GenerateRuntimeConfigurationFilesPropertyName = "GenerateRuntimeConfigurationFiles";
internal const string UserRuntimeConfigPropertyName = "UserRuntimeConfig";
internal const string ProjectRuntimeConfigFilePathPropertyName = "ProjectRuntimeConfigFilePath";
internal const string ProjectRuntimeConfigDevFilePathPropertyName = "ProjectRuntimeConfigDevFilePath";

/// <inheritdoc/>
public void PredictInputsAndOutputs(
ProjectInstance projectInstance,
ProjectPredictionReporter predictionReporter)
{
if (!projectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
return;
}

string userRuntimeConfig = projectInstance.GetPropertyValue(UserRuntimeConfigPropertyName);
string userRuntimeConfigFullPath = Path.Combine(projectInstance.Directory, userRuntimeConfig);
if (File.Exists(userRuntimeConfigFullPath))
{
predictionReporter.ReportInputFile(userRuntimeConfigFullPath);
}

predictionReporter.ReportOutputFile(projectInstance.GetPropertyValue(ProjectRuntimeConfigFilePathPropertyName));
predictionReporter.ReportOutputFile(projectInstance.GetPropertyValue(ProjectRuntimeConfigDevFilePathPropertyName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public sealed class GetCopyToOutputDirectoryItemsGraphPredictor : IProjectGraphP
internal const string UseCommonOutputDirectoryPropertyName = "UseCommonOutputDirectory";
internal const string OutDirPropertyName = "OutDir";
internal const string MSBuildCopyContentTransitivelyPropertyName = "MSBuildCopyContentTransitively";
internal const string HasRuntimeOutputPropertyName = "HasRuntimeOutput";

/// <inheritdoc/>
public void PredictInputsAndOutputs(ProjectGraphNode projectGraphNode, ProjectPredictionReporter predictionReporter)
Expand Down Expand Up @@ -62,6 +63,29 @@ private static void PredictInputsAndOutputs(

// Process each item type considered in GetCopyToOutputDirectoryXamlAppDefs
ReportCopyToOutputDirectoryItemsAsInputs(dependency.ProjectInstance, XamlAppDefPredictor.XamlAppDefItemName, outDir, predictionReporter);

// Process items added by AddDepsJsonAndRuntimeConfigToCopyItemsForReferencingProjects
bool hasRuntimeOutput = dependency.ProjectInstance.GetPropertyValue(HasRuntimeOutputPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase);
if (hasRuntimeOutput)
{
if (dependency.ProjectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.GenerateDependencyFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
string projectDepsFilePath = dependency.ProjectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.ProjectDepsFilePathPropertyName);
predictionReporter.ReportInputFile(projectDepsFilePath);
predictionReporter.ReportOutputFile(Path.Combine(outDir, Path.GetFileName(projectDepsFilePath)));
}

if (dependency.ProjectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPredictor.GenerateRuntimeConfigurationFilesPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
string projectRuntimeConfigFilePath = dependency.ProjectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPredictor.ProjectRuntimeConfigFilePathPropertyName);
predictionReporter.ReportInputFile(projectRuntimeConfigFilePath);
predictionReporter.ReportOutputFile(Path.Combine(outDir, Path.GetFileName(projectRuntimeConfigFilePath)));

string projectRuntimeConfigDevFilePath = dependency.ProjectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPredictor.ProjectRuntimeConfigDevFilePathPropertyName);
predictionReporter.ReportInputFile(projectRuntimeConfigDevFilePath);
predictionReporter.ReportOutputFile(Path.Combine(outDir, Path.GetFileName(projectRuntimeConfigDevFilePath)));
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,37 @@ private static void ReportCopyToPublishDirectoryItems(
ReportCopyToPublishDirectoryItems(projectInstance, EmbeddedResourceItemsPredictor.EmbeddedResourceItemName, publishDir, predictionReporter);
ReportCopyToPublishDirectoryItems(projectInstance, CompileItemsPredictor.CompileItemName, publishDir, predictionReporter);
ReportCopyToPublishDirectoryItems(projectInstance, NoneItemsPredictor.NoneItemName, publishDir, predictionReporter);

// Process items added by AddDepsJsonAndRuntimeConfigToPublishItemsForReferencingProjects
bool hasRuntimeOutput = projectInstance.GetPropertyValue(GetCopyToOutputDirectoryItemsGraphPredictor.HasRuntimeOutputPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase);
if (hasRuntimeOutput)
{
if (projectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.GenerateDependencyFilePropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
if (GeneratePublishDependencyFilePredictor.ShouldUseBuildDependencyFile(projectInstance))
{
string projectDepsFilePath = projectInstance.GetPropertyValue(GenerateBuildDependencyFilePredictor.ProjectDepsFilePathPropertyName);
predictionReporter.ReportInputFile(projectDepsFilePath);
predictionReporter.ReportOutputFile(Path.Combine(publishDir, Path.GetFileName(projectDepsFilePath)));
}
else
{
string publishDepsFilePath = GeneratePublishDependencyFilePredictor.GetEffectivePublishDepsFilePath(projectInstance);
if (publishDepsFilePath is not null)
{
predictionReporter.ReportInputFile(publishDepsFilePath);
predictionReporter.ReportOutputFile(Path.Combine(publishDir, Path.GetFileName(publishDepsFilePath)));
}
}
}

if (projectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPredictor.GenerateRuntimeConfigurationFilesPropertyName).Equals("true", StringComparison.OrdinalIgnoreCase))
{
string projectRuntimeConfigFilePath = projectInstance.GetPropertyValue(GenerateRuntimeConfigurationFilesPredictor.ProjectRuntimeConfigFilePathPropertyName);
predictionReporter.ReportInputFile(projectRuntimeConfigFilePath);
predictionReporter.ReportOutputFile(Path.Combine(publishDir, Path.GetFileName(projectRuntimeConfigFilePath)));
}
}
}

private static void ReportCopyToPublishDirectoryItems(
Expand Down
6 changes: 6 additions & 0 deletions src/BuildPrediction/ProjectPredictors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public static class ProjectPredictors
/// <item><see cref="CppContentFilesProjectOutputGroupPredictor"/></item>
/// <item><see cref="LinkItemsPredictor"/></item>
/// <item><see cref="DotnetSdkPredictor"/></item>
/// <item><see cref="GenerateBuildDependencyFilePredictor"/></item>
/// <item><see cref="GeneratePublishDependencyFilePredictor"/></item>
/// <item><see cref="GenerateRuntimeConfigurationFilesPredictor"/></item>
/// </list>
/// </remarks>
/// <returns>A collection of <see cref="IProjectPredictor"/>.</returns>
Expand Down Expand Up @@ -108,6 +111,9 @@ public static class ProjectPredictors
new CppContentFilesProjectOutputGroupPredictor(),
new LinkItemsPredictor(),
new DotnetSdkPredictor(),
new GenerateBuildDependencyFilePredictor(),
new GeneratePublishDependencyFilePredictor(),
new GenerateRuntimeConfigurationFilesPredictor(),
//// NOTE! When adding a new predictor here, be sure to update the doc comment above.
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using Microsoft.Build.Construction;
using Microsoft.Build.Execution;
using Microsoft.Build.Prediction.Predictors;
using Xunit;

namespace Microsoft.Build.Prediction.Tests.Predictors;

public class GenerateBuildDependencyFilePredictorTests
{
[Fact]
public void DoesNotGenerateDependencyFile()
{
ProjectRootElement projectRootElement = ProjectRootElement.Create("project.csproj");

string projectAssetsFile = Path.Combine(projectRootElement.DirectoryPath, @"obj\project.assets.json");
projectRootElement.AddProperty(GenerateBuildDependencyFilePredictor.ProjectAssetsFilePropertyName, projectAssetsFile);

string projectDepsFilePath = Path.Combine(projectRootElement.DirectoryPath, @"bin\x64\Debug\net8.0\project.deps.json");
projectRootElement.AddProperty(GenerateBuildDependencyFilePredictor.ProjectDepsFilePathPropertyName, projectDepsFilePath);

ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);

new GenerateBuildDependencyFilePredictor()
.GetProjectPredictions(projectInstance)
.AssertPredictions(
projectInstance,
null,
null,
null,
null);
}

[Fact]
public void GeneratesDependencyFile()
{
ProjectRootElement projectRootElement = ProjectRootElement.Create("project.csproj");
projectRootElement.AddProperty(GenerateBuildDependencyFilePredictor.GenerateDependencyFilePropertyName, "true");

string projectAssetsFile = Path.Combine(projectRootElement.DirectoryPath, @"obj\project.assets.json");
projectRootElement.AddProperty(GenerateBuildDependencyFilePredictor.ProjectAssetsFilePropertyName, projectAssetsFile);

string projectDepsFilePath = Path.Combine(projectRootElement.DirectoryPath, @"bin\x64\Debug\net8.0\project.deps.json");
projectRootElement.AddProperty(GenerateBuildDependencyFilePredictor.ProjectDepsFilePathPropertyName, projectDepsFilePath);

ProjectInstance projectInstance = TestHelpers.CreateProjectInstanceFromRootElement(projectRootElement);

var expectedInputFiles = new[]
{
new PredictedItem(projectAssetsFile, nameof(GenerateBuildDependencyFilePredictor)),
};
var expectedOutputFiles = new[]
{
new PredictedItem(projectDepsFilePath, nameof(GenerateBuildDependencyFilePredictor)),
};

new GenerateBuildDependencyFilePredictor()
.GetProjectPredictions(projectInstance)
.AssertPredictions(
projectInstance,
expectedInputFiles,
null,
expectedOutputFiles,
null);
}
}
Loading

0 comments on commit 58b6802

Please sign in to comment.