File: RestoreCommand\CompatibilityChecker.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using NuGet.Client;
using NuGet.Common;
using NuGet.ContentModel;
using NuGet.DependencyResolver;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;
using NuGet.Repositories;

namespace NuGet.Commands
{
    internal class CompatibilityChecker
    {
        private readonly IReadOnlyList<NuGetv3LocalRepository> _localRepositories;
        private readonly LockFile _lockFile;
        private readonly ILogger _log;
        private readonly bool _validateRuntimeAssets;

        public CompatibilityChecker(IReadOnlyList<NuGetv3LocalRepository> localRepositories, LockFile lockFile, bool validateRuntimeAssets, ILogger log)
        {
            _localRepositories = localRepositories;
            _lockFile = lockFile;
            _log = log;
            _validateRuntimeAssets = validateRuntimeAssets;
        }

        internal async Task<CompatibilityCheckResult> CheckAsync(
            RestoreTargetGraph graph,
            Dictionary<string, LibraryIncludeFlags> includeFlags,
            PackageSpec packageSpec)
        {
            // The Compatibility Check is designed to alert the user to cases where packages are not behaving as they would
            // expect, due to compatibility issues.
            //
            // During this check, we scan all packages for a given restore graph and check the following conditions
            // (using an example TxM 'foo' and an example Runtime ID 'bar'):
            //
            // * If any package provides a "ref/foo/Thingy.dll", there MUST be a matching "lib/foo/Thingy.dll" or
            //   "runtimes/bar/lib/foo/Thingy.dll" provided by a package in the graph.
            // * All packages that contain Managed Assemblies must provide assemblies for 'foo'. If a package
            //   contains any of 'ref/' folders, 'lib/' folders, or framework assemblies, it must provide at least
            //   one of those for the 'foo' framework. Otherwise, the package is intending to provide managed assemblies
            //   but it does not support the target platform. If a package contains only 'content/', 'build/', 'tools/' or
            //   other NuGet convention folders, it is exempt from this check. Thus, content-only packages are always considered
            //   compatible, regardless of if they actually provide useful content.
            //
            // It is up to callers to invoke the compatibility check on the graphs they wish to check, but the general behavior in
            // the restore command is to invoke a compatibility check for each of:
            //
            // * The Targets (TxMs) defined in the project.json, with no Runtimes
            // * All combinations of TxMs and Runtimes defined in the project.json
            // * Additional (TxMs, Runtime) pairs defined by the "supports" mechanism in project.json
            var runtimeAssemblies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var compileAssemblies = new Dictionary<string, LibraryIdentity>(StringComparer.OrdinalIgnoreCase);
            var issues = new List<CompatibilityIssue>();

            // Verify framework assets also as part of runtime assets validation.
            foreach (var node in graph.Flattened)
            {
                // Check project compatibility
                if (node.Key.Type == LibraryType.Project)
                {
                    // Get the full library
                    var localMatch = node.Data?.Match as LocalMatch;
                    if (localMatch == null || !IsProjectFrameworkCompatible(localMatch.LocalLibrary))
                    {
                        var available = new List<NuGetFramework>();

                        // If the project info is available find all available frameworks
                        if (localMatch?.LocalLibrary != null)
                        {
                            available = GetProjectFrameworks(localMatch.LocalLibrary);
                        }

                        // Create issue
                        var issue = CompatibilityIssue.IncompatibleProject(
                            new PackageIdentity(node.Key.Name, node.Key.Version),
                            graph.Framework,
                            graph.RuntimeIdentifier,
                            available);

                        issues.Add(issue);
                        await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1201, issue, graph));
                    }

                    // Skip further checks on projects
                    continue;
                }

                // Find the include/exclude flags for this package
                LibraryIncludeFlags packageIncludeFlags;
                if (!includeFlags.TryGetValue(node.Key.Name, out packageIncludeFlags))
                {
                    packageIncludeFlags = LibraryIncludeFlags.All;
                }

                // If the package has compile and runtime assets excluded the compatibility check
                // is not needed. Packages with no ref or lib entries are considered
                // compatible in IsCompatible.
                if ((packageIncludeFlags &
                        (LibraryIncludeFlags.Compile
                        | LibraryIncludeFlags.Runtime)) == LibraryIncludeFlags.None)
                {
                    continue;
                }

