diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs index 1c4d7a30df4..d33878cef46 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/DependencyGraphResolver.cs @@ -104,7 +104,7 @@ public async Task, RuntimeGraph>> Reso // This is guaranteed to be computed before any graph with a RID, so we can assume this will return a value. // PCL Projects with Supports have a runtime graph but no matching framework. - var runtimeGraphPath = projectTargetFramework?.RuntimeIdentifierGraphPath; + var runtimeGraphPath = projectTargetFramework.RuntimeIdentifierGraphPath; RuntimeGraph? projectProviderRuntimeGraph = default; if (runtimeGraphPath != null) @@ -152,6 +152,8 @@ public async Task, RuntimeGraph>> Reso } } + Dictionary? prunedPackageVersions = GetAndIndexPackagesToPrune(libraryDependencyInterningTable, projectTargetFramework); + DependencyGraphItem rootProjectRefItem = new() { LibraryDependency = initialProject, @@ -635,11 +637,22 @@ async static (state) => 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))) { @@ -648,9 +661,6 @@ async static (state) => VersionRange? pinnedVersionRange = null; - bool isPackage = dep.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package); - bool isDirectPackageReferenceFromRootProject = (currentRefRangeIndex == rootProjectRefItem.LibraryRangeIndex) && isPackage; - if (!isDirectPackageReferenceFromRootProject && directPackageReferences?.Contains(depIndex) == true) { continue; @@ -752,10 +762,19 @@ async static (state) => { 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 = findLibraryCachedAsyncResult.GetDependencyIndexForDependency(runtimeDependencyIndex), + LibraryDependencyIndex = libraryDependencyIndex, LibraryRangeIndex = findLibraryCachedAsyncResult.GetRangeIndexForDependency(runtimeDependencyIndex), Path = LibraryRangeInterningTable.CreatePathToRef(pathToCurrentRef, currentRefRangeIndex), Parent = currentRefRangeIndex, @@ -786,6 +805,12 @@ async static (state) => } } } + + // If the latest item was chosen, keep track of the pruned dependency indices. + if (chosenResolvedItems.TryGetValue(currentRefDependencyIndex, out ResolvedDependencyGraphItem? resolvedGraphItem)) + { + resolvedGraphItem.PrunedDependencies = prunedPackageIndices; + } } //Now that we've completed import, figure out the short real flattened list @@ -829,13 +854,10 @@ async static (state) => LibraryRangeIndex[] pathToChosenRef = foundItem.Path; bool directPackageReferenceFromRootProject = foundItem.IsDirectPackageReferenceFromRootProject; List> chosenSuppressions = foundItem.Suppressions; - if (findLibraryEntryCache.TryGetValue(chosenRefRangeIndex, out Task? nodeTask)) { FindLibraryEntryResult node = await nodeTask; - flattenedGraphItems.Add(node.Item); - for (int i = 0; i < node.Item.Data.Dependencies.Count; i++) { var dep = node.Item.Data.Dependencies[i]; @@ -845,6 +867,11 @@ async static (state) => 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 @@ -1034,6 +1061,31 @@ async static (state) => 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); } } @@ -1204,6 +1256,66 @@ async static (state) => 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 (dep.LibraryRange!.VersionRange!.Satisfies(prunableVersion!.MaxVersion!)) + { + if (!isPackage) + { + 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))); + } + } + else if (isDirectPackageReferenceFromRootProject) + { + if (SdkAnalysisLevelMinimums.IsEnabled( + _request.Project!.RestoreMetadata!.SdkAnalysisLevel, + _request.Project.RestoreMetadata.UsingMicrosoftNETSdk, + SdkAnalysisLevelMinimums.PruningWarnings)) + { + _logger.Log(RestoreLogMessage.CreateWarning(NuGetLogCode.NU1510, string.Format(CultureInfo.CurrentCulture, Strings.Error_RestorePruningDirectPackageReference, dep.Name))); + } + } + else + { + _logger.LogDebug(string.Format(CultureInfo.CurrentCulture, Strings.RestoreDebugPruningPackageReference, $"{dep.Name} {dep.LibraryRange.VersionRange.OriginalString}", refItemResult.Item.Key, prunableVersion.MaxVersion)); + return true; + } + } + } + return false; + } + private static bool EvictOnTypeConstraint(LibraryDependencyTarget current, LibraryDependencyTarget previous) { if (current == previous) @@ -1295,6 +1407,8 @@ private class ResolvedDependencyGraphItem public required LibraryRangeIndex[] Path { get; set; } public required List> Suppressions { get; set; } + + public HashSet? PrunedDependencies { get; set; } } internal sealed class LibraryDependencyInterningTable @@ -1334,6 +1448,21 @@ public LibraryDependencyIndex Intern(CentralPackageVersion centralPackageVersion return index; } + + public LibraryDependencyIndex Intern(PrunePackageReference prunePackageReference) + { + lock (_lockObject) + { + string key = prunePackageReference.Name; + if (!_table.TryGetValue(key, out LibraryDependencyIndex index)) + { + index = (LibraryDependencyIndex)_nextIndex++; + _table.TryAdd(key, index); + } + + return index; + } + } } internal sealed class LibraryRangeInterningTable diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/LockFileBuilderCache.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/LockFileBuilderCache.cs index a2e2654171c..4a99afe7620 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/LockFileBuilderCache.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/LockFileBuilderCache.cs @@ -29,7 +29,7 @@ public class LockFileBuilderCache private readonly ConcurrentDictionary, bool)>> _criteriaSets = new(); - private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags), Lazy<(LockFileTargetLibrary, bool)>> _lockFileTargetLibraryCache = + private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount), Lazy<(LockFileTargetLibrary, bool)>> _lockFileTargetLibraryCache = new(); /// @@ -104,7 +104,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka /// /// Try to get a LockFileTargetLibrary from the cache. /// - internal (LockFileTargetLibrary, bool) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, Func<(LockFileTargetLibrary, bool)> valueFactory) + internal (LockFileTargetLibrary, bool) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List dependencies, Func<(LockFileTargetLibrary, bool)> valueFactory) { // Comparing RuntimeGraph for equality is very expensive, // so in case of a request where the RuntimeGraph is not empty we avoid using the cache. @@ -114,7 +114,7 @@ public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPacka localPackageInfo = localPackageInfo ?? throw new ArgumentNullException(nameof(localPackageInfo)); var criteriaKey = new CriteriaKey(graph.TargetGraphName, framework); var packagePath = localPackageInfo.ExpandedPath; - return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags), + return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags, dependencies.Count), key => new Lazy<(LockFileTargetLibrary, bool)>(valueFactory)).Value; } diff --git a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/LockFileUtils.cs b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/LockFileUtils.cs index a1ba4b8e609..8c306c4bc24 100644 --- a/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/LockFileUtils.cs +++ b/src/NuGet.Core/NuGet.Commands/RestoreCommand/Utility/LockFileUtils.cs @@ -69,7 +69,7 @@ internal static (LockFileTargetLibrary, bool) CreateLockFileTargetLibrary( var runtimeIdentifier = targetGraph.RuntimeIdentifier; var framework = targetFrameworkOverride ?? targetGraph.Framework; - return cache.GetLockFileTargetLibrary(targetGraph, framework, package, aliases, dependencyType, + return cache.GetLockFileTargetLibrary(targetGraph, framework, package, aliases, dependencyType, dependencies, () => { LockFileTargetLibrary lockFileLib = null; diff --git a/src/NuGet.Core/NuGet.Commands/Strings.Designer.cs b/src/NuGet.Core/NuGet.Commands/Strings.Designer.cs index 0d2c64b682a..3ba39f756a8 100644 --- a/src/NuGet.Core/NuGet.Commands/Strings.Designer.cs +++ b/src/NuGet.Core/NuGet.Commands/Strings.Designer.cs @@ -798,6 +798,24 @@ internal static string Error_RestoreInLockedMode { } } + /// + /// Looks up a localized string similar to PackageReference {0} will not be pruned. Consider removing this package from your dependencies, as it is likely unnecessary.. + /// + internal static string Error_RestorePruningDirectPackageReference { + get { + return ResourceManager.GetString("Error_RestorePruningDirectPackageReference", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A ProjectReference cannot be pruned, {0}.. + /// + internal static string Error_RestorePruningProjectReference { + get { + return ResourceManager.GetString("Error_RestorePruningProjectReference", resourceCulture); + } + } + /// /// Looks up a localized string similar to The repository service index '{0}' is not a valid HTTPS url.. /// @@ -1879,6 +1897,15 @@ internal static string ResolverRequest_ToStringFormat { } } + /// + /// Looks up a localized string similar to Pruning the package '{0}' as a dependency of '{1}'. The maximum prunable version is '{2}'. + /// + internal static string RestoreDebugPruningPackageReference { + get { + return ResourceManager.GetString("RestoreDebugPruningPackageReference", resourceCulture); + } + } + /// /// Looks up a localized string similar to Certificate file '{0}' not found. For a list of accepted ways to provide a certificate, visit https://docs.nuget.org/docs/reference/command-line-reference. /// diff --git a/src/NuGet.Core/NuGet.Commands/Strings.resx b/src/NuGet.Core/NuGet.Commands/Strings.resx index 2d2c25ed9fc..a56375623d5 100644 --- a/src/NuGet.Core/NuGet.Commands/Strings.resx +++ b/src/NuGet.Core/NuGet.Commands/Strings.resx @@ -1127,4 +1127,16 @@ NuGet requires HTTPS sources. Refer to https://aka.ms/nuget-https-everywhere for Audit source '{0}' did not provide any vulnerability data. {0} is the source name + + Pruning the package '{0}' as a dependency of '{1}'. The maximum prunable version is '{2}' + 0 - package id and version, 1 - version + + + PackageReference {0} will not be pruned. Consider removing this package from your dependencies, as it is likely unnecessary. + 0 - package id and version + + + A ProjectReference cannot be pruned, {0}. + 0 - project reference + \ No newline at end of file diff --git a/src/NuGet.Core/NuGet.Commands/Utility/SdkAnalysisLevelMinimums.cs b/src/NuGet.Core/NuGet.Commands/Utility/SdkAnalysisLevelMinimums.cs index e838adac818..a66dfd70902 100644 --- a/src/NuGet.Core/NuGet.Commands/Utility/SdkAnalysisLevelMinimums.cs +++ b/src/NuGet.Core/NuGet.Commands/Utility/SdkAnalysisLevelMinimums.cs @@ -15,6 +15,11 @@ internal static class SdkAnalysisLevelMinimums /// internal static readonly NuGetVersion HttpErrorSdkAnalysisLevelMinimumValue = new("9.0.100"); + /// + /// Minimum SDK Analysis Level required for warning for packages and projects that cannot be pruned. + /// + internal static readonly NuGetVersion PruningWarnings = new("10.0.100"); + /// /// Determines whether the feature is enabled based on the SDK analysis level. /// diff --git a/src/NuGet.Core/NuGet.Common/Errors/NuGetLogCode.cs b/src/NuGet.Core/NuGet.Common/Errors/NuGetLogCode.cs index 560adfc802e..b03bbaf25d1 100644 --- a/src/NuGet.Core/NuGet.Common/Errors/NuGetLogCode.cs +++ b/src/NuGet.Core/NuGet.Common/Errors/NuGetLogCode.cs @@ -297,6 +297,16 @@ public enum NuGetLogCode /// NU1509 = 1509, + /// + /// Direct reference to a package that will not be pruned. + /// + NU1510 = 1510, + + /// + /// Project references cannot be pruned + /// + NU1511 = 1511, + /// /// Dependency bumped up /// diff --git a/src/NuGet.Core/NuGet.Common/PublicAPI.Unshipped.txt b/src/NuGet.Core/NuGet.Common/PublicAPI.Unshipped.txt index 85a8c91febf..838b379d7b7 100644 --- a/src/NuGet.Core/NuGet.Common/PublicAPI.Unshipped.txt +++ b/src/NuGet.Core/NuGet.Common/PublicAPI.Unshipped.txt @@ -1,3 +1,5 @@ #nullable enable NuGet.Common.NuGetLogCode.NU1509 = 1509 -> NuGet.Common.NuGetLogCode +NuGet.Common.NuGetLogCode.NU1510 = 1510 -> NuGet.Common.NuGetLogCode +NuGet.Common.NuGetLogCode.NU1511 = 1511 -> NuGet.Common.NuGetLogCode static NuGet.Common.MSBuildStringUtility.GetNuGetLogCodes(string! s) -> System.Collections.Immutable.ImmutableArray diff --git a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRestoreTests.cs b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRestoreTests.cs index bea18580957..b9ff10996b1 100644 --- a/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRestoreTests.cs +++ b/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRestoreTests.cs @@ -267,7 +267,6 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } } -#if IS_SIGNING_SUPPORTED [PlatformFact(Platform.Windows, Platform.Linux)] public async Task DotnetRestore_WithUnSignedPackageAndSignatureValidationModeAsRequired_FailsAsync() { @@ -736,7 +735,6 @@ public void DotnetRestore_WithAuthorSignedPackageAndSignatureValidationModeAsReq _dotnetFixture.RestoreProjectExpectSuccess(workingDirectory, projectName, testOutputHelper: _testOutputHelper); } } -#endif //IS_SIGNING_SUPPORTED [PlatformTheory(Platform.Windows)] [InlineData(true)] @@ -1307,7 +1305,6 @@ public async Task DotnetRestore_MultiTargettingWithAliases_Succeeds() } } -#if NET5_0_OR_GREATER [Fact] public async Task DotnetRestore_WithTargetFrameworksProperty_StaticGraphAndRegularRestore_AreEquivalent() { @@ -1385,7 +1382,6 @@ public void GenerateRestoreGraphFile_StandardAndStaticGraphRestore_AreEquivalent regularDgSpec.Should().BeEquivalentTo(staticGraphDgSpec); } } -#endif [Theory] [InlineData("netcoreapp3.0;net7.0;net472", true)] @@ -2799,6 +2795,69 @@ private string GetProjectFileForRestoreTaskOutputTests(string targetFramework) return csprojContents; } + /// ClassLibrary1 -> X 1.0.0 -> Y 1.0.0 + /// Prune Y 2.0.0 + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DotnetRestore_WithConditionalPrunedPackageReference_Succeeds(bool isStaticGraphRestore) + { + using SimpleTestPathContext pathContext = _dotnetFixture.CreateSimpleTestPathContext(); + var projectName = "ClassLibrary1"; + var workingDirectory = Path.Combine(pathContext.SolutionRoot, projectName); + var projectFile = Path.Combine(workingDirectory, $"{projectName}.csproj"); + string tfm = Constants.DefaultTargetFramework.GetShortFolderName(); + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, new SimpleTestPackageContext("X", "1.0.0") { Dependencies = [new SimpleTestPackageContext("Y", "1.0.0")] }); + + _dotnetFixture.CreateDotnetNewProject(pathContext.SolutionRoot, projectName, "classlib -f netstandard2.1", testOutputHelper: _testOutputHelper); + + using (var stream = File.Open(projectFile, FileMode.Open, FileAccess.ReadWrite)) + { + var xml = XDocument.Load(stream); + ProjectFileUtils.SetTargetFrameworkForProject(xml, "TargetFrameworks", $"netstandard2.1;{tfm}"); + ProjectFileUtils.AddProperty(xml, "RestoreEnablePackagePruning", "true"); + + ProjectFileUtils.AddItem( + xml, + "PackageReference", + "X", + string.Empty, + [], + new Dictionary() { { "Version", "1.0.0" } }); + + ProjectFileUtils.AddItem( + xml, + "PrunePackageReference", + "Y", + string.Empty, + [], + new Dictionary() { { "Version", "2.0.0" } }); + + ProjectFileUtils.AddProperty(xml, "RestoreEnablePackagePruning", "false", "'$(TargetFramework)' == 'netstandard2.1'"); + + ProjectFileUtils.WriteXmlToFile(xml, stream); + } + + var result = _dotnetFixture.RunDotnetExpectSuccess(workingDirectory, $"restore {projectFile}" + (isStaticGraphRestore ? " /p:RestoreUseStaticGraphEvaluation=true" : string.Empty), testOutputHelper: _testOutputHelper); + result.AllOutput.Should().NotContain("Warning"); + string assetsFilePath = Path.Combine(workingDirectory, "obj", LockFileFormat.AssetsFileName); + LockFile assetsFile = new LockFileFormat().Read(assetsFilePath); + assetsFile.Targets.Should().HaveCount(2); + assetsFile.PackageSpec.TargetFrameworks.Should().HaveCount(2); + assetsFile.PackageSpec.TargetFrameworks[0].TargetAlias.Should().Be(tfm); + assetsFile.PackageSpec.TargetFrameworks[0].PackagesToPrune.Should().NotBeEmpty(); + assetsFile.PackageSpec.TargetFrameworks[1].TargetAlias.Should().Be("netstandard2.1"); + assetsFile.PackageSpec.TargetFrameworks[1].PackagesToPrune.Should().BeEmpty(); + + // netstandard2.1 + assetsFile.Targets[0].TargetFramework.Should().Be(assetsFile.PackageSpec.TargetFrameworks[1].FrameworkName); + assetsFile.Targets[0].Libraries.Should().Contain(e => e.Name.Equals("X")); + assetsFile.Targets[0].Libraries.Should().Contain(e => e.Name.Equals("Y")); + //net9.0 + assetsFile.Targets[1].Libraries.Should().Contain(e => e.Name.Equals("X")); + assetsFile.Targets[1].Libraries.Should().NotContain(e => e.Name.Equals("Y")); + } + private void AssertRelatedProperty(IList items, string path, string related) { var item = items.Single(i => i.Path.Equals(path)); diff --git a/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildRestoreTaskTests.cs b/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildRestoreTaskTests.cs index 4e2d2f1a86c..c6f706ad4cd 100644 --- a/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildRestoreTaskTests.cs +++ b/test/NuGet.Core.FuncTests/Msbuild.Integration.Test/MsbuildRestoreTaskTests.cs @@ -2012,5 +2012,69 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( kvp.Value.Name.Should().Be("y"); kvp.Value.VersionRange.Should().Be(VersionRange.Parse("(,2.0.0]")); } + + // A -> X 1.0.0 -> Y 1.0.0 + // Prune: Y 2.0.0 + [PlatformTheory(Platform.Windows)] + [InlineData(true)] + [InlineData(false)] + public async Task MsbuildRestore_WithPackagesToPrune_PrunesCorrectly(bool isStaticGraphRestore) + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + // Set up solution, project, and packages + var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot); + + var project = SimpleTestProjectContext.CreateLegacyPackageReference( + "a", + pathContext.SolutionRoot, + NuGetFramework.Parse("net472")); + + var packageX = new SimpleTestPackageContext() + { + Id = "x", + Version = "1.0.0", + Dependencies = [new SimpleTestPackageContext("y", "1.0.0")] + }; + + project.AddPackageToAllFrameworks(packageX); + solution.Projects.Add(project); + solution.Create(pathContext.SolutionRoot); + + File.WriteAllText( + Path.Combine(pathContext.SolutionRoot, "Directory.Build.props"), + @$" + + true + + + + + "); + + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + packageX); + + // Act + CommandRunnerResult result = _msbuildFixture.RunMsBuild(pathContext.WorkingDirectory, $"/t:restore {project.ProjectPath} " + + (isStaticGraphRestore ? " /p:RestoreUseStaticGraphEvaluation=true" : string.Empty), + ignoreExitCode: true, + testOutputHelper: _testOutputHelper); + + // Assert + result.Success.Should().BeTrue(because: result.AllOutput); + project.AssetsFile.Targets.Select(e => e.TargetFramework).Distinct().Should().HaveCount(1); + project.AssetsFile.Targets[0].Libraries.Should().HaveCount(1); + project.AssetsFile.Targets[0].Libraries[0].Name.Should().Be("x"); + project.AssetsFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + project.AssetsFile.PackageSpec.TargetFrameworks.Should().HaveCount(1); + project.AssetsFile.PackageSpec.TargetFrameworks[0].PackagesToPrune.Should().HaveCount(1); + var kvp = project.AssetsFile.PackageSpec.TargetFrameworks[0].PackagesToPrune.First(); + kvp.Key.Should().Be("y"); + kvp.Value.Name.Should().Be("y"); + kvp.Value.VersionRange.Should().Be(VersionRange.Parse("(,2.0.0]")); + } } } diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs index ad713d0bd6d..3918ad977f3 100644 --- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommandTests.cs @@ -4276,6 +4276,987 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( logger.Errors.Should().Be(0, because: logger.ShowErrors()); logger.Warnings.Should().Be(1, because: logger.ShowWarnings()); } + + // P -> A 1.0.0 -> B 1.0.0 + // Prune B 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesTransitivesDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + } + } + }"; + + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + + // P -> A 1.0.0 -> B 1.0.0 + // -> C 1.0.0 + // -> D 1.0.0 + // Prune C 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferencesAndManyDependencies_PrunesTransitiveDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("A", "1.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("B", "1.0.0"), + new SimpleTestPackageContext("C", "1.0.0"), + new SimpleTestPackageContext("D", "1.0.0") + ] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""A"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""C"" : ""(,1.0.0]"" + } + } + } + }"; + + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(3); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("A"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("B"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[0].Libraries[2].Name.Should().Be("D"); + result.LockFile.Targets[0].Libraries[2].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[2].Dependencies.Should().BeEmpty(); + + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(3); + } + + // P -> A 1.0.0 -> B 1.0.0 + // P -> C 1.0.0 + // Prune C 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_DoesNotPruneDirectDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("A", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("B", "1.0.0")] + }; + var packageC = new SimpleTestPackageContext("C", "1.0.0"); + + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA, + packageC); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""A"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + ""C"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""C"" : ""(,1.0.0]"" + } + } + } + }"; + + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(3); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("A"); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("B"); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().HaveCount(0); + result.LockFile.Targets[0].Libraries[2].Name.Should().Be("C"); + result.LockFile.Targets[0].Libraries[2].Dependencies.Should().HaveCount(0); + result.LockFile.LogMessages.Should().HaveCount(1); + result.LockFile.LogMessages[0].Code.Should().Be(NuGetLogCode.NU1510); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(3); + } + + // P -> P2 -> B 1.0.0 -> C 1.0.0 + // P -> A 1.0.0 + // Prune B 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesTransitivesDependenciesThroughProjects_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0"); + var packageB = new SimpleTestPackageContext("packageB", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageC", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA, + packageB); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + } + } + }"; + var leafProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageB"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + var projectSpec2 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project2", pathContext.SolutionRoot, leafProject); + projectSpec = projectSpec.WithTestProjectReference(projectSpec2); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec, projectSpec2); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("Project2"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + + // P -> A 1.0.0 -> B 1.0.0 + // -> C 1.0.0 + // -> D 1.0.0 -> E 2.0.0 + // -> F 1.0.0 -> G 2.0.0 + // P -> F 2.0.0 -> G 3.0.0 + // P -> H 1.0.0 -> B 2.0.0 + // + // Prune C 1.0.0, D 1.0.0, G 2.0.0, B 1.5.0 + // Leaves: A 1.0.0, B 2.0.0, F 2.0.0, G 3.0.0, H 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesManyDependenciesFromSinglePackage_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + new SimpleTestPackageContext("A", "1.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("B", "1.0.0"), + new SimpleTestPackageContext("C", "1.0.0"), + new SimpleTestPackageContext("D", "1.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("E", "2.0.0"), + ] + }, + new SimpleTestPackageContext("F", "1.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("G", "2.0.0"), + ] + } + ] + }, + new SimpleTestPackageContext("F", "2.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("G", "3.0.0"), + ] + }, + new SimpleTestPackageContext("H", "1.0.0") + { + Dependencies = [ + new SimpleTestPackageContext("B", "2.0.0"), + ] + }); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""A"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + ""F"": { + ""version"": ""[2.0.0,)"", + ""target"": ""Package"", + }, + ""H"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""C"" : ""(,1.0.0]"", + ""D"" : ""(,1.0.0]"", + ""G"" : ""(,2.0.0]"", + ""B"" : ""(,1.5.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(5); + + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("A"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[0].Dependencies[0].Id.Should().Be("F"); + + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("B"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("2.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + + result.LockFile.Targets[0].Libraries[2].Name.Should().Be("F"); + result.LockFile.Targets[0].Libraries[2].Version.Should().Be(new NuGetVersion("2.0.0")); + result.LockFile.Targets[0].Libraries[2].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[2].Dependencies[0].Id.Should().Be("G"); + + result.LockFile.Targets[0].Libraries[3].Name.Should().Be("G"); + result.LockFile.Targets[0].Libraries[3].Version.Should().Be(new NuGetVersion("3.0.0")); + result.LockFile.Targets[0].Libraries[3].Dependencies.Should().BeEmpty(); + + result.LockFile.Targets[0].Libraries[4].Name.Should().Be("H"); + result.LockFile.Targets[0].Libraries[4].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[4].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[4].Dependencies[0].Id.Should().Be("B"); + } + + // P -> A 1.0.0 -> B 1.0.0 + // Prune B 1.0.0 + [Fact] + public async Task RestoreCommand_WithMultiTargetedPrunePackageReferences_PrunesTransitivesDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + }, + ""net48"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + } + } + } + }"; + + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[1].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[1].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[1].Name.Should().Be("packageB"); + result.LockFile.Targets[1].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[1].Dependencies.Should().BeEmpty(); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(2); + } + + // P1 -> A 1.0.0 -> B 1.0.0 + // P1 -> P2 (project) + // Prune B 1.0.0 + [Theory] + [InlineData("10.0.100", true, true)] + [InlineData("9.0.100", true, false)] + [InlineData("", false, true)] + public async Task RestoreCommand_WithDirectProjectReferenceSpecifiedForPruning_SkipsPruning(string sdkAnalysisLevel, bool usingMicrosoftNETSdk, bool shouldWarn) + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"", + ""Project2"" : ""(,3.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + projectSpec.RestoreMetadata.SdkAnalysisLevel = !string.IsNullOrEmpty(sdkAnalysisLevel) ? NuGetVersion.Parse(sdkAnalysisLevel) : null; + projectSpec.RestoreMetadata.UsingMicrosoftNETSdk = usingMicrosoftNETSdk; + var projectSpec2 = ProjectTestHelpers.GetPackageSpec("Project2", framework: "net472"); + + projectSpec = projectSpec.WithTestProjectReference(projectSpec2); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec, projectSpec2); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("Project2"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + if (shouldWarn) + { + result.LockFile.LogMessages.Should().HaveCount(1); + result.LockFile.LogMessages[0].Code.Should().Be(NuGetLogCode.NU1511); + } + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + + // P1 -> A 1.0.0 -> B 1.0.0 + // P1 -> P2 (project) -> P3 (project) + // Prune B 1.0.0 + [Theory] + [InlineData("10.0.100", true, true)] + [InlineData("9.0.100", true, false)] + [InlineData("", false, true)] + public async Task RestoreCommand_WithTransitiveProjectReferenceSpecifiedForPruning_SkipsPruning_AndVerifiesEquivalency(string sdkAnalysisLevel, bool usingMicrosoftNETSdk, bool shouldWarn) + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"", + ""Project3"" : ""(,3.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + projectSpec.RestoreMetadata.SdkAnalysisLevel = !string.IsNullOrEmpty(sdkAnalysisLevel) ? NuGetVersion.Parse(sdkAnalysisLevel) : null; + projectSpec.RestoreMetadata.UsingMicrosoftNETSdk = usingMicrosoftNETSdk; + var projectSpec2 = ProjectTestHelpers.GetPackageSpec("Project2", framework: "net472"); + var projectSpec3 = ProjectTestHelpers.GetPackageSpec("Project3", framework: "net472"); + + projectSpec2 = projectSpec2.WithTestProjectReference(projectSpec3); + projectSpec = projectSpec.WithTestProjectReference(projectSpec2); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec, projectSpec2, projectSpec3); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(3); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("Project2"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[2].Name.Should().Be("Project3"); + result.LockFile.Targets[0].Libraries[2].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[2].Dependencies.Should().BeEmpty(); + if (shouldWarn) + { + result.LockFile.LogMessages.Should().HaveCount(1); + result.LockFile.LogMessages[0].Code.Should().Be(NuGetLogCode.NU1511); + } + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + + // P -> A 1.0.0 -> B 1.0.0 + // Prune B 1.0.0 + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_AndMissingVersion_PrunesTransitivesDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + var packageB200 = new SimpleTestPackageContext("packageB", "2.0.0"); + + await SimpleTestPackageUtility.CreatePackagesWithoutDependenciesAsync( + pathContext.PackageSource, + packageA, + packageB200); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + }, + ""net48"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[1].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[1].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[1].Name.Should().Be("packageB"); + result.LockFile.Targets[1].Libraries[1].Version.Should().Be(new NuGetVersion("2.0.0")); + result.LockFile.Targets[1].Libraries[1].Dependencies.Should().BeEmpty(); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(2); + } + + // P -> A 1.0.0 -> B (, 2.0.0] + // Prune B 1.0.0 + // It prunes B 1.0.0, because the missing lower bound means min version. + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_AndMissingLowerBoundVersion_PrunesTransitivesDependencies_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "(, 2.0.0]")] + }; + var packageB = new SimpleTestPackageContext("packageB", "1.0.0"); + + await SimpleTestPackageUtility.CreatePackagesWithoutDependenciesAsync( + pathContext.PackageSource, + packageA, + packageB); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + } + } + }"; + + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + + // P -> A 1.0.0 -> B 1.0.0 + // -> (win) runtime.a 1.0.0 + // -> (win) runtime.a.win 1.0.0 + // Prune runtime.A 1.0.0 + // Leaves B and runtime.a.win + + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesRuntimeDependencies_AndVerifiesEquivalency() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + new SimpleTestPackageContext("a", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("b", "1.0.0")], + RuntimeJson = @"{ + ""runtimes"": { + ""win"": { + ""a"": { + ""runtime.a"": ""1.0.0"", + ""runtime.a.win"": ""1.0.0"" + } + } + } + }" + }, + new SimpleTestPackageContext("runtime.a", "1.0.0"), + new SimpleTestPackageContext("runtime.a.win", "1.0.0")); + var rootProject = @" + { + ""runtimes"": { + ""win"": {} + }, + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""a"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""runtime.A"" : ""(,1.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.Success.Should().BeTrue(because: string.Join(Environment.NewLine, result.LogMessages.Select(e => e.Message))); + result.LockFile.Targets.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries.Should().HaveCount(2); + result.LockFile.Targets[1].Libraries.Should().HaveCount(3); + + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[1].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[0].Dependencies.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("b"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[1].Libraries[1].Name.Should().Be("b"); + result.LockFile.Targets[1].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[1].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[1].Libraries[2].Name.Should().Be("runtime.a.win"); + result.LockFile.Targets[1].Libraries[2].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[2].Dependencies.Should().BeEmpty(); + + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(3); + } + + // P -> A 1.0.0 -> (win) runtime.a 1.0.0 -> runtime.a.core 1.0.0 + // -> runtime.a.extensions 1.0.0 + // Prune runtime.a.core 1.0.0 + // Leaves A, runtime.a and runtime.a.extensions + + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesTransitiveRuntimeDependencies_AndVerifiesEquivalency() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + new SimpleTestPackageContext("a", "1.0.0") + { + RuntimeJson = @"{ + ""runtimes"": { + ""win"": { + ""a"": { + ""runtime.a"": ""1.0.0"", + } + } + } + }" + }, + new SimpleTestPackageContext("runtime.a", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("runtime.a.core", "1.0.0"), new SimpleTestPackageContext("runtime.a.extensions", "1.0.0")] + } + ); + var rootProject = @" + { + ""runtimes"": { + ""win"": {} + }, + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""a"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""runtime.a.core"" : ""(,1.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.Success.Should().BeTrue(because: string.Join(Environment.NewLine, result.LogMessages.Select(e => e.Message))); + result.LockFile.Targets.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries.Should().HaveCount(3); + + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + + result.LockFile.Targets[1].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[1].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[1].Name.Should().Be("runtime.a"); + result.LockFile.Targets[1].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[1].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[2].Name.Should().Be("runtime.a.extensions"); + result.LockFile.Targets[1].Libraries[2].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[2].Dependencies.Should().BeEmpty(); + + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(3); + } + + // P -> A 1.0.0 -> B 1.0.0 + // -> (win) runtime.a 1.0.0 + // -> (win) runtime.a.win 1.0.0 + // Prune runtime.A 1.0.0, B 1.0.0 + // Leaves only runtime.a.win + [Fact] + public async Task RestoreCommand_WithPrunePackageReferences_PrunesBothTypesOfDependencies_AndVerifiesEquivalency() + { + // Arrange + using var pathContext = new SimpleTestPathContext(); + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + new SimpleTestPackageContext("a", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("b", "1.0.0")], + RuntimeJson = @"{ + ""runtimes"": { + ""win"": { + ""a"": { + ""runtime.a"": ""1.0.0"", + ""runtime.a.win"": ""1.0.0"" + } + } + } + }" + }, + new SimpleTestPackageContext("runtime.a", "1.0.0"), + new SimpleTestPackageContext("runtime.a.win", "1.0.0")); + var rootProject = @" + { + ""runtimes"": { + ""win"": {} + }, + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""a"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""runtime.A"" : ""(,1.0.0]"", + ""B"" : ""(,3.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec); + result.Success.Should().BeTrue(because: string.Join(Environment.NewLine, result.LogMessages.Select(e => e.Message))); + result.LockFile.Targets.Should().HaveCount(2); + result.LockFile.Targets[0].Libraries.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries.Should().HaveCount(2); + + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[1].Libraries[0].Name.Should().Be("a"); + result.LockFile.Targets[1].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[0].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[1].Libraries[1].Name.Should().Be("runtime.a.win"); + result.LockFile.Targets[1].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[1].Libraries[1].Dependencies.Should().BeEmpty(); + + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(2); + } + + // P1 -> A 1.0.0 -> ProjectAndPackage 1.0.0 + // P1 -> P2 (project) -> ProjectAndPackage (project) + [Fact] + public async Task RestoreCommand_WithTransitiveProjectReferenceSpecifiedForPruningCoalescingWithPackageReference_SkipsPruning_AndVerifiesEquivalency() + { + using var pathContext = new SimpleTestPathContext(); + + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("ProjectAndPackage", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var rootProject = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""ProjectAndPackage"" : ""(,3.0.0]"" + } + } + } + }"; + + // Setup project + var projectSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, rootProject); + projectSpec.RestoreMetadata.SdkAnalysisLevel = !string.IsNullOrEmpty("10.0.100") ? NuGetVersion.Parse("10.0.100") : null; + projectSpec.RestoreMetadata.UsingMicrosoftNETSdk = true; + var projectSpec2 = ProjectTestHelpers.GetPackageSpec("Project2", framework: "net472"); + var projectSpec3 = ProjectTestHelpers.GetPackageSpec("ProjectAndPackage", framework: "net472"); + + projectSpec2 = projectSpec2.WithTestProjectReference(projectSpec3); + projectSpec = projectSpec.WithTestProjectReference(projectSpec2); + + // Act & Assert + var result = await RunRestoreAsync(pathContext, projectSpec, projectSpec2, projectSpec3); + result.LockFile.Targets.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries.Should().HaveCount(3); + result.LockFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + result.LockFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + result.LockFile.Targets[0].Libraries[1].Name.Should().Be("Project2"); + result.LockFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[1].Dependencies.Should().HaveCount(1); + result.LockFile.Targets[0].Libraries[2].Name.Should().Be("ProjectAndPackage"); + result.LockFile.Targets[0].Libraries[2].Version.Should().Be(new NuGetVersion("1.0.0")); + result.LockFile.Targets[0].Libraries[2].Dependencies.Should().BeEmpty(); + + result.LockFile.LogMessages.Should().HaveCount(1); + result.LockFile.LogMessages[0].Code.Should().Be(NuGetLogCode.NU1511); + + ISet installedPackages = result.GetAllInstalled(); + installedPackages.Should().HaveCount(1); + } + private static void CreateFakeProjectFile(PackageSpec project2spec) { Directory.CreateDirectory(Path.GetDirectoryName(project2spec.RestoreMetadata.ProjectUniqueName)); diff --git a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreRunnerTests.cs b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreRunnerTests.cs index 8252c718c31..d71a0eb99de 100644 --- a/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreRunnerTests.cs +++ b/test/NuGet.Core.Tests/NuGet.Commands.Test/RestoreRunnerTests.cs @@ -6,14 +6,17 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using FluentAssertions; using NuGet.Common; using NuGet.Configuration; using NuGet.Frameworks; using NuGet.LibraryModel; +using NuGet.Packaging; using NuGet.ProjectModel; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Test.Utility; +using NuGet.Versioning; using Xunit; namespace NuGet.Commands.Test @@ -1993,5 +1996,106 @@ await SimpleTestPackageUtility.CreateFolderFeedV3Async( } } } + + [Fact] + public async Task RestoreRunner_WithMultipleProjects_AndPackagePruningOnOnlyOne_PrunesCorrectly() + { + using var pathContext = new SimpleTestPathContext(); + + // Arrange + // Setup packages + var packageA = new SimpleTestPackageContext("packageA", "1.0.0") + { + Dependencies = [new SimpleTestPackageContext("packageB", "1.0.0")] + }; + + await SimpleTestPackageUtility.CreateFolderFeedV3Async( + pathContext.PackageSource, + PackageSaveMode.Defaultv3, + packageA); + + var p1 = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + }, + ""packagesToPrune"": { + ""packageB"" : ""(,1.0.0]"" + } + } + } + }"; + + var p2 = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""packageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + }, + } + } + } + }"; + + // Setup project + var projectSpec1 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, p1).WithSettingsBasedRestoreMetadata(Settings.LoadDefaultSettings(pathContext.SolutionRoot)); + var projectSpec2 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project2", pathContext.SolutionRoot, p2).WithSettingsBasedRestoreMetadata(Settings.LoadDefaultSettings(pathContext.SolutionRoot)); + + // set up the dg spec. + var dgFile = new DependencyGraphSpec(); + dgFile.AddProject(projectSpec1); + dgFile.AddProject(projectSpec2); + dgFile.AddRestore(projectSpec1.RestoreMetadata.ProjectUniqueName); + dgFile.AddRestore(projectSpec2.RestoreMetadata.ProjectUniqueName); + + var logger = new TestLogger(); + using var cacheContext = new SourceCacheContext(); + + var settings = Settings.LoadDefaultSettings(pathContext.SolutionRoot); + + var restoreContext = new RestoreArgs() + { + CacheContext = cacheContext, + DisableParallel = true, + Log = logger, + GlobalPackagesFolder = pathContext.UserPackagesFolder, + CachingSourceProvider = new CachingSourceProvider(new TestPackageSourceProvider([new PackageSource(pathContext.PackageSource)])), + PreLoadedRequestProviders = new List() + { + new DependencyGraphSpecRequestProvider(new RestoreCommandProvidersCache(), dgFile) + } + }; + + // Act + var summaries = await RestoreRunner.RunAsync(restoreContext); + Assert.True(summaries.All(e => e.Success), string.Join(Environment.NewLine, logger.Messages)); + + var lockFormat = new LockFileFormat(); + var p1AssetsFile = lockFormat.Read(Path.Combine(projectSpec1.RestoreMetadata.OutputPath, LockFileFormat.AssetsFileName)); + var p2AssetsFile = lockFormat.Read(Path.Combine(projectSpec2.RestoreMetadata.OutputPath, LockFileFormat.AssetsFileName)); + + p1AssetsFile.Targets.Should().HaveCount(1); + p1AssetsFile.Targets[0].Libraries.Should().HaveCount(1); + p1AssetsFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + p1AssetsFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + p1AssetsFile.Targets[0].Libraries[0].Dependencies.Should().BeEmpty(); + + p2AssetsFile.Targets.Should().HaveCount(1); + p2AssetsFile.Targets[0].Libraries.Should().HaveCount(2); + p2AssetsFile.Targets[0].Libraries[0].Name.Should().Be("packageA"); + p2AssetsFile.Targets[0].Libraries[0].Version.Should().Be(new NuGetVersion("1.0.0")); + p2AssetsFile.Targets[0].Libraries[0].Dependencies.Should().HaveCount(1); + p2AssetsFile.Targets[0].Libraries[1].Name.Should().Be("packageB"); + p2AssetsFile.Targets[0].Libraries[1].Version.Should().Be(new NuGetVersion("1.0.0")); + p2AssetsFile.Targets[0].Libraries[1].Dependencies.Should().BeEmpty(); + } } }