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

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

Expand All @@ -35,6 +38,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 Down Expand Up @@ -113,18 +118,18 @@ protected override void ExecuteCore()
return;
}

PackagesToPrune = LoadPackagesToPrune(key, TargetingPackRoots, PrunePackageDataRoot, Log, AllowMissingPrunePackageData);
PackagesToPrune = LoadPackagesToPrune(key, TargetingPackRoots, PrunePackageDataRoot, Log, AllowMissingPrunePackageData, LoadPrunePackageDataFromNearestFramework);

BuildEngine4.RegisterTaskObject(key, PackagesToPrune, RegisteredTaskObjectLifetime.Build, true);
}

static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots, string prunePackageDataRoot, Logger log, bool allowMissingPrunePackageData)
static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots, string prunePackageDataRoot, Logger log, bool allowMissingPrunePackageData, bool loadPrunePackageDataFromNearestFramework)
{
Dictionary<string, NuGetVersion> packagesToPrune = new();

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 >= MinSupportedFrameworkMajorVersion)
{
// For .NET Core projects (3.0 and higher), don't prune any packages if there are no framework references
return Array.Empty<TaskItem>();
Expand Down Expand Up @@ -162,25 +167,7 @@ static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots,
}
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);
}
packagesForFrameworkReference = TryLoadPackagesToPruneForVersion(log, key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference, targetingPackRoots, prunePackageDataRoot, loadPrunePackageDataFromNearestFramework);
}

if (packagesForFrameworkReference == null)
Expand Down Expand Up @@ -276,6 +263,45 @@ 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)
{
log.LogMessage("Loading prune package data from PrunePackageData folder");
var packages = LoadPackagesToPruneFromPrunePackageData(targetFrameworkIdentifier, 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 (packages == null)
{
log.LogMessage("Failed to load prune package data from PrunePackageData folder, loading from targeting packs instead");
packages = LoadPackagesToPruneFromTargetingPack(log, targetFrameworkIdentifier, targetFrameworkVersion, 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, targetFrameworkVersion, frameworkReference,
acceptNearestMatch: true);
}

// If LoadPrunePackageDataFromNearestFramework is true and we still haven't found data, try the previous framework version
if (packages == null && loadPrunePackageDataFromNearestFramework)
{
var targetVersion = Version.Parse(targetFrameworkVersion);

// If we can go to a lower version, recursively try it
if (targetVersion.Major > MinSupportedFrameworkMajorVersion)
{
string fallbackVersion = $"{targetVersion.Major - 1}.0";
Copy link
Member

Choose a reason for hiding this comment

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

What if we have frameworks with minor versions?

Copy link
Member

Choose a reason for hiding this comment

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

I think it's unlikely we'll ship minor versions again. I'm OK with this breaking or not working correctly if we eventually do create a minor version.

Copy link
Member

Choose a reason for hiding this comment

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

You might consider at a minimum just decrementing minor if it's non-zero first. That's a small change to handle this possibility. It might also address the 2.2 case Rich mentioned offline.

log.LogMessage($"LoadPrunePackageDataFromNearestFramework is enabled, trying to load from framework version {fallbackVersion}");
packages = TryLoadPackagesToPruneForVersion(log, targetFrameworkIdentifier, fallbackVersion, frameworkReference, targetingPackRoots, prunePackageDataRoot, loadPrunePackageDataFromNearestFramework);
Copy link
Member

@ericstj ericstj Dec 3, 2025

Choose a reason for hiding this comment

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

Do we want this to be recursive? That means someone can stack overflow if someone has this setting enabled and targets something like net5000.0? This also would be pretty expensive to fallback through each version 1-by-1... Find nearest might be better if you can determine which frameworks you have data for.

Copy link
Member

Choose a reason for hiding this comment

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

You'll get an error way before this target runs if you target a higher version of .NET than the SDK supports:

NETSDK1045: The current .NET SDK does not support targeting .NET 20.0. Either target .NET 10.0 or lower, or use a version of the .NET SDK that supports .NET 20.0. Download the .NET SDK from https://aka.ms/dotnet/download

Recursion seemed simpler to me in this case.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but I can also disable that warning. I'm not sure why someone would, but I try to avoid recursive algorithms where recursion is controlled by user input.

Seems to me like making it iterative wouldn't be all that hard but it's your call. Make sure to document that fallback logic of this is different than that of NuGet.

}
}

return packages;
}

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