File: ResolvePackageDependencies.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.NET.Build.Tasks\Microsoft.NET.Build.Tasks.csproj (Microsoft.NET.Build.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.ProjectModel;
 
namespace Microsoft.NET.Build.Tasks
{
    /// <summary>
    /// Raises Nuget LockFile representation to MSBuild items and resolves
    /// assets specified in the lock file.
    /// </summary>
    /// <remarks>
    /// Only called for backwards compatability, when <c>ResolvePackageDependencies</c> is true.
    /// </remarks>
    public sealed class ResolvePackageDependencies : TaskBase
    {
        private readonly Dictionary<string, string> _fileTypes = new(StringComparer.OrdinalIgnoreCase);
 
        private HashSet<string> _projectFileDependencies;
        private IPackageResolver _packageResolver;
        private LockFile _lockFile;
 
        #region Output Items
 
        private readonly List<ITaskItem> _targetDefinitions = new();
        private readonly List<ITaskItem> _packageDefinitions = new();
        private readonly List<ITaskItem> _fileDefinitions = new();
        private readonly List<ITaskItem> _packageDependencies = new();
        private readonly List<ITaskItem> _fileDependencies = new();
 
        /// <summary>
        /// All the targets in the lock file.
        /// </summary>
        [Output]
        public ITaskItem[] TargetDefinitions
        {
            get { return _targetDefinitions.ToArray(); }
        }
 
        /// <summary>
        /// All the libraries/packages in the lock file.
        /// </summary>
        [Output]
        public ITaskItem[] PackageDefinitions
        {
            get { return _packageDefinitions.ToArray(); }
        }
 
        /// <summary>
        /// All the files in the lock file.
        /// </summary>
        [Output]
        public ITaskItem[] FileDefinitions
        {
            get { return _fileDefinitions.ToArray(); }
        }
 
        /// <summary>
        /// All the dependencies between packages. Each package has metadata 'ParentPackage'
        /// to refer to the package that depends on it. For top level packages this value is blank.
        /// </summary>
        [Output]
        public ITaskItem[] PackageDependencies
        {
            get { return _packageDependencies.ToArray(); }
        }
 
        /// <summary>
        /// All the dependencies between files and packages, labeled by the group containing
        /// the file (e.g. CompileTimeAssembly, RuntimeAssembly, etc.).
        /// </summary>
        [Output]
        public ITaskItem[] FileDependencies
        {
            get { return _fileDependencies.ToArray(); }
        }
 
        #endregion
 
        #region Inputs
 
        /// <summary>
        /// The path to the current project.
        /// </summary>
        [Required]
        public string ProjectPath
        {
            get; set;
        }
 
        /// <summary>
        /// The assets file to process
        /// </summary>
        public string ProjectAssetsFile
        {
            get; set;
        }
 
        /// <summary>
        /// Optional the Project Language (E.g. C#, VB)
        /// </summary>
        public string ProjectLanguage
        {
            get; set;
        }
 
        public string TargetFramework { get; set; }
 
        #endregion
 
        public ResolvePackageDependencies()
        {
        }
 
        #region Test Support
 
        internal ResolvePackageDependencies(LockFile lockFile, IPackageResolver packageResolver)
            : this()
        {
            _lockFile = lockFile;
            _packageResolver = packageResolver;
        }
 
        #endregion
 
        private IPackageResolver PackageResolver => _packageResolver ??= NuGetPackageResolver.CreateResolver(LockFile);
 
        private LockFile LockFile => _lockFile ??= new LockFileCache(this).GetLockFile(ProjectAssetsFile);
 
        private Dictionary<string, string> _targetNameToAliasMap;
 
        /// <summary>
        /// Raise Nuget LockFile representation to MSBuild items
        /// </summary>
        protected override void ExecuteCore()
        {
            _targetNameToAliasMap = LockFile.Targets.ToDictionary(t => t.Name, t =>
            {
                var alias = LockFile.GetLockFileTargetAlias(t);
                if (string.IsNullOrEmpty(t.RuntimeIdentifier))
                {
                    return alias;
                }
                else
                {
                    return alias + "/" + t.RuntimeIdentifier;
                }
            });
 
            ReadProjectFileDependencies(string.IsNullOrEmpty(TargetFramework) || !_targetNameToAliasMap.ContainsKey(TargetFramework) ? null : _targetNameToAliasMap[TargetFramework]);
            RaiseLockFileTargets();
            GetPackageAndFileDefinitions();
        }
 
        private void ReadProjectFileDependencies(string frameworkAlias)
        {
            _projectFileDependencies = LockFile.GetProjectFileDependencySet(frameworkAlias);
        }
 
        // get library and file definitions
        private void GetPackageAndFileDefinitions()
        {
            foreach (var package in LockFile.Libraries)
            {
                var packageName = package.Name;
                var packageVersion = package.Version.ToNormalizedString();
                string packageId = $"{packageName}/{packageVersion}";
                var item = new TaskItem(packageId);
                item.SetMetadata(MetadataKeys.Name, packageName);
                item.SetMetadata(MetadataKeys.Type, package.Type);
                item.SetMetadata(MetadataKeys.Version, packageVersion);
 
                item.SetMetadata(MetadataKeys.Path, package.Path ?? string.Empty);
 
                string resolvedPackagePath = ResolvePackagePath(package);
                item.SetMetadata(MetadataKeys.ResolvedPath, resolvedPackagePath ?? string.Empty);
 
                item.SetMetadata(MetadataKeys.DiagnosticLevel, GetPackageDiagnosticLevel(package));
 
                _packageDefinitions.Add(item);
 
                foreach (var file in package.Files)
                {
                    if (NuGetUtils.IsPlaceholderFile(file))
                    {
                        continue;
                    }
 
                    var fileKey = $"{packageId}/{file}";
                    var fileItem = new TaskItem(fileKey);
                    fileItem.SetMetadata(MetadataKeys.Path, file);
                    fileItem.SetMetadata(MetadataKeys.NuGetPackageId, packageName);
                    fileItem.SetMetadata(MetadataKeys.NuGetPackageVersion, packageVersion);
 
                    string resolvedPath = ResolveFilePath(file, resolvedPackagePath);
                    fileItem.SetMetadata(MetadataKeys.ResolvedPath, resolvedPath ?? string.Empty);
 
                    if (NuGetUtils.IsApplicableAnalyzer(file, ProjectLanguage))
                    {
                        fileItem.SetMetadata(MetadataKeys.Analyzer, "true");
                        fileItem.SetMetadata(MetadataKeys.Type, "AnalyzerAssembly");
 
                        // get targets that contain this package
                        var parentTargets = LockFile.Targets
                            .Where(t => t.Libraries.Any(lib => lib.Name == package.Name));
 
                        foreach (var target in parentTargets)
                        {
                            string frameworkAlias = _targetNameToAliasMap[target.Name];
 
                            var fileDepsItem = new TaskItem(fileKey);
                            fileDepsItem.SetMetadata(MetadataKeys.ParentTarget, frameworkAlias); // Foreign Key
                            fileDepsItem.SetMetadata(MetadataKeys.ParentPackage, packageId); // Foreign Key
 
                            _fileDependencies.Add(fileDepsItem);
                        }
                    }
                    else
                    {
                        // get a type for the file if one is available
                        if (!_fileTypes.TryGetValue(fileKey, out string fileType))
                        {
                            fileType = "unknown";
                        }
                        fileItem.SetMetadata(MetadataKeys.Type, fileType);
                    }
 
                    _fileDefinitions.Add(fileItem);
                }
            }
 
            string GetPackageDiagnosticLevel(LockFileLibrary package)
            {
                string target = TargetFramework ?? "";
 
                LogLevel? logLevel = null;
 
                foreach (var message in LockFile.LogMessages)
                {
                    if (message.LibraryId == package.Name)
                    {
                        foreach (var targetGraph in message.TargetGraphs)
                        {
                            string effectiveTargetGraphName = targetGraph;
                            // If the target graph is not in the map, then very likely aliases are being used.
                            if (_targetNameToAliasMap.ContainsKey(targetGraph))
                            {
                                var parsedTargetGraph = NuGetFramework.Parse(targetGraph);
                                effectiveTargetGraphName = _lockFile.PackageSpec.TargetFrameworks.FirstOrDefault(tf => tf.FrameworkName == parsedTargetGraph)?.TargetAlias;
                            }
 
                            if (effectiveTargetGraphName == target)
                            {
                                if (logLevel == null || message.Level > logLevel)
                                {
                                    logLevel = message.Level;
                                }
                                break;
                            }
                        }
                    }
                }
 
                return logLevel != null ? logLevel.ToString() : string.Empty;
            }
        }
 
        // get target definitions and package and file dependencies
        private void RaiseLockFileTargets()
        {
            foreach (var target in LockFile.Targets)
            {
                TaskItem item = new(target.Name);
                item.SetMetadata(MetadataKeys.RuntimeIdentifier, target.RuntimeIdentifier ?? string.Empty);
                item.SetMetadata(MetadataKeys.TargetFramework, TargetFramework);
                item.SetMetadata(MetadataKeys.TargetFrameworkMoniker, target.TargetFramework.DotNetFrameworkName);
                item.SetMetadata(MetadataKeys.FrameworkName, target.TargetFramework.Framework);
                item.SetMetadata(MetadataKeys.FrameworkVersion, target.TargetFramework.Version.ToString());
                item.SetMetadata(MetadataKeys.Type, "target");
 
                _targetDefinitions.Add(item);
 
                // raise each library in the target
                GetPackageAndFileDependencies(target);
            }
        }
 
        private void GetPackageAndFileDependencies(LockFileTarget target)
        {
            var resolvedPackageVersions = target.Libraries
                .ToDictionary(pkg => pkg.Name, pkg => pkg.Version.ToNormalizedString(), StringComparer.OrdinalIgnoreCase);
 
            string frameworkAlias = _targetNameToAliasMap[target.Name];
 
            var transitiveProjectRefs = new HashSet<string>(
                target.Libraries
                    .Where(lib => lib.IsTransitiveProjectReference(LockFile, ref _projectFileDependencies, frameworkAlias))
                    .Select(pkg => pkg.Name),
                StringComparer.OrdinalIgnoreCase);
 
            foreach (var package in target.Libraries)
            {
                string packageId = $"{package.Name}/{package.Version.ToNormalizedString()}";
 
                if (_projectFileDependencies.Contains(package.Name))
                {
                    TaskItem item = new(packageId);
                    item.SetMetadata(MetadataKeys.ParentTarget, frameworkAlias); // Foreign Key
                    item.SetMetadata(MetadataKeys.ParentPackage, string.Empty); // Foreign Key
 
                    _packageDependencies.Add(item);
                }
 
                // get sub package dependencies
                GetPackageDependencies(package, target.Name, resolvedPackageVersions, transitiveProjectRefs);
 
                // get file dependencies on this package
                GetFileDependencies(package, target.Name);
            }
        }
 
        private void GetPackageDependencies(
            LockFileTargetLibrary package,
            string targetName,
            Dictionary<string, string> resolvedPackageVersions,
            HashSet<string> transitiveProjectRefs)
        {
            string packageId = $"{package.Name}/{package.Version.ToNormalizedString()}";
            string frameworkAlias = _targetNameToAliasMap[targetName];
            foreach (var deps in package.Dependencies)
            {
                if (!resolvedPackageVersions.TryGetValue(deps.Id, out string version))
                {
                    continue;
                }
 
                string depsName = $"{deps.Id}/{version}";
 
                TaskItem item = new(depsName);
                item.SetMetadata(MetadataKeys.ParentTarget, frameworkAlias); // Foreign Key
                item.SetMetadata(MetadataKeys.ParentPackage, packageId); // Foreign Key
 
                if (transitiveProjectRefs.Contains(deps.Id))
                {
                    item.SetMetadata(MetadataKeys.TransitiveProjectReference, "true");
                }
 
                _packageDependencies.Add(item);
            }
        }
 
        private void GetFileDependencies(LockFileTargetLibrary package, string targetName)
        {
            string packageId = $"{package.Name}/{package.Version.ToNormalizedString()}";
            string frameworkAlias = _targetNameToAliasMap[targetName];
 
            // for each type of file group
            foreach (var fileGroup in (FileGroup[])Enum.GetValues(typeof(FileGroup)))
            {
                var filePathList = fileGroup.GetFilePathAndProperties(package);
                foreach (var entry in filePathList)
                {
                    string filePath = entry.Item1;
                    IDictionary<string, string> properties = entry.Item2;
 
                    if (NuGetUtils.IsPlaceholderFile(filePath))
                    {
                        continue;
                    }
 
                    var fileKey = $"{packageId}/{filePath}";
                    var item = new TaskItem(fileKey);
                    item.SetMetadata(MetadataKeys.FileGroup, fileGroup.ToString());
                    item.SetMetadata(MetadataKeys.ParentTarget, frameworkAlias); // Foreign Key
                    item.SetMetadata(MetadataKeys.ParentPackage, packageId); // Foreign Key
 
                    if (fileGroup == FileGroup.FrameworkAssembly)
                    {
                        // NOTE: the path provided for framework assemblies is the name of the framework reference
                        item.SetMetadata("FrameworkAssembly", filePath);
                        item.SetMetadata(MetadataKeys.NuGetPackageId, package.Name);
                        item.SetMetadata(MetadataKeys.NuGetPackageVersion, package.Version.ToNormalizedString());
                    }
 
                    foreach (var property in properties)
                    {
                        item.SetMetadata(property.Key, property.Value);
                    }
 
                    _fileDependencies.Add(item);
 
                    // map each file key to a Type metadata value
                    SaveFileKeyType(fileKey, fileGroup);
                }
            }
        }
 
        // save file type metadata based on the group the file appears in
        private void SaveFileKeyType(string fileKey, FileGroup fileGroup)
        {
            string fileType = fileGroup.GetTypeMetadata();
            if (fileType != null)
            {
                if (!_fileTypes.TryGetValue(fileKey, out string currentFileType))
                {
                    _fileTypes.Add(fileKey, fileType);
                }
                else if (currentFileType != fileType)
                {
                    throw new BuildErrorException(Strings.UnexpectedFileType, fileKey, fileType, currentFileType);
                }
            }
        }
 
        private string ResolvePackagePath(LockFileLibrary package)
        {
            if (package.IsProject())
            {
                var relativeMSBuildProjectPath = package.MSBuildProject;
 
                if (string.IsNullOrEmpty(relativeMSBuildProjectPath))
                {
                    throw new BuildErrorException(Strings.ProjectAssetsConsumedWithoutMSBuildProjectPath, package.Name, ProjectAssetsFile);
                }
 
                return GetAbsolutePathFromProjectRelativePath(relativeMSBuildProjectPath);
            }
            else
            {
                return PackageResolver.GetPackageDirectory(package.Name, package.Version);
            }
        }
 
        private static string ResolveFilePath(string relativePath, string resolvedPackagePath)
        {
            if (NuGetUtils.IsPlaceholderFile(relativePath))
            {
                return null;
            }
 
            if (resolvedPackagePath == null)
            {
                return string.Empty;
            }
 
            if (Path.DirectorySeparatorChar != '/')
            {
                relativePath = relativePath.Replace('/', Path.DirectorySeparatorChar);
            }
 
            if (Path.DirectorySeparatorChar != '\\')
            {
                relativePath = relativePath.Replace('\\', Path.DirectorySeparatorChar);
            }
 
            return Path.Combine(resolvedPackagePath, relativePath);
        }
 
        private string GetAbsolutePathFromProjectRelativePath(string path)
        {
            return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(ProjectPath), path));
        }
    }
}