forked from dotnet/sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGetPackagesToPrune.cs
299 lines (253 loc) · 15.9 KB
/
GetPackagesToPrune.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.ComponentDetection.Detectors.NuGet;
using Microsoft.NET.Build.Tasks.ConflictResolution;
using NuGet.Frameworks;
using NuGet.Versioning;
namespace Microsoft.NET.Build.Tasks
{
public class GetPackagesToPrune : TaskBase
{
[Required]
public string TargetFrameworkIdentifier { get; set; }
[Required]
public string TargetFrameworkVersion { get; set; }
[Required]
public ITaskItem[] FrameworkReferences { get; set; }
[Required]
public ITaskItem[] TargetingPacks { get; set; }
[Required]
public string[] TargetingPackRoots { get; set; }
[Required]
public string PrunePackageDataRoot { get; set; }
[Required]
public bool AllowMissingPrunePackageData { get; set; }
[Output]
public ITaskItem[] PackagesToPrune { get; set; }
class CacheKey
{
public string TargetFrameworkIdentifier { get; set; }
public string TargetFrameworkVersion { get; set; }
public HashSet<string> FrameworkReferences { get; set; }
public override bool Equals(object? obj) => obj is CacheKey key &&
TargetFrameworkIdentifier == key.TargetFrameworkIdentifier &&
TargetFrameworkVersion == key.TargetFrameworkVersion &&
FrameworkReferences.SetEquals(key.FrameworkReferences);
public override int GetHashCode()
{
#if NET
var hashCode = new HashCode();
hashCode.Add(TargetFrameworkIdentifier);
hashCode.Add(TargetFrameworkVersion);
foreach (var frameworkReference in FrameworkReferences)
{
hashCode.Add(frameworkReference);
}
return hashCode.ToHashCode();
#else
int hashCode = 1436330440;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(TargetFrameworkIdentifier);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(TargetFrameworkVersion);
foreach (var frameworkReference in FrameworkReferences)
{
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(frameworkReference);
}
return hashCode;
#endif
}
}
protected override void ExecuteCore()
{
// Filter out transitive framework references. Normally they wouldn't be passed to this task, but in Visual Studio design-time builds
// the ResolvePackageAssets and AddTransitiveFrameworkReferences targets may have already run. Filtering these references out should
// avoid a bug similar to https://github.com/dotnet/sdk/issues/14641
var filteredFrameworkReferences = FrameworkReferences.Where(
i => i.GetMetadata("IsTransitiveFrameworkReference") is string transitiveVal && !transitiveVal.Equals("true", StringComparison.OrdinalIgnoreCase)).ToList();
// Map framework references to runtime frameworks, so we can correctly handle framework references to profiles.
// For example, for a framework reference of Microsoft.WindowsDesktop.App.WindowsForms, we map it to the
// runtime framework of Microsoft.WindowsDesktop.App, which is what the pruned packages are defined in terms of
List<string> runtimeFrameworks = new List<string>();
foreach (var frameworkReference in filteredFrameworkReferences)
{
// Number of framework references is generally low enough that it's not worth putting the targeting packs into a hash set
var targetingPack = TargetingPacks.FirstOrDefault(tp => tp.ItemSpec.Equals(frameworkReference.ItemSpec, StringComparison.OrdinalIgnoreCase));
if (targetingPack != null)
{
runtimeFrameworks.Add(targetingPack.GetMetadata("RuntimeFrameworkName"));
}
}
CacheKey key = new()
{
TargetFrameworkIdentifier = TargetFrameworkIdentifier,
TargetFrameworkVersion = TargetFrameworkVersion,
FrameworkReferences = runtimeFrameworks.ToHashSet()
};
// Cache framework package values per build
var existingResult = BuildEngine4.GetRegisteredTaskObject(key, RegisteredTaskObjectLifetime.Build);
if (existingResult != null)
{
PackagesToPrune = (TaskItem[])existingResult;
return;
}
PackagesToPrune = LoadPackagesToPrune(key, TargetingPackRoots, PrunePackageDataRoot, Log, AllowMissingPrunePackageData);
BuildEngine4.RegisterTaskObject(key, PackagesToPrune, RegisteredTaskObjectLifetime.Build, true);
}
static TaskItem[] LoadPackagesToPrune(CacheKey key, string[] targetingPackRoots, string prunePackageDataRoot, Logger log, bool allowMissingPrunePackageData)
{
Dictionary<string, NuGetVersion> packagesToPrune = new();
var targetFrameworkVersion = Version.Parse(key.TargetFrameworkVersion);
if (key.FrameworkReferences.Count == 0 && key.TargetFrameworkIdentifier.Equals(".NETCoreApp") && targetFrameworkVersion.Major >= 3)
{
// 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(""))
{
// Filter out framework references we don't expect to have prune data for, such as Microsoft.Windows.SDK.NET.Ref
if (!frameworkReference.Equals(string.Empty, StringComparison.OrdinalIgnoreCase) &&
!frameworkReference.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase) &&
!frameworkReference.Equals("Microsoft.AspNetCore.App", StringComparison.OrdinalIgnoreCase) &&
!frameworkReference.Equals("Microsoft.WindowsDesktop.App", StringComparison.OrdinalIgnoreCase))
{
continue;
}
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);
}
}
if (packagesForFrameworkReference == null)
{
// We didn't find the data for packages to prune. This indicates that there's a bug in the SDK construction, so fail hard here so that we fix that
// (rather than a warning that might be missed).
// Since this indicates an error in the SDK build, the message probably doesn't need to be localized.
if (allowMissingPrunePackageData)
{
log.LogMessage($"Prune package data not found for {key.TargetFrameworkIdentifier} {key.TargetFrameworkVersion} {frameworkReference}");
}
else
{
log.LogError(Strings.PrunePackageDataNotFound, key.TargetFrameworkIdentifier, key.TargetFrameworkVersion, frameworkReference);
}
}
else
{
AddPackagesToPrune(packagesToPrune, packagesForFrameworkReference.Select(kvp => (kvp.Key, kvp.Value)), log);
}
}
return packagesToPrune.Select(p =>
{
var item = new TaskItem(p.Key);
string version;
if (key.TargetFrameworkIdentifier.Equals(".NETCoreApp", StringComparison.OrdinalIgnoreCase) && !p.Value.IsPrerelease)
{
// If a given version of a package is included in a framework, assume that any patches
// to that package will be included in patches to the framework, and thus should be pruned.
// See https://github.com/dotnet/sdk/issues/44566
// To do this, we set the patch version for the package to be pruned to 32767, which should be
// higher than any actual patch version.
var maxPatch = new NuGetVersion(p.Value.Major, p.Value.Minor, 32767);
version = maxPatch.ToString();
}
else
{
version = p.Value.ToString();
}
item.SetMetadata("Version", version.ToString());
return item;
}).ToArray();
}
static Dictionary<string, NuGetVersion> LoadPackagesToPruneFromFrameworkPackages(string targetFrameworkIdentifier, string targetFrameworkVersion, string frameworkReference, bool acceptNearestMatch = false)
{
var nugetFramework = new NuGetFramework(targetFrameworkIdentifier, Version.Parse(targetFrameworkVersion));
// FrameworkPackages just has data for .NET Framework 4.6.1, so turn on fallback for anything greater than that so it will resolve to the .NET Framework 4.6.1 data
if (!acceptNearestMatch && nugetFramework.IsDesktop() && nugetFramework.Version > new Version(4,6,1))
{
acceptNearestMatch = true;
}
var frameworkPackages = FrameworkPackages.GetFrameworkPackages(nugetFramework, [frameworkReference], acceptNearestMatch)
.SelectMany(packages => packages)
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
return frameworkPackages;
}
static Dictionary<string, NuGetVersion> LoadPackagesToPruneFromPrunePackageData(string targetFrameworkIdentifier, string targetFrameworkVersion, string frameworkReference, string prunePackageDataRoot)
{
if (frameworkReference.Equals("Microsoft.NETCore.App", StringComparison.OrdinalIgnoreCase))
{
string packageOverridesPath = Path.Combine(prunePackageDataRoot, targetFrameworkVersion, frameworkReference, "PackageOverrides.txt");
if (File.Exists(packageOverridesPath))
{
var packageOverrideLines = File.ReadAllLines(packageOverridesPath);
var overrides = PackageOverride.CreateOverriddenPackages(packageOverrideLines);
return overrides.ToDictionary(o => o.id, o => o.version);
}
}
return null;
}
static Dictionary<string, NuGetVersion> LoadPackagesToPruneFromTargetingPack(Logger log, string targetFrameworkIdentifier, string targetFrameworkVersion, string frameworkReference, string [] targetingPackRoots)
{
var nugetFramework = new NuGetFramework(targetFrameworkIdentifier, Version.Parse(targetFrameworkVersion));
foreach (var targetingPackRoot in targetingPackRoots)
{
var frameworkPackages = FrameworkPackages.LoadFrameworkPackagesFromPack(log, nugetFramework, frameworkReference, targetingPackRoot)
?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (frameworkPackages != null)
{
// We found the framework packages in the targeting pack, so return them
return frameworkPackages;
}
}
return null;
}
static void AddPackagesToPrune(Dictionary<string, NuGetVersion> packagesToPrune, IEnumerable<(string id, NuGetVersion version)> packagesToAdd, Logger log)
{
foreach (var package in packagesToAdd)
{
// There are some "inconsistent" versions in the FrameworkPackages data. This happens because, for example, the ASP.NET Core shared framework for .NET 9 inherits
// from the ASP.NET Core shared framework for .NET 8, but not from the base Microsoft.NETCore.App framework for .NET 9. So for something like System.IO.Pipelines,
// which was in ASP.NET in .NET 8 but moved to the base shared framework in .NET 9, we will see an 8.0 version from the ASP.NET shared framework and a 9.0 version
// from the base shared framework. As long as the base shared framework is always referenced together with the ASP.NET shared framework, this shouldn't be a
// problem, and we can just pick the latest version of the package that we see.
if (!packagesToPrune.TryGetValue(package.id, out NuGetVersion existingVersion) || package.version > existingVersion)
{
packagesToPrune[package.id] = package.version;
}
}
}
}
}