-
Notifications
You must be signed in to change notification settings - Fork 674
/
FrameworkReducer.cs
561 lines (478 loc) · 24.4 KB
/
FrameworkReducer.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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace NuGet.Frameworks
{
/// <summary>
/// Reduces a list of frameworks into the smallest set of frameworks required.
/// </summary>
public class FrameworkReducer
{
private readonly IFrameworkNameProvider _mappings;
private readonly IFrameworkCompatibilityProvider _compat;
/// <summary>
/// Creates a FrameworkReducer using the default framework mappings.
/// </summary>
public FrameworkReducer()
: this(DefaultFrameworkNameProvider.Instance, DefaultCompatibilityProvider.Instance)
{
}
/// <summary>
/// Creates a FrameworkReducer using custom framework mappings.
/// </summary>
public FrameworkReducer(IFrameworkNameProvider mappings, IFrameworkCompatibilityProvider compat)
{
_mappings = mappings ?? throw new ArgumentNullException(nameof(mappings));
_compat = compat ?? throw new ArgumentNullException(nameof(compat));
}
/// <summary>
/// Returns the nearest matching framework that is compatible.
/// </summary>
/// <param name="framework">Project target framework</param>
/// <param name="possibleFrameworks">Possible frameworks to narrow down</param>
/// <returns>Nearest compatible framework. If no frameworks are compatible null is returned.</returns>
public NuGetFramework? GetNearest(NuGetFramework framework, IEnumerable<NuGetFramework> possibleFrameworks)
{
if (framework == null) throw new ArgumentNullException(nameof(framework));
if (possibleFrameworks == null) throw new ArgumentNullException(nameof(possibleFrameworks));
var nearest = GetNearestInternal(framework, possibleFrameworks);
var fallbackFramework = framework as FallbackFramework;
if (fallbackFramework != null)
{
if (nearest == null || nearest.IsAny)
{
foreach (var supportFramework in fallbackFramework.Fallback)
{
nearest = GetNearestInternal(supportFramework, possibleFrameworks);
if (nearest != null)
{
break;
}
}
}
}
return nearest;
}
private NuGetFramework? GetNearestInternal(NuGetFramework framework, IEnumerable<NuGetFramework> possibleFrameworks)
{
NuGetFramework? nearest = null;
// Unsupported frameworks always lose, throw them out unless it's all we were given
if (possibleFrameworks.Any(e => e != NuGetFramework.UnsupportedFramework))
{
possibleFrameworks = possibleFrameworks.Where(e => e != NuGetFramework.UnsupportedFramework);
}
// Try exact matches first
nearest = possibleFrameworks.Where(f => NuGetFrameworkFullComparer.Instance.Equals(framework, f)).FirstOrDefault();
if (nearest == null)
{
// Elimate non-compatible frameworks
var compatible = possibleFrameworks.Where(f => _compat.IsCompatible(framework, f));
// Remove lower versions of compatible frameworks
var reduced = ReduceUpwards(compatible);
bool isNet6Era = framework.IsNet5Era && framework.Version.Major >= 6;
// Reduce to the same framework name if possible, with an exception for Xamarin, MonoAndroid and Tizen when net6.0+
if (reduced.Count() > 1 && reduced.Any(f => NuGetFrameworkNameComparer.Instance.Equals(f, framework)))
{
reduced = reduced.Where(f =>
{
if (isNet6Era && framework.HasPlatform && (
f.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.MonoAndroid, StringComparison.OrdinalIgnoreCase)
|| f.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.Tizen, StringComparison.OrdinalIgnoreCase)
))
{
return true;
}
else
{
return NuGetFrameworkNameComparer.Instance.Equals(f, framework);
}
});
}
// PCL reduce
if (reduced.Count() > 1)
{
// if we have a pcl and non-pcl mix, throw out the pcls
if (reduced.Any(f => f.IsPCL)
&& reduced.Any(f => !f.IsPCL))
{
reduced = reduced.Where(f => !f.IsPCL);
}
else if (reduced.All(f => f.IsPCL))
{
// decide between PCLs
if (framework.IsPCL)
{
reduced = GetNearestPCLtoPCL(framework, reduced);
}
else
{
reduced = GetNearestNonPCLtoPCL(framework, reduced);
}
if (reduced.Count() > 1)
{
// For scenarios where we are unable to decide between PCLs, choose the PCL with the
// least frameworks. Less frameworks means less compatibility which means it is nearer to the target.
reduced = new NuGetFramework[] { GetBestPCL(reduced)! };
}
}
}
// Packages based framework reduce, only if the project is not packages based
if (reduced.Count() > 1
&& !framework.IsPackageBased
&& reduced.Any(f => f.IsPackageBased)
&& reduced.Any(f => !f.IsPackageBased))
{
// If we have a packages based and non-packages based mix, throw out the packages based frameworks.
// This situation is unlikely but it could happen with framework mappings that do not provide
// a relationship between the frameworks and the compatible packages based frameworks.
// Ex: net46, dotnet -> net46
reduced = reduced.Where(f => !f.IsPackageBased);
}
// Profile reduce
if (reduced.Count() > 1
&& !reduced.Any(f => f.IsPCL))
{
// Prefer the same framework and profile
if (framework.HasProfile)
{
var sameProfile = reduced.Where(f => NuGetFrameworkNameComparer.Instance.Equals(framework, f)
&& StringComparer.OrdinalIgnoreCase.Equals(framework.Profile, f.Profile));
if (sameProfile.Any())
{
reduced = sameProfile;
}
}
// Prefer frameworks without profiles
if (reduced.Count() > 1
&& reduced.Any(f => f.HasProfile)
&& reduced.Any(f => !f.HasProfile))
{
reduced = reduced.Where(f => !f.HasProfile);
}
}
// Platforms reduce
if (reduced.Count() > 1
&& framework.HasPlatform)
{
if (!isNet6Era || reduced.Any(f => NuGetFrameworkNameComparer.Instance.Equals(framework, f) && f.Version.Major >= 6))
{
// Prefer the highest framework version, likely to be the non-platform specific option.
reduced = reduced.Where(f => NuGetFrameworkNameComparer.Instance.Equals(framework, f)).GroupBy(f => f.Version).OrderByDescending(f => f.Key).First();
}
else if (isNet6Era && reduced.Any(f =>
{
return f.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.MonoAndroid, StringComparison.OrdinalIgnoreCase)
|| f.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.Tizen, StringComparison.OrdinalIgnoreCase);
}))
{
// We have a special case for *some* Xamarin-related frameworks here. For specific precedence rules, please see:
// https://github.com/dotnet/designs/blob/main/accepted/2021/net6.0-tfms/net6.0-tfms.md#compatibility-rules
reduced = reduced.GroupBy(f => f.Framework).OrderByDescending(f => f.Key).First(f =>
{
NuGetFramework first = f.First();
return first.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.MonoAndroid, StringComparison.OrdinalIgnoreCase)
|| first.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.Tizen, StringComparison.OrdinalIgnoreCase);
});
}
}
// if we have reduced down to a single framework, use that
if (reduced.Count() == 1)
{
nearest = reduced.Single();
}
// this should be a very rare occurrence
// at this point we are unable to decide between the remaining frameworks in any useful way
// just take the first one by rev alphabetical order if we can't narrow it down at all
if (nearest == null && reduced.Any())
{
// Sort by precedence rules, then by name in the case of a tie
nearest = reduced
.OrderBy(f => f, new FrameworkPrecedenceSorter(_mappings, false))
.ThenByDescending(f => f, NuGetFrameworkSorter.Instance)
.ThenBy(f => f.GetHashCode())
.First();
}
}
return nearest;
}
/// <summary>
/// Remove duplicates found in the equivalence mappings.
/// </summary>
public IEnumerable<NuGetFramework> ReduceEquivalent(IEnumerable<NuGetFramework> frameworks)
{
if (frameworks == null) throw new ArgumentNullException(nameof(frameworks));
// order first so we get consistent results for equivalent frameworks
var input = frameworks
.OrderBy(f => f, new FrameworkPrecedenceSorter(_mappings, true))
.ThenByDescending(f => f, NuGetFrameworkSorter.Instance)
.ToArray();
var duplicates = new HashSet<NuGetFramework>();
foreach (var framework in input)
{
if (duplicates.Contains(framework))
{
continue;
}
yield return framework;
duplicates.Add(framework);
if (_mappings.TryGetEquivalentFrameworks(framework, out IEnumerable<NuGetFramework>? eqFrameworks))
{
foreach (var eqFramework in eqFrameworks)
{
duplicates.Add(eqFramework);
}
}
}
}
/// <summary>
/// Reduce to the highest framework
/// Ex: net45, net403, net40 -> net45
/// </summary>
public IEnumerable<NuGetFramework> ReduceUpwards(IEnumerable<NuGetFramework> frameworks)
{
if (frameworks is null) throw new ArgumentNullException(nameof(frameworks));
// NuGetFramework.AnyFramework is a special case
if (frameworks.Any(e => e != NuGetFramework.AnyFramework))
{
// Remove all instances of Any unless it is the only one in the list
frameworks = frameworks.Where(e => e != NuGetFramework.AnyFramework);
}
// x: net40 j: net45 -> remove net40
// x: wp8 j: win8 -> keep wp8
return ReduceCore(frameworks, (x, y) => _compat.IsCompatible(y, x)).ToArray();
}
/// <summary>
/// Reduce to the lowest framework
/// Ex: net45, net403, net40 -> net40
/// </summary>
public IEnumerable<NuGetFramework> ReduceDownwards(IEnumerable<NuGetFramework> frameworks)
{
if (frameworks is null) throw new ArgumentNullException(nameof(frameworks));
// NuGetFramework.AnyFramework is a special case
if (frameworks.Any(e => e == NuGetFramework.AnyFramework))
{
// Any is always the lowest
return new[] { NuGetFramework.AnyFramework };
}
return ReduceCore(frameworks, (x, y) => _compat.IsCompatible(x, y)).ToArray();
}
private IEnumerable<NuGetFramework> ReduceCore(IEnumerable<NuGetFramework> frameworks, Func<NuGetFramework, NuGetFramework, bool> isCompat)
{
// remove duplicate frameworks
var input = frameworks.Distinct(NuGetFrameworkFullComparer.Instance).ToArray();
var results = new List<NuGetFramework>(input.Length);
for (var i = 0; i < input.Length; i++)
{
var dupe = false;
var x = input[i];
for (var j = 0; !dupe && j < input.Length; j++)
{
if (j != i)
{
var y = input[j];
// remove frameworks that are compatible with other frameworks in the list
// do not remove frameworks which tie with others, for example: net40 and net40-client
// these equivalent frameworks should both be returned to let the caller decide between them
if (isCompat(x, y))
{
var revCompat = isCompat(y, x);
dupe = !revCompat;
// for scenarios where the framework identifiers are the same dupe the zero version
// Ex: win, win8 - these are equivalent, but only one is needed
if (revCompat && NuGetFrameworkNameComparer.Instance.Equals(x, y))
{
// Throw out the zero version
// Profile, Platform, and all other aspects should have been covered by the compat check already
dupe = x.Version == FrameworkConstants.EmptyVersion && y.Version != FrameworkConstants.EmptyVersion;
}
}
}
}
if (!dupe)
{
results.Add(input[i]);
}
}
// sort the results just to make this more deterministic for the callers
return results.OrderBy(f => f.Framework, StringComparer.OrdinalIgnoreCase).ThenBy(f => f.ToString());
}
private IEnumerable<NuGetFramework> GetNearestNonPCLtoPCL(NuGetFramework framework, IEnumerable<NuGetFramework> reduced)
{
// If framework is not a PCL, find the PCL with the sub framework nearest to framework
// Collect all frameworks from all PCLs we are considering
var pclToFrameworks = ExplodePortableFrameworks(reduced);
var allPclFrameworks = pclToFrameworks.Values.SelectMany(f => f);
// Find the nearest (no PCLs are involved at this point)
Debug.Assert(allPclFrameworks.All(f => !f.IsPCL), "a PCL returned a PCL as its profile framework");
var nearestProfileFramework = GetNearest(framework, allPclFrameworks);
// Reduce to only PCLs that include the nearest match
reduced = pclToFrameworks.Where(pair =>
pair.Value.Contains(nearestProfileFramework))
.Select(pair => pair.Key);
return reduced;
}
private IEnumerable<NuGetFramework> GetNearestPCLtoPCL(NuGetFramework framework, IEnumerable<NuGetFramework> reduced)
{
// Compare each framework in the target framework individually
// against the list of possible PCLs. This effectively lets
// each sub-framework vote on which PCL is nearest.
var subFrameworks = ExplodePortableFramework(framework);
// reduce the sub frameworks - this would only have an effect if the PCL is
// poorly formed and contains duplicates such as portable-win8+win81
subFrameworks = ReduceEquivalent(subFrameworks);
// Find all frameworks in all PCLs
var pclToFrameworks = ExplodePortableFrameworks(reduced);
var allPclFrameworks = pclToFrameworks.Values.SelectMany(f => f).Distinct(NuGetFrameworkFullComparer.Instance);
var scores = new Dictionary<NuGetFramework, int>();
// find the nearest PCL for each framework
foreach (var sub in subFrameworks)
{
Debug.Assert(!sub.IsPCL, "a PCL returned a PCL as its profile framework");
// from all possible frameworks, find the best match
var nearestForSub = GetNearest(sub, allPclFrameworks);
if (nearestForSub != null)
{
// +1 each framework containing the best match
foreach (KeyValuePair<NuGetFramework, IEnumerable<NuGetFramework>> pair in pclToFrameworks)
{
if (pair.Value.Contains(nearestForSub, NuGetFrameworkFullComparer.Instance))
{
if (!scores.ContainsKey(pair.Key))
{
scores.Add(pair.Key, 1);
}
else
{
scores[pair.Key]++;
}
}
}
}
}
// take the highest vote count, this will be at least one
reduced = scores.GroupBy(pair => pair.Value).OrderByDescending(g => g.Key).First().Select(e => e.Key);
return reduced;
}
/// <summary>
/// Create lookup of the given PCLs to their actual frameworks
/// </summary>
private Dictionary<NuGetFramework, IEnumerable<NuGetFramework>> ExplodePortableFrameworks(IEnumerable<NuGetFramework> pcls)
{
var result = new Dictionary<NuGetFramework, IEnumerable<NuGetFramework>>();
foreach (var pcl in pcls)
{
var frameworks = ExplodePortableFramework(pcl);
result.Add(pcl, frameworks);
}
return result;
}
/// <summary>
/// portable-net45+win8 -> net45, win8
/// </summary>
private IEnumerable<NuGetFramework> ExplodePortableFramework(NuGetFramework pcl, bool includeOptional = true)
{
if (!_mappings.TryGetPortableFrameworks(pcl.Profile, includeOptional, out IEnumerable<NuGetFramework>? frameworks))
{
Debug.Fail("Unable to get portable frameworks from: " + pcl.ToString());
frameworks = Enumerable.Empty<NuGetFramework>();
}
return frameworks;
}
/// <summary>
/// Order PCLs when there is no other way to decide.
/// </summary>
private NuGetFramework? GetBestPCL(IEnumerable<NuGetFramework> reduced)
{
NuGetFramework? current = null;
foreach (var considering in reduced)
{
if (current == null
|| IsBetterPCL(current, considering))
{
current = considering;
}
}
return current;
}
/// <summary>
/// Sort PCLs using these criteria
/// 1. Lowest number of frameworks (highest surface area) wins first
/// 2. Profile with the highest version numbers wins next
/// 3. String compare is used as a last resort
/// </summary>
private bool IsBetterPCL(NuGetFramework current, NuGetFramework considering)
{
Debug.Assert(considering.IsPCL && current.IsPCL, "This method should be used only to compare PCLs");
// Find all frameworks in the profile
var consideringFrameworks = ExplodePortableFramework(considering, false);
var currentFrameworks = ExplodePortableFramework(current, false);
// The PCL with the least frameworks (highest surface area) goes first
if (consideringFrameworks.Count() < currentFrameworks.Count())
{
return true;
}
else if (currentFrameworks.Count() < consideringFrameworks.Count())
{
return false;
}
// If both frameworks have the same number of frameworks take the framework that has the highest
// overall set of framework versions
// Find Frameworks that both profiles have in common
var sharedFrameworkIds = consideringFrameworks.Select(f => f.Framework)
.Where(f =>
currentFrameworks.Any(consideringFramework => StringComparer.OrdinalIgnoreCase.Equals(f, consideringFramework.Framework)));
var consideringHighest = 0;
var currentHighest = 0;
// Determine which framework has the highest version of each shared framework
foreach (var sharedId in sharedFrameworkIds)
{
var consideringFramework = consideringFrameworks.First(f => StringComparer.OrdinalIgnoreCase.Equals(f.Framework, sharedId));
var currentFramework = currentFrameworks.First(f => StringComparer.OrdinalIgnoreCase.Equals(f.Framework, sharedId));
if (consideringFramework.Version < currentFramework.Version)
{
currentHighest++;
}
else if (currentFramework.Version < consideringFramework.Version)
{
consideringHighest++;
}
}
// Prefer the highest count
if (currentHighest < consideringHighest)
{
return true;
}
else if (consideringHighest < currentHighest)
{
return false;
}
// Take the highest version of .NET if no winner could be determined, this is usually a good indicator of which is newer
var consideringNet = consideringFrameworks.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Framework, FrameworkConstants.FrameworkIdentifiers.Net));
var currentNet = currentFrameworks.FirstOrDefault(f => StringComparer.OrdinalIgnoreCase.Equals(f.Framework, FrameworkConstants.FrameworkIdentifiers.Net));
// Compare using .NET only if both frameworks have it. PCLs should always have .NET, but since users can make these strings up we should
// try to handle that as best as possible.
if (consideringNet != null
&& currentNet != null)
{
if (currentNet.Version < consideringNet.Version)
{
return true;
}
else if (consideringNet.Version < currentNet.Version)
{
return false;
}
}
// In the very rare case that both frameworks are still equal, we have to pick one.
// There is nothing but we need to be deterministic, so compare the profiles as strings.
if (StringComparer.OrdinalIgnoreCase.Compare(considering.GetShortFolderName(_mappings), current.GetShortFolderName(_mappings)) < 0)
{
return true;
}
return false;
}
}
}