Skip to content
136 changes: 94 additions & 42 deletions src/Tasks/Microsoft.NET.Build.Tasks/GetPackagesToPrune.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ namespace Microsoft.NET.Build.Tasks
{
public class GetPackagesToPrune : TaskBase
{
// Minimum .NET Core version that supports package pruning
private const int FrameworkReferenceMinVersion = 3;

// Minimum .NET Core version that uses prune package data instead of framework package data
private const int PrunePackageDataMinMajorVersion = 10;

[Required]
public string TargetFrameworkIdentifier { get; set; }

Expand All @@ -35,6 +41,8 @@ public class GetPackagesToPrune : TaskBase
[Required]
public bool AllowMissingPrunePackageData { get; set; }

public bool LoadPrunePackageDataFromNearestFramework { get; set; }
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces a new feature (LoadPrunePackageDataFromNearestFramework parameter) that changes behavior of the package pruning system, but no tests have been added. According to the repository's coding guidelines, large changes should always include test changes.

Consider adding integration tests that verify:

  1. The new parameter works correctly when enabled (e.g., building .NET 11 with .NET 10 SDK succeeds)
  2. The parameter is disabled by default (backward compatibility)
  3. The fallback logic correctly tries previous framework versions
  4. The parameter is included in the cache key (different parameter values produce different results)

Example test location: test/Microsoft.NET.Build.Tests/ (similar to existing tests in GivenFrameworkReferences.cs or GivenThatWeWantToResolveConflicts.cs)

Copilot uses AI. Check for mistakes.

[Output]
public ITaskItem[] PackagesToPrune { get; set; }

Expand All @@ -43,11 +51,13 @@ class CacheKey
public string TargetFrameworkIdentifier { get; set; }
public string TargetFrameworkVersion { get; set; }
public HashSet<string> FrameworkReferences { get; set; }
public bool LoadPrunePackageDataFromNearestFramework { get; set; }

public override bool Equals(object obj) => obj is CacheKey key &&
TargetFrameworkIdentifier == key.TargetFrameworkIdentifier &&
TargetFrameworkVersion == key.TargetFrameworkVersion &&
FrameworkReferences.SetEquals(key.FrameworkReferences);
FrameworkReferences.SetEquals(key.FrameworkReferences) &&
LoadPrunePackageDataFromNearestFramework == key.LoadPrunePackageDataFromNearestFramework;
public override int GetHashCode()
{
#if NET
Expand All @@ -58,6 +68,7 @@ public override int GetHashCode()
{
hashCode.Add(frameworkReference);
}
hashCode.Add(LoadPrunePackageDataFromNearestFramework);
return hashCode.ToHashCode();
#else
int hashCode = 1436330440;
Expand All @@ -68,6 +79,7 @@ public override int GetHashCode()
{
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(frameworkReference);
}
hashCode = hashCode * -1521134295 + LoadPrunePackageDataFromNearestFramework.GetHashCode();
return hashCode;
#endif
}
Expand Down Expand Up @@ -102,7 +114,8 @@ protected override void ExecuteCore()
{
TargetFrameworkIdentifier = TargetFrameworkIdentifier,
TargetFrameworkVersion = TargetFrameworkVersion,
FrameworkReferences = runtimeFrameworks.ToHashSet()
FrameworkReferences = runtimeFrameworks.ToHashSet(),
LoadPrunePackageDataFromNearestFramework = LoadPrunePackageDataFromNearestFramework
};

// Cache framework package values per build
Expand All @@ -124,16 +137,12 @@ static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots,

var targetFrameworkVersion = Version.Parse(key.TargetFrameworkVersion);

if (key.FrameworkReferences.Count == 0 && key.TargetFrameworkIdentifier.Equals(".NETCoreApp") && targetFrameworkVersion.Major >= 3)
if (key.FrameworkReferences.Count == 0 && key.TargetFrameworkIdentifier.Equals(".NETCoreApp") && targetFrameworkVersion.Major >= FrameworkReferenceMinVersion)
{
// For .NET Core projects (3.0 and higher), don't prune any packages if there are no framework references
return Array.Empty<TaskItem>();
}

// Use hard-coded / generated "framework package data" for .NET 9 and lower, .NET Framework, and .NET Standard
// Use bundled "prune package data" for .NET 10 and higher. During the redist build, this comes from targeting packs and is laid out in the PrunePackageData folder.
bool useFrameworkPackageData = !key.TargetFrameworkIdentifier.Equals(".NETCoreApp") || targetFrameworkVersion.Major < 10;

// Call DefaultIfEmpty() so that target frameworks without framework references will load data
foreach (var frameworkReference in key.FrameworkReferences.DefaultIfEmpty(""))
{
Expand All @@ -147,41 +156,7 @@ static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots,
}
log.LogMessage(MessageImportance.Low, $"Loading packages to prune for {key.TargetFrameworkIdentifier} {key.TargetFrameworkVersion} {frameworkReference}");

Dictionary<string, NuGetVersion> packagesForFrameworkReference;
if (useFrameworkPackageData)
{
packagesForFrameworkReference = LoadPackagesToPruneFromFrameworkPackages(key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference);
if (packagesForFrameworkReference != null)
{
log.LogMessage("Loaded prune package data from framework packages");
}
else
{
log.LogMessage("Failed to load prune package data from framework packages");
}
}
else
{
log.LogMessage("Loading prune package data from PrunePackageData folder");
packagesForFrameworkReference = LoadPackagesToPruneFromPrunePackageData(key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference, prunePackageDataRoot);

// For the version of the runtime that matches the current SDK version, we don't include the prune package data in the PrunePackageData folder. Rather,
// we can load it from the targeting packs that are packaged with the SDK.
if (packagesForFrameworkReference == null)
{
log.LogMessage("Failed to load prune package data from PrunePackageData folder, loading from targeting packs instead");
packagesForFrameworkReference = LoadPackagesToPruneFromTargetingPack(log, key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference, targetingPackRoots);
}

// Fall back to framework packages data for older framework for WindowsDesktop if necessary
// https://github.com/dotnet/windowsdesktop/issues/4904
if (packagesForFrameworkReference == null && frameworkReference.Equals("Microsoft.WindowsDesktop.App", StringComparison.OrdinalIgnoreCase))
{
log.LogMessage("Failed to load prune package data for WindowsDesktop from targeting packs, loading from framework packages instead");
packagesForFrameworkReference = LoadPackagesToPruneFromFrameworkPackages(key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference,
acceptNearestMatch: true);
}
}
Dictionary<string, NuGetVersion> packagesForFrameworkReference = TryLoadPackagesToPruneForVersion(log, key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference, targetingPackRoots, prunePackageDataRoot, key.LoadPrunePackageDataFromNearestFramework);

if (packagesForFrameworkReference == null)
{
Expand Down Expand Up @@ -276,6 +251,83 @@ static Dictionary<string, NuGetVersion> LoadPackagesToPruneFromTargetingPack(Log
return null;
}

static Dictionary<string, NuGetVersion> TryLoadPackagesToPruneForVersion(Logger log, string targetFrameworkIdentifier, string targetFrameworkVersion, string frameworkReference, string[] targetingPackRoots, string prunePackageDataRoot, bool loadPrunePackageDataFromNearestFramework)
{
var currentVersion = Version.Parse(targetFrameworkVersion);

// Try loading for the current version and then iteratively try previous versions if enabled
while (true)
{
string currentVersionString = $"{currentVersion.Major}.{currentVersion.Minor}";

// Use hard-coded / generated "framework package data" for .NET 9 and lower, .NET Framework, and .NET Standard
// Use bundled "prune package data" for .NET 10 and higher. During the redist build, this comes from targeting packs and is laid out in the PrunePackageData folder.
bool useFrameworkPackageData = !targetFrameworkIdentifier.Equals(".NETCoreApp") || currentVersion.Major < PrunePackageDataMinMajorVersion;

Dictionary<string, NuGetVersion> packages = null;

if (useFrameworkPackageData)
{
packages = LoadPackagesToPruneFromFrameworkPackages(targetFrameworkIdentifier, currentVersionString, frameworkReference);
if (packages != null)
{
log.LogMessage("Loaded prune package data from framework packages");
}
else
{
log.LogMessage("Failed to load prune package data from framework packages");
}
}
else
{
log.LogMessage("Loading prune package data from PrunePackageData folder");
packages = LoadPackagesToPruneFromPrunePackageData(targetFrameworkIdentifier, currentVersionString, frameworkReference, prunePackageDataRoot);

// For the version of the runtime that matches the current SDK version, we don't include the prune package data in the PrunePackageData folder. Rather,
// we can load it from the targeting packs that are packaged with the SDK.
if (packages == null)
{
log.LogMessage("Failed to load prune package data from PrunePackageData folder, loading from targeting packs instead");
packages = LoadPackagesToPruneFromTargetingPack(log, targetFrameworkIdentifier, currentVersionString, frameworkReference, targetingPackRoots);
}

// Fall back to framework packages data for older framework for WindowsDesktop if necessary
// https://github.com/dotnet/windowsdesktop/issues/4904
if (packages == null && frameworkReference.Equals("Microsoft.WindowsDesktop.App", StringComparison.OrdinalIgnoreCase))
{
log.LogMessage("Failed to load prune package data for WindowsDesktop from targeting packs, loading from framework packages instead");
packages = LoadPackagesToPruneFromFrameworkPackages(targetFrameworkIdentifier, currentVersionString, frameworkReference,
acceptNearestMatch: true);
}
}

// If we found packages or we're not supposed to fall back, return what we have
if (packages != null || !loadPrunePackageDataFromNearestFramework)
{
return packages;
}

// Determine the next version to try
// If minor version is non-zero, decrement it first
if (currentVersion.Minor > 0)
{
currentVersion = new Version(currentVersion.Major, currentVersion.Minor - 1);
log.LogMessage($"LoadPrunePackageDataFromNearestFramework is enabled, trying to load from framework version {currentVersion.Major}.{currentVersion.Minor}");
}
// Otherwise, decrement major version and reset minor to 0
else if (currentVersion.Major > 2)
{
currentVersion = new Version(currentVersion.Major - 1, 0);
log.LogMessage($"LoadPrunePackageDataFromNearestFramework is enabled, trying to load from framework version {currentVersion.Major}.{currentVersion.Minor}");
}
else
{
// We've exhausted all versions to try
return null;
}
}
}

static void AddPackagesToPrune(Dictionary<string, NuGetVersion> packagesToPrune, IEnumerable<(string id, NuGetVersion version)> packagesToAdd, Logger log)
{
foreach (var package in packagesToAdd)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Copyright (c) .NET Foundation. All rights reserved.
<PrunePackageDataRoot Condition="'$(PrunePackageDataRoot)' == ''">$(NetCoreRoot)\sdk\$(NETCoreSdkVersion)\PrunePackageData\</PrunePackageDataRoot>
<PrunePackageTargetingPackRoots Condition="'$(PrunePackageTargetingPackRoots)' == ''">$(NetCoreTargetingPackRoot)</PrunePackageTargetingPackRoots>
<AllowMissingPrunePackageData Condition="'$(AllowMissingPrunePackageData)' == ''">false</AllowMissingPrunePackageData>
<LoadPrunePackageDataFromNearestFramework Condition="'$(LoadPrunePackageDataFromNearestFramework)' == ''">false</LoadPrunePackageDataFromNearestFramework>
</PropertyGroup>

<GetPackagesToPrune TargetFrameworkIdentifier="$(TargetFrameworkIdentifier)"
Expand All @@ -69,7 +70,8 @@ Copyright (c) .NET Foundation. All rights reserved.
TargetingPacks="@(TargetingPack)"
TargetingPackRoots="$(PrunePackageTargetingPackRoots)"
PrunePackageDataRoot="$(PrunePackageDataRoot)"
AllowMissingPrunePackageData="$(AllowMissingPrunePackageData)">
AllowMissingPrunePackageData="$(AllowMissingPrunePackageData)"
LoadPrunePackageDataFromNearestFramework="$(LoadPrunePackageDataFromNearestFramework)">
<Output TaskParameter="PackagesToPrune" ItemName="PrunePackageReference" />
</GetPackagesToPrune>

Expand Down
Loading