|
// 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 enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;
using NuGet.Versioning;
namespace NuGet.CommandLine.XPlat.Commands.Why
{
internal static class DependencyGraphFinder
{
/// <summary>
/// Finds all dependency graphs for a given project.
/// </summary>
/// <param name="assetsFile">Assets file for the project.</param>
/// <param name="targetPackage">The package we want the dependency paths for.</param>
/// <param name="userInputFrameworks">List of target framework aliases.</param>
/// <returns>
/// Dictionary mapping target framework aliases to their respective dependency graphs.
/// Returns null if the project does not have a dependency on the target package.
/// </returns>
public static Dictionary<string, List<DependencyNode>?>? GetAllDependencyGraphsForTarget(
LockFile assetsFile,
string targetPackage,
List<string> userInputFrameworks)
{
var result = new Dictionary<string, List<DependencyNode>?>(StringComparer.OrdinalIgnoreCase);
bool foundPackage = false;
bool useTargetAlias = assetsFile.PackageSpec.TargetFrameworks.All(tf => !string.IsNullOrEmpty(tf.TargetAlias));
if (!useTargetAlias
&& (assetsFile.PackageSpec.RestoreMetadata.OriginalTargetFrameworks.Count != 1
|| assetsFile.PackageSpec.TargetFrameworks.Count != 1
|| assetsFile.PackageSpec.RestoreMetadata.TargetFrameworks.Count != 1
))
{
throw new FileFormatException(Strings.WhyCommand_Error_InconsistentAssetsFile);
}
foreach (var target in assetsFile.Targets)
{
(string targetAlias,
ImmutableArray<LibraryDependency> directPackages,
IList<ProjectRestoreReference> directProjectReferences)
= GetTargetFrameworkData(useTargetAlias, target, assetsFile.PackageSpec);
if (userInputFrameworks.Count > 0
&& !userInputFrameworks.Any(f => string.Equals(targetAlias, f, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
LockFileTargetLibrary projectAsLibrary = ConvertToLibrary(directPackages, directProjectReferences, assetsFile, target);
DependencyNode? projectNode = CreateNode(target, targetPackage, projectAsLibrary, VersionRange.All);
string displayName = string.IsNullOrEmpty(target.RuntimeIdentifier)
? targetAlias
: $"{targetAlias}/{target.RuntimeIdentifier}";
result[displayName] = projectNode?.Children.ToList();
foundPackage |= projectNode != null;
}
return foundPackage ? result : null;
static (string targetAlias, ImmutableArray<LibraryDependency> directPackages, IList<ProjectRestoreReference> directProjectReferences)
GetTargetFrameworkData(bool useTargetAlias, LockFileTarget target, PackageSpec packageSpec)
{
string targetAlias;
ImmutableArray<LibraryDependency> directPackages;
IList<ProjectRestoreReference> directProjectReferences;
if (useTargetAlias)
{
targetAlias = target.TargetAlias;
directPackages = packageSpec.GetTargetFramework(targetAlias)!.Dependencies;
directProjectReferences = packageSpec.GetRestoreMetadataFramework(targetAlias)!.ProjectReferences;
}
else
{
targetAlias = packageSpec.RestoreMetadata.OriginalTargetFrameworks[0];
directPackages = packageSpec.TargetFrameworks[0].Dependencies;
directProjectReferences = packageSpec.RestoreMetadata.TargetFrameworks[0].ProjectReferences;
}
return (targetAlias, directPackages, directProjectReferences);
}
}
private static LockFileTargetLibrary ConvertToLibrary(
ImmutableArray<LibraryDependency> directPackages,
IList<ProjectRestoreReference> directProjectReferences,
LockFile assetsFile,
LockFileTarget target)
{
List<PackageDependency> dependencies = new List<PackageDependency>(directPackages.Length + directProjectReferences.Count);
dependencies.AddRange(directPackages.Select(p => new PackageDependency(p.Name, p.LibraryRange.VersionRange ?? VersionRange.All)));
string projectDirectory = Path.GetDirectoryName(assetsFile.PackageSpec.FilePath)!;
var projectsByPath = assetsFile
.Libraries
.Where(l => string.Equals(l.Type, "project", StringComparison.OrdinalIgnoreCase))
.ToDictionary(l => Path.GetFullPath(Path.Combine(projectDirectory, l.Path)), l => l, StringComparer.OrdinalIgnoreCase);
dependencies.AddRange(directProjectReferences
.Select(p =>
{
LockFileLibrary projectLibrary = projectsByPath[p.ProjectPath];
LibraryRange libraryRange = new LibraryRange(
projectLibrary.Name,
VersionRange.Parse(projectLibrary.Version.ToString()),
LibraryDependencyTarget.Project);
var dependency = new PackageDependency(
libraryRange.Name,
VersionRange.Parse(projectLibrary.Version.OriginalVersion ?? projectLibrary.Version.ToString()));
return dependency;
}));
LockFileTargetLibrary project = new LockFileTargetLibrary
{
Name = assetsFile.PackageSpec.Name,
Type = LibraryType.Project,
Dependencies = dependencies
};
return project;
}
/// <summary>
/// Convert the flat list of LockFileTargetLibrary into a display graph.
/// </summary>
/// <param name="target">The assets file target for the current target framework, used to look up the resolved version and dependencies.</param>
/// <param name="filterPackage">The package that needs to be displayed</param>
/// <param name="library"> The current node in the package graph to convert</param>
/// <param name="requestedVersion">The requested version of the current node.</param>
/// <returns>If the current node's package id matches the filter package id, or the filter package is a dependency of the current node, then
/// a graph node instance is returned. Otherwise null is returned to signal that this part of the package graph does not contribute
/// to the output display.</returns>
public static DependencyNode? CreateNode(LockFileTarget target, string filterPackage, LockFileTargetLibrary library, VersionRange requestedVersion)
{
if (filterPackage.Equals(library.Name, StringComparison.OrdinalIgnoreCase))
{
// NuGet doesn't allow circular dependencies, and why only shows the graph up to the filter package,
// so there's no point checking dependencies.
return new PackageNode(
library.Name!,
library.Version!,
requestedVersion,
[]);
}
HashSet<DependencyNode>? children = null;
foreach (var dependency in library.Dependencies)
{
LockFileTargetLibrary? dependencyLibrary = target.Libraries.FirstOrDefault(l => l.Name!.Equals(dependency.Id, StringComparison.OrdinalIgnoreCase));
if (dependencyLibrary is null)
{
// When a package reference suppresses a package with PrivateAssets, but the same package is also a dependency of another
// package, the assets file will list the suppressed package as a dependency of a library node, but will not create a library node
// for the package itself. See https://github.com/NuGet/Home/issues/14698
continue;
}
DependencyNode? childNode = CreateNode(target, filterPackage, dependencyLibrary, dependency.VersionRange);
if (childNode is not null)
{
if (children is null)
{
children = new HashSet<DependencyNode>();
}
children.Add(childNode);
}
}
// Why only show the parts of the graph that lead to the target package.
if (children is null)
{
return null;
}
if (library.Type!.Equals(LibraryType.Package, StringComparison.OrdinalIgnoreCase))
{
NuGetVersion resolvedVersion = library.Version!;
var newNode = new PackageNode(
library.Name!,
resolvedVersion,
requestedVersion,
children ?? []);
return newNode;
}
else
{
var newNode = new ProjectNode(library.Name!, children ?? []);
return newNode;
}
}
}
}
|