                var compatibilityData = GetCompatibilityData(graph, node.Key, packageSpec);
                if (compatibilityData == null)
                {
                    continue;
                }

                if (!IsPackageCompatible(compatibilityData))
                {
                    var available = GetPackageFrameworks(compatibilityData, graph);

                    var issue = CompatibilityIssue.IncompatiblePackage(
                        new PackageIdentity(node.Key.Name, node.Key.Version),
                        graph.Framework,
                        graph.RuntimeIdentifier,
                        available);

                    issues.Add(issue);
                    await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1202, issue, graph));
                }

                if (!IsPackageTypeCompatible(compatibilityData))
                {
                    var issue = CompatibilityIssue.IncompatiblePackageType(
                        new PackageIdentity(node.Key.Name, node.Key.Version),
                        graph.Framework,
                        graph.RuntimeIdentifier);

                    issues.Add(issue);
                    await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1213, issue, graph));
                }

                await VerifyDotnetToolCompatibilityChecks(compatibilityData, node, graph, issues);

                // Check for matching ref/libs if we're checking a runtime-specific graph
                var targetLibrary = compatibilityData.TargetLibrary;
                if (_validateRuntimeAssets && !string.IsNullOrEmpty(graph.RuntimeIdentifier))
                {
                    // Skip runtime checks for packages that have runtime references excluded,
                    // this allows compile only packages that do not have runtimes for the 
                    // graph RID to be used.
                    if ((packageIncludeFlags & LibraryIncludeFlags.Runtime) == LibraryIncludeFlags.Runtime)
                    {
                        // Scan the package for ref assemblies
                        foreach (var compile in targetLibrary.CompileTimeAssemblies
                            .Where(p => Path.GetExtension(p.Path)
                                .Equals(".dll", StringComparison.OrdinalIgnoreCase)))
                        {
                            var name = Path.GetFileNameWithoutExtension(compile.Path);

                            // If we haven't already started tracking this compile-time assembly, AND there isn't already a runtime-loadable version
                            if (!compileAssemblies.ContainsKey(name) && !runtimeAssemblies.Contains(name))
                            {
                                // Track this assembly as potentially compile-time-only
                                compileAssemblies.Add(name, node.Key);
                            }
                        }

                        // Match up runtime assemblies
                        foreach (var runtime in targetLibrary.RuntimeAssemblies
                            .Where(p => Path.GetExtension(p.Path)
                                .Equals(".dll", StringComparison.OrdinalIgnoreCase)))
                        {
                            var name = Path.GetFileNameWithoutExtension(runtime.Path);

                            // If there was a compile-time-only assembly under this name...
                            if (compileAssemblies.ContainsKey(name))
                            {
                                // Remove it, we've found a matching runtime ref
                                compileAssemblies.Remove(name);
                            }

                            // Track this assembly as having a runtime assembly
                            runtimeAssemblies.Add(name);

                            // Fix for NuGet/Home#752 - Consider ".ni.dll" (native image/ngen) files matches for ref/ assemblies
                            if (name.EndsWith(".ni", StringComparison.OrdinalIgnoreCase))
                            {
                                var withoutNi = name.Substring(0, name.Length - 3);

                                if (compileAssemblies.ContainsKey(withoutNi))
                                {
                                    compileAssemblies.Remove(withoutNi);
                                }

                                runtimeAssemblies.Add(withoutNi);
                            }
                        }
                    }
                }
            }

            // Generate errors for un-matched reference assemblies, if we're checking a runtime-specific graph
            if (_validateRuntimeAssets && !string.IsNullOrEmpty(graph.RuntimeIdentifier))
            {
                foreach (var compile in compileAssemblies)
                {
                    var issue = CompatibilityIssue.ReferenceAssemblyNotImplemented(
                        compile.Key,
                        new PackageIdentity(compile.Value.Name, compile.Value.Version),
                        graph.Framework,
                        graph.RuntimeIdentifier);

                    issues.Add(issue);
                    await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1203, issue, graph));
                }
            }

            return new CompatibilityCheckResult(graph, issues);
        }


        /// <summary>
        /// Create an error message for the given issue.
        /// </summary>
        private static RestoreLogMessage GetErrorMessage(NuGetLogCode logCode, CompatibilityIssue issue, RestoreTargetGraph graph)
        {
            return RestoreLogMessage.CreateError(logCode, issue.Format(), issue.Package.Id, graph.TargetGraphName);
        }

        private static IEnumerable<NuGetFramework> GetPackageFrameworks(
            CompatibilityData compatibilityData,
            RestoreTargetGraph graph)
        {
            var available = new HashSet<NuGetFramework>();

            var contentItems = new ContentItemCollection();
            contentItems.Load(compatibilityData.Files);

            var patterns = new[]
            {
                graph.Conventions.Patterns.ResourceAssemblies,
                graph.Conventions.Patterns.CompileRefAssemblies,
                graph.Conventions.Patterns.RuntimeAssemblies,
                graph.Conventions.Patterns.EmbedAssemblies,
                graph.Conventions.Patterns.ContentFiles
            };

            List<ContentItemGroup> itemGroups = new();
            foreach (var pattern in patterns)
            {
                itemGroups.Clear();
                contentItems.PopulateItemGroups(pattern, itemGroups);
                foreach (var group in itemGroups)
                {
                    // lib/net45/subfolder/a.dll will be returned as a group with zero items since sub
                    // folders are not allowed. Completely empty groups are not compatible, a group with
                    // _._ would contain _._ as an item.
                    if (group.Items.Count > 0)
                    {
                        group.Properties.TryGetValue(ManagedCodeConventions.PropertyNames.RuntimeIdentifier, out var ridObj);
                        group.Properties.TryGetValue(ManagedCodeConventions.PropertyNames.TargetFrameworkMoniker, out var tfmObj);

                        var tfm = tfmObj as NuGetFramework;

                        // RID specific items should be ignored here since they are only used in the runtime assembly check
                        if (ridObj == null && tfm?.IsSpecificFramework == true)
                        {
                            available.Add(tfm);
                        }
                    }
                }
            }

            return available;
        }

        private static List<NuGetFramework> GetProjectFrameworks(Library localLibrary)
        {
            var available = new List<NuGetFramework>();

            object frameworksObject;
            if (localLibrary.Items.TryGetValue(
                KnownLibraryProperties.ProjectFrameworks,
                out frameworksObject))
            {
                available = (List<NuGetFramework>)frameworksObject;
            }

            return available;
        }

        private static bool IsProjectFrameworkCompatible(Library library)
        {
            object frameworkInfoObject;
            if (library.Items.TryGetValue(
                KnownLibraryProperties.TargetFrameworkInformation,
                out frameworkInfoObject))
            {
                var targetFrameworkInformation = (TargetFrameworkInformation)frameworkInfoObject;

                // Verify that a valid framework was selected
                return (targetFrameworkInformation.FrameworkName != null
                    && targetFrameworkInformation.FrameworkName != NuGetFramework.UnsupportedFramework);
            }
            else
            {
                // For external projects that do not have any target framework info, assume
                // compatibility was checked before hand
                return true;
            }
        }

        private bool IsPackageCompatible(CompatibilityData compatibilityData)
        {
            // A package is compatible if it has...
            return
                HasCompatibleAssets(compatibilityData.TargetLibrary) ||
                !compatibilityData.Files.Any(p =>
                    p.StartsWith("ref/", StringComparison.OrdinalIgnoreCase)
                    || p.StartsWith("lib/", StringComparison.OrdinalIgnoreCase));                       // No assemblies at all (for any TxM)
        }

        /// <summary>
        /// Whether a package has an incompatible package type. Currently, only 1 package type is incompatible in every project graph and that's DotnetPlatform.
        /// </summary>
        /// <param name="compatibilityData"></param>
        private bool IsPackageTypeCompatible(CompatibilityData compatibilityData)
        {
            // A package is compatible if it is not DotnetPlatform package type
            return !compatibilityData.TargetLibrary.PackageType.Contains(PackageType.DotnetPlatform);
        }

        private async Task VerifyDotnetToolCompatibilityChecks(CompatibilityData compatibilityData, GraphItem<RemoteResolveResult> node, RestoreTargetGraph graph, List<CompatibilityIssue> issues)
        {
            if (compatibilityData.TargetLibrary.PackageType.Contains(PackageType.DotnetTool))
            {
                var issue = CompatibilityIssue.IncompatiblePackageWithDotnetTool(new PackageIdentity(node.Key.Name, node.Key.Version));
                issues.Add(issue);
                await _log.LogAsync(GetErrorMessage(NuGetLogCode.NU1212, issue, graph));
            }
        }

        /// <summary>
        /// Check if the library contains assets.
        /// </summary>
        internal static bool HasCompatibleAssets(LockFileTargetLibrary targetLibrary)
        {
            // A package is compatible if it has...
            return
                targetLibrary.RuntimeAssemblies.Count > 0 ||                          // Runtime Assemblies, or
                targetLibrary.CompileTimeAssemblies.Count > 0 ||                      // Compile-time Assemblies, or
                targetLibrary.FrameworkAssemblies.Count > 0 ||                        // Framework Assemblies, or
                targetLibrary.FrameworkReferences.Count > 0 ||                        // Framework References, or
                targetLibrary.ContentFiles.Count > 0 ||                               // Shared content
                targetLibrary.ResourceAssemblies.Count > 0 ||                         // Resources (satellite package)
                targetLibrary.Build.Count > 0 ||                                      // Build
                targetLibrary.BuildMultiTargeting.Count > 0;                          // Cross targeting build
        }

        private CompatibilityData GetCompatibilityData(RestoreTargetGraph graph, LibraryIdentity libraryId, PackageSpec packageSpec)
        {
            // Use data from the current lock file if it exists.
            LockFileTargetLibrary targetLibrary = null;

            for (int i = 0; i < _lockFile.Targets.Count; ++i)
            {
                var target = _lockFile.Targets[i];
                // When comparing the target alias, a null targetAlias means that we have a support added and as such the alias won't be available.
                if (Equals(target.TargetFramework, graph.Framework) && (target.TargetAlias == null || Equals(target.TargetAlias, graph.TargetAlias)) && string.Equals(target.RuntimeIdentifier, graph.RuntimeIdentifier, StringComparison.Ordinal))
                {
                    for (int j = 0; j < target.Libraries.Count; ++j)
                    {
                        var library = target.Libraries[j];
                        if (library.Name.Equals(libraryId.Name, StringComparison.OrdinalIgnoreCase) && library.Version.Equals(libraryId.Version))
                        {
                            targetLibrary = library;
                            break;
                        }
                    }

                    break;
                }
            }

            IEnumerable<string> files = null;
            for (var i = 0; i < _lockFile.Libraries.Count; i++)
            {
                LockFileLibrary library = _lockFile.Libraries[i];
                if (library.Name.Equals(libraryId.Name, StringComparison.OrdinalIgnoreCase) && library.Version.Equals(libraryId.Version))
                {
                    files = library.Files;
                    break;
                }
            }

            if (files == null || targetLibrary == null)
            {
                // We need to generate some of the data. We'll need the local package info to do that
                var packageInfo = NuGetv3LocalRepositoryUtility.GetPackage(
                    _localRepositories,
                    libraryId.Name,
                    libraryId.Version);

                if (packageInfo == null)
                {
                    return null;
                }

                // Collect the file list if necessary
                if (files == null)
                {
                    files = packageInfo.Package.Files;
                }

                // Generate the target library if necessary
                if (targetLibrary == null)
                {
                    targetLibrary = LockFileUtils.CreateLockFileTargetLibrary(
                        library: null,
                        package: packageInfo.Package,
                        targetGraph: graph,
                        dependencyType: LibraryIncludeFlags.All);
                }
            }

            return new CompatibilityData(files.ToImmutableArray(), targetLibrary, packageSpec);
        }

        private class CompatibilityData
        {
            public ImmutableArray<string> Files { get; }
            public LockFileTargetLibrary TargetLibrary { get; }

            public PackageSpec PackageSpec { get; }

            public CompatibilityData(ImmutableArray<string> files, LockFileTargetLibrary targetLibrary, PackageSpec packageSpec)
            {
                Files = files;
                TargetLibrary = targetLibrary;
                PackageSpec = packageSpec;
            }
        }
    }
}