diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItem.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItem.cs
new file mode 100644
index 00000000000..1348856d235
--- /dev/null
+++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItem.cs
@@ -0,0 +1,208 @@
+// 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.Diagnostics;
+using System.Globalization;
+using System.Threading.Tasks;
+using NuGet.Common;
+using NuGet.DependencyResolver;
+using NuGet.LibraryModel;
+
+namespace NuGet.Commands
+{
+ internal sealed partial class DependencyGraphResolver
+ {
+ ///
+ /// A class representing a declared dependency graph item to consider for the resolved graph.
+ ///
+ [DebuggerDisplay("{LibraryDependency}, DependencyIndex={LibraryDependencyIndex}, RangeIndex={LibraryRangeIndex}")]
+ public class DependencyGraphItem
+ {
+ ///
+ /// Gets or sets a that returns a containing a that represents the resolved graph item after looking it up in the configured feeds.
+ ///
+ public required Task> FindLibraryTask { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not this dependency graph item is a transitively pinned dependency.
+ ///
+ public bool IsCentrallyPinnedTransitivePackage { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not this dependency graph item is a direct package reference from the root project.
+ ///
+ public bool IsDirectPackageReferenceFromRootProject { get; set; }
+
+ ///
+ /// Gets or sets the used to declare this dependency graph item.
+ ///
+ public required LibraryDependency LibraryDependency { get; set; }
+
+ ///
+ /// Gets or sets the associated with this dependency graph item.
+ ///
+ public LibraryDependencyIndex LibraryDependencyIndex { get; set; } = LibraryDependencyIndex.Invalid;
+
+ ///
+ /// Gets or sets the associated with this dependency graph item.
+ ///
+ public LibraryRangeIndex LibraryRangeIndex { get; set; } = LibraryRangeIndex.Invalid;
+
+ ///
+ /// Gets or sets the of this dependency graph item's parent.
+ ///
+ public LibraryRangeIndex Parent { get; set; }
+
+ ///
+ /// Gets or sets an array representing all of the parent values of this dependency graph item.
+ ///
+ public LibraryRangeIndex[] Path { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets a containing objects representing any runtime specific dependencies.
+ ///
+ public HashSet? RuntimeDependencies { get; set; }
+
+ ///
+ /// Gets or sets a containing values representing any libraries that should be suppressed under this dependency graph item.
+ ///
+ public HashSet? Suppressions { get; set; }
+
+ ///
+ /// Gets the with the of this dependency graph item from calling the delegate specified in .
+ ///
+ /// The of the current restore.
+ /// An optional containing any packages to prune from the defined dependencies.
+ /// Indicates whether or not the dependency graph item is the root project.
+ /// The of the restore.
+ /// An used for logging.
+ /// A with the of the result of finding the library.
+ public async Task> GetGraphItemAsync(
+ RestoreRequest restoreRequest,
+ IReadOnlyDictionary? packagesToPrune,
+ bool isRootProject,
+ RemoteWalkContext context,
+ ILogger logger)
+ {
+ // Call the task to get the library, this may returned a cached result
+ GraphItem item = await FindLibraryTask;
+
+ // If there are no runtime dependencies or packages to prune, just return the original result
+ if ((RuntimeDependencies == null || RuntimeDependencies.Count == 0) && (packagesToPrune == null || packagesToPrune.Count == 0))
+ {
+ return item;
+ }
+
+ // Create a new list that is big enough for the existing items plus any runtime dependencies
+ List dependencies = new(RuntimeDependencies?.Count ?? 0 + item.Data.Dependencies.Count);
+
+ // Loop through the defined dependencies, leaving out any pruned packages or runtime packages that replace them
+ for (int i = 0; i < item.Data.Dependencies.Count; i++)
+ {
+ LibraryDependency dependency = item.Data.Dependencies[i];
+
+ // Skip any packages that should be pruned or will be replaced with a runtime dependency
+ if (ShouldPrunePackage(restoreRequest, packagesToPrune, dependency, item.Key, isRootProject, context, logger)
+ || RuntimeDependencies?.Contains(dependency) == true)
+ {
+ continue;
+ }
+
+ dependencies.Add(dependency);
+ }
+
+ if (RuntimeDependencies?.Count > 0)
+ {
+ // Add any runtime dependencies unless they should be pruned
+ foreach (LibraryDependency runtimeDependency in RuntimeDependencies)
+ {
+ if (ShouldPrunePackage(restoreRequest, packagesToPrune, runtimeDependency, item.Key, isRootProject, context, logger) == true)
+ {
+ continue;
+ }
+
+ dependencies.Add(runtimeDependency);
+ }
+ }
+
+ // Return a new graph item containing the mutated list of dependencies
+ return new GraphItem(item.Key)
+ {
+ Data = new RemoteResolveResult()
+ {
+ Dependencies = dependencies,
+ Match = item.Data.Match
+ }
+ };
+ }
+
+ ///
+ /// Determines whether or not a package should be pruned from the list of defined dependencies.
+ ///
+ /// The of the current restore.
+ /// An optional containing any packages to prune from the defined dependencies.
+ /// The of the defined dependency.
+ /// The of the parent library that defined this dependency.
+ /// Indicates if the parent library is the root project.
+ /// The of the restore.
+ /// An used for logging.
+ /// if the package should be pruned, otherwise .
+ private static bool ShouldPrunePackage(
+ RestoreRequest restoreRequest,
+ IReadOnlyDictionary? packagesToPrune,
+ LibraryDependency dependency,
+ LibraryIdentity parentLibrary,
+ bool isRootProject,
+ RemoteWalkContext context,
+ ILogger logger)
+ {
+ // Do not prune the package if the prune list is null, it is not in the list, or if the version is not satisfied by the range of versions to of the library to prune
+ if (packagesToPrune?.TryGetValue(dependency.Name, out PrunePackageReference? packageToPrune) != true
+ || !dependency.LibraryRange!.VersionRange!.Satisfies(packageToPrune!.VersionRange!.MaxVersion!))
+ {
+ return false;
+ }
+
+ bool isPackage = dependency.LibraryRange?.TypeConstraintAllows(LibraryDependencyTarget.Package) == true;
+
+ if (!isPackage)
+ {
+ // Do not prune the package if it is a project reference
+ if (SdkAnalysisLevelMinimums.IsEnabled(
+ restoreRequest.Project!.RestoreMetadata!.SdkAnalysisLevel,
+ restoreRequest.Project.RestoreMetadata.UsingMicrosoftNETSdk,
+ SdkAnalysisLevelMinimums.PruningWarnings))
+ {
+ // Log a warning only if configured
+ logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1511, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningProjectReference, dependency.Name)));
+ }
+
+ return false;
+ }
+
+ if (isRootProject)
+ {
+ // Do not prune direct package references
+ if (SdkAnalysisLevelMinimums.IsEnabled(
+ restoreRequest.Project!.RestoreMetadata!.SdkAnalysisLevel,
+ restoreRequest.Project.RestoreMetadata.UsingMicrosoftNETSdk,
+ SdkAnalysisLevelMinimums.PruningWarnings))
+ {
+ // Log a warning only if configured
+ logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1510, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningDirectPackageReference, dependency.Name)));
+ }
+
+ return false;
+ }
+
+ logger.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.RestoreDebugPruningPackageReference, $"{dependency.Name} {dependency.LibraryRange!.VersionRange.OriginalString}", parentLibrary, packageToPrune.VersionRange.MaxVersion));
+
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItemIndexer.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItemIndexer.cs
new file mode 100644
index 00000000000..274533576a0
--- /dev/null
+++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.DependencyGraphItemIndexer.cs
@@ -0,0 +1,176 @@
+// 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.Concurrent;
+using NuGet.LibraryModel;
+
+namespace NuGet.Commands
+{
+ internal sealed partial class DependencyGraphResolver
+ {
+ ///
+ /// Represents a unique number for a library by its name, case-insensitive, so that any libraries with the same name will have the same index.
+ ///
+ public enum LibraryDependencyIndex : int
+ {
+ ///
+ /// An invalid value used only for default initialization.
+ ///
+ Invalid = -1,
+
+ ///
+ /// The index of the root project.
+ ///
+ Project = 0,
+ }
+
+ ///
+ /// Represents a unique number for a library by its library range. This generally means that two libraries with the same name and version will have the same index.
+ ///
+ public enum LibraryRangeIndex : int
+ {
+ ///
+ /// An invalid value used only for default initialization.
+ ///
+ Invalid = -2,
+
+ ///
+ /// The index used as a parent of the root project.
+ ///
+ None = -1,
+
+ ///
+ /// The index of the root project.
+ ///
+ Project = 0,
+ }
+
+ ///
+ /// A class used to translate library dependency names and versions to integers for faster dictionary lookup.
+ ///
+ ///
+ /// Using a dictionary key of a string is relatively slow compared to an integer. This class uses a dictionary and assigned incrementing numbers to the libraries by name and version and stores them.
+ ///
+ public sealed class DependencyGraphItemIndexer
+ {
+ ///
+ /// An object used to access to the library dependency table.
+ ///
+ private readonly object _libraryDependencyLockObject = new();
+
+ ///
+ /// An object used to access to the library range table.
+ ///
+ private readonly object _libraryRangeLockObject = new();
+
+ ///
+ /// A case-insensitive dictionary that stores a by the name of a library.
+ ///
+ private readonly ConcurrentDictionary _libraryDependencyTable = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// A case-insensitive dictionary that stores a by the of a library. This dictionary uses the to compare objects.
+ ///
+ private readonly ConcurrentDictionary _libraryRangeTable = new(LibraryRangeComparer.Instance);
+
+ ///
+ /// Stores the next index to use if a unique library is indexed.
+ ///
+ private LibraryDependencyIndex _nextLibraryDependencyIndex = LibraryDependencyIndex.Project + 1;
+
+ ///
+ /// Stores the next index to use if a unique is indexed.
+ ///
+ private LibraryRangeIndex _nextLibraryRangeIndex = LibraryRangeIndex.Project + 1;
+
+ ///
+ /// Indexes a and returns a associated with the its name.
+ ///
+ /// The of the library to index.
+ /// A representing the unique integer associated with the name of the library.
+ public LibraryDependencyIndex Index(LibraryDependency libraryDependency) => Index(libraryDependency.Name);
+
+ ///
+ /// Indexes a and returns a associated with the its name."/>
+ ///
+ /// The to index.
+ /// A representing the unique integer associated with the name of the library.
+ public LibraryDependencyIndex Index(CentralPackageVersion centralPackageVersion) => Index(centralPackageVersion.Name);
+
+ ///
+ /// Indexes a library by its name and returns a associated with it.
+ ///
+ /// The name of the library.
+ /// A representing the unique integer associated with the name of the library.
+ private LibraryDependencyIndex Index(string name)
+ {
+ lock (_libraryDependencyLockObject)
+ {
+ // Attempt to get an existing index value
+ if (_libraryDependencyTable.TryGetValue(name, out LibraryDependencyIndex index))
+ {
+ return index;
+ }
+
+ // Increment the index since this is a new library
+ index = _nextLibraryDependencyIndex++;
+
+ // Store the index in the table so it can be used for equal library names
+ _libraryDependencyTable.TryAdd(name, index);
+
+ return index;
+ }
+ }
+
+ ///
+ /// Indexes a and returns a unique associated with it.
+ ///
+ ///
+ /// In general, this method treats version ranges as equal by name and version. However, certain type constraints are treated as different.
+ /// The is used to determine if two objects are equal.
+ ///
+ /// The to index.
+ /// A associated with the .
+ public LibraryRangeIndex Index(LibraryRange libraryRange)
+ {
+ lock (_libraryRangeLockObject)
+ {
+ // Attempt to get an existing index value
+ if (_libraryRangeTable.TryGetValue(libraryRange, out LibraryRangeIndex index))
+ {
+ return index;
+ }
+
+ // Increment the index since this is a new library range
+ index = _nextLibraryRangeIndex++;
+
+ // Store the index in the table so it can be re-used by equal library ranges
+ _libraryRangeTable.TryAdd(libraryRange, index);
+
+ return index;
+ }
+ }
+
+ ///
+ /// Creates an array containing the parents of a dependency representing its path.
+ ///
+ /// An existing array of values of the parent.
+ /// The of the library range.
+ /// An array containing the existing path and the specified .
+ public static LibraryRangeIndex[] CreatePathToRef(LibraryRangeIndex[] existingPath, LibraryRangeIndex libraryRangeIndex)
+ {
+ // Create a new array, copy the existing path into it, and add the new library range index
+ LibraryRangeIndex[] newPath = new LibraryRangeIndex[existingPath.Length + 1];
+
+ Array.Copy(existingPath, newPath, existingPath.Length);
+
+ newPath[newPath.Length - 1] = libraryRangeIndex;
+
+ return newPath;
+ }
+ }
+ }
+}
diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.LibraryRangeComparer.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.LibraryRangeComparer.cs
new file mode 100644
index 00000000000..3e58c2e23e5
--- /dev/null
+++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.LibraryRangeComparer.cs
@@ -0,0 +1,115 @@
+// 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 NuGet.LibraryModel;
+using NuGet.Shared;
+using NuGet.Versioning;
+
+namespace NuGet.Commands
+{
+ internal sealed partial class DependencyGraphResolver
+ {
+ ///
+ /// Represents an of that considers them to be equal based on the same functionality of .
+ ///
+ public sealed class LibraryRangeComparer : IEqualityComparer
+ {
+ private LibraryRangeComparer()
+ {
+ }
+
+ ///
+ /// Gets an instance of .
+ ///
+ public static LibraryRangeComparer Instance { get; } = new LibraryRangeComparer();
+
+ public bool Equals(LibraryRange? x, LibraryRange? y)
+ {
+ if (x == null || y == null || x.VersionRange == null || y.VersionRange == null)
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(x, y))
+ {
+ return true;
+ }
+
+ // All of this logic is copied from LibraryRange.ToString()
+ LibraryDependencyTarget typeConstraint1 = LibraryDependencyTarget.None;
+ LibraryDependencyTarget typeConstraint2 = LibraryDependencyTarget.None;
+
+ switch (x.TypeConstraint)
+ {
+ case LibraryDependencyTarget.Reference:
+ typeConstraint1 = LibraryDependencyTarget.Reference;
+ break;
+
+ case LibraryDependencyTarget.ExternalProject:
+ typeConstraint1 = LibraryDependencyTarget.ExternalProject;
+ break;
+
+ case LibraryDependencyTarget.Project:
+ case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
+ typeConstraint1 = LibraryDependencyTarget.Project;
+ break;
+ }
+
+ switch (y.TypeConstraint)
+ {
+ case LibraryDependencyTarget.Reference:
+ typeConstraint2 = LibraryDependencyTarget.Reference;
+ break;
+
+ case LibraryDependencyTarget.ExternalProject:
+ typeConstraint2 = LibraryDependencyTarget.ExternalProject;
+ break;
+
+ case LibraryDependencyTarget.Project:
+ case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
+ typeConstraint2 = LibraryDependencyTarget.Project;
+ break;
+ }
+
+ return typeConstraint1 == typeConstraint2
+ && x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase)
+ && x.VersionRange.Equals(y.VersionRange);
+ }
+
+ public int GetHashCode(LibraryRange obj)
+ {
+ LibraryDependencyTarget typeConstraint = LibraryDependencyTarget.None;
+
+ switch (obj.TypeConstraint)
+ {
+ case LibraryDependencyTarget.Reference:
+ typeConstraint = LibraryDependencyTarget.Reference;
+ break;
+
+ case LibraryDependencyTarget.ExternalProject:
+ typeConstraint = LibraryDependencyTarget.ExternalProject;
+ break;
+
+ case LibraryDependencyTarget.Project:
+ case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
+ typeConstraint = LibraryDependencyTarget.Project;
+ break;
+ }
+
+ VersionRange versionRange = obj.VersionRange ?? VersionRange.None;
+
+ HashCodeCombiner combiner = new();
+
+ combiner.AddObject((int)typeConstraint);
+ combiner.AddStringIgnoreCase(obj.Name);
+ combiner.AddObject(versionRange);
+
+ return combiner.CombinedHash;
+ }
+ }
+ }
+}
diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.ResolvedDependencyGraphItem.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.ResolvedDependencyGraphItem.cs
new file mode 100644
index 00000000000..29ac64c6600
--- /dev/null
+++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.ResolvedDependencyGraphItem.cs
@@ -0,0 +1,142 @@
+// 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.Diagnostics;
+using NuGet.DependencyResolver;
+using NuGet.LibraryModel;
+
+namespace NuGet.Commands
+{
+ internal sealed partial class DependencyGraphResolver
+ {
+ ///
+ /// Represents a resolved dependency graph item for the dependency graph resolver.
+ ///
+ [DebuggerDisplay("{LibraryDependency}, RangeIndex={LibraryRangeIndex}")]
+ public class ResolvedDependencyGraphItem
+ {
+ ///
+ /// Stores an array containing all of the of dependencies of this item. This allows fast lookup of their indices rather than having to convert the name to an index.
+ ///
+ private LibraryDependencyIndex[] _dependencyIndices;
+
+ ///
+ /// Stores an array containing all of the of dependencies of this item. This allows fast lookup of their range indices rather than having to convert the to an index.
+ ///
+ private LibraryRangeIndex[] _rangeIndices;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The with a of the resolved library.
+ /// The to use when indexing dependencies.
+ public ResolvedDependencyGraphItem(
+ GraphItem item,
+ DependencyGraphItemIndexer indexingTable)
+ {
+ Item = item;
+
+ int dependencyCount = item.Data.Dependencies.Count;
+
+ if (dependencyCount == 0)
+ {
+ _dependencyIndices = Array.Empty();
+ _rangeIndices = Array.Empty();
+ }
+ else
+ {
+ // Index all of the dependencies ahead of time so that later on the value can be retrieved very quickly
+ _dependencyIndices = new LibraryDependencyIndex[dependencyCount];
+ _rangeIndices = new LibraryRangeIndex[dependencyCount];
+
+ for (int i = 0; i < dependencyCount; i++)
+ {
+ LibraryDependency dependency = item.Data.Dependencies[i];
+
+ _dependencyIndices[i] = indexingTable.Index(dependency);
+ _rangeIndices[i] = indexingTable.Index(dependency.LibraryRange);
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a value indicating whether or not the dependency graph item is transitively pinned.
+ ///
+ public bool IsCentrallyPinnedTransitivePackage { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether or not the dependency graph item is a direct package reference.
+ ///
+ public bool IsDirectPackageReferenceFromRootProject { get; set; }
+
+ ///
+ /// Gets the with a of the resolved library.
+ ///
+ public GraphItem Item { get; }
+
+ ///
+ /// Gets or sets the of the declared dependency in the graph.
+ ///
+ public required LibraryDependency LibraryDependency { get; set; }
+
+ ///
+ /// Gets or sets the of this dependency graph item.
+ ///
+ public LibraryRangeIndex LibraryRangeIndex { get; set; }
+
+ ///
+ /// Gets or sets a of parent that have been eclipsed by this dependency graph item.
+ ///
+ public HashSet? ParentPathsThatHaveBeenEclipsed { get; set; }
+
+ ///
+ /// Gets or sets a of all of the parent of this dependency graph item.
+ ///
+ public HashSet? Parents { get; set; }
+
+ ///
+ /// Gets or sets an array containing the values of all parent dependency graph items and their parent up to the root.
+ ///
+ public required LibraryRangeIndex[] Path { get; set; }
+
+ ///
+ /// Gets or sets a of containing representing dependencies that should be suppressed under this item.
+ ///
+ public required List> Suppressions { get; set; }
+
+ ///
+ /// Gets the of the dependency at the specified position.
+ ///
+ /// The position of the dependency to get the of.
+ /// The of the dependency at the specified position.
+ public LibraryDependencyIndex GetDependencyIndexForDependencyAt(int position)
+ {
+ return _dependencyIndices[position];
+ }
+
+ ///
+ /// Gets the of the dependency at the specified position.
+ ///
+ /// The position of the dependency to get the of.
+ /// The of the dependency at the specified position.
+ public LibraryRangeIndex GetRangeIndexForDependencyAt(int position)
+ {
+ return _rangeIndices[position];
+ }
+
+ ///
+ /// Sets the of the dependency at the specified position.
+ ///
+ /// The position of the dependency to set the of.
+ /// The new to set.
+ public void SetRangeIndexForDependencyAt(int position, LibraryRangeIndex libraryRangeIndex)
+ {
+ _rangeIndices[position] = libraryRangeIndex;
+ }
+ }
+ }
+}
diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs
index d33878cef46..eb9a6afe1a2 100644
--- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs
+++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs
@@ -4,9 +4,8 @@
#nullable enable
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Threading;
@@ -19,26 +18,61 @@
using NuGet.ProjectModel;
using NuGet.Repositories;
using NuGet.RuntimeModel;
-using NuGet.Shared;
using NuGet.Versioning;
-using LibraryDependencyIndex = NuGet.Commands.DependencyGraphResolver.LibraryDependencyInterningTable.LibraryDependencyIndex;
-using LibraryRangeIndex = NuGet.Commands.DependencyGraphResolver.LibraryRangeInterningTable.LibraryRangeIndex;
namespace NuGet.Commands
{
///
/// Represents a class that can resolve a dependency graph.
///
- internal sealed class DependencyGraphResolver
+ internal sealed partial class DependencyGraphResolver
{
+ ///
+ /// Defines the default size for the queue used to process dependency graph items.
+ ///
private const int DependencyGraphItemQueueSize = 4096;
+
+ ///
+ /// Defines the default size for the evictions queue used to process evictions.
+ ///
private const int EvictionsDictionarySize = 1024;
- private const int FindLibraryEntryResultCacheSize = 2048;
- private const int ResolvedDependencyGraphItemQueueSize = 2048;
+
+ ///
+ /// Defines the default size for the dictionary that stores the resolved dependency graph items.
+ ///
+ private const int ResolvedDependencyGraphItemDictionarySize = 2048;
+
+ ///
+ /// A used to index dependency graph items.
+ ///
+ private readonly DependencyGraphItemIndexer _indexingTable = new();
+
+ ///
+ /// A used for logging.
+ ///
private readonly RestoreCollectorLogger _logger;
+
+ ///
+ /// The of the current restore that provides information about the project's configuration.
+ ///
private readonly RestoreRequest _request;
+
+ ///
+ /// Represents the path for any dependency that is directly referenced by the root project.
+ ///
+ private readonly LibraryRangeIndex[] _rootedDependencyPath = new[] { LibraryRangeIndex.Project };
+
+ ///
+ /// A instance used for telemetry.
+ ///
private readonly TelemetryActivity _telemetryActivity;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// A to use for logging.
+ /// The for the restore.
+ /// A instance to use for logging telemetry.
public DependencyGraphResolver(RestoreCollectorLogger logger, RestoreRequest restoreRequest, TelemetryActivity telemetryActivity)
{
_logger = logger;
@@ -46,1292 +80,674 @@ public DependencyGraphResolver(RestoreCollectorLogger logger, RestoreRequest res
_telemetryActivity = telemetryActivity;
}
-#pragma warning disable CA1505 // 'ResolveAsync' has a maintainability index of '0'. Rewrite or refactor the code to increase its maintainability index (MI) above '9'. This will be refactored in a future change.
- public async Task, RuntimeGraph>> ResolveAsync(NuGetv3LocalRepository userPackageFolder, IReadOnlyList fallbackPackageFolders, RemoteWalkContext context, ProjectRestoreCommand projectRestoreCommand, List localRepositories, CancellationToken token)
-#pragma warning restore CA1505
+ ///
+ /// Resolves the dependency graph for the project for all of its configured target frameworks and runtime identifiers.
+ ///
+ /// A representing the global packages folder configured for restore.
+ /// A of objects that represent the package fallback folders configured for restore.
+ /// The to use for this restore.
+ /// A instance to use for installing packages.
+ /// A of objects that represent the local package repositories configured for restore.
+ /// A to use for cancellation.
+ /// A with:
+ ///
+ /// - A representing the overall success of the dependency graph resolution.
+ /// - A of objects representing the resolved dependency graphs for each pair of target framework and runtime identifier.
+ /// - A representing the runtime graph of the resolved dependency graph.
+ ///
+ ///
+ public async Task, RuntimeGraph>> ResolveAsync(
+ NuGetv3LocalRepository userPackageFolder,
+ IReadOnlyList fallbackPackageFolders,
+ RemoteWalkContext context,
+ ProjectRestoreCommand projectRestoreCommand,
+ List localRepositories,
+ CancellationToken token)
{
- bool _success = true;
+ _telemetryActivity.StartIntervalMeasure();
+
+ // Keeps track of the overall success of the dependency graph resolution
+ bool success = true;
+
+ // Calculate whether or not transitive pinning is enabled
bool isCentralPackageTransitivePinningEnabled = _request.Project.RestoreMetadata != null && _request.Project.RestoreMetadata.CentralPackageVersionsEnabled & _request.Project.RestoreMetadata.CentralPackageTransitivePinningEnabled;
- var uniquePackages = new HashSet();
+ // Keeps track of all of the packages that were installed
+ HashSet installedPackages = new();
- var projectRange = new LibraryRange()
- {
- Name = _request.Project.Name,
- VersionRange = new VersionRange(_request.Project.Version),
- TypeConstraint = LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject
- };
+ // Stores the list of all graphs
+ List allGraphs = new();
- // Resolve dependency graphs
- var allGraphs = new List();
- var runtimeGraphs = new List();
- var graphByTFM = new Dictionary();
- var runtimeIds = RequestRuntimeUtility.GetRestoreRuntimes(_request);
- List projectFrameworkRuntimePairs = RestoreCommand.CreateFrameworkRuntimePairs(_request.Project, runtimeIds);
- RuntimeGraph allRuntimes = RuntimeGraph.Empty;
+ // Stores the list of graphs without a runtime identifier by their target framework
+ Dictionary graphsByTargetFramework = new();
- LibraryRangeInterningTable libraryRangeInterningTable = new LibraryRangeInterningTable();
- LibraryDependencyInterningTable libraryDependencyInterningTable = new LibraryDependencyInterningTable();
+ // Stores the list graphs that are runtime identifier specific
+ List runtimeGraphs = new();
- _telemetryActivity.StartIntervalMeasure();
+ // Keeps track of all runtime graphs that exist in the dependency graph
+ RuntimeGraph allRuntimes = RuntimeGraph.Empty;
+ // Keeps track of whether or not we've installed all of the RID-less packages since they contain a runtime.json and are needed before the RID-specific graphs can be resolved
bool hasInstallBeenCalledAlready = false;
+
+ // Stores the list of download results which contribute to the overall success of the graph resolution
DownloadDependencyResolutionResult[]? downloadDependencyResolutionResults = default;
- Dictionary resolvedRuntimeGraphs = new();
+ // A LibraryDependency representing the root of the graph which is the project itself
+ LibraryDependency mainProjectDependency = new(
+ new LibraryRange()
+ {
+ Name = _request.Project.Name,
+ VersionRange = new VersionRange(_request.Project.Version),
+ TypeConstraint = LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject
+ });
+
+ // Create the list of framework/runtime pairs to resolve graphs for. This method returns the pairs in order with the target framework pairs without runtime identifiers come first
+ List projectFrameworkRuntimePairs = RestoreCommand.CreateFrameworkRuntimePairs(_request.Project, runtimeIds: RequestRuntimeUtility.GetRestoreRuntimes(_request));
- foreach (FrameworkRuntimePair pair in projectFrameworkRuntimePairs.NoAllocEnumerate())
+ // Loop through the target framework and runtime identifier pairs to resolve each graph.
+ // The pairs are sorted with all of the RID-less framework pairs first
+ foreach (FrameworkRuntimePair frameworkRuntimePair in projectFrameworkRuntimePairs.NoAllocEnumerate())
{
- if (!string.IsNullOrWhiteSpace(pair.RuntimeIdentifier) && !hasInstallBeenCalledAlready)
+ // Since the FrameworkRuntimePair objects are sorted, the packages found so far need to be installed before resolving any runtime identifier specific graphs because
+ // the runtime.json is in the package which is used to determine what runtime packages to add
+ if (!string.IsNullOrWhiteSpace(frameworkRuntimePair.RuntimeIdentifier) && !hasInstallBeenCalledAlready)
{
downloadDependencyResolutionResults = await ProjectRestoreCommand.DownloadDependenciesAsync(_request.Project, context, _telemetryActivity, telemetryPrefix: string.Empty, token);
- _success &= await projectRestoreCommand.InstallPackagesAsync(uniquePackages, allGraphs, downloadDependencyResolutionResults, userPackageFolder, token);
+ success &= await projectRestoreCommand.InstallPackagesAsync(installedPackages, allGraphs, downloadDependencyResolutionResults, userPackageFolder, token);
+ // This ensures that the packages for the RID-less graph are only installed once
hasInstallBeenCalledAlready = true;
}
- TargetFrameworkInformation? projectTargetFramework = _request.Project.GetTargetFramework(pair.Framework);
+ // Get the corresponding TargetFrameworkInformation from the restore request
+ TargetFrameworkInformation? projectTargetFramework = _request.Project.GetTargetFramework(frameworkRuntimePair.Framework);
- var unresolvedPackages = new HashSet();
+ // Keeps track of the unresolved packages
+ HashSet unresolvedPackages = new();
- var resolvedDependencies = new HashSet();
+ // Keeps track of the resolved packages
+ HashSet resolvedPackages = new();
- RuntimeGraph? runtimeGraph = default;
- if (!string.IsNullOrEmpty(pair.RuntimeIdentifier) && !resolvedRuntimeGraphs.TryGetValue(pair.Framework, out runtimeGraph) && graphByTFM.TryGetValue(pair.Framework, out var tfmNonRidGraph))
+ if (TryGetRuntimeGraph(localRepositories, graphsByTargetFramework, frameworkRuntimePair, projectTargetFramework, out RuntimeGraph? runtimeGraph))
{
- // We start with the non-RID TFM graph.
- // This is guaranteed to be computed before any graph with a RID, so we can assume this will return a value.
+ // Merge all of the runtime graphs together
+ allRuntimes = RuntimeGraph.Merge(allRuntimes, runtimeGraph);
+ }
- // PCL Projects with Supports have a runtime graph but no matching framework.
- var runtimeGraphPath = projectTargetFramework.RuntimeIdentifierGraphPath;
+ // Stores a dictionary of central package versions by their LibraryDependencyIndex for faster lookup when using CPM
+ Dictionary? pinnedPackageVersions = IndexPinnedPackageVersions(isCentralPackageTransitivePinningEnabled, projectTargetFramework);
- RuntimeGraph? projectProviderRuntimeGraph = default;
- if (runtimeGraphPath != null)
- {
- projectProviderRuntimeGraph = ProjectRestoreCommand.GetRuntimeGraph(runtimeGraphPath, _logger);
- }
+ // Create a DependencyGraphItem representing the root project to add to the queue for processing
+ DependencyGraphItem rootProjectDependencyGraphItem = new()
+ {
+ LibraryDependency = mainProjectDependency,
+ LibraryDependencyIndex = LibraryDependencyIndex.Project,
+ LibraryRangeIndex = LibraryRangeIndex.Project,
+ Suppressions = new HashSet(),
+ FindLibraryTask = ResolverUtility.FindLibraryCachedAsync(
+ mainProjectDependency.LibraryRange,
+ frameworkRuntimePair.Framework,
+ runtimeIdentifier: string.IsNullOrWhiteSpace(frameworkRuntimePair.RuntimeIdentifier) ? null : frameworkRuntimePair.RuntimeIdentifier,
+ context,
+ token),
+ Parent = LibraryRangeIndex.None
+ };
- runtimeGraph = ProjectRestoreCommand.GetRuntimeGraph(tfmNonRidGraph, localRepositories, projectRuntimeGraph: projectProviderRuntimeGraph, _logger);
- allRuntimes = RuntimeGraph.Merge(allRuntimes, runtimeGraph);
- }
+ // Resolve the entire dependency graph
+ Dictionary resolvedDependencyGraphItems = await ResolveDependencyGraphItemsAsync(
+ isCentralPackageTransitivePinningEnabled,
+ frameworkRuntimePair,
+ projectTargetFramework,
+ runtimeGraph,
+ pinnedPackageVersions,
+ rootProjectDependencyGraphItem,
+ context,
+ token);
+
+ // Now that the graph has been resolved, we need to create walk all of the defined dependencies again to detect any cycles and downgrades. The RestoreTargetGraph stores all of the
+ // information about the graph including the nodes with their parent/child relationships, cycles, downgrades, and conflicts.
+ (bool wasRestoreTargetGraphCreationSuccessful, RestoreTargetGraph restoreTargetGraph) = await CreateRestoreTargetGraphAsync(frameworkRuntimePair, runtimeGraph, isCentralPackageTransitivePinningEnabled, unresolvedPackages, resolvedPackages, resolvedDependencyGraphItems, context);
+
+ success &= wasRestoreTargetGraphCreationSuccessful;
- //Now build up our new flattened graph
- var initialProject = new LibraryDependency(new LibraryRange()
+ // Track all of the RestoreTargetGraph objects
+ allGraphs.Add(restoreTargetGraph);
+
+ if (!string.IsNullOrWhiteSpace(frameworkRuntimePair.RuntimeIdentifier))
{
- Name = _request.Project.Name,
- VersionRange = new VersionRange(_request.Project.Version),
- TypeConstraint = LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject
- });
+ // Track all of the runtime specific graphs
+ runtimeGraphs.Add(restoreTargetGraph);
+ }
+ else
+ {
+ // Track all of the RID-less graphs by their target framework
+ graphsByTargetFramework.Add(frameworkRuntimePair.Framework, restoreTargetGraph);
+ }
+ }
- //If we find newer nodes of things while we walk, we'll evict them.
- //A subset of evictions cause a re-import. For instance if a newer chosen node has fewer refs,
- //then we might have a dangling over-elevated ref on the old one, it'll be hard evicted.
- //The second item here is the path to root. When we add a hard-evictee, we'll also remove anything
- //added to the eviction list that contains the evictee on their path to root.
- Queue refImport =
- new Queue(DependencyGraphItemQueueSize);
+ _telemetryActivity.EndIntervalMeasure(ProjectRestoreCommand.WalkFrameworkDependencyDuration);
- TaskResultCache findLibraryEntryCache = new(FindLibraryEntryResultCacheSize);
+ // Install packages if they weren't already. If the graph has not runtimes, installing of packages won't be called until this point
+ if (!hasInstallBeenCalledAlready)
+ {
+ downloadDependencyResolutionResults = await ProjectRestoreCommand.DownloadDependenciesAsync(_request.Project, context, _telemetryActivity, telemetryPrefix: string.Empty, token);
- Dictionary chosenResolvedItems = new(ResolvedDependencyGraphItemQueueSize);
+ success &= await projectRestoreCommand.InstallPackagesAsync(installedPackages, allGraphs, downloadDependencyResolutionResults, userPackageFolder, token);
- Dictionary evictions = new Dictionary(EvictionsDictionarySize);
+ hasInstallBeenCalledAlready = true;
+ }
- Dictionary? pinnedPackageVersions = null;
+ // Install runtime specific packages if applicable
+ if (runtimeGraphs.Count > 0)
+ {
+ success &= await projectRestoreCommand.InstallPackagesAsync(installedPackages, runtimeGraphs, Array.Empty(), userPackageFolder, token);
+ }
- if (isCentralPackageTransitivePinningEnabled && projectTargetFramework != null && projectTargetFramework.CentralPackageVersions != null)
+ foreach (KeyValuePair profile in _request.Project.RuntimeGraph.Supports)
+ {
+ CompatibilityProfile? compatProfile;
+ if (profile.Value.RestoreContexts.Any())
{
- pinnedPackageVersions = new Dictionary(capacity: projectTargetFramework.CentralPackageVersions.Count);
+ // Just use the contexts from the project definition
+ compatProfile = profile.Value;
+ }
+ else if (!allRuntimes.Supports.TryGetValue(profile.Value.Name, out compatProfile))
+ {
+ // No definition of this profile found, so just continue to the next one
+ await _logger.LogAsync(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1502, string.Format(CultureInfo.CurrentCulture, Strings.Log_UnknownCompatibilityProfile, profile.Key)));
- foreach (var item in projectTargetFramework.CentralPackageVersions)
- {
- LibraryDependencyIndex depIndex = libraryDependencyInterningTable.Intern(item.Value);
+ continue;
+ }
- pinnedPackageVersions[depIndex] = item.Value.VersionRange;
- }
+ foreach (FrameworkRuntimePair? frameworkRuntimePair in compatProfile.RestoreContexts)
+ {
+ _logger.LogDebug($" {profile.Value.Name} -> +{frameworkRuntimePair}");
+ _request.CompatibilityProfiles.Add(frameworkRuntimePair);
}
+ }
- Dictionary? prunedPackageVersions = GetAndIndexPackagesToPrune(libraryDependencyInterningTable, projectTargetFramework);
+ // Log the final results like downgrades, conflicts, and cycles
+ _logger.ApplyRestoreOutput(allGraphs);
- DependencyGraphItem rootProjectRefItem = new()
- {
- LibraryDependency = initialProject,
- LibraryDependencyIndex = libraryDependencyInterningTable.Intern(initialProject),
- LibraryRangeIndex = libraryRangeInterningTable.Intern(initialProject.LibraryRange),
- Suppressions = new HashSet(),
- IsDirectPackageReferenceFromRootProject = false,
- };
+ // Log information for unexpected dependencies
+ await UnexpectedDependencyMessages.LogAsync(allGraphs, _request.Project, _logger);
- LibraryRangeIndex[] rootedDependencyPath = new[] { rootProjectRefItem.LibraryRangeIndex };
+ // Determine if the graph resolution was successful (no conflicts or unresolved packages)
+ success &= await projectRestoreCommand.ResolutionSucceeded(allGraphs, downloadDependencyResolutionResults, context, token);
- _ = findLibraryEntryCache.GetOrAddAsync(
- rootProjectRefItem.LibraryRangeIndex,
- async static (state) =>
- {
- GraphItem refItem = await ResolverUtility.FindLibraryEntryAsync(
- state.rootProjectRefItem.LibraryDependency!.LibraryRange,
- state.Framework,
- runtimeIdentifier: null,
- state.context,
- state.token);
-
- return new FindLibraryEntryResult(
- state.rootProjectRefItem.LibraryDependency!,
- refItem,
- state.rootProjectRefItem.LibraryDependencyIndex,
- state.rootProjectRefItem.LibraryRangeIndex,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable);
- },
- (rootProjectRefItem, pair.Framework, context, libraryDependencyInterningTable, libraryRangeInterningTable, token),
- token);
+ return (success, allGraphs, allRuntimes);
+ }
- HashSet? directPackageReferences = default;
+ ///
+ /// Creates a from the resolved dependency graph items and analyzes the graph for cycles, downgrades, and conflicts.
+ ///
+ /// The of the dependency graph.
+ /// The of the dependency graph.
+ /// A indicating whether or not central transitive pinning is enabled.
+ /// A containing objects representing packages that could not be resolved.
+ /// A containing objects representing packages that were successfully resolved.
+ /// The for the restore.
+ /// A with:
+ ///
+ /// - A indicating if the dependency graph contains no issues.
+ /// - A representing the fully resolved and analyzed dependency graph.
+ ///
+ ///
+ private static async Task<(bool Success, RestoreTargetGraph RestoreTargetGraph)> CreateRestoreTargetGraphAsync(
+ FrameworkRuntimePair frameworkRuntimePair,
+ RuntimeGraph? runtimeGraph,
+ bool isCentralPackageTransitivePinningEnabled,
+ HashSet unresolvedPackages,
+ HashSet resolvedPackages,
+ Dictionary resolvedDependencyGraphItems,
+ RemoteWalkContext context)
+ {
+ bool success = true;
- ProcessDeepEviction:
+ // Stores results of analyzing the graph including conflicts, cycles, and downgrades
+ AnalyzeResult analyzeResult = new();
- refImport.Clear();
- chosenResolvedItems.Clear();
+ // Stores the list of items in as a flat list
+ HashSet> flattenedGraphItems = new();
- refImport.Enqueue(rootProjectRefItem);
+ // Stores the list of graph nodes which point to their outer and inner nodes which represent the graph as a tree
+ List> graphNodes = new();
- while (refImport.Count > 0)
- {
- DependencyGraphItem importRefItem = refImport.Dequeue();
- LibraryDependency currentRef = importRefItem.LibraryDependency!;
- LibraryDependencyIndex currentRefDependencyIndex = importRefItem.LibraryDependencyIndex;
- LibraryRangeIndex currentRefRangeIndex = importRefItem.LibraryRangeIndex;
- LibraryRangeIndex[] pathToCurrentRef = importRefItem.Path;
- HashSet? currentSuppressions = importRefItem.Suppressions;
- bool directPackageReferenceFromRootProject = importRefItem.IsDirectPackageReferenceFromRootProject;
-
- if (!findLibraryEntryCache.TryGetValue(currentRefRangeIndex, out Task? refItemTask))
- {
- Debug.Fail("This should not happen");
- continue;
- }
+ // Stores the list of nodes by their LibraryRangeIndex for faster lookup
+ Dictionary> nodesById = new();
- FindLibraryEntryResult refItemResult = await refItemTask;
+ // Keeps track of visited items to detect when we come across a dependency that was already visited. Any time a dependency is seen again, we need to determine if there was a downgrade or conflict.
+ HashSet visitedItems = new();
- if (importRefItem.LibraryRangeIndex == rootProjectRefItem.LibraryRangeIndex)
- {
- directPackageReferences = new HashSet();
+ // Stores the items to process, starting with the project itself and its children
+ Queue<(LibraryDependencyIndex, LibraryRangeIndex, GraphNode)> itemsToFlatten = new();
- for (int i = 0; i < refItemResult.Item.Data.Dependencies.Count; i++)
- {
- LibraryDependency dep = refItemResult.Item.Data.Dependencies[i];
+ Dictionary> versionConflicts = new();
- if (dep.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package))
- {
- directPackageReferences.Add(refItemResult.GetDependencyIndexForDependency(i));
- }
- }
- }
+ // Stores the list of downgrades
+ Dictionary downgrades = new();
- LibraryDependencyTarget typeConstraint = currentRef.LibraryRange.TypeConstraint;
- if (evictions.TryGetValue(currentRefRangeIndex, out var eviction))
- {
- (LibraryRangeIndex[] evictedPath, LibraryDependencyIndex evictedDepIndex, LibraryDependencyTarget evictedTypeConstraint) = eviction;
+ // Get the resolved item for the project
+ ResolvedDependencyGraphItem projectResolvedDependencyGraphItem = resolvedDependencyGraphItems[LibraryDependencyIndex.Project];
- // If we evicted this same version previously, but the type constraint of currentRef is more stringent (package), then do not skip the current item - this is the one we want.
- // This is tricky. I don't really know what this means. Normally we'd key off of versions instead.
- if (!((evictedTypeConstraint == LibraryDependencyTarget.PackageProjectExternal || evictedTypeConstraint == LibraryDependencyTarget.ExternalProject) &&
- currentRef.LibraryRange.TypeConstraint == LibraryDependencyTarget.Package))
- {
- continue;
- }
- }
+ // Create a node representing the root for the project
+ GraphNode rootGraphNode = new GraphNode(projectResolvedDependencyGraphItem.LibraryDependency.LibraryRange)
+ {
+ Item = projectResolvedDependencyGraphItem.Item
+ };
- HashSet? runtimeDependencies = null;
+ graphNodes.Add(rootGraphNode);
- if (runtimeGraph != null && !string.IsNullOrWhiteSpace(pair.RuntimeIdentifier))
- {
- runtimeDependencies = new HashSet();
+ // Enqueue the project to be processed
+ itemsToFlatten.Enqueue((LibraryDependencyIndex.Project, projectResolvedDependencyGraphItem.LibraryRangeIndex, rootGraphNode));
+
+ nodesById.Add(projectResolvedDependencyGraphItem.LibraryRangeIndex, rootGraphNode);
+
+ while (itemsToFlatten.Count > 0)
+ {
+ (LibraryDependencyIndex currentLibraryDependencyIndex, LibraryRangeIndex currentLibraryRangeIndex, GraphNode currentGraphNode) = itemsToFlatten.Dequeue();
+
+ // If there was no dependency in the resolved graph with the same name, it can be skipped and left out of the final graph
+ if (!resolvedDependencyGraphItems.TryGetValue(currentLibraryDependencyIndex, out ResolvedDependencyGraphItem? resolvedDependencyGraphItem))
+ {
+ continue;
+ }
+
+ flattenedGraphItems.Add(resolvedDependencyGraphItem.Item);
+
+ for (int i = 0; i < resolvedDependencyGraphItem.Item.Data.Dependencies.Count; i++)
+ {
+ LibraryDependency childLibraryDependency = resolvedDependencyGraphItem.Item.Data.Dependencies[i];
- LibraryRange libraryRange = currentRef.LibraryRange;
+ if (childLibraryDependency.LibraryRange.VersionRange == null)
+ {
+ continue;
+ }
- if (RemoteDependencyWalker.EvaluateRuntimeDependencies(ref libraryRange, pair.RuntimeIdentifier, runtimeGraph, ref runtimeDependencies))
+ if (StringComparer.OrdinalIgnoreCase.Equals(childLibraryDependency.Name, resolvedDependencyGraphItem.Item.Key.Name) || StringComparer.OrdinalIgnoreCase.Equals(childLibraryDependency.Name, rootGraphNode.Key.Name))
+ {
+ // A cycle exists since the current child dependency has the same name as its parent or as the root node
+ GraphNode nodeWithCycle = new(childLibraryDependency.LibraryRange)
{
- importRefItem.LibraryRangeIndex = currentRefRangeIndex = libraryRangeInterningTable.Intern(libraryRange);
+ OuterNode = currentGraphNode,
+ Disposition = Disposition.Cycle
+ };
- currentRef = new LibraryDependency(currentRef) { LibraryRange = libraryRange };
+ analyzeResult.Cycles.Add(nodeWithCycle);
- importRefItem.LibraryDependency = currentRef;
+ continue;
+ }
- refItemResult = await findLibraryEntryCache.GetOrAddAsync(
- currentRefRangeIndex,
- async static state =>
- {
- return await FindLibraryEntryResult.CreateAsync(
- state.libraryDependency,
- state.dependencyIndex,
- state.rangeIndex,
- state.Framework,
- state.context,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable,
- state.token);
- },
- (libraryDependency: currentRef, dependencyIndex: currentRefDependencyIndex, rangeIndex: currentRefRangeIndex, pair.Framework, context, libraryDependencyInterningTable, libraryRangeInterningTable, token),
- token);
- }
+ LibraryDependencyIndex childLibraryDependencyIndex = resolvedDependencyGraphItem.GetDependencyIndexForDependencyAt(i);
+
+ if (!resolvedDependencyGraphItems.TryGetValue(childLibraryDependencyIndex, out ResolvedDependencyGraphItem? childResolvedDependencyGraphItem))
+ {
+ // If there was no dependency in the resolved graph with the same name as this child dependency, it can be skipped and left out of the final graph
+ continue;
}
- //else if we've seen this ref (but maybe not version) before check to see if we need to upgrade
- if (chosenResolvedItems.TryGetValue(currentRefDependencyIndex, out ResolvedDependencyGraphItem? chosenResolvedItem))
+ LibraryRangeIndex childResolvedLibraryRangeIndex = childResolvedDependencyGraphItem.LibraryRangeIndex;
+ LibraryDependency childResolvedLibraryDependency = childResolvedDependencyGraphItem.LibraryDependency;
+
+ // Determine if this dependency has already been visited
+ if (!visitedItems.Add(childLibraryDependencyIndex))
{
- LibraryDependency chosenRef = chosenResolvedItem.LibraryDependency;
- LibraryRangeIndex chosenRefRangeIndex = chosenResolvedItem.LibraryRangeIndex;
- LibraryRangeIndex[] pathChosenRef = chosenResolvedItem.Path;
- bool packageReferenceFromRootProject = chosenResolvedItem.IsDirectPackageReferenceFromRootProject;
- List> chosenSuppressions = chosenResolvedItem.Suppressions;
+ LibraryRangeIndex currentRangeIndex = resolvedDependencyGraphItem.GetRangeIndexForDependencyAt(i);
- if (packageReferenceFromRootProject) // direct dependencies always win.
+ if (resolvedDependencyGraphItem.Path.Contains(currentRangeIndex))
{
- continue;
- }
+ // If the dependency exists in the its own path, then a cycle exists
+ analyzeResult.Cycles.Add(
+ new GraphNode(childLibraryDependency.LibraryRange)
+ {
+ OuterNode = currentGraphNode,
+ Disposition = Disposition.Cycle
+ });
- // A project reference should always win over a package reference. In this case, a project has already been added to the graph
- // with the same name as a transitive package reference. FindLibraryEntryAsync() will return the project and not the package.
- if (chosenResolvedItem.LibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.ExternalProject
- && currentRef.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package)
- && refItemResult.Item.Key.Type == LibraryType.Project)
- {
continue;
}
- // We should evict on type constraint if the type constraint of the current ref is more stringent than the chosen ref.
- // This happens when a previous type constraint is broader (e.g. PackageProjectExternal) than the current type constraint (e.g. Package).
- bool evictOnTypeConstraint = false;
- if ((chosenRefRangeIndex == currentRefRangeIndex) && EvictOnTypeConstraint(currentRef.LibraryRange.TypeConstraint, chosenRef.LibraryRange.TypeConstraint))
+ // Verify downgrades only if the resolved dependency has a lower version than what was defined
+ if (!RemoteDependencyWalker.IsGreaterThanOrEqualTo(childResolvedLibraryDependency.LibraryRange.VersionRange, childLibraryDependency.LibraryRange.VersionRange))
{
- if (findLibraryEntryCache.TryGetValue(chosenRefRangeIndex, out Task? resolvedItemTask))
+ // It is not a downgrade if: the dependency is transitive and is suppressed its parent or any of those parents' parent because the suppressions is an aggregate of everything suppressed above.
+ // For example, A -> B (PrivateAssets=All) -> C
+ // When processing C, it is not suppressed but its parent is, which is tracked in ResolvedDependencyGraphItem.Suppressions
+ if ((childLibraryDependencyIndex != LibraryDependencyIndex.Project && childLibraryDependency.SuppressParent == LibraryIncludeFlags.All)
+ || resolvedDependencyGraphItem.Suppressions.Count > 0 && resolvedDependencyGraphItem.Suppressions[0].Contains(childLibraryDependencyIndex))
{
- FindLibraryEntryResult resolvedItem = await resolvedItemTask;
-
- // We need to evict the chosen item because this one has a more stringent type constraint.
- evictOnTypeConstraint = resolvedItem.Item.Key.Type == LibraryType.Project;
+ continue;
}
- }
- // We should also evict if a package was chosen and a project is being considered since projects always in over packages
- if (chosenResolvedItem.LibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.PackageProjectExternal
- && currentRef.LibraryRange.TypeConstraint == LibraryDependencyTarget.ExternalProject)
- {
- evictOnTypeConstraint = true;
- }
+ // Get the resolved version in case a floating version like 1.* was specified
+ NuGetVersion? resolvedVersion = childResolvedDependencyGraphItem.Item.Data.Match?.Library?.Version;
- // TODO: Handle null version ranges
- VersionRange nvr = currentRef.LibraryRange.VersionRange ?? VersionRange.All;
- VersionRange ovr = chosenRef.LibraryRange.VersionRange ?? VersionRange.All;
+ if (resolvedVersion != null && childLibraryDependency.LibraryRange.VersionRange.Satisfies(resolvedVersion))
+ {
+ // Ignore the lower version if the resolved version satisfies the range of the dependency. This can happen when a floating version like 1.* was specified
+ // and the resolved version is 1.2.3, which satisfies the range of 1.*
+ continue;
+ }
- if (evictOnTypeConstraint || !RemoteDependencyWalker.IsGreaterThanOrEqualTo(ovr, nvr))
- {
- if (chosenRef.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package) && currentRef.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package))
+ // This lower version could be a downgrade if it hasn't already been seen
+ if (!downgrades.ContainsKey(childResolvedLibraryRangeIndex))
{
- if (chosenResolvedItem.Parents != null)
+ // Determine if any parents have actually eclipsed this version
+ if (childResolvedDependencyGraphItem.ParentPathsThatHaveBeenEclipsed != null)
{
- bool atLeastOneCommonAncestor = false;
+ bool hasBeenEclipsedByParent = false;
- foreach (LibraryRangeIndex parentRangeIndex in chosenResolvedItem.Parents.NoAllocEnumerate())
+ foreach (LibraryRangeIndex parent in childResolvedDependencyGraphItem.ParentPathsThatHaveBeenEclipsed)
{
- if (importRefItem.Path.Length > 2 && importRefItem.Path[importRefItem.Path.Length - 2] == parentRangeIndex)
+ if (resolvedDependencyGraphItem.Path.Contains(parent))
{
- atLeastOneCommonAncestor = true;
+ hasBeenEclipsedByParent = true;
break;
}
}
- if (atLeastOneCommonAncestor)
+ if (hasBeenEclipsedByParent)
{
continue;
}
}
- if (HasCommonAncestor(chosenResolvedItem.Path, importRefItem.Path))
- {
- continue;
- }
+ // Look through all of the parent nodes to see if any are a downgrade
+ bool foundParentDowngrade = false;
- if (chosenResolvedItem.ParentPathsThatHaveBeenEclipsed != null)
+ if (childResolvedDependencyGraphItem.Parents != null)
{
- bool hasAlreadyBeenEclipsed = false;
-
- foreach (LibraryRangeIndex parentRangeIndex in chosenResolvedItem.ParentPathsThatHaveBeenEclipsed)
+ foreach (LibraryRangeIndex parentLibraryRangeIndex in childResolvedDependencyGraphItem.Parents)
{
- if (importRefItem.Path.Contains(parentRangeIndex))
+ if (resolvedDependencyGraphItem.Path.Contains(parentLibraryRangeIndex) && !resolvedDependencyGraphItem.IsDirectPackageReferenceFromRootProject)
{
- hasAlreadyBeenEclipsed = true;
+ downgrades.Add(
+ childResolvedLibraryRangeIndex,
+ (
+ FromParentLibraryRangeIndex: resolvedDependencyGraphItem.LibraryRangeIndex,
+ FromLibraryDependency: childLibraryDependency,
+ ToParentLibraryRangeIndex: parentLibraryRangeIndex,
+ ToLibraryDependencyIndex: childLibraryDependencyIndex,
+ IsCentralTransitive: isCentralPackageTransitivePinningEnabled ? childResolvedDependencyGraphItem.IsCentrallyPinnedTransitivePackage : false
+ ));
+
+ foundParentDowngrade = true;
break;
}
}
-
- if (hasAlreadyBeenEclipsed)
- {
- continue;
- }
}
- }
-
- //If we think the newer thing we are looking at is better, remove the old one and let it fall thru.
-
- chosenResolvedItems.Remove(currentRefDependencyIndex);
- //Record an eviction for the node we are replacing. The eviction path is for the current node.
- LibraryRangeIndex evictedLR = chosenRefRangeIndex;
-
- // If we're evicting on typeconstraint, then there is already an item in allResolvedItems that matches the old typeconstraint.
- // We must remove it, otherwise we won't call FindLibraryCachedAsync again to load the correct item and save it into allResolvedItems.
- if (evictOnTypeConstraint)
- {
- refItemResult = await findLibraryEntryCache.GetOrAddAsync(
- currentRefRangeIndex,
- refresh: true,
- async static state =>
- {
- return await FindLibraryEntryResult.CreateAsync(
- state.libraryDependency,
- state.dependencyIndex,
- state.rangeIndex,
- state.Framework,
- state.context,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable,
- state.token);
- },
- (libraryDependency: currentRef, dependencyIndex: currentRefDependencyIndex, rangeIndex: currentRefRangeIndex, pair.Framework, context, libraryDependencyInterningTable, libraryRangeInterningTable, token),
- token);
- }
-
- int deepEvictions = 0;
- //unwind anything chosen by the node we're evicting..
- HashSet? evicteesToRemove = default;
- foreach (var evictee in evictions)
- {
- (LibraryRangeIndex[] evicteePath, LibraryDependencyIndex evicteeDepIndex, LibraryDependencyTarget evicteeTypeConstraint) = evictee.Value;
- if (evicteePath.Contains(evictedLR))
- {
- // if evictee.Key (depIndex) == currentDepIndex && evictee.TypeConstraint == ExternalProject --> Don't remove it. It must remain evicted.
- // If the evictee to remove is the same dependency, but the project version of said dependency, then do not remove it - it must remain evicted in favor of the package.
- if (!(evicteeDepIndex == currentRefDependencyIndex &&
- (evicteeTypeConstraint == LibraryDependencyTarget.ExternalProject || evicteeTypeConstraint == LibraryDependencyTarget.PackageProjectExternal)))
- {
- if (evicteesToRemove == null)
- evicteesToRemove = new HashSet();
- evicteesToRemove.Add(evictee.Key);
- }
- }
- }
- if (evicteesToRemove != null)
- {
- foreach (var evicteeToRemove in evicteesToRemove)
- {
- evictions.Remove(evicteeToRemove);
- deepEvictions++;
- }
- }
- foreach (var chosenItem in chosenResolvedItems)
- {
- if (chosenItem.Value.Path.Contains(evictedLR))
+ // It is a downgrade if central transitive pinning is not being used or if the child is not a direct package reference
+ if (!foundParentDowngrade && (!isCentralPackageTransitivePinningEnabled || !childResolvedDependencyGraphItem.IsDirectPackageReferenceFromRootProject))
{
- deepEvictions++;
- break;
+ downgrades.Add(
+ childResolvedLibraryRangeIndex,
+ (
+ FromParentLibraryRangeIndex: resolvedDependencyGraphItem.LibraryRangeIndex,
+ FromLibraryDependency: childLibraryDependency,
+ ToParentLibraryRangeIndex: childResolvedDependencyGraphItem.Path[childResolvedDependencyGraphItem.Path.Length - 1],
+ ToLibraryDependencyIndex: childLibraryDependencyIndex,
+ IsCentralTransitive: isCentralPackageTransitivePinningEnabled ? childResolvedDependencyGraphItem.IsCentrallyPinnedTransitivePackage : false
+ ));
}
}
- evictions[evictedLR] = (LibraryRangeInterningTable.CreatePathToRef(pathToCurrentRef, currentRefRangeIndex), currentRefDependencyIndex, chosenRef.LibraryRange.TypeConstraint);
-
- if (deepEvictions > 0)
- {
- goto ProcessDeepEviction;
- }
- bool isCentrallyPinnedTransitivePackage = importRefItem.IsCentrallyPinnedTransitivePackage;
-
- //Since this is a "new" choice, its gets a new import context list
- chosenResolvedItems.Add(
- currentRefDependencyIndex,
- new ResolvedDependencyGraphItem
- {
- LibraryDependency = currentRef,
- LibraryRangeIndex = currentRefRangeIndex,
- Parents = isCentrallyPinnedTransitivePackage && !directPackageReferenceFromRootProject ? new HashSet() { importRefItem.Parent } : null,
- Path = pathToCurrentRef,
- IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
- IsDirectPackageReferenceFromRootProject = directPackageReferenceFromRootProject,
- Suppressions = new List>
- {
- currentSuppressions!
- }
- });
-
- //if we are going to live with this queue and chosen state, we need to also kick
- // any queue members who were descendants of the thing we just evicted.
- var newRefImport =
- new Queue(DependencyGraphItemQueueSize);
- while (refImport.Count > 0)
- {
- DependencyGraphItem item = refImport.Dequeue();
- if (!item.Path.Contains(evictedLR))
- newRefImport.Enqueue(item);
- }
- refImport = newRefImport;
+ // Ignore this child dependency since it was a downgrade
+ continue;
}
- //if its lower we'll never do anything other than skip it.
- else if (!VersionRangePreciseEquals(ovr, nvr))
- {
- bool hasCommonAncestor = HasCommonAncestor(chosenResolvedItem.Path, pathToCurrentRef);
- if (!hasCommonAncestor)
+ // If it wasn't a downgrade, then it was a version conflict like A -> B [1.0.0] but B 1.0.0 was not in the resolved graph
+ if (versionConflicts.ContainsKey(childResolvedLibraryRangeIndex) && !nodesById.ContainsKey(currentRangeIndex))
+ {
+ GraphNode nodeWithConflict = new(childResolvedLibraryDependency.LibraryRange)
{
- if (chosenResolvedItem.ParentPathsThatHaveBeenEclipsed == null)
- {
- chosenResolvedItem.ParentPathsThatHaveBeenEclipsed = new HashSet();
- }
+ Item = childResolvedDependencyGraphItem.Item,
+ Disposition = Disposition.Acceptable,
+ OuterNode = currentGraphNode,
+ };
- chosenResolvedItem.ParentPathsThatHaveBeenEclipsed.Add(pathToCurrentRef[pathToCurrentRef.Length - 1]);
- }
+ currentGraphNode.InnerNodes.Add(nodeWithConflict);
+
+ nodesById.Add(currentRangeIndex, nodeWithConflict);
continue;
}
- else
- //we are looking at same. consider if its an upgrade.
- {
- if (chosenResolvedItem.Parents == null)
- {
- chosenResolvedItem.Parents = new HashSet();
- }
-
- if (!chosenResolvedItem.IsDirectPackageReferenceFromRootProject)
- {
- chosenResolvedItem.Parents?.Add(importRefItem.Parent);
- }
-
- //If the one we already have chosen is pure, then we can skip this one. Processing it wont bring any new info
- if (chosenSuppressions.Count == 1 && chosenSuppressions[0].Count == 0 && HasCommonAncestor(chosenResolvedItem.Path, pathToCurrentRef))
- {
- continue;
- }
- //if the one we are now looking at is pure, then we should replace the one we have chosen because if we're here it isnt pure.
- else if (currentSuppressions!.Count == 0)
- {
- chosenResolvedItems.Remove(currentRefDependencyIndex);
- bool isCentrallyPinnedTransitivePackage = chosenResolvedItem.IsCentrallyPinnedTransitivePackage;
+ // Ignore this child dependency since it was not a cycle, downgrade, or version conflict but was already visited
+ continue;
+ }
- //slightly evil, but works.. we should just shift to the current thing as ref?
- chosenResolvedItems.Add(
- currentRefDependencyIndex,
- new ResolvedDependencyGraphItem
- {
- LibraryDependency = currentRef,
- LibraryRangeIndex = currentRefRangeIndex,
- Parents = chosenResolvedItem.Parents,
- Path = pathToCurrentRef,
- IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
- IsDirectPackageReferenceFromRootProject = packageReferenceFromRootProject,
- Suppressions = new List>
- {
- currentSuppressions,
- }
- });
- }
- else
- //check to see if we are equal to one of the dispositions or if we are less restrictive than one
- {
- bool isEqualOrSuperSetDisposition = false;
- foreach (var chosenImportDisposition in chosenSuppressions)
- {
- if (currentSuppressions.IsSupersetOf(chosenImportDisposition))
- {
- isEqualOrSuperSetDisposition = true;
- }
- }
+ // Create a GraphNode for the item
+ GraphNode newGraphNode = new(childResolvedLibraryDependency.LibraryRange)
+ {
+ Item = childResolvedDependencyGraphItem.Item
+ };
- if (isEqualOrSuperSetDisposition)
- {
- continue;
- }
- else
- {
- bool isCentrallyPinnedTransitivePackage = chosenResolvedItem.IsCentrallyPinnedTransitivePackage;
+ if (childResolvedDependencyGraphItem.IsCentrallyPinnedTransitivePackage && !childResolvedDependencyGraphItem.IsDirectPackageReferenceFromRootProject)
+ {
+ // If this child is transitively pinned, the GraphNode needs to have certain properties set
+ newGraphNode.Disposition = Disposition.Accepted;
+ newGraphNode.Item.IsCentralTransitive = true;
- //case of differently restrictive dispositions or less restrictive... we should technically be able to remove
- //a disposition if its less restrictive than another. But we'll just add it to the list.
- chosenResolvedItems.Remove(currentRefDependencyIndex);
- List> newImportDisposition = new()
- {
- currentSuppressions
- };
-
- newImportDisposition.AddRange(chosenSuppressions);
- //slightly evil, but works.. we should just shift to the current thing as ref?
- chosenResolvedItems.Add(
- currentRefDependencyIndex,
- new ResolvedDependencyGraphItem
- {
- LibraryDependency = currentRef,
- LibraryRangeIndex = currentRefRangeIndex,
- Parents = chosenResolvedItem.Parents,
- Path = pathToCurrentRef,
- IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
- IsDirectPackageReferenceFromRootProject = packageReferenceFromRootProject,
- Suppressions = newImportDisposition
- });
- }
- }
- }
+ // Treat the transitively pinned dependency as a child of the root node
+ newGraphNode.OuterNode = rootGraphNode;
+ rootGraphNode.InnerNodes.Add(newGraphNode);
}
else
{
- bool isCentrallyPinnedTransitivePackage = importRefItem.IsCentrallyPinnedTransitivePackage;
+ // Set properties for the node to represent a parent/child relationship
+ newGraphNode.OuterNode = currentGraphNode;
+ currentGraphNode.InnerNodes.Add(newGraphNode);
+ }
- //This is now the thing we think is the highest version of this ref
- chosenResolvedItems.Add(
- currentRefDependencyIndex,
- new ResolvedDependencyGraphItem
- {
- LibraryDependency = currentRef,
- LibraryRangeIndex = currentRefRangeIndex,
- Parents = isCentrallyPinnedTransitivePackage && !directPackageReferenceFromRootProject ? new HashSet() { importRefItem.Parent } : null,
- Path = pathToCurrentRef,
- IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
- IsDirectPackageReferenceFromRootProject = directPackageReferenceFromRootProject,
- Suppressions = new List>
- {
- currentSuppressions!
- }
- });
+ if (!childResolvedDependencyGraphItem.IsDirectPackageReferenceFromRootProject && isCentralPackageTransitivePinningEnabled && childLibraryDependency.SuppressParent != LibraryIncludeFlags.All && !downgrades.ContainsKey(childResolvedLibraryRangeIndex) && !RemoteDependencyWalker.IsGreaterThanOrEqualTo(childResolvedDependencyGraphItem.LibraryDependency.LibraryRange.VersionRange, childLibraryDependency.LibraryRange.VersionRange))
+ {
+ // This is a downgrade if:
+ // 1. This is not a direct dependency
+ // 2. This is a central transitive pinned dependency
+ // 3. This is a transitive dependency which is not PrivateAssets=All
+ // 4. This has not already been detected
+ // 5. The version is lower
+ downgrades.Add(
+ childResolvedDependencyGraphItem.LibraryRangeIndex,
+ (
+ FromParentLibraryRangeIndex: currentLibraryRangeIndex,
+ FromLibraryDependency: childLibraryDependency,
+ ToParentLibraryRangeIndex: LibraryRangeIndex.Project,
+ ToLibraryDependencyIndex: childLibraryDependencyIndex,
+ IsCentralTransitive: true
+ ));
}
- HashSet? suppressions = default;
- //Scan for suppressions
- for (int i = 0; i < refItemResult.Item.Data.Dependencies.Count; i++)
+ // This is a version conflict if:
+ // 1. The node is not a project and isn't unresolved
+ // 2. The conflict has not already been detected
+ // 3. The dependency is transitive and doesn't have PrivateAssets=All
+ // 4. The dependency has a version specified
+ // 5. The version range is not satisfied by the resolved version
+ // 6. A corresponding downgrade was not detected
+ if (newGraphNode.Item.Key.Type != LibraryType.Project
+ && newGraphNode.Item.Key.Type != LibraryType.ExternalProject
+ && newGraphNode.Item.Key.Type != LibraryType.Unresolved
+ && !versionConflicts.ContainsKey(childResolvedLibraryRangeIndex)
+ && childLibraryDependency.SuppressParent != LibraryIncludeFlags.All
+ && childLibraryDependency.LibraryRange.VersionRange != null
+ && !childLibraryDependency.LibraryRange.VersionRange!.Satisfies(newGraphNode.Item.Key.Version)
+ && !downgrades.ContainsKey(childResolvedLibraryRangeIndex))
{
- var dep = refItemResult.Item.Data.Dependencies[i];
- // Packages with missing versions should not be added to the graph
- if (dep.LibraryRange.VersionRange == null)
- {
- continue;
- }
+ // Remove the existing node so it can be replaced with a node representing the conflict
+ currentGraphNode.InnerNodes.Remove(newGraphNode);
- LibraryDependencyIndex depIndex = refItemResult.GetDependencyIndexForDependency(i);
- if ((dep.SuppressParent == LibraryIncludeFlags.All) && (importRefItem.LibraryDependencyIndex != rootProjectRefItem.LibraryDependencyIndex))
+ GraphNode conflictingNode = new(childLibraryDependency.LibraryRange)
{
- if (suppressions == null)
- {
- suppressions = new HashSet();
- }
- suppressions.Add(depIndex);
- }
- }
-
- // If the suppressions have been mutated, then add the rest of the suppressions.
- // Otherwise just use teh incoming set of suppressions.
- if (suppressions != null)
- {
- suppressions.AddRange(currentSuppressions);
- }
- else
- {
- suppressions = currentSuppressions;
- }
-
- HashSet? prunedPackageIndices = null;
- for (int i = 0; i < refItemResult.Item.Data.Dependencies.Count; i++)
- {
- LibraryDependency dep = refItemResult.Item.Data.Dependencies[i];
- bool isPackage = dep.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package);
- bool isDirectPackageReferenceFromRootProject = (currentRefRangeIndex == rootProjectRefItem.LibraryRangeIndex) && isPackage;
-
- LibraryDependencyIndex depIndex = refItemResult.GetDependencyIndexForDependency(i);
-
- if (ShouldPrunePackage(prunedPackageVersions, refItemResult, dep, depIndex, isPackage, isDirectPackageReferenceFromRootProject))
- {
- prunedPackageIndices ??= [];
- prunedPackageIndices.Add(i);
- continue;
- }
-
- // Skip this node if the VersionRange is null or if its not transitively pinned and PrivateAssets=All
- if (dep.LibraryRange.VersionRange == null || (!importRefItem.IsCentrallyPinnedTransitivePackage && suppressions!.Contains(depIndex)))
- {
- continue;
- }
-
- VersionRange? pinnedVersionRange = null;
-
- if (!isDirectPackageReferenceFromRootProject && directPackageReferences?.Contains(depIndex) == true)
- {
- continue;
- }
-
- bool isCentrallyPinnedTransitiveDependency = isCentralPackageTransitivePinningEnabled
- && isPackage
- && pinnedPackageVersions?.TryGetValue(depIndex, out pinnedVersionRange) == true;
-
- LibraryRangeIndex rangeIndex = LibraryRangeIndex.Invalid;
-
- LibraryDependency actualLibraryDependency = dep;
-
- if (isCentrallyPinnedTransitiveDependency && !isDirectPackageReferenceFromRootProject)
- {
- actualLibraryDependency = new LibraryDependency(dep)
- {
- LibraryRange = new LibraryRange(actualLibraryDependency.LibraryRange) { VersionRange = pinnedVersionRange },
- };
-
- isCentrallyPinnedTransitiveDependency = true;
-
- rangeIndex = libraryRangeInterningTable.Intern(actualLibraryDependency.LibraryRange);
- }
- else
- {
- rangeIndex = refItemResult.GetRangeIndexForDependency(i);
- }
-
- DependencyGraphItem dependencyGraphItem = new()
- {
- LibraryDependency = actualLibraryDependency,
- LibraryDependencyIndex = depIndex,
- LibraryRangeIndex = rangeIndex,
- Path = isCentrallyPinnedTransitiveDependency || isDirectPackageReferenceFromRootProject ? rootedDependencyPath : LibraryRangeInterningTable.CreatePathToRef(pathToCurrentRef, currentRefRangeIndex),
- Parent = currentRefRangeIndex,
- Suppressions = suppressions,
- IsDirectPackageReferenceFromRootProject = isDirectPackageReferenceFromRootProject,
- IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitiveDependency
+ Disposition = Disposition.Acceptable,
+ Item = new GraphItem(
+ new LibraryIdentity(
+ childLibraryDependency.Name,
+ childLibraryDependency.LibraryRange.VersionRange.MinVersion!,
+ LibraryType.Package)),
+ OuterNode = currentGraphNode,
};
- refImport.Enqueue(dependencyGraphItem);
+ // Add the conflict node to the parent
+ currentGraphNode.InnerNodes.Add(conflictingNode);
- _ = findLibraryEntryCache.GetOrAddAsync(
- rangeIndex,
- async static state =>
- {
- return await FindLibraryEntryResult.CreateAsync(
- state.libraryDependency,
- state.dependencyIndex,
- state.rangeIndex,
- state.Framework,
- state.context,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable,
- state.token);
- },
- (libraryDependency: actualLibraryDependency, dependencyIndex: depIndex, rangeIndex, pair.Framework, context, libraryDependencyInterningTable, libraryRangeInterningTable, token),
- token);
- }
-
- // Add runtime dependencies of the current node if a runtime identifier has been specified.
- if (!string.IsNullOrEmpty(pair.RuntimeIdentifier) && runtimeDependencies != null && runtimeDependencies.Count > 0)
- {
- // Check for runtime dependencies.
- FindLibraryEntryResult? findLibraryCachedAsyncResult = default;
-
- // Runtime dependencies start after non-runtime dependencies.
- // Keep track of the first index for any runtime dependencies so that it can be used to enqueue later.
- int runtimeDependencyIndex = refItemResult.Item.Data.Dependencies.Count;
-
- // If there are runtime dependencies that need to be added, remove the currentRef from allResolvedItems,
- // and add the newly created version that contains the previously detected dependencies and newly detected runtime dependencies.
- bool rootHasInnerNodes = (refItemResult.Item.Data.Dependencies.Count + (runtimeDependencies == null ? 0 : runtimeDependencies.Count)) > 0;
- GraphNode rootNode = new GraphNode(currentRef.LibraryRange, rootHasInnerNodes, false)
- {
- Item = refItemResult.Item,
- };
- RemoteDependencyWalker.MergeRuntimeDependencies(runtimeDependencies, rootNode);
-
- findLibraryCachedAsyncResult = await findLibraryEntryCache.GetOrAddAsync(
- currentRefRangeIndex,
- refresh: true,
- static state =>
- {
- return Task.FromResult(new FindLibraryEntryResult(
- state.currentRef,
- state.rootNode.Item,
- state.currentRefDependencyIndex,
- state.currentRefRangeIndex,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable));
- },
- (currentRef, rootNode, currentRefDependencyIndex, currentRefRangeIndex, libraryDependencyInterningTable, libraryRangeInterningTable),
- token);
-
- // Enqueue each of the runtime dependencies, but only if they weren't already present in refItemResult before merging the runtime dependencies above.
- if ((rootNode.Item.Data.Dependencies.Count - runtimeDependencyIndex) == runtimeDependencies!.Count)
- {
- foreach (var dep in runtimeDependencies)
- {
- var libraryDependencyIndex = findLibraryCachedAsyncResult.GetDependencyIndexForDependency(runtimeDependencyIndex);
- if (ShouldPrunePackage(prunedPackageVersions, refItemResult, dep, libraryDependencyIndex, isPackage: true, isDirectPackageReferenceFromRootProject: false))
- {
- prunedPackageIndices ??= [];
- prunedPackageIndices.Add(runtimeDependencyIndex);
- runtimeDependencyIndex++;
- continue;
- }
-
- DependencyGraphItem runtimeDependencyGraphItem = new()
- {
- LibraryDependency = dep,
- LibraryDependencyIndex = libraryDependencyIndex,
- LibraryRangeIndex = findLibraryCachedAsyncResult.GetRangeIndexForDependency(runtimeDependencyIndex),
- Path = LibraryRangeInterningTable.CreatePathToRef(pathToCurrentRef, currentRefRangeIndex),
- Parent = currentRefRangeIndex,
- Suppressions = suppressions,
- IsDirectPackageReferenceFromRootProject = false,
- };
+ // Track the version conflict for later
+ versionConflicts.Add(childResolvedLibraryRangeIndex, conflictingNode);
- refImport.Enqueue(runtimeDependencyGraphItem);
-
- _ = findLibraryEntryCache.GetOrAddAsync(
- runtimeDependencyGraphItem.LibraryRangeIndex,
- async static state =>
- {
- return await FindLibraryEntryResult.CreateAsync(
- state.libraryDependency,
- state.dependencyIndex,
- state.rangeIndex,
- state.Framework,
- state.context,
- state.libraryDependencyInterningTable,
- state.libraryRangeInterningTable,
- state.token);
- },
- (libraryDependency: dep, dependencyIndex: runtimeDependencyGraphItem.LibraryDependencyIndex, rangeIndex: runtimeDependencyGraphItem.LibraryRangeIndex, pair.Framework, context, libraryDependencyInterningTable, libraryRangeInterningTable, token),
- token);
-
- runtimeDependencyIndex++;
- }
- }
- }
-
- // If the latest item was chosen, keep track of the pruned dependency indices.
- if (chosenResolvedItems.TryGetValue(currentRefDependencyIndex, out ResolvedDependencyGraphItem? resolvedGraphItem))
- {
- resolvedGraphItem.PrunedDependencies = prunedPackageIndices;
+ // Process the next child
+ continue;
}
- }
-
- //Now that we've completed import, figure out the short real flattened list
- var flattenedGraphItems = new HashSet>();
- HashSet visitedItems = new HashSet();
- Queue<(LibraryDependencyIndex, LibraryRangeIndex, GraphNode)> itemsToFlatten = new();
- var graphNodes = new List>();
- LibraryDependencyIndex initialProjectIndex = rootProjectRefItem.LibraryDependencyIndex;
- var cri = chosenResolvedItems[initialProjectIndex];
- LibraryDependency startRef = cri.LibraryDependency;
+ // Add the node to the lookup for later
+ nodesById.Add(childResolvedLibraryRangeIndex, newGraphNode);
- var rootGraphNode = new GraphNode(startRef.LibraryRange);
- LibraryRangeIndex startRefLibraryRangeIndex = cri.LibraryRangeIndex;
+ // Enqueue the child for processing
+ itemsToFlatten.Enqueue((childLibraryDependencyIndex, childResolvedLibraryRangeIndex, newGraphNode));
- FindLibraryEntryResult startRefNode = await findLibraryEntryCache.GetValueAsync(startRefLibraryRangeIndex);
-
- rootGraphNode.Item = startRefNode.Item;
- graphNodes.Add(rootGraphNode);
-
- var analyzeResult = new AnalyzeResult();
- var nodesById = new Dictionary>();
-
- var downgrades = new Dictionary();
-
- var versionConflicts = new Dictionary>();
-
- itemsToFlatten.Enqueue((initialProjectIndex, cri.LibraryRangeIndex, rootGraphNode));
-
- nodesById.Add(cri.LibraryRangeIndex, rootGraphNode);
-
- while (itemsToFlatten.Count > 0)
- {
- (LibraryDependencyIndex currentDependencyIndex, LibraryRangeIndex currentLibraryRangeIndex, GraphNode currentGraphNode) = itemsToFlatten.Dequeue();
- if (!chosenResolvedItems.TryGetValue(currentDependencyIndex, out var foundItem))
+ if (newGraphNode.Item.Key.Type == LibraryType.Unresolved)
{
- continue;
+ // Keep track of unresolved packages and fail the restore
+ unresolvedPackages.Add(childResolvedLibraryDependency.LibraryRange);
+
+ success = false;
}
- LibraryDependency chosenRef = foundItem.LibraryDependency;
- LibraryRangeIndex chosenRefRangeIndex = foundItem.LibraryRangeIndex;
- LibraryRangeIndex[] pathToChosenRef = foundItem.Path;
- bool directPackageReferenceFromRootProject = foundItem.IsDirectPackageReferenceFromRootProject;
- List> chosenSuppressions = foundItem.Suppressions;
- if (findLibraryEntryCache.TryGetValue(chosenRefRangeIndex, out Task? nodeTask))
+ else
{
- FindLibraryEntryResult node = await nodeTask;
-
- for (int i = 0; i < node.Item.Data.Dependencies.Count; i++)
- {
- var dep = node.Item.Data.Dependencies[i];
-
- if (dep.LibraryRange.VersionRange == null)
- {
- continue;
- }
-
- if (foundItem.PrunedDependencies?.Contains(i) == true)
- {
- continue;
- }
-
- if (StringComparer.OrdinalIgnoreCase.Equals(dep.Name, node.Item.Key.Name) || StringComparer.OrdinalIgnoreCase.Equals(dep.Name, rootGraphNode.Key.Name))
- {
- // Cycle
- var nodeWithCycle = new GraphNode(dep.LibraryRange)
- {
- OuterNode = currentGraphNode,
- Disposition = Disposition.Cycle
- };
-
- analyzeResult.Cycles.Add(nodeWithCycle);
-
- continue;
- }
-
- LibraryDependencyIndex depIndex = node.GetDependencyIndexForDependency(i);
-
- if (!chosenResolvedItems.TryGetValue(depIndex, out var chosenItem))
- {
- continue;
- }
-
- var chosenItemRangeIndex = chosenItem.LibraryRangeIndex;
- LibraryDependency actualDep = chosenItem.LibraryDependency;
-
- if (!visitedItems.Add(depIndex))
- {
- LibraryRangeIndex currentRangeIndex = node.GetRangeIndexForDependency(i);
-
- if (pathToChosenRef.Contains(currentRangeIndex))
- {
- // Cycle
- var nodeWithCycle = new GraphNode(dep.LibraryRange);
- nodeWithCycle.OuterNode = currentGraphNode;
- nodeWithCycle.Disposition = Disposition.Cycle;
- analyzeResult.Cycles.Add(nodeWithCycle);
-
- continue;
- }
-
- if (!RemoteDependencyWalker.IsGreaterThanOrEqualTo(actualDep.LibraryRange.VersionRange, dep.LibraryRange.VersionRange))
- {
- if (node.DependencyIndex != rootProjectRefItem.LibraryDependencyIndex && dep.SuppressParent == LibraryIncludeFlags.All)
- {
- continue;
- }
-
- if (chosenSuppressions.Count > 0 && chosenSuppressions[0].Contains(depIndex))
- {
- continue;
- }
-
- if (findLibraryEntryCache.TryGetValue(chosenItemRangeIndex, out Task? chosenResolvedItemTask))
- {
- FindLibraryEntryResult chosenResolvedItem = await chosenResolvedItemTask;
-
- var resolvedVersion = chosenResolvedItem.Item.Data.Match?.Library?.Version;
- if (resolvedVersion != null && dep.LibraryRange.VersionRange.Satisfies(resolvedVersion))
- {
- continue;
- }
- }
-
- // Downgrade
- if (!downgrades.ContainsKey(chosenItemRangeIndex))
- {
- if (chosenItem.ParentPathsThatHaveBeenEclipsed != null)
- {
- bool hasBeenEclipsedByParent = false;
-
- foreach (var parent in chosenItem.ParentPathsThatHaveBeenEclipsed)
- {
- if (foundItem.Path.Contains(parent))
- {
- hasBeenEclipsedByParent = true;
- break;
- }
- }
-
- if (hasBeenEclipsedByParent)
- {
- continue;
- }
- }
-
- bool foundParentDowngrade = false;
-
- if (chosenItem.Parents != null)
- {
- foreach (var parent in chosenItem.Parents)
- {
- if (foundItem.Path.Contains(parent) && !foundItem.IsDirectPackageReferenceFromRootProject)
- {
- downgrades.Add(chosenItemRangeIndex, (foundItem.LibraryRangeIndex, dep, parent, chosenItem.LibraryDependency, isCentralPackageTransitivePinningEnabled ? chosenItem.IsCentrallyPinnedTransitivePackage : false));
-
- foundParentDowngrade = true;
- break;
- }
- }
- }
-
- if (!foundParentDowngrade && (!isCentralPackageTransitivePinningEnabled || !chosenItem.IsDirectPackageReferenceFromRootProject))
- {
- downgrades.Add(chosenItemRangeIndex, (foundItem.LibraryRangeIndex, dep, chosenItem.Path[chosenItem.Path.Length - 1], chosenItem.LibraryDependency, isCentralPackageTransitivePinningEnabled ? chosenItem.IsCentrallyPinnedTransitivePackage : false));
- }
- }
-
- continue;
- }
-
- if (versionConflicts.ContainsKey(chosenItemRangeIndex) && !nodesById.ContainsKey(currentRangeIndex) && findLibraryEntryCache.TryGetValue(chosenItemRangeIndex, out Task? itemTask))
- {
- FindLibraryEntryResult conflictingNode = await itemTask;
-
- // Version conflict
- var selectedConflictingNode = new GraphNode(actualDep.LibraryRange)
- {
- Item = conflictingNode.Item,
- Disposition = Disposition.Acceptable,
- OuterNode = currentGraphNode,
- };
- currentGraphNode.InnerNodes.Add(selectedConflictingNode);
-
- nodesById.Add(currentRangeIndex, selectedConflictingNode);
-
- continue;
- }
-
- continue;
- }
-
- FindLibraryEntryResult findLibraryEntryResult = await findLibraryEntryCache.GetValueAsync(chosenItemRangeIndex);
-
- var newGraphNode = new GraphNode(actualDep.LibraryRange);
- newGraphNode.Item = findLibraryEntryResult.Item;
-
- if (chosenItem.IsCentrallyPinnedTransitivePackage && !chosenItem.IsDirectPackageReferenceFromRootProject)
- {
- newGraphNode.Disposition = Disposition.Accepted;
- newGraphNode.Item.IsCentralTransitive = true;
- newGraphNode.OuterNode = rootGraphNode;
- rootGraphNode.InnerNodes.Add(newGraphNode);
- }
- else
- {
- newGraphNode.OuterNode = currentGraphNode;
- currentGraphNode.InnerNodes.Add(newGraphNode);
- }
-
- if (dep.SuppressParent != LibraryIncludeFlags.All && isCentralPackageTransitivePinningEnabled && !chosenItem.IsDirectPackageReferenceFromRootProject && !downgrades.ContainsKey(chosenItemRangeIndex) && !RemoteDependencyWalker.IsGreaterThanOrEqualTo(chosenItem.LibraryDependency.LibraryRange.VersionRange, dep.LibraryRange.VersionRange))
- {
- downgrades.Add(chosenItem.LibraryRangeIndex, (currentLibraryRangeIndex, dep, rootProjectRefItem.LibraryRangeIndex, chosenItem.LibraryDependency, true));
- }
-
- if (newGraphNode.Item.Key.Type != LibraryType.Project && newGraphNode.Item.Key.Type != LibraryType.ExternalProject && newGraphNode.Item.Key.Type != LibraryType.Unresolved && !versionConflicts.ContainsKey(chosenItemRangeIndex) && dep.SuppressParent != LibraryIncludeFlags.All && dep.LibraryRange.VersionRange != null && !dep.LibraryRange.VersionRange!.Satisfies(newGraphNode.Item.Key.Version) && !downgrades.ContainsKey(chosenItemRangeIndex))
- {
- currentGraphNode.InnerNodes.Remove(newGraphNode);
-
- // Conflict
- var conflictingNode = new GraphNode(dep.LibraryRange)
- {
- Disposition = Disposition.Acceptable
- };
-
- conflictingNode.Item = new GraphItem(new LibraryIdentity(dep.Name, dep.LibraryRange.VersionRange.MinVersion!, LibraryType.Package));
- currentGraphNode.InnerNodes.Add(conflictingNode);
- conflictingNode.OuterNode = currentGraphNode;
-
- versionConflicts.Add(chosenItemRangeIndex, conflictingNode);
-
- continue;
- }
-
- nodesById.Add(chosenItemRangeIndex, newGraphNode);
- itemsToFlatten.Enqueue((depIndex, chosenItemRangeIndex, newGraphNode));
-
- if (newGraphNode.Item.Key.Type == LibraryType.Unresolved)
- {
- unresolvedPackages.Add(actualDep.LibraryRange);
-
- _success = false;
-
- continue;
- }
-
- resolvedDependencies.Add(new ResolvedDependencyKey(
- parent: newGraphNode.OuterNode.Item.Key,
- range: newGraphNode.Key.VersionRange,
- child: newGraphNode.Item.Key));
- }
-
- if (foundItem.PrunedDependencies?.Count > 0)
- {
- int dependencyCount = node.Item.Data.Dependencies.Count - foundItem.PrunedDependencies.Count;
-
- List dependencies = dependencyCount > 0 ? new(dependencyCount) : [];
-
- for (int i = 0; dependencyCount > 0 && i < node.Item.Data.Dependencies.Count; i++)
- {
- if (!foundItem.PrunedDependencies.Contains(i))
- {
- dependencies.Add(node.Item.Data.Dependencies[i]);
- }
- }
-
- RemoteResolveResult remoteResolveResult = new RemoteResolveResult()
- {
- Match = node.Item.Data.Match,
- Dependencies = dependencies,
- };
-
- node.Item.Data = remoteResolveResult;
- }
-
- flattenedGraphItems.Add(node.Item);
+ // Keep track of the resolved packages
+ resolvedPackages.Add(new ResolvedDependencyKey(
+ parent: newGraphNode.OuterNode.Item.Key,
+ range: newGraphNode.Key.VersionRange,
+ child: newGraphNode.Item.Key));
}
}
+ } // End of walking all declared dependencies for cycles, downgrades, and conflicts
- if (versionConflicts.Count > 0)
+ // Add applicable version conflicts to the analyze results
+ if (versionConflicts.Count > 0)
+ {
+ foreach (KeyValuePair> versionConflict in versionConflicts)
{
- foreach (var versionConflict in versionConflicts)
+ if (nodesById.TryGetValue(versionConflict.Key, out GraphNode? selected))
{
- if (nodesById.TryGetValue(versionConflict.Key, out var selected))
- {
- analyzeResult.VersionConflicts.Add(new VersionConflictResult
+ analyzeResult.VersionConflicts.Add(
+ new VersionConflictResult
{
Conflicting = versionConflict.Value,
- Selected = selected
+ Selected = selected,
});
- }
}
}
+ }
- if (downgrades.Count > 0)
+ // Add applicable downgrades to the analyze results
+ if (downgrades.Count > 0)
+ {
+ foreach ((LibraryRangeIndex FromParentLibraryRangeIndex, LibraryDependency FromLibraryDependency, LibraryRangeIndex ToParentLibraryRangeIndex, LibraryDependencyIndex ToLibraryDependencyIndex, bool IsCentralTransitive) downgrade in downgrades.Values)
{
- foreach (var downgrade in downgrades)
+ // Ignore the downgrade if a node was not created for its from or to, or if it never ended up in the resolved graph. Sometimes a downgrade is detected but later during graph
+ // resolution it is resolved so this verifies if the downgrade ended up in the final graph
+ if (!nodesById.TryGetValue(downgrade.FromParentLibraryRangeIndex, out GraphNode? fromParentNode)
+ || !nodesById.TryGetValue(downgrade.ToParentLibraryRangeIndex, out GraphNode? toParentNode)
+ || !resolvedDependencyGraphItems.TryGetValue(downgrade.ToLibraryDependencyIndex, out ResolvedDependencyGraphItem? toResolvedDependencyGraphItem))
{
- if (!nodesById.TryGetValue(downgrade.Value.FromParent, out GraphNode? fromNode) || !nodesById.TryGetValue(downgrade.Value.ToParent, out GraphNode? toNode))
- {
- continue;
- }
-
- if (!findLibraryEntryCache.TryGetValue(downgrade.Key, out Task? findLibraryEntryResultTask))
- {
- continue;
- }
-
- FindLibraryEntryResult findLibraryEntryResult = await findLibraryEntryResultTask;
-
- analyzeResult.Downgrades.Add(new DowngradeResult
- {
- DowngradedFrom = new GraphNode(downgrade.Value.FromLibraryDependency.LibraryRange)
- {
- Item = new GraphItem(new LibraryIdentity(downgrade.Value.FromLibraryDependency.Name, downgrade.Value.FromLibraryDependency.LibraryRange.VersionRange?.MinVersion!, LibraryType.Package)),
- OuterNode = fromNode
- },
- DowngradedTo = new GraphNode(downgrade.Value.ToLibraryDependency.LibraryRange)
- {
- Item = new GraphItem(findLibraryEntryResult.Item.Key)
- {
- IsCentralTransitive = downgrade.Value.IsCentralTransitive
- },
- OuterNode = downgrade.Value.IsCentralTransitive ? rootGraphNode : toNode,
- }
- });
+ continue;
}
- }
- if (isCentralPackageTransitivePinningEnabled)
- {
- foreach (KeyValuePair item in chosenResolvedItems)
+ // Add the downgrade
+ analyzeResult.Downgrades.Add(new DowngradeResult
{
- ResolvedDependencyGraphItem chosenResolvedItem = item.Value;
-
- if (!chosenResolvedItem.IsCentrallyPinnedTransitivePackage || chosenResolvedItem.IsDirectPackageReferenceFromRootProject || chosenResolvedItem.Parents == null || chosenResolvedItem.Parents.Count == 0)
+ DowngradedFrom = new GraphNode(downgrade.FromLibraryDependency.LibraryRange)
{
- continue;
- }
-
- if (nodesById.TryGetValue(chosenResolvedItem.LibraryRangeIndex, out GraphNode? currentNode))
+ Item = new GraphItem(
+ new LibraryIdentity(
+ downgrade.FromLibraryDependency.Name,
+ downgrade.FromLibraryDependency.LibraryRange.VersionRange?.MinVersion!,
+ LibraryType.Package)),
+ OuterNode = fromParentNode
+ },
+ DowngradedTo = new GraphNode(toResolvedDependencyGraphItem.LibraryDependency.LibraryRange)
{
- foreach (LibraryRangeIndex parent in chosenResolvedItem.Parents)
+ Item = new GraphItem(toResolvedDependencyGraphItem.Item.Key)
{
- if (nodesById.TryGetValue(parent, out GraphNode? parentNode))
- {
- currentNode.ParentNodes.Add(parentNode);
- }
- }
- }
- }
- }
-
- HashSet packagesToInstall = new();
-
- foreach (var cacheKey in findLibraryEntryCache.Keys)
- {
- if (findLibraryEntryCache.TryGetValue(cacheKey, out var task))
- {
- var result = await task;
-
- if (result.Item.Key.Type != LibraryType.Unresolved && context.RemoteLibraryProviders.Contains(result.Item.Data.Match.Provider))
- {
- packagesToInstall.Add(result.Item.Data.Match);
+ IsCentralTransitive = downgrade.IsCentralTransitive
+ },
+ OuterNode = downgrade.IsCentralTransitive ? rootGraphNode : toParentNode,
}
- }
- }
-
- var restoreTargetGraph = new RestoreTargetGraph(
- Array.Empty(),
- pair.Framework,
- string.IsNullOrWhiteSpace(pair.RuntimeIdentifier) ? null : pair.RuntimeIdentifier,
- runtimeGraph,
- graphNodes,
- install: packagesToInstall,
- flattened: flattenedGraphItems,
- unresolved: unresolvedPackages,
- analyzeResult,
- resolvedDependencies: resolvedDependencies);
-
- allGraphs.Add(restoreTargetGraph);
-
- if (!string.IsNullOrWhiteSpace(pair.RuntimeIdentifier))
- {
- runtimeGraphs.Add(restoreTargetGraph);
- }
-
- if (string.IsNullOrEmpty(pair.RuntimeIdentifier))
- {
- graphByTFM.Add(pair.Framework, restoreTargetGraph);
- }
- }
-
- _telemetryActivity.EndIntervalMeasure(ProjectRestoreCommand.WalkFrameworkDependencyDuration);
-
- if (!hasInstallBeenCalledAlready)
- {
- downloadDependencyResolutionResults = await ProjectRestoreCommand.DownloadDependenciesAsync(_request.Project, context, _telemetryActivity, telemetryPrefix: string.Empty, token);
-
- _success &= await projectRestoreCommand.InstallPackagesAsync(uniquePackages, allGraphs, downloadDependencyResolutionResults, userPackageFolder, token);
-
- hasInstallBeenCalledAlready = true;
- }
-
- if (runtimeGraphs.Count > 0)
- {
- _success &= await projectRestoreCommand.InstallPackagesAsync(uniquePackages, runtimeGraphs, Array.Empty(), userPackageFolder, token);
- }
-
- foreach (var profile in _request.Project.RuntimeGraph.Supports)
- {
- var runtimes = allRuntimes;
-
- CompatibilityProfile? compatProfile;
- if (profile.Value.RestoreContexts.Any())
- {
- // Just use the contexts from the project definition
- compatProfile = profile.Value;
- }
- else if (!runtimes.Supports.TryGetValue(profile.Value.Name, out compatProfile))
- {
- // No definition of this profile found, so just continue to the next one
- var message = string.Format(CultureInfo.CurrentCulture, Strings.Log_UnknownCompatibilityProfile, profile.Key);
-
- await _logger.LogAsync(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1502, message));
- continue;
- }
-
- foreach (var frameworkRuntimePair in compatProfile.RestoreContexts)
- {
- _logger.LogDebug($" {profile.Value.Name} -> +{frameworkRuntimePair}");
- _request.CompatibilityProfiles.Add(frameworkRuntimePair);
+ });
}
}
- // Update the logger with the restore target graphs
- // This allows lazy initialization for the Transitive Warning Properties
- _logger.ApplyRestoreOutput(allGraphs);
-
- await UnexpectedDependencyMessages.LogAsync(allGraphs, _request.Project, _logger);
-
- _success &= await projectRestoreCommand.ResolutionSucceeded(allGraphs, downloadDependencyResolutionResults, context, token);
-
- return (_success, allGraphs, allRuntimes);
- }
-
- private static Dictionary? GetAndIndexPackagesToPrune(LibraryDependencyInterningTable libraryDependencyInterningTable, TargetFrameworkInformation? projectTargetFramework)
- {
- Dictionary? prunedPackageVersions = null;
-
- if (projectTargetFramework?.PackagesToPrune.Count > 0)
- {
- prunedPackageVersions = new Dictionary(capacity: projectTargetFramework.PackagesToPrune.Count);
-
- foreach (var item in projectTargetFramework.PackagesToPrune)
- {
- LibraryDependencyIndex depIndex = libraryDependencyInterningTable.Intern(item.Value);
- prunedPackageVersions[depIndex] = item.Value.VersionRange;
- }
- }
-
- return prunedPackageVersions;
- }
-
- private bool ShouldPrunePackage(
- IReadOnlyDictionary? packagesToPrune,
- FindLibraryEntryResult refItemResult,
- LibraryDependency dep,
- LibraryDependencyIndex libraryDependencyIndex,
- bool isPackage,
- bool isDirectPackageReferenceFromRootProject)
- {
- if (packagesToPrune?.TryGetValue(libraryDependencyIndex, out VersionRange? prunableVersion) == true)
+ // If central transitive pinning is enabled, we need to add all of its parent nodes. This has to happen at the end after all of the nodes in graph have been created
+ if (isCentralPackageTransitivePinningEnabled)
{
- if (dep.LibraryRange!.VersionRange!.Satisfies(prunableVersion!.MaxVersion!))
+ foreach (KeyValuePair resolvedDependencyGraphItemEntry in resolvedDependencyGraphItems)
{
- if (!isPackage)
+ ResolvedDependencyGraphItem resolvedDependencyGraphItem = resolvedDependencyGraphItemEntry.Value;
+
+ // Skip this item if:
+ // 1. It is not pinned
+ // 2. It is a direct package reference
+ // 3. It has not parents
+ // 4. A node was not created for it
+ if (!resolvedDependencyGraphItem.IsCentrallyPinnedTransitivePackage
+ || resolvedDependencyGraphItem.IsDirectPackageReferenceFromRootProject
+ || resolvedDependencyGraphItem.Parents == null
+ || resolvedDependencyGraphItem.Parents.Count == 0
+ || !nodesById.TryGetValue(resolvedDependencyGraphItem.LibraryRangeIndex, out GraphNode? currentNode))
{
- if (SdkAnalysisLevelMinimums.IsEnabled(
- _request.Project!.RestoreMetadata!.SdkAnalysisLevel,
- _request.Project.RestoreMetadata.UsingMicrosoftNETSdk,
- SdkAnalysisLevelMinimums.PruningWarnings))
- {
- _logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1511, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningProjectReference, dep.Name)));
- }
+ continue;
}
- else if (isDirectPackageReferenceFromRootProject)
+
+ // Get the corresponding node in the graph for each parent and add it the list of parent nodes
+ foreach (LibraryRangeIndex parentLibraryRangeIndex in resolvedDependencyGraphItem.Parents.NoAllocEnumerate())
{
- if (SdkAnalysisLevelMinimums.IsEnabled(
- _request.Project!.RestoreMetadata!.SdkAnalysisLevel,
- _request.Project.RestoreMetadata.UsingMicrosoftNETSdk,
- SdkAnalysisLevelMinimums.PruningWarnings))
+ if (!nodesById.TryGetValue(parentLibraryRangeIndex, out GraphNode? parentNode))
{
- _logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1510, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningDirectPackageReference, dep.Name)));
+ // Skip nodes that weren't created
+ continue;
}
- }
- else
- {
- _logger.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.RestoreDebugPruningPackageReference, $"{dep.Name} {dep.LibraryRange.VersionRange.OriginalString}", refItemResult.Item.Key, prunableVersion.MaxVersion));
- return true;
+ currentNode.ParentNodes.Add(parentNode);
}
}
}
- return false;
+
+ // Get the list of packages to install
+ HashSet packagesToInstall = await context.GetUnresolvedRemoteMatchesAsync();
+
+ // Create a RestoreTargetGraph with all of the information
+ RestoreTargetGraph restoreTargetGraph = new(
+ Array.Empty(),
+ frameworkRuntimePair.Framework,
+ string.IsNullOrWhiteSpace(frameworkRuntimePair.RuntimeIdentifier) ? null : frameworkRuntimePair.RuntimeIdentifier,
+ runtimeGraph,
+ graphNodes,
+ install: packagesToInstall,
+ flattened: flattenedGraphItems,
+ unresolved: unresolvedPackages,
+ analyzeResult,
+ resolvedDependencies: resolvedPackages);
+
+ return (success, restoreTargetGraph);
}
- private static bool EvictOnTypeConstraint(LibraryDependencyTarget current, LibraryDependencyTarget previous)
+ private static bool EvaluateRuntimeDependencies(ref LibraryDependency libraryDependency, RuntimeGraph? runtimeGraph, string? runtimeIdentifier, ref HashSet? runtimeDependencies)
{
- if (current == previous)
+ LibraryRange libraryRange = libraryDependency.LibraryRange;
+
+ if (runtimeGraph == null || string.IsNullOrEmpty(runtimeIdentifier) || !RemoteDependencyWalker.EvaluateRuntimeDependencies(ref libraryRange, runtimeIdentifier, runtimeGraph, ref runtimeDependencies))
{
return false;
}
- if (previous == LibraryDependencyTarget.PackageProjectExternal)
+ libraryDependency = new LibraryDependency(libraryDependency)
{
- LibraryDependencyTarget ppeFlags = current & LibraryDependencyTarget.PackageProjectExternal;
- LibraryDependencyTarget nonPpeFlags = current & ~LibraryDependencyTarget.PackageProjectExternal;
- return (ppeFlags != LibraryDependencyTarget.None && nonPpeFlags == LibraryDependencyTarget.None);
- }
+ LibraryRange = libraryRange
+ };
- // TODO: Should there be other cases here?
- return false;
+ return true;
}
private static bool HasCommonAncestor(LibraryRangeIndex[] left, LibraryRangeIndex[] right)
@@ -1347,40 +763,94 @@ private static bool HasCommonAncestor(LibraryRangeIndex[] left, LibraryRangeInde
return true;
}
+ ///
+ /// Determine if the chosen item should be evicted based on the .
+ ///
+ ///
+ /// We should evict on type constraint if the type constraint of the current item has the same version but has a more restrictive type constraint than the chosen item.
+ /// This happens when the chosen item's type constraint is broader (e.g. PackageProjectExternal) than the current item's type constraint (e.g. Package).
+ ///
+ ///
+ private static bool ShouldEvictOnTypeConstraint(DependencyGraphItem currentDependencyGraphItem, ResolvedDependencyGraphItem resolvedDependencyGraphItem)
+ {
+ LibraryDependency currentLibraryDependency = currentDependencyGraphItem.LibraryDependency;
+ LibraryDependency chosenLibraryDependency = resolvedDependencyGraphItem.LibraryDependency;
+
+ // We should evict the chosen item if it is a package but the current item is a project since projects should be chosen over packages
+ if (chosenLibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.PackageProjectExternal
+ && currentLibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.ExternalProject)
+ {
+ return true;
+ }
+
+ LibraryRangeIndex currentLibraryRangeIndex = currentDependencyGraphItem.LibraryRangeIndex;
+ LibraryRangeIndex chosenLibraryRangeIndex = resolvedDependencyGraphItem.LibraryRangeIndex;
+
+ // Do not evict if:
+ // 1. The current item and chosen item are not the same version
+ // 2. The current item and chosen item have the same type constraint
+ // 3. The chosen item has a strict type constraint instead of the more generic "PackageProjectExternal"
+ if (currentLibraryRangeIndex != chosenLibraryRangeIndex
+ || currentLibraryDependency.LibraryRange.TypeConstraint == chosenLibraryDependency.LibraryRange.TypeConstraint
+ || chosenLibraryDependency.LibraryRange.TypeConstraint != LibraryDependencyTarget.PackageProjectExternal)
+ {
+ return false;
+ }
+
+ LibraryDependencyTarget packageProjectExternalFlags = currentLibraryDependency.LibraryRange.TypeConstraint & LibraryDependencyTarget.PackageProjectExternal;
+ LibraryDependencyTarget nonPackageProjectExternalFlats = currentLibraryDependency.LibraryRange.TypeConstraint & ~LibraryDependencyTarget.PackageProjectExternal;
+
+ // Evict if the type constraint of the current item is more precise than "PackageProjectExternal" and the current item is a project
+ if (packageProjectExternalFlags != LibraryDependencyTarget.None && nonPackageProjectExternalFlats == LibraryDependencyTarget.None)
+ {
+ return resolvedDependencyGraphItem.Item.Key.Type == LibraryType.Project;
+ }
+
+ return false;
+ }
+
private static bool VersionRangePreciseEquals(VersionRange a, VersionRange b)
{
if (ReferenceEquals(a, b))
{
return true;
}
+
if ((a.MinVersion != null) != (b.MinVersion != null))
{
return false;
}
+
if (a.MinVersion != b.MinVersion)
{
return false;
}
+
if ((a.MaxVersion != null) != (b.MaxVersion != null))
{
return false;
}
+
if (a.MaxVersion != b.MaxVersion)
{
return false;
}
+
if (a.IsMinInclusive != b.IsMinInclusive)
{
return false;
}
+
if (a.IsMaxInclusive != b.IsMaxInclusive)
{
return false;
}
+
if ((a.Float != null) != (b.Float != null))
{
return false;
}
+
if (a.Float != b.Float)
{
return false;
@@ -1389,307 +859,562 @@ private static bool VersionRangePreciseEquals(VersionRange a, VersionRange b)
return true;
}
- [DebuggerDisplay("{LibraryDependency}, RangeIndex={LibraryRangeIndex}")]
- private class ResolvedDependencyGraphItem
+ ///
+ /// Indexes all central package versions if central transitive pinning is enabled.
+ ///
+ /// Indicates whether or not central transitive pinning is enabled.
+ /// The of the project.
+ /// A of indexed version ranges by their if central transitive pinning is enabled, otherwise .
+ private Dictionary? IndexPinnedPackageVersions(bool isCentralPackageTransitivePinningEnabled, TargetFrameworkInformation? projectTargetFramework)
{
- public bool IsCentrallyPinnedTransitivePackage { get; set; }
-
- public bool IsDirectPackageReferenceFromRootProject { get; set; }
-
- public required LibraryDependency LibraryDependency { get; set; }
-
- public LibraryRangeIndex LibraryRangeIndex { get; set; }
-
- public HashSet? Parents { get; set; }
+ if (!isCentralPackageTransitivePinningEnabled || projectTargetFramework == null || projectTargetFramework.CentralPackageVersions == null)
+ {
+ return null;
+ }
- public HashSet? ParentPathsThatHaveBeenEclipsed { get; set; }
+ Dictionary? pinnedPackageVersions = new(capacity: projectTargetFramework.CentralPackageVersions.Count);
- public required LibraryRangeIndex[] Path { get; set; }
+ foreach (KeyValuePair item in projectTargetFramework.CentralPackageVersions.NoAllocEnumerate())
+ {
+ LibraryDependencyIndex libraryDependencyIndex = _indexingTable.Index(item.Value);
- public required List> Suppressions { get; set; }
+ pinnedPackageVersions[libraryDependencyIndex] = item.Value.VersionRange;
+ }
- public HashSet? PrunedDependencies { get; set; }
+ return pinnedPackageVersions;
}
- internal sealed class LibraryDependencyInterningTable
+ private async Task> ResolveDependencyGraphItemsAsync(
+ bool isCentralPackageTransitivePinningEnabled,
+ FrameworkRuntimePair pair,
+ TargetFrameworkInformation? projectTargetFramework,
+ RuntimeGraph? runtimeGraph,
+ Dictionary? pinnedPackageVersions,
+ DependencyGraphItem rootProjectDependencyGraphItem,
+ RemoteWalkContext context,
+ CancellationToken token)
{
- private readonly object _lockObject = new();
- private readonly ConcurrentDictionary _table = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
- private int _nextIndex = 0;
-
- public enum LibraryDependencyIndex : int
+ // Stores the resolved dependency graph items
+ Dictionary resolvedDependencyGraphItems = new(ResolvedDependencyGraphItemDictionarySize);
+
+ // Stores a list of direct package references by their LibraryDependencyIndex so that we can quickly determine if a transitive package can be ignored
+ HashSet directPackageReferences = new();
+
+ // Stores the queue of DependencyGraphItem objects to process
+ Queue dependencyGraphItemQueue = new(DependencyGraphItemQueueSize);
+
+ // Stores any evictions to process
+ Dictionary evictions = new Dictionary(EvictionsDictionarySize);
+
+ // Used to start over when a dependency has multiple descendants of an item to be evicted.
+ //
+ // Project
+ // ├── A 1.0.0
+ // │ └── B 1.0.0
+ // │ └── C 1.0.0
+ // │ └── D 1.0.0
+ // └── X 2.0.0
+ // └── Y 2.0.0
+ // └── G 2.0.0
+ // └── B 2.0.0
+ // The items are processed in the following order:
+ // Chose A 1.0.0 and X 1.0.0
+ // Chose B 1.0.0 and Y 2.0.0
+ // Chose C 1.0.0 and G 2.0.0
+ // Chose D 1.0.0 and B 2.0.0, but B 2.0.0 should evict C 1.0.0 and D 1.0.0
+ //
+ // In this case, the entire walk is started over and B 1.0.0 is left out of the graph, leading to C 1.0.0 and D 1.0.0 also being left out.
+ //
+ StartOver:
+
+ dependencyGraphItemQueue.Clear();
+ resolvedDependencyGraphItems.Clear();
+
+ dependencyGraphItemQueue.Enqueue(rootProjectDependencyGraphItem);
+
+ while (dependencyGraphItemQueue.Count > 0)
{
- Invalid = -1,
- }
+ DependencyGraphItem currentDependencyGraphItem = dependencyGraphItemQueue.Dequeue();
+ LibraryDependency currentLibraryDependency = currentDependencyGraphItem.LibraryDependency!;
+ LibraryDependencyIndex currentLibraryDependencyIndex = currentDependencyGraphItem.LibraryDependencyIndex;
+ LibraryRangeIndex currentLibraryRangeIndex = currentDependencyGraphItem.LibraryRangeIndex;
+ LibraryRangeIndex[] currentDependencyGraphItemPath = currentDependencyGraphItem.Path;
+ HashSet? currentDependencyGraphItemSuppressions = currentDependencyGraphItem.Suppressions;
+ bool currentDependencyGraphItemIsDirectPackageReferenceFromRootProject = currentDependencyGraphItem.IsDirectPackageReferenceFromRootProject;
- public LibraryDependencyIndex Intern(LibraryDependency libraryDependency)
- {
- lock (_lockObject)
+ // Determine if what is being processed is the root project itself which has different rules vs a transitive dependency
+ bool isRootProject = currentDependencyGraphItem.LibraryDependencyIndex == LibraryDependencyIndex.Project;
+
+ GraphItem currentGraphItem = await currentDependencyGraphItem.GetGraphItemAsync(_request, projectTargetFramework?.PackagesToPrune, isRootProject, context, _logger);
+
+ LibraryDependencyTarget typeConstraint = currentLibraryDependency.LibraryRange.TypeConstraint;
+ if (evictions.TryGetValue(currentLibraryRangeIndex, out (LibraryRangeIndex[], LibraryDependencyIndex, LibraryDependencyTarget) eviction))
{
- string key = libraryDependency.Name;
- if (!_table.TryGetValue(key, out LibraryDependencyIndex index))
+ (LibraryRangeIndex[] evictedPath, LibraryDependencyIndex evictedDepIndex, LibraryDependencyTarget evictedTypeConstraint) = eviction;
+
+ // If we evicted this same version previously, but the type constraint of currentRef is more stringent (package), then do not skip the current item - this is the one we want.
+ // This is tricky. I don't really know what this means. Normally we'd key off of versions instead.
+ if (!((evictedTypeConstraint == LibraryDependencyTarget.PackageProjectExternal || evictedTypeConstraint == LibraryDependencyTarget.ExternalProject) &&
+ currentLibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.Package))
{
- index = (LibraryDependencyIndex)_nextIndex++;
- _table.TryAdd(key, index);
+ continue;
}
-
- return index;
}
- }
- public LibraryDependencyIndex Intern(CentralPackageVersion centralPackageVersion)
- {
- string key = centralPackageVersion.Name;
- if (!_table.TryGetValue(key, out LibraryDependencyIndex index))
+ // Determine if a dependency with the same name has not already been chosen
+ if (!resolvedDependencyGraphItems.TryGetValue(currentLibraryDependencyIndex, out ResolvedDependencyGraphItem? chosenResolvedItem))
{
- index = (LibraryDependencyIndex)_nextIndex++;
- _table.TryAdd(key, index);
- }
-
- return index;
- }
+ // Create a resolved dependency graph item and add it to the list of chosen items
+ chosenResolvedItem = new ResolvedDependencyGraphItem(currentGraphItem, _indexingTable)
+ {
+ LibraryDependency = currentLibraryDependency,
+ LibraryRangeIndex = currentLibraryRangeIndex,
+ Parents = currentDependencyGraphItem.IsCentrallyPinnedTransitivePackage && !currentDependencyGraphItemIsDirectPackageReferenceFromRootProject ? new HashSet() { currentDependencyGraphItem.Parent } : null,
+ Path = currentDependencyGraphItemPath,
+ IsCentrallyPinnedTransitivePackage = currentDependencyGraphItem.IsCentrallyPinnedTransitivePackage,
+ IsDirectPackageReferenceFromRootProject = currentDependencyGraphItemIsDirectPackageReferenceFromRootProject,
+ Suppressions = new List>
+ {
+ currentDependencyGraphItemSuppressions!
+ }
+ };
- public LibraryDependencyIndex Intern(PrunePackageReference prunePackageReference)
- {
- lock (_lockObject)
+ resolvedDependencyGraphItems.Add(currentLibraryDependencyIndex, chosenResolvedItem);
+ }
+ else // A dependency with the same name has already been chosen so we need to decide what to do with it
{
- string key = prunePackageReference.Name;
- if (!_table.TryGetValue(key, out LibraryDependencyIndex index))
+ LibraryDependency chosenLibraryDependency = chosenResolvedItem.LibraryDependency;
+ LibraryRangeIndex chosenLibraryRangeIndex = chosenResolvedItem.LibraryRangeIndex;
+ LibraryRangeIndex[] chosenDependencyGraphItemPath = chosenResolvedItem.Path;
+ bool chosenDependencyGraphItemIsDirectPackageReferenceFromRootProject = chosenResolvedItem.IsDirectPackageReferenceFromRootProject;
+ List> chosenDependencyGraphItemSuppressions = chosenResolvedItem.Suppressions;
+ GraphItem chosenGraphItem = chosenResolvedItem.Item;
+
+ if (chosenDependencyGraphItemIsDirectPackageReferenceFromRootProject)
{
- index = (LibraryDependencyIndex)_nextIndex++;
- _table.TryAdd(key, index);
+ // If the chosen dependency graph item is a direct dependency, it should always be chosen regardless of version so do not process this dependency
+ continue;
}
- return index;
- }
- }
- }
+ if (chosenResolvedItem.LibraryDependency.LibraryRange.TypeConstraint == LibraryDependencyTarget.ExternalProject
+ && currentLibraryDependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package)
+ && currentGraphItem.Key.Type == LibraryType.Project)
+ {
+ // If the chosen dependency graph item is a project reference, it should always be chosen over a package reference. In this case, a project has already been chosen
+ // for the graph with the same name as a transitive package reference, so this item does not need to be processed.
+ continue;
+ }
- internal sealed class LibraryRangeInterningTable
- {
- private readonly object _lockObject = new();
- private readonly ConcurrentDictionary _table = new(LibraryRangeComparer.Instance);
- private int _nextIndex = 0;
+ // Determine if the chosen item should be evicted based on type constraint.
+ bool evictOnTypeConstraint = ShouldEvictOnTypeConstraint(currentDependencyGraphItem, chosenResolvedItem);
- public enum LibraryRangeIndex : int
- {
- Invalid = -1,
- }
+ VersionRange nvr = currentLibraryDependency.LibraryRange.VersionRange ?? VersionRange.All;
+ VersionRange ovr = chosenLibraryDependency.LibraryRange.VersionRange ?? VersionRange.All;
- public LibraryRangeIndex Intern(LibraryRange libraryRange)
- {
- lock (_lockObject)
- {
- if (!_table.TryGetValue(libraryRange, out LibraryRangeIndex index))
+ // The chosen item should be evicted or the current item has a greater version, determine if the current item should be chosen instead
+ if (evictOnTypeConstraint || !RemoteDependencyWalker.IsGreaterThanOrEqualTo(ovr, nvr))
{
- index = (LibraryRangeIndex)_nextIndex++;
- _table.TryAdd(libraryRange, index);
- }
+ if (chosenLibraryDependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package) && currentLibraryDependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package))
+ {
+ if (chosenResolvedItem.Parents != null)
+ {
+ bool atLeastOneCommonAncestor = false;
- return index;
- }
- }
+ foreach (LibraryRangeIndex parentRangeIndex in chosenResolvedItem.Parents.NoAllocEnumerate())
+ {
+ if (currentDependencyGraphItem.Path.Length > 2 && currentDependencyGraphItem.Path[currentDependencyGraphItem.Path.Length - 2] == parentRangeIndex)
+ {
+ atLeastOneCommonAncestor = true;
+ break;
+ }
+ }
- internal static LibraryRangeIndex[] CreatePathToRef(LibraryRangeIndex[] existingPath, LibraryRangeIndex currentRef)
- {
- LibraryRangeIndex[] newPath = new LibraryRangeIndex[existingPath.Length + 1];
- Array.Copy(existingPath, newPath, existingPath.Length);
- newPath[newPath.Length - 1] = currentRef;
+ if (atLeastOneCommonAncestor)
+ {
+ // At least one of the parents of the chosen item is a common ancestor of the current item, so the current item should be skipped
+ continue;
+ }
+ }
- return newPath;
- }
- }
+ if (HasCommonAncestor(chosenResolvedItem.Path, currentDependencyGraphItem.Path))
+ {
+ // The current item has a common ancestor of the chosen item and should not be chosen since children in the same leaf of the graph do not eclipse each other
+ continue;
+ }
- [DebuggerDisplay("{LibraryDependency}, DependencyIndex={LibraryDependencyIndex}, RangeIndex={LibraryRangeIndex}")]
- private class DependencyGraphItem
- {
- public bool IsCentrallyPinnedTransitivePackage { get; set; }
+ if (chosenResolvedItem.ParentPathsThatHaveBeenEclipsed != null)
+ {
+ // Determine if the current item is under a parent that has already been eclipsed
+ bool hasAlreadyBeenEclipsed = false;
+
+ foreach (LibraryRangeIndex parentRangeIndex in chosenResolvedItem.ParentPathsThatHaveBeenEclipsed)
+ {
+ if (currentDependencyGraphItem.Path.Contains(parentRangeIndex))
+ {
+ hasAlreadyBeenEclipsed = true;
+ break;
+ }
+ }
- public bool IsDirectPackageReferenceFromRootProject { get; set; }
+ if (hasAlreadyBeenEclipsed)
+ {
+ continue;
+ }
+ }
+ }
- public LibraryDependency? LibraryDependency { get; set; }
+ // Remove the chosen item
+ resolvedDependencyGraphItems.Remove(currentLibraryDependencyIndex);
+ chosenResolvedItem = null;
- public LibraryDependencyIndex LibraryDependencyIndex { get; set; } = LibraryDependencyIndex.Invalid;
+ // Record an eviction for the item we are replacing. The eviction path is for the current item.
+ LibraryRangeIndex evictedLibraryRangeIndex = chosenLibraryRangeIndex;
- public LibraryRangeIndex LibraryRangeIndex { get; set; } = LibraryRangeIndex.Invalid;
+ bool shouldStartOver = false;
- public LibraryRangeIndex[] Path { get; set; } = Array.Empty();
+ // To "evict" a chosen item, we need to also remove all of its transitive children from the chosen list.
+ HashSet? evicteesToRemove = default;
- public LibraryRangeIndex Parent { get; set; }
+ foreach (KeyValuePair evictee in evictions)
+ {
+ (LibraryRangeIndex[] evicteePath, LibraryDependencyIndex evicteeDepIndex, LibraryDependencyTarget evicteeTypeConstraint) = evictee.Value;
- public HashSet? Suppressions { get; set; }
- }
+ // See if the evictee is a descendant of the evicted item
+ if (evicteePath.Contains(evictedLibraryRangeIndex))
+ {
+ // if evictee.Key (depIndex) == currentDepIndex && evictee.TypeConstraint == ExternalProject --> Don't remove it. It must remain evicted.
+ // If the evictee to remove is the same dependency, but the project version of said dependency, then do not remove it - it must remain evicted in favor of the package.
+ if (!(evicteeDepIndex == currentLibraryDependencyIndex &&
+ (evicteeTypeConstraint == LibraryDependencyTarget.ExternalProject || evicteeTypeConstraint == LibraryDependencyTarget.PackageProjectExternal)))
+ {
+ evicteesToRemove ??= new HashSet();
- private class FindLibraryEntryResult
- {
- private LibraryDependencyIndex[] _dependencyIndices;
- private LibraryRangeIndex[] _rangeIndices;
-
- public FindLibraryEntryResult(
- LibraryDependency libraryDependency,
- GraphItem resolvedItem,
- LibraryDependencyIndex itemDependencyIndex,
- LibraryRangeIndex itemRangeIndex,
- LibraryDependencyInterningTable libraryDependencyInterningTable,
- LibraryRangeInterningTable libraryRangeInterningTable)
- {
- Item = resolvedItem;
- DependencyIndex = itemDependencyIndex;
- RangeIndex = itemRangeIndex;
- int dependencyCount = resolvedItem.Data.Dependencies.Count;
+ evicteesToRemove.Add(evictee.Key);
+ }
+ }
+ }
- if (dependencyCount == 0)
- {
- _dependencyIndices = Array.Empty();
- _rangeIndices = Array.Empty();
- }
- else
- {
- _dependencyIndices = new LibraryDependencyIndex[dependencyCount];
- _rangeIndices = new LibraryRangeIndex[dependencyCount];
+ if (evicteesToRemove != null)
+ {
+ foreach (LibraryRangeIndex evicteeToRemove in evicteesToRemove)
+ {
+ evictions.Remove(evicteeToRemove);
+
+ // Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
+ shouldStartOver = true;
+ }
+ }
+
+ foreach (KeyValuePair chosenItem in resolvedDependencyGraphItems)
+ {
+ if (chosenItem.Value.Path.Contains(evictedLibraryRangeIndex))
+ {
+ // Indicate that we can't simply evict this item and instead we need to start over knowing that this item should be skipped
+ shouldStartOver = true;
+ break;
+ }
+ }
- for (int i = 0; i < dependencyCount; i++)
+ // Add the eviction to be used later
+ evictions[evictedLibraryRangeIndex] = (DependencyGraphItemIndexer.CreatePathToRef(currentDependencyGraphItemPath, currentLibraryRangeIndex), currentLibraryDependencyIndex, chosenLibraryDependency.LibraryRange.TypeConstraint);
+
+ if (shouldStartOver)
+ {
+ goto StartOver;
+ }
+
+ bool isCentrallyPinnedTransitivePackage = currentDependencyGraphItem.IsCentrallyPinnedTransitivePackage;
+
+ // Add the item to the list of chosen items
+ chosenResolvedItem = new ResolvedDependencyGraphItem(currentGraphItem, _indexingTable)
+ {
+ LibraryDependency = currentLibraryDependency,
+ LibraryRangeIndex = currentLibraryRangeIndex,
+ Parents = isCentrallyPinnedTransitivePackage && !currentDependencyGraphItemIsDirectPackageReferenceFromRootProject ? new HashSet() { currentDependencyGraphItem.Parent } : null,
+ Path = currentDependencyGraphItemPath,
+ IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
+ IsDirectPackageReferenceFromRootProject = currentDependencyGraphItemIsDirectPackageReferenceFromRootProject,
+ Suppressions = new List>
+ {
+ currentDependencyGraphItemSuppressions!
+ },
+ };
+
+ resolvedDependencyGraphItems.Add(currentLibraryDependencyIndex, chosenResolvedItem);
+
+ // Recreate the queue but leave out any items that are children of the chosen item that was just removed which essentially evicts unprocessed children from the queue
+ Queue newDependencyGraphItemQueue = new(DependencyGraphItemQueueSize);
+
+ while (dependencyGraphItemQueue.Count > 0)
+ {
+ DependencyGraphItem item = dependencyGraphItemQueue.Dequeue();
+
+ if (!item.Path.Contains(evictedLibraryRangeIndex))
+ {
+ newDependencyGraphItemQueue.Enqueue(item);
+ }
+ }
+
+ dependencyGraphItemQueue = newDependencyGraphItemQueue;
+ }
+ else if (!VersionRangePreciseEquals(ovr, nvr)) // The current item has a lower version
{
- LibraryDependency dependency = resolvedItem.Data.Dependencies[i];
- _dependencyIndices[i] = libraryDependencyInterningTable.Intern(dependency);
- _rangeIndices[i] = libraryRangeInterningTable.Intern(dependency.LibraryRange);
+ bool hasCommonAncestor = HasCommonAncestor(chosenResolvedItem.Path, currentDependencyGraphItemPath);
+
+ if (!hasCommonAncestor)
+ {
+ chosenResolvedItem.ParentPathsThatHaveBeenEclipsed ??= new HashSet();
+
+ // Keeps track of parents that have been eclipsed
+ chosenResolvedItem.ParentPathsThatHaveBeenEclipsed.Add(currentDependencyGraphItemPath[currentDependencyGraphItemPath.Length - 1]);
+ }
+
+ // Do not process this item
+ continue;
}
- }
- }
+ else // The current item and chosen item have the same version
+ {
+ chosenResolvedItem.Parents ??= new HashSet();
- public LibraryDependencyIndex DependencyIndex { get; }
+ if (!chosenResolvedItem.IsDirectPackageReferenceFromRootProject)
+ {
+ // Keep track of the parents of this item
+ chosenResolvedItem.Parents?.Add(currentDependencyGraphItem.Parent);
+ }
- public GraphItem Item { get; }
+ if (chosenDependencyGraphItemSuppressions.Count == 1 && chosenDependencyGraphItemSuppressions[0].Count == 0 && HasCommonAncestor(chosenResolvedItem.Path, currentDependencyGraphItemPath))
+ {
+ // Skip this item if it has no suppressions and has a common ancestor
+ continue;
+ }
+ else if (currentDependencyGraphItemSuppressions!.Count == 0) // The current item has no suppressions
+ {
+ // Replace the chosen item with the current one since they are basically the same and process its children
+ resolvedDependencyGraphItems.Remove(currentLibraryDependencyIndex);
- public LibraryRangeIndex RangeIndex { get; }
+ bool isCentrallyPinnedTransitivePackage = chosenResolvedItem.IsCentrallyPinnedTransitivePackage;
- public LibraryDependencyIndex GetDependencyIndexForDependency(int dependencyIndex)
- {
- return _dependencyIndices[dependencyIndex];
- }
+ chosenResolvedItem = new ResolvedDependencyGraphItem(currentGraphItem, _indexingTable)
+ {
+ LibraryDependency = currentLibraryDependency,
+ LibraryRangeIndex = currentLibraryRangeIndex,
+ Parents = chosenResolvedItem.Parents,
+ Path = currentDependencyGraphItemPath,
+ IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
+ IsDirectPackageReferenceFromRootProject = chosenDependencyGraphItemIsDirectPackageReferenceFromRootProject,
+ Suppressions = new List>
+ {
+ currentDependencyGraphItemSuppressions,
+ },
+ };
- public LibraryRangeIndex GetRangeIndexForDependency(int dependencyIndex)
- {
- return _rangeIndices[dependencyIndex];
- }
+ resolvedDependencyGraphItems.Add(currentLibraryDependencyIndex, chosenResolvedItem);
+ }
+ else // The chosen item and current item have a different set of suppressions
+ {
+ bool isEqualOrSuperSetDisposition = false;
+ foreach (HashSet chosenDependencyGraphItemSuppression in chosenDependencyGraphItemSuppressions)
+ {
+ if (currentDependencyGraphItemSuppressions.IsSupersetOf(chosenDependencyGraphItemSuppression))
+ {
+ isEqualOrSuperSetDisposition = true;
+ }
+ }
- public async static Task CreateAsync(LibraryDependency libraryDependency, LibraryDependencyIndex dependencyIndex, LibraryRangeIndex rangeIndex, NuGetFramework framework, RemoteWalkContext context, LibraryDependencyInterningTable libraryDependencyInterningTable, LibraryRangeInterningTable libraryRangeInterningTable, CancellationToken cancellationToken)
- {
- GraphItem refItem = await ResolverUtility.FindLibraryEntryAsync(
- libraryDependency.LibraryRange,
- framework,
- runtimeIdentifier: null,
- context,
- cancellationToken);
-
- return new FindLibraryEntryResult(
- libraryDependency,
- refItem,
- dependencyIndex,
- rangeIndex,
- libraryDependencyInterningTable,
- libraryRangeInterningTable);
- }
- }
+ if (isEqualOrSuperSetDisposition)
+ {
+ // Do not process the current item if its suppressions are identical
+ continue;
+ }
+ else
+ {
+ bool isCentrallyPinnedTransitivePackage = chosenResolvedItem.IsCentrallyPinnedTransitivePackage;
- ///
- /// Represents an of that considers them to be equal based on the same functionality of .
- ///
- internal sealed class LibraryRangeComparer : IEqualityComparer
- {
- ///
- /// Gets an instance of .
- ///
- public static LibraryRangeComparer Instance { get; } = new LibraryRangeComparer();
+ // Replace the chosen item with the current item with the the combined list of suppressions and process its children
+ resolvedDependencyGraphItems.Remove(currentLibraryDependencyIndex);
- private LibraryRangeComparer()
- {
- }
+ chosenResolvedItem = new ResolvedDependencyGraphItem(currentGraphItem, _indexingTable)
+ {
+ LibraryDependency = currentLibraryDependency,
+ LibraryRangeIndex = currentLibraryRangeIndex,
+ Parents = chosenResolvedItem.Parents,
+ Path = currentDependencyGraphItemPath,
+ IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitivePackage,
+ IsDirectPackageReferenceFromRootProject = chosenDependencyGraphItemIsDirectPackageReferenceFromRootProject,
+ Suppressions =
+ [
+ currentDependencyGraphItemSuppressions,
+ .. chosenDependencyGraphItemSuppressions
+ ],
+ };
- public bool Equals(LibraryRange? x, LibraryRange? y)
- {
- if (x == null || y == null || x.VersionRange == null || y.VersionRange == null)
- {
- return false;
+ resolvedDependencyGraphItems.Add(currentLibraryDependencyIndex, chosenResolvedItem);
+ }
+ }
+ }
}
- if (ReferenceEquals(x, y))
+ // Determine the list of root dependencies if the current item is the project, this is used for a faster lookup later to determine if a transitive dependency
+ // should be ignored when there is a direct dependency.
+ if (isRootProject)
{
- return true;
- }
+ for (int i = 0; i < chosenResolvedItem.Item.Data.Dependencies.Count; i++)
+ {
+ LibraryDependency rootLibraryDependency = chosenResolvedItem.Item.Data.Dependencies[i];
+ if (rootLibraryDependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package))
+ {
+ directPackageReferences!.Add(chosenResolvedItem.GetDependencyIndexForDependencyAt(i));
+ }
+ }
+ }
- // All of this logic is copied from LibraryRange.ToString()
- LibraryDependencyTarget typeConstraint1 = LibraryDependencyTarget.None;
- LibraryDependencyTarget typeConstraint2 = LibraryDependencyTarget.None;
+ HashSet? suppressions = default;
- switch (x.TypeConstraint)
+ // Only gather suppressed dependencies if the current item is not the root project
+ if (!isRootProject)
{
- case LibraryDependencyTarget.Reference:
- typeConstraint1 = LibraryDependencyTarget.Reference;
- break;
-
- case LibraryDependencyTarget.ExternalProject:
- typeConstraint1 = LibraryDependencyTarget.ExternalProject;
- break;
-
- case LibraryDependencyTarget.Project:
- case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
- typeConstraint1 = LibraryDependencyTarget.Project;
- break;
+ // If this is not the root project, loop through the dependencies and keep track of which ones have PrivateAssets=All which we consider a "suppression"
+ for (int i = 0; i < chosenResolvedItem.Item.Data.Dependencies.Count; i++)
+ {
+ LibraryDependency chosenResolvedItemChildLibraryDependency = chosenResolvedItem.Item.Data.Dependencies[i];
+
+ // Skip any packages with a missing versions
+ if (chosenResolvedItemChildLibraryDependency.LibraryRange.VersionRange == null)
+ {
+ continue;
+ }
+
+ LibraryDependencyIndex chosenResolvedItemChildLibraryDependencyIndex = chosenResolvedItem.GetDependencyIndexForDependencyAt(i);
+
+ // Suppress this dependency if PrivateAssets is set to "All"
+ if (chosenResolvedItemChildLibraryDependency.SuppressParent == LibraryIncludeFlags.All)
+ {
+ suppressions ??= new HashSet();
+
+ suppressions.Add(chosenResolvedItemChildLibraryDependencyIndex);
+ }
+ }
}
- switch (y.TypeConstraint)
+ // The list of suppressions should be an aggregate of all parent item's suppressions so add the parent suppressions to the list, otherwise just use the current item's suppressions
+ if (suppressions != null)
+ {
+ suppressions.AddRange(currentDependencyGraphItemSuppressions);
+ }
+ else
{
- case LibraryDependencyTarget.Reference:
- typeConstraint2 = LibraryDependencyTarget.Reference;
- break;
-
- case LibraryDependencyTarget.ExternalProject:
- typeConstraint2 = LibraryDependencyTarget.ExternalProject;
- break;
-
- case LibraryDependencyTarget.Project:
- case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
- typeConstraint2 = LibraryDependencyTarget.Project;
- break;
+ suppressions = currentDependencyGraphItemSuppressions;
}
- return typeConstraint1 == typeConstraint2
- && x.Name.Equals(y.Name, StringComparison.OrdinalIgnoreCase)
- && x.VersionRange.Equals(y.VersionRange);
- }
+ // Loop through the dependencies now that we know which ones to suppress
+ for (int i = 0; i < chosenResolvedItem.Item.Data.Dependencies.Count; i++)
+ {
+ LibraryDependency childLibraryDependency = chosenResolvedItem.Item.Data.Dependencies[i];
+ LibraryDependencyIndex childLibraryDependencyIndex = chosenResolvedItem.GetDependencyIndexForDependencyAt(i);
- public int GetHashCode(LibraryRange obj)
- {
- LibraryDependencyTarget typeConstraint = LibraryDependencyTarget.None;
+ HashSet? runtimeDependencies = default;
- switch (obj.TypeConstraint)
- {
- case LibraryDependencyTarget.Reference:
- typeConstraint = LibraryDependencyTarget.Reference;
- break;
-
- case LibraryDependencyTarget.ExternalProject:
- typeConstraint = LibraryDependencyTarget.ExternalProject;
- break;
-
- case LibraryDependencyTarget.Project:
- case LibraryDependencyTarget.Project | LibraryDependencyTarget.ExternalProject:
- typeConstraint = LibraryDependencyTarget.Project;
- break;
- }
+ // Evaluate the runtime dependencies if any
+ if (EvaluateRuntimeDependencies(ref childLibraryDependency, runtimeGraph, pair.RuntimeIdentifier, ref runtimeDependencies))
+ {
+ // EvaluateRuntimeDependencies() returns true if the version of the dependency was changed, which also changes the LibraryRangeIndex so that must be updated in the chosen item's array of library range indices.
+ chosenResolvedItem.SetRangeIndexForDependencyAt(i, _indexingTable.Index(childLibraryDependency.LibraryRange));
+ }
+
+ bool isPackage = childLibraryDependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package);
+ bool isDirectPackageReferenceFromRootProject = (currentDependencyGraphItem.LibraryDependencyIndex == LibraryDependencyIndex.Project) && isPackage;
+
+ // Skip this dependency if:
+ // 1. the VersionRange is null
+ // 2. It is not transitively pinned and PrivateAssets=All
+ // 3. This child is not a direct package reference and there is already a direct package reference to it
+ if (childLibraryDependency.LibraryRange.VersionRange == null
+ || (!currentDependencyGraphItem.IsCentrallyPinnedTransitivePackage
+ && suppressions!.Contains(childLibraryDependencyIndex))
+ || (!isDirectPackageReferenceFromRootProject && directPackageReferences?.Contains(childLibraryDependencyIndex) == true))
+ {
+ continue;
+ }
+
+ VersionRange? pinnedVersionRange = null;
+
+ // Determine if the package is transitively pinned
+ bool isCentrallyPinnedTransitiveDependency = isCentralPackageTransitivePinningEnabled
+ && isPackage
+ && pinnedPackageVersions?.TryGetValue(childLibraryDependencyIndex, out pinnedVersionRange) == true;
+
+ LibraryRangeIndex childLibraryRangeIndex = chosenResolvedItem.GetRangeIndexForDependencyAt(i);
+
+ if (isCentrallyPinnedTransitiveDependency && !isDirectPackageReferenceFromRootProject)
+ {
+ // If central transitive pinning is enabled the LibraryDependency must be recreated as not to mutate the in-memory copy
+ childLibraryDependency = new LibraryDependency(childLibraryDependency)
+ {
+ LibraryRange = new LibraryRange(childLibraryDependency.LibraryRange) { VersionRange = pinnedVersionRange },
+ };
- VersionRange versionRange = obj.VersionRange ?? VersionRange.None;
+ // Since the version range could have changed, we must also update the LibraryRangeIndex
+ childLibraryRangeIndex = _indexingTable.Index(childLibraryDependency.LibraryRange);
+ }
+
+ // Create a DependencyGraphItem and add it to the queue for processing
+ DependencyGraphItem dependencyGraphItem = new()
+ {
+ LibraryDependency = childLibraryDependency,
+ LibraryDependencyIndex = childLibraryDependencyIndex,
+ LibraryRangeIndex = childLibraryRangeIndex,
+ Path = isCentrallyPinnedTransitiveDependency || isDirectPackageReferenceFromRootProject ? _rootedDependencyPath : DependencyGraphItemIndexer.CreatePathToRef(currentDependencyGraphItemPath, currentLibraryRangeIndex),
+ Parent = currentLibraryRangeIndex,
+ Suppressions = suppressions,
+ IsDirectPackageReferenceFromRootProject = isDirectPackageReferenceFromRootProject,
+ IsCentrallyPinnedTransitivePackage = isCentrallyPinnedTransitiveDependency,
+ RuntimeDependencies = runtimeDependencies,
+ FindLibraryTask = ResolverUtility.FindLibraryCachedAsync(
+ childLibraryDependency.LibraryRange,
+ pair.Framework,
+ runtimeIdentifier: string.IsNullOrWhiteSpace(pair.RuntimeIdentifier) ? null : pair.RuntimeIdentifier,
+ context,
+ token)
+ };
+
+ dependencyGraphItemQueue.Enqueue(dependencyGraphItem);
+ }
+ }
- var combiner = new HashCodeCombiner();
+ return resolvedDependencyGraphItems;
+ }
- combiner.AddObject((int)typeConstraint);
- combiner.AddStringIgnoreCase(obj.Name);
- combiner.AddObject(versionRange);
+ ///
+ /// Attempts to get a runtime graph for the specified target framework.
+ ///
+ /// A containing objects to find local packages in.
+ /// A containing the dependency graphs by target framework.
+ /// The the current target framework and runtime identifier to get a runtime graph for.
+ /// The containing target framework information for the project.
+ /// Receives the for the if one was found, otherwise .
+ /// if a runtime was found, otherwise .
+ private bool TryGetRuntimeGraph(List localRepositories, Dictionary graphsByTargetFramework, FrameworkRuntimePair frameworkRuntimePair, TargetFrameworkInformation? projectTargetFramework, [NotNullWhen(true)] out RuntimeGraph? runtimeGraph)
+ {
+ runtimeGraph = null;
- return combiner.CombinedHash;
+ if (string.IsNullOrEmpty(frameworkRuntimePair.RuntimeIdentifier) || !graphsByTargetFramework.TryGetValue(frameworkRuntimePair.Framework, out RestoreTargetGraph? restoreTargetGraphForTargetFramework))
+ {
+ // If this this pair has no runtime or there is no corresponding RID-less dependency graph for the target framework, there is no runtime graph
+ return false;
}
+
+ // Get the path to the runtime graph for the project and target framework
+ string? runtimeGraphPath = projectTargetFramework?.RuntimeIdentifierGraphPath;
+
+ // Load the runtime graph for the project if one is specified
+ RuntimeGraph? projectProviderRuntimeGraph = string.IsNullOrWhiteSpace(runtimeGraphPath) ? default : ProjectRestoreCommand.GetRuntimeGraph(runtimeGraphPath, _logger);
+
+ // Gets a merged runtime graph for the project and target framework
+ runtimeGraph = ProjectRestoreCommand.GetRuntimeGraph(restoreTargetGraphForTargetFramework, localRepositories, projectRuntimeGraph: projectProviderRuntimeGraph, _logger);
+
+ return true;
}
}
}
diff --git a/src/NuGet.Core/NuGet.DependencyResolver.Core/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.DependencyResolver.Core/PublicAPI.Unshipped.txt
index 7dc5c58110b..536c11e56b3 100644
--- a/src/NuGet.Core/NuGet.DependencyResolver.Core/PublicAPI.Unshipped.txt
+++ b/src/NuGet.Core/NuGet.DependencyResolver.Core/PublicAPI.Unshipped.txt
@@ -1 +1,2 @@
#nullable enable
+NuGet.DependencyResolver.RemoteWalkContext.GetUnresolvedRemoteMatchesAsync() -> System.Threading.Tasks.Task!>!
diff --git a/src/NuGet.Core/NuGet.DependencyResolver.Core/Remote/RemoteWalkContext.cs b/src/NuGet.Core/NuGet.DependencyResolver.Core/Remote/RemoteWalkContext.cs
index 07087329db0..750f2defd74 100644
--- a/src/NuGet.Core/NuGet.DependencyResolver.Core/Remote/RemoteWalkContext.cs
+++ b/src/NuGet.Core/NuGet.DependencyResolver.Core/Remote/RemoteWalkContext.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.LibraryModel;
@@ -92,5 +93,36 @@ public IList FilterDependencyProvidersForLibrary(Libr
}
return RemoteLibraryProviders;
}
+
+ ///
+ /// Returns a list of unresolved remote matches.
+ ///
+ ///
+ /// The is internal but the dependency resolver needs to know what packages were unresolved after walking the dependency graph.
+ ///
+ /// A containing the objects representing unresolved packages.
+ public async Task> GetUnresolvedRemoteMatchesAsync()
+ {
+ HashSet packagesToInstall = new();
+
+ foreach (LibraryRangeCacheKey key in FindLibraryEntryCache.Keys.NoAllocEnumerate())
+ {
+ if (!FindLibraryEntryCache.TryGetValue(key, out Task>? task))
+ {
+ continue;
+ }
+
+ GraphItem item = await task;
+
+ if (item.Key.Type == LibraryType.Unresolved || !RemoteLibraryProviders.Contains(item.Data.Match.Provider))
+ {
+ continue;
+ }
+
+ packagesToInstall.Add(item.Data.Match);
+ }
+
+ return packagesToInstall;
+ }
}
}
diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs
index 3918ad977f3..8666d991ef0 100644
--- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs
+++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs
@@ -13,6 +13,7 @@
using NuGet.Commands.Test;
using NuGet.Common;
using NuGet.Configuration;
+using NuGet.DependencyResolver;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging;
@@ -3560,11 +3561,19 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async(
var restoreGraph = result.RestoreGraphs.ElementAt(0);
Assert.Equal(0, restoreGraph.Unresolved.Count);
//packageA should be installed from source2
- string packageASource = restoreGraph.Install.ElementAt(0).Provider.Source.Name;
- Assert.Equal(packageSource2, packageASource);
- //packageB should be installed from source
- string packageBSource = restoreGraph.Install.ElementAt(1).Provider.Source.Name;
- Assert.Equal(pathContext.PackageSource, packageBSource);
+ restoreGraph.Install.Count.Should().Be(2);
+
+ foreach (RemoteMatch match in restoreGraph.Install)
+ {
+ if (match.Library.Name == packageA)
+ {
+ match.Provider.Source.Name.Should().Be(packageSource2);
+ }
+ else if (match.Library.Name == packageB)
+ {
+ match.Provider.Source.Name.Should().Be(pathContext.PackageSource);
+ }
+ }
}
[Fact]
diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_AlgorithmEquivalencyTests.cs b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_AlgorithmEquivalencyTests.cs
index 3457b6b04c0..60f6462dc2c 100644
--- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_AlgorithmEquivalencyTests.cs
+++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_AlgorithmEquivalencyTests.cs
@@ -103,7 +103,7 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async(
result.RestoreGraphs.Should().HaveCount(1);
result.RestoreGraphs.First().Install.Should().HaveCount(2);
- string.Join(" ", result.RestoreGraphs.First().Install.Select(i => $"{i.Library.Name}/{i.Library.Version}")).Should().Be("a/1.0.0 b/2.0.0");
+ string.Join(" ", result.RestoreGraphs.First().Install.Select(i => $"{i.Library.Name}/{i.Library.Version}").OrderBy(i => i)).Should().Be("a/1.0.0 b/2.0.0");
}
// Project 1 -> X 1.0 -> B 2.0 -> E 1.0
@@ -505,7 +505,6 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async(
// Project 1 -> f 1.0.0 -> a 1.0.0 -> b 1.0.0
// -> c 1.0.0 -> b 2.0.0
// Centrally managed versions f & a, f 1.0.0 and a 1.0.0
-
[Fact]
public async Task RestoreCommand_WithPackageDrivenDowngradeAndTransitivePinning_RespectsDowngrade_AndRaisesWarning()
{