| File: CompatibilityProvider.cs | Web Access |
| Project: src\nuget-client\src\NuGet.Core\NuGet.Frameworks\NuGet.Frameworks.csproj (NuGet.Frameworks) |
// 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.Concurrent; using System.Collections.Generic; using System.Linq; namespace NuGet.Frameworks { public class CompatibilityProvider : IFrameworkCompatibilityProvider { private readonly IFrameworkNameProvider _mappings; private readonly FrameworkExpander _expander; private static readonly NuGetFrameworkFullComparer FullComparer = NuGetFrameworkFullComparer.Instance; private readonly ConcurrentDictionary<CompatibilityCacheKey, bool> _cache; public CompatibilityProvider(IFrameworkNameProvider mappings) { _mappings = mappings ?? throw new ArgumentNullException(nameof(mappings)); _expander = new FrameworkExpander(mappings); _cache = new ConcurrentDictionary<CompatibilityCacheKey, bool>(); } /// <summary> /// Check if the frameworks are compatible. /// </summary> /// <param name="target">Project framework</param> /// <param name="candidate">Other framework to check against the project framework</param> /// <returns>True if framework supports other</returns> public bool IsCompatible(NuGetFramework target, NuGetFramework candidate) { if (target == null) throw new ArgumentNullException(nameof(target)); if (candidate == null) throw new ArgumentNullException(nameof(candidate)); // check the cache for a solution var cacheKey = new CompatibilityCacheKey(target, candidate); if (!_cache.TryGetValue(cacheKey, out bool result)) { result = IsCompatibleCore(target, candidate) == true; _cache.TryAdd(cacheKey, result); } return result; } /// <summary> /// Actual compatibility check without caching /// </summary> private bool? IsCompatibleCore(NuGetFramework target, NuGetFramework candidate) { bool? result = null; // check if they are the exact same if (FullComparer.Equals(target, candidate)) { return true; } // special cased frameworks if (!target.IsSpecificFramework || !candidate.IsSpecificFramework) { result = IsSpecialFrameworkCompatible(target, candidate); } if (result == null) { if (target.IsPCL || candidate.IsPCL) { // PCL compat logic result = IsPCLCompatible(target, candidate); } else { // regular framework compat check result = IsCompatibleWithTarget(target, candidate); } } return result; } private bool? IsSpecialFrameworkCompatible(NuGetFramework target, NuGetFramework candidate) { // TODO: Revist these if (target.IsAny || candidate.IsAny) { return true; } if (target.IsUnsupported) { return false; } if (candidate.IsAgnostic) { return true; } if (candidate.IsUnsupported) { return false; } return null; } private bool IsPCLCompatible(NuGetFramework target, NuGetFramework candidate) { if (target.IsPCL && !candidate.IsPCL) { return IsCompatibleWithTarget(target, candidate); } IEnumerable<NuGetFramework>? targetFrameworks; IEnumerable<NuGetFramework>? candidateFrameworks; if (target.IsPCL) { // do not include optional frameworks here since we might be unable to tell what is optional on the other framework if (!_mappings.TryGetPortableFrameworks(target.Profile, includeOptional: false, out targetFrameworks)) { targetFrameworks = Array.Empty<NuGetFramework>(); } } else { targetFrameworks = new NuGetFramework[] { target }; } if (candidate.IsPCL) { // include optional frameworks here, the larger the list the more compatible it is if (!_mappings.TryGetPortableFrameworks(candidate.Profile, includeOptional: true, out candidateFrameworks)) { candidateFrameworks = Array.Empty<NuGetFramework>(); } } else { candidateFrameworks = new NuGetFramework[] { candidate }; } // check if we this is a compatible superset return PCLInnerCompare(targetFrameworks, candidateFrameworks); } private bool PCLInnerCompare(IEnumerable<NuGetFramework> targetFrameworks, IEnumerable<NuGetFramework> candidateFrameworks) { // TODO: Does this check need to make sure multiple frameworks aren't matched against a single framework from the other list? return targetFrameworks.Count() <= candidateFrameworks.Count() && targetFrameworks.All(f => candidateFrameworks.Any(ff => IsCompatible(f, ff))); } private bool IsCompatibleWithTarget(NuGetFramework target, NuGetFramework candidate) { // find all possible substitutions var targetSet = new List<NuGetFramework>() { target }; targetSet.AddRange(_expander.Expand(target)); var candidateSet = new List<NuGetFramework>() { candidate }; candidateSet.AddRange(GetEquivalentFrameworksClosure(candidate)); // check for compat foreach (var currentCandidate in candidateSet) { if (targetSet.Any(framework => IsCompatibleWithTargetCore(framework, currentCandidate))) { return true; } } return false; } private static bool IsCompatibleWithTargetCore(NuGetFramework target, NuGetFramework candidate) { bool result = true; bool isNet6Era = target.IsNet5Era && target.Version.Major >= 6; if (isNet6Era && target.HasPlatform && !NuGetFramework.FrameworkNameComparer.Equals(target, candidate)) { if (candidate.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.MonoAndroid, StringComparison.OrdinalIgnoreCase)) { result = result && StringComparer.OrdinalIgnoreCase.Equals(target.Platform, "android"); } else if (candidate.Framework.Equals(FrameworkConstants.FrameworkIdentifiers.Tizen, StringComparison.OrdinalIgnoreCase)) { result = result && StringComparer.OrdinalIgnoreCase.Equals(target.Platform, "tizen"); } else { result = false; } } else { result = NuGetFramework.FrameworkNameComparer.Equals(target, candidate) && IsVersionCompatible(target.Version, candidate.Version) && StringComparer.OrdinalIgnoreCase.Equals(target.Profile, candidate.Profile); if (target.IsNet5Era && candidate.HasPlatform) { // net*.0-windows, where the windows version is 10 or higher, will make WinRT APIs available. // Under the covers it uses CsWinRT to convert WinRT ABIs to .NET API/ABIs. // Different major versions of CsWinRT are incompatible with each other, so we're special // casing windows to encode CsWinRT as the platform revision. // Therefore, net10.0-windows10.0.2610.0 should not be compatible with // net10.0-windows10.0.22000.1, even though the platform version is higher // because the revision number is different, signaling an incompatible CsWinRT version. if (result // Scope to .NET 10 and later for the Windows 10 TFM && target.Version.Major >= 10 && StringComparer.OrdinalIgnoreCase.Equals(target.Platform, "windows") && target.PlatformVersion.Major >= 10 && candidate.PlatformVersion.Major >= 10 // Check if both are targeting the same version of CsWinRT support && target.PlatformVersion.Revision != candidate.PlatformVersion.Revision) { result = false; } result = result && StringComparer.OrdinalIgnoreCase.Equals(target.Platform, candidate.Platform) && IsVersionCompatible(target.PlatformVersion, candidate.PlatformVersion); } } return result; } private static bool IsVersionCompatible(Version target, Version candidate) { return candidate == FrameworkConstants.EmptyVersion || candidate <= target; } /// <summary> /// Find all equivalent frameworks, and their equivalent frameworks. /// Example: /// Mappings: /// A <‒> B /// B <‒> C /// C <‒> D /// For A we need to find B, C, and D so we must retrieve equivalent frameworks for A, B, and C /// also as we discover them. /// </summary> private IEnumerable<NuGetFramework> GetEquivalentFrameworksClosure(NuGetFramework framework) { // add the current framework to the seen list to avoid returning it later var seen = new HashSet<NuGetFramework>() { framework }; var toExpand = new Stack<NuGetFramework>(); toExpand.Push(framework); while (toExpand.Count > 0) { var frameworkToExpand = toExpand.Pop(); if (_mappings.TryGetEquivalentFrameworks(frameworkToExpand, out IEnumerable<NuGetFramework>? compatibleFrameworks)) { foreach (var curFramework in compatibleFrameworks) { if (seen.Add(curFramework)) { yield return curFramework; toExpand.Push(curFramework); } } } } yield break; } } }