From d09bc1efa1b95c321f7a58f5ce6b30b51bf6a0c4 Mon Sep 17 00:00:00 2001 From: Nadia Bugarin Date: Tue, 13 Aug 2024 07:32:00 -0700 Subject: [PATCH] feature-intern-ResolveDependencyConflictsNew to main (#10343) * Adding NugetDependencySolver Experiment * TEMP redirecting smoke tests branch * Fixed formatting --------- Co-authored-by: Nadia Bugarin --- .dockerignore | 2 + .github/workflows/smoke.yml | 2 +- .gitignore | 1 + nuget/.dockerignore | 2 + .../EntryPointTests.Update.cs | 1 + .../Update/UpdateWorkerTests.Sdk.cs | 75 +- .../Utilities/MSBuildHelperTests.cs | 786 +++++++++++++++++- .../Analyze/CompatabilityChecker.cs | 29 +- .../Updater/SdkPackageUpdater.cs | 19 +- .../Utilities/DependencyConflictResolver.cs | 689 +++++++++++++++ .../Utilities/MSBuildHelper.cs | 196 ++++- nuget/lib/dependabot/nuget/native_helpers.rb | 7 +- 12 files changed, 1772 insertions(+), 37 deletions(-) create mode 100644 nuget/.dockerignore create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs diff --git a/.dockerignore b/.dockerignore index 67aeed3d5e..339cc526fb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,3 +25,5 @@ git.store Dockerfile* *.md CODEOWNERS +**/.vs +**/NuGetUpdater/artifacts diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 92affbf433..60995d722c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ concurrency: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SMOKE_TEST_BRANCH: main + SMOKE_TEST_BRANCH: feature-DependencySolver jobs: discover: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6cfd98c4b8..51ec621923 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ coverage/ # Ignore spoom coverage report spoom_data/ spoom_report.html +.vs/ diff --git a/nuget/.dockerignore b/nuget/.dockerignore new file mode 100644 index 0000000000..6c023a5a73 --- /dev/null +++ b/nuget/.dockerignore @@ -0,0 +1,2 @@ +**/.vs +**/NuGetUpdater/artifacts \ No newline at end of file diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index a9a5f6c958..b2b981bf43 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -407,6 +407,7 @@ private static async Task Run(Func getArgs, (string Path, stri try { await MockNuGetPackagesInDirectory(packages, path); + var args = getArgs(path); var result = await Program.Main(args); if (result != 0) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs index 7bb17cff7a..92f1b9f7cd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs @@ -52,10 +52,13 @@ await TestUpdateForProject("Some.Package", "9.0.1", "13.0.1", ); } - [Fact] - public async Task UpdateVersionChildElement_InProjectFile_ForPackageReferenceInclude() + [Theory] + [InlineData("true")] + [InlineData(null)] + public async Task UpdateVersionChildElement_InProjectFile_ForPackageReferenceIncludeTheory(string variableValue) { // update Some.Package from 9.0.1 to 13.0.1 + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", variableValue)]); await TestUpdateForProject("Some.Package", "9.0.1", "13.0.1", packages: [ @@ -91,6 +94,43 @@ await TestUpdateForProject("Some.Package", "9.0.1", "13.0.1", ); } + [Fact] + public async Task CallingResolveDependencyConflictsNew() + { + // update Microsoft.CodeAnalysis.Common from 4.9.2 to 4.10.0 + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", "true")]); + await TestUpdateForProject("Microsoft.CodeAnalysis.Common", "4.9.2", "4.10.0", + // initial + projectContents: $""" + + + net8.0 + + + + + + + + + """, + // expected + expectedProjectContents: $""" + + + net8.0 + + + + + + + + + """ + ); + } + [Fact] public async Task UpdateVersions_InProjectFile_ForDuplicatePackageReferenceInclude() { @@ -489,9 +529,12 @@ await TestUpdateForProject("Some.Package", "9.0.1", "13.0.1", ); } - [Fact] - public async Task AddPackageReference_InProjectFile_ForTransientDependency() + [Theory] + [InlineData(null)] + [InlineData("true")] + public async Task AddPackageReference_InProjectFile_ForTransientDependency(string variableValue) { + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", variableValue)]); // add transient package Some.Transient.Dependency from 5.0.1 to 5.0.2 await TestUpdateForProject("Some.Transient.Dependency", "5.0.1", "5.0.2", isTransitive: true, packages: @@ -2862,9 +2905,13 @@ await TestUpdateForProject("Some.Package", "12.0.1", "13.0.1", ); } - [Fact] - public async Task NoChange_IfThereAreIncoherentVersions() + [Theory] + [InlineData("true")] + [InlineData(null)] + public async Task NoChange_IfThereAreIncoherentVersions(string variableValue) { + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", variableValue)]); + // trying to update `Transitive.Dependency` to 1.1.0 would normally pull `Some.Package` from 1.0.0 to 1.1.0, // but the TFM doesn't allow it await TestNoChangeforProject("Transitive.Dependency", "1.0.0", "1.1.0", @@ -2948,9 +2995,13 @@ await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1", ); } - [Fact] - public async Task UnresolvablePropertyDoesNotStopOtherUpdates() + [Theory] + [InlineData("true")] + [InlineData(null)] + public async Task UnresolvablePropertyDoesNotStopOtherUpdates(string variableValue) { + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", variableValue)]); + // the property `$(SomeUnresolvableProperty)` cannot be resolved await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1", packages: @@ -2984,9 +3035,13 @@ await TestUpdateForProject("Some.Package", "7.0.1", "13.0.1", ); } - [Fact] - public async Task UpdatingPackageAlsoUpdatesAnythingWithADependencyOnTheUpdatedPackage() + [Theory] + [InlineData("true")] + [InlineData(null)] + public async Task UpdatingPackageAlsoUpdatesAnythingWithADependencyOnTheUpdatedPackage(string variableValue) { + using var env = new TemporaryEnvironment([("UseNewNugetPackageResolver", variableValue)]); + // updating Some.Package from 3.3.30 requires that Some.Package.Extensions also be updated await TestUpdateForProject("Some.Package", "3.3.30", "3.4.3", packages: diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 10c3c7a2c6..4bbaf65df0 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -479,7 +479,11 @@ await File.WriteAllTextAsync(projectPath, """ new Dependency("Some.Package", "1.2.0", DependencyType.PackageReference), new Dependency("Some.Other.Package", "1.0.0", DependencyType.PackageReference), }; - var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRoot.FullName, projectPath, "net8.0", dependencies, new Logger(true)); + var update = new[] + { + new Dependency("Some.Other.Package", "1.2.0", DependencyType.PackageReference), + }; + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); Assert.NotNull(resolvedDependencies); Assert.Equal(2, resolvedDependencies.Length); Assert.Equal("Some.Package", resolvedDependencies[0].Name); @@ -493,6 +497,786 @@ await File.WriteAllTextAsync(projectPath, """ } } + #region + // Updating root package + // CS-Script Code to 2.0.0 requires its dependency Microsoft.CodeAnalysis.CSharp.Scripting to be 3.6.0 and its transitive dependency Microsoft.CodeAnalysis.Common to be 3.6.0 + [Fact] + public async Task DependencyConflictsCanBeResolvedNewUpdatingTopLevelPackage() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewUpdatingTopLevelPackage)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + """); + + var dependencies = new[] + { + // Add comment about root and dependencies + new Dependency("CS-Script.Core", "1.3.1", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "3.4.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Scripting.Common", "3.4.0", DependencyType.PackageReference), + }; + var update = new[] + { + new Dependency("CS-Script.Core", "2.0.0", DependencyType.PackageReference), + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(3, resolvedDependencies.Length); + Assert.Equal("CS-Script.Core", resolvedDependencies[0].Name); + Assert.Equal("2.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[1].Name); + Assert.Equal("3.6.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.Scripting.Common", resolvedDependencies[2].Name); + Assert.Equal("3.6.0", resolvedDependencies[2].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating a dependency (Microsoft.Bcl.AsyncInterfaces) of the root package (Azure.Core) will require the root package to also update, but since the dependency is not in the existing list, we do not include it + [Fact] + public async Task DependencyConflictsCanBeResolvedNewUpdatingNonExistingDependency() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewUpdatingNonExistingDependency)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + """); + + var dependencies = new[] + { + new Dependency("Azure.Core", "1.21.0", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.Bcl.AsyncInterfaces", "1.1.1", DependencyType.Unknown) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Single(resolvedDependencies); + Assert.Equal("Azure.Core", resolvedDependencies[0].Name); + Assert.Equal("1.22.0", resolvedDependencies[0].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Adding a reference + // Newtonsoft.Json needs to update to 13.0.1. Although Newtonsoft.Json.Bson can use the original version of 12.0.1, for security vulnerabilities and + // because there is no later version of Newtonsoft.Json.Bson 1.0.2, Newtonsoft.Json would be added to the existing list to prevent resolution + [Fact] + public async Task DependencyConflictsCanBeResolvedNewUpdatingNonExistentDependencyAndKeepingReference() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewUpdatingNonExistentDependencyAndKeepingReference)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + """); + + var dependencies = new[] + { + new Dependency("Newtonsoft.Json.Bson", "1.0.2", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Newtonsoft.Json", "13.0.1", DependencyType.Unknown) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(2, resolvedDependencies.Length); + Assert.Equal("Newtonsoft.Json.Bson", resolvedDependencies[0].Name); + Assert.Equal("1.0.2", resolvedDependencies[0].Version); + Assert.Equal("Newtonsoft.Json", resolvedDependencies[1].Name); + Assert.Equal("13.0.1", resolvedDependencies[1].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating unreferenced dependency + // Root package (Microsoft.CodeAnalysis.Compilers) and its dependencies (Microsoft.CodeAnalysis.CSharp), (Microsoft.CodeAnalysis.VisualBasic) are all 4.9.2 + // These packages all require the transitive dependency of the root package (Microsoft.CodeAnalysis.Common) to be 4.9.2, but it's not in the existing list + // If Microsoft.CodeAnalysis.Common is updated to 4.10.0, everything else updates and Microsoft.CoseAnalysis.Common is not kept in the existing list + [Fact] + public async Task DependencyConflictsCanBeResolvedNewTransitiveDependencyNotExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewTransitiveDependencyNotExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.CodeAnalysis.Compilers", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.VisualBasic", "4.9.2", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.CodeAnalysis.Common", "4.10.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(3, resolvedDependencies.Length); + Assert.Equal("Microsoft.CodeAnalysis.Compilers", resolvedDependencies[0].Name); + Assert.Equal("4.10.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[1].Name); + Assert.Equal("4.10.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.VisualBasic", resolvedDependencies[2].Name); + Assert.Equal("4.10.0", resolvedDependencies[2].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating referenced dependency + // The same as previous test, but the transitive dependency (Microsoft.CodeAnalysis.Common) is in the existing list + [Fact] + public async Task DependencyConflictsCanBeResolvedNewSingleTransitiveDependencyExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewSingleTransitiveDependencyExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.CodeAnalysis.Compilers", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.VisualBasic", "4.9.2", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.CodeAnalysis.Common", "4.10.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(4, resolvedDependencies.Length); + Assert.Equal("Microsoft.CodeAnalysis.Compilers", resolvedDependencies[0].Name); + Assert.Equal("4.10.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[1].Name); + Assert.Equal("4.10.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[2].Name); + Assert.Equal("4.10.0", resolvedDependencies[2].Version); + Assert.Equal("Microsoft.CodeAnalysis.VisualBasic", resolvedDependencies[3].Name); + Assert.Equal("4.10.0", resolvedDependencies[3].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // A combination of the third and fourth test, to measure efficiency of updating separate families + // Keeping a dependency that was not included in the original list (Newtonsoft.Json) + // Not keeping a dependency that was not included in the original list (Microsoft.CodeAnalysis.Common) + [Fact] + public async Task DependencyConflictsCanBeResolvedNewSelectiveAdditionPackages() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewSelectiveAdditionPackages)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.CodeAnalysis.Compilers", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "4.9.2", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.VisualBasic", "4.9.2", DependencyType.PackageReference), + new Dependency("Newtonsoft.Json.Bson", "1.0.2", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.CodeAnalysis.Common", "4.10.0", DependencyType.PackageReference), + new Dependency("Newtonsoft.Json", "13.0.1", DependencyType.Unknown) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(5, resolvedDependencies.Length); + Assert.Equal("Microsoft.CodeAnalysis.Compilers", resolvedDependencies[0].Name); + Assert.Equal("4.10.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[1].Name); + Assert.Equal("4.10.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.VisualBasic", resolvedDependencies[2].Name); + Assert.Equal("4.10.0", resolvedDependencies[2].Version); + Assert.Equal("Newtonsoft.Json.Bson", resolvedDependencies[3].Name); + Assert.Equal("1.0.2", resolvedDependencies[3].Version); + Assert.Equal("Newtonsoft.Json", resolvedDependencies[4].Name); + Assert.Equal("13.0.1", resolvedDependencies[4].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Two top level packages (Buildalyzer), (Microsoft.CodeAnalysis.CSharp.Scripting) that share a dependency (Microsoft.CodeAnalysis.Csharp) + // Updating ONE of the top level packages, which updates the dependencies and their other "parents" + // First family: Buildalyzer 7.0.1 requires Microsoft.CodeAnalysis.CSharp to be >= 4.0.0 and Microsoft.CodeAnalysis.Common to be 4.0.0 (@ 6.0.4, Microsoft.CodeAnalysis.Common isn't a dependency of buildalyzer) + // Second family: Microsoft.CodeAnalysis.CSharp.Scripting 4.0.0 requires Microsoft.CodeAnalysis.CSharp 4.0.0 and Microsoft.CodeAnalysis.Common to be 4.0.0 (Specific version) + // Updating Buildalyzer to 7.0.1 will update its transitive dependency (Microsoft.CodeAnalysis.Common) and then its transitive dependency's "family" + [Fact] + public async Task DependencyConflictsCanBeResolvedNewSharingDependency() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewSharingDependency)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Buildalyzer", "6.0.4", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp.Scripting", "3.10.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "3.10.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "3.10.0", DependencyType.PackageReference), + }; + var update = new[] + { + new Dependency("Buildalyzer", "7.0.1", DependencyType.PackageReference), + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(4, resolvedDependencies.Length); + Assert.Equal("Buildalyzer", resolvedDependencies[0].Name); + Assert.Equal("7.0.1", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp.Scripting", resolvedDependencies[1].Name); + Assert.Equal("4.0.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[2].Name); + Assert.Equal("4.0.0", resolvedDependencies[2].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[3].Name); + Assert.Equal("4.0.0", resolvedDependencies[3].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating two families at once to test efficiency + // First family: Direct dependency (Microsoft.CodeAnalysis.Common) needs to be updated, which will then need to update in the existing list its dependency (System.Collections.Immutable) and "parent" (Microsoft.CodeAnalysis.Csharp.Scripting) + // Second family: Updating the root package (Azure.Core) in the existing list will also need to update its dependency (Microsoft.Bcl.AsyncInterfaces) + [Fact] + public async Task DependencyConflictsCanBeResolvedNewUpdatingEntireFamily() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewUpdatingEntireFamily)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("System.Collections.Immutable", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp.Scripting", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.Bcl.AsyncInterfaces", "1.0.0", DependencyType.Unknown), + new Dependency("Azure.Core", "1.21.0", DependencyType.PackageReference), + + }; + var update = new[] + { + new Dependency("Microsoft.CodeAnalysis.Common", "4.10.0", DependencyType.PackageReference), + new Dependency("Azure.Core", "1.22.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(4, resolvedDependencies.Length); + Assert.Equal("System.Collections.Immutable", resolvedDependencies[0].Name); + Assert.Equal("8.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp.Scripting", resolvedDependencies[1].Name); + Assert.Equal("4.10.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.Bcl.AsyncInterfaces", resolvedDependencies[2].Name); + Assert.Equal("1.1.1", resolvedDependencies[2].Version); + Assert.Equal("Azure.Core", resolvedDependencies[3].Name); + Assert.Equal("1.22.0", resolvedDependencies[3].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Similar to the last test, except Microsoft.CodeAnalysis.Common is in the existing list + [Fact] + public async Task DependencyConflictsCanBeResolvedNewUpdatingTopLevelAndDependency() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewUpdatingTopLevelAndDependency)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("System.Collections.Immutable", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp.Scripting", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.Bcl.AsyncInterfaces", "1.0.0", DependencyType.Unknown), + new Dependency("Azure.Core", "1.21.0", DependencyType.PackageReference), + + }; + var update = new[] + { + new Dependency("Microsoft.CodeAnalysis.Common", "4.10.0", DependencyType.PackageReference), + new Dependency("Azure.Core", "1.22.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(5, resolvedDependencies.Length); + Assert.Equal("System.Collections.Immutable", resolvedDependencies[0].Name); + Assert.Equal("8.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp.Scripting", resolvedDependencies[1].Name); + Assert.Equal("4.10.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[2].Name); + Assert.Equal("4.10.0", resolvedDependencies[2].Version); + Assert.Equal("Microsoft.Bcl.AsyncInterfaces", resolvedDependencies[3].Name); + Assert.Equal("1.1.1", resolvedDependencies[3].Version); + Assert.Equal("Azure.Core", resolvedDependencies[4].Name); + Assert.Equal("1.22.0", resolvedDependencies[4].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Out of scope test: AutoMapper.Extensions.Microsoft.DependencyInjection's versions are not yet compatible + // To update root package (AutoMapper.Collection) to 10.0.0, its dependency (AutoMapper) needs to update to 13.0.0. + // However, there is no higher version of AutoMapper's other "parent" (AutoMapper.Extensions.Microsoft.DependencyInjection) that is compatible with the new version + [Fact] + public async Task DependencyConflictsCanBeResolvedNewOutOfScope() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewOutOfScope)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("AutoMapper.Extensions.Microsoft.DependencyInjection", "12.0.1", DependencyType.PackageReference), + new Dependency("AutoMapper", "12.0.1", DependencyType.PackageReference), + new Dependency("AutoMapper.Collection", "9.0.0", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("AutoMapper.Collection", "10.0.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(3, resolvedDependencies.Length); + Assert.Equal("AutoMapper.Extensions.Microsoft.DependencyInjection", resolvedDependencies[0].Name); + Assert.Equal("12.0.1", resolvedDependencies[0].Version); + Assert.Equal("AutoMapper", resolvedDependencies[1].Name); + Assert.Equal("12.0.1", resolvedDependencies[1].Version); + Assert.Equal("AutoMapper.Collection", resolvedDependencies[2].Name); + Assert.Equal("9.0.0", resolvedDependencies[2].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Two dependencies (Microsoft.Extensions.Caching.Memory), (Microsoft.EntityFrameworkCore.Analyzers) used by the same parent (Microsoft.EntityFrameworkCore), updating one of the dependencies + [Fact] + public async Task DependencyConflictsCanBeResolvedNewTwoDependenciesShareSameParent() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewTwoDependenciesShareSameParent)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.EntityFrameworkCore", "7.0.11", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore.Analyzers", "7.0.11", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.Extensions.Caching.Memory", "8.0.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(2, resolvedDependencies.Length); + Assert.Equal("Microsoft.EntityFrameworkCore", resolvedDependencies[0].Name); + Assert.Equal("8.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.EntityFrameworkCore.Analyzers", resolvedDependencies[1].Name); + Assert.Equal("8.0.0", resolvedDependencies[1].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating referenced package + // 4 dependency chain to be updated. Since the package to be updated is in the existing list, do not update its parents since we want to change as little as possible + [Fact] + public async Task DependencyConflictsCanBeResolvedNewFamilyOfFourExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewFamilyOfFourExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.EntityFrameworkCore.Design", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore.Relational", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore.Analyzers", "7.0.0", DependencyType.PackageReference) + }; + var update = new[] + { + new Dependency("Microsoft.EntityFrameworkCore.Analyzers", "8.0.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(4, resolvedDependencies.Length); + Assert.Equal("Microsoft.EntityFrameworkCore.Design", resolvedDependencies[0].Name); + Assert.Equal("7.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.EntityFrameworkCore.Relational", resolvedDependencies[1].Name); + Assert.Equal("7.0.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.EntityFrameworkCore", resolvedDependencies[2].Name); + Assert.Equal("7.0.0", resolvedDependencies[2].Version); + Assert.Equal("Microsoft.EntityFrameworkCore.Analyzers", resolvedDependencies[3].Name); + Assert.Equal("8.0.0", resolvedDependencies[3].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating unreferenced package + // 4 dependency chain to be updated, dependency to be updated is not in the existing list, so its family will all be updated + [Fact] + public async Task DependencyConflictsCanBeResolvedNewFamilyOfFourNotInExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewFamilyOfFourNotInExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.EntityFrameworkCore.Design", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore.Relational", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.EntityFrameworkCore", "7.0.0", DependencyType.PackageReference), + }; + var update = new[] + { + new Dependency("Microsoft.EntityFrameworkCore.Analyzers", "8.0.0", DependencyType.PackageReference) + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(3, resolvedDependencies.Length); + Assert.Equal("Microsoft.EntityFrameworkCore.Design", resolvedDependencies[0].Name); + Assert.Equal("8.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.EntityFrameworkCore.Relational", resolvedDependencies[1].Name); + Assert.Equal("8.0.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.EntityFrameworkCore", resolvedDependencies[2].Name); + Assert.Equal("8.0.0", resolvedDependencies[2].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Updating a referenced transitive dependency + // Updating a transtitive dependency (System.Collections.Immutable) to 8.0.0, which will update its "parent" (Microsoft.CodeAnalysis.CSharp) and its "grandparent" (Microsoft.CodeAnalysis.CSharp.Workspaces) to update + [Fact] + public async Task DependencyConflictsCanBeResolvedNewFamilyOfFourSpecificExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewFamilyOfFourSpecificExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("System.Collections.Immutable", "7.0.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp.Workspaces", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0", DependencyType.PackageReference), + }; + var update = new[] + { + new Dependency("System.Collections.Immutable", "8.0.0", DependencyType.PackageReference), + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(4, resolvedDependencies.Length); + Assert.Equal("System.Collections.Immutable", resolvedDependencies[0].Name); + Assert.Equal("8.0.0", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp.Workspaces", resolvedDependencies[1].Name); + Assert.Equal("4.8.0", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[2].Name); + Assert.Equal("4.8.0", resolvedDependencies[2].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[3].Name); + Assert.Equal("4.8.0", resolvedDependencies[3].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + + // Similar to the last test, with the "grandchild" (System.Collections.Immutable) not in the existing list + [Fact] + public async Task DependencyConflictsCanBeResolvedNewFamilyOfFourSpecificNotInExisting() + { + var repoRoot = Directory.CreateTempSubdirectory($"test_{nameof(DependencyConflictsCanBeResolvedNewFamilyOfFourSpecificNotInExisting)}_"); + + try + { + var projectPath = Path.Join(repoRoot.FullName, "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ + + + net8.0 + + + + + + + + """); + + var dependencies = new[] + { + new Dependency("Microsoft.CodeAnalysis.CSharp.Workspaces", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.CSharp", "4.8.0", DependencyType.PackageReference), + new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0", DependencyType.PackageReference), + + }; + var update = new[] + { + new Dependency("System.Collections.Immutable", "8.0.0", DependencyType.PackageReference), + }; + + var resolvedDependencies = await MSBuildHelper.ResolveDependencyConflictsNew(repoRoot.FullName, projectPath, "net8.0", dependencies, update, new Logger(true)); + Assert.NotNull(resolvedDependencies); + Assert.Equal(3, resolvedDependencies.Length); + Assert.Equal("Microsoft.CodeAnalysis.CSharp.Workspaces", resolvedDependencies[0].Name); + Assert.Equal("4.9.2", resolvedDependencies[0].Version); + Assert.Equal("Microsoft.CodeAnalysis.CSharp", resolvedDependencies[1].Name); + Assert.Equal("4.9.2", resolvedDependencies[1].Version); + Assert.Equal("Microsoft.CodeAnalysis.Common", resolvedDependencies[2].Name); + Assert.Equal("4.9.2", resolvedDependencies[2].Version); + } + finally + { + repoRoot.Delete(recursive: true); + } + } + #endregion + public static IEnumerable GetTopLevelPackageDependencyInfosTestData() { // simple case diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs index 1625b661c3..08bf0f5f99 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -84,24 +84,27 @@ internal static async Task GetPackageInfoAsync( var reader = new NuspecReader(nuspecStream); var isDevDependency = reader.GetDevelopmentDependency(); + var tfms = new HashSet(); + var dependencyGroups = reader.GetDependencyGroups().ToArray(); - var tfms = reader.GetDependencyGroups() - .Select(d => d.TargetFramework) - .ToImmutableArray(); - if (tfms.Length == 0) + foreach (var d in dependencyGroups) { - // If the nuspec doesn't have any dependency groups, - // try to get the TargetFramework from files in the lib folder. var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList(); - if (libItems.Count == 0) + + foreach (var item in libItems) { - // If there is no lib folder in this package, then assume it is a dev dependency. - isDevDependency = true; + tfms.Add(item.TargetFramework); } - tfms = libItems.Select(item => item.TargetFramework) - .Distinct() - .ToImmutableArray(); + if (!d.TargetFramework.IsAny) + { + tfms.Add(d.TargetFramework); + } + } + + if (!tfms.Any()) + { + tfms.Add(NuGetFramework.AnyFramework); } // The interfaces we given are not disposable but the underlying type can be. @@ -109,7 +112,7 @@ internal static async Task GetPackageInfoAsync( (readers.CoreReader as IDisposable)?.Dispose(); (readers.ContentReader as IDisposable)?.Dispose(); - return (isDevDependency, tfms); + return (isDevDependency, tfms.ToImmutableArray()); } internal static PackageReaders ReadPackage(string tempPackagePath) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 870675bd14..7eca5d6bba 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -24,6 +24,7 @@ public static async Task UpdateDependencyAsync( // Get the set of all top-level dependencies in the current project var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); + if (!await DoesDependencyRequireUpdateAsync(repoRootPath, projectPath, tfms, topLevelDependencies, dependencyName, newDependencyVersion, logger)) { return; @@ -306,6 +307,7 @@ private static async Task UpdateTopLevelDepdendency( IDictionary peerDependencies, Logger logger) { + var result = TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger); if (result == UpdateResult.NotFound) { @@ -324,7 +326,20 @@ private static async Task UpdateTopLevelDepdendency( { foreach (string tfm in targetFrameworks) { - Dependency[]? resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRootPath, projectFile.Path, tfm, updatedTopLevelDependencies, logger); + if (MSBuildHelper.UseNewDependencySolver()) + { + // Find the index of the dependency we are updating and revert it to the previous version + int dependencyIndex = Array.FindIndex(updatedTopLevelDependencies, d => string.Equals(d.Name, dependencyName, StringComparison.OrdinalIgnoreCase)); + if (dependencyIndex != -1) + { + var originalDependency = updatedTopLevelDependencies[dependencyIndex]; + updatedTopLevelDependencies[dependencyIndex] = originalDependency with { Version = previousDependencyVersion }; + } + + } + Dependency[] update = [new Dependency(dependencyName, newDependencyVersion, DependencyType.PackageReference)]; + Dependency[]? resolvedDependencies = await MSBuildHelper.ResolveDependencyConflicts(repoRootPath, projectFile.Path, tfm, updatedTopLevelDependencies, update, logger); + if (resolvedDependencies is null) { logger.Log($" Unable to resolve dependency conflicts for {projectFile.Path}."); @@ -345,7 +360,7 @@ private static async Task UpdateTopLevelDepdendency( continue; } - // update all other dependencies + // update all dependencies foreach (Dependency resolvedDependency in resolvedDependencies .Where(d => !d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase)) .Where(d => d.Version is not null)) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs new file mode 100644 index 0000000000..39167a99fa --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/DependencyConflictResolver.cs @@ -0,0 +1,689 @@ +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Task = System.Threading.Tasks.Task; + +namespace NuGetUpdater.Core; + +// Data type to store information of a given package +public class PackageToUpdate +{ + public string PackageName { get; set; } + public string CurrentVersion { get; set; } + public string NewVersion { get; set; } + + // Second version in case there's a "bounds" on the package version + public string SecondVersion { get; set; } + + // Bool to determine if a package has to be a specific version + public bool IsSpecific { get; set; } +} + +public class PackageManager +{ + // Dictionaries to store the relationships of a package (dependencies and parents) + private readonly Dictionary> packageDependencies = new Dictionary>(); + private readonly Dictionary> reverseDependencies = new Dictionary>(); + + // Path of the repository + private readonly string repoRoot; + + // Path to the project within the repository + private readonly string projectPath; + + public PackageManager(string repoRoot, string projectPath) + { + this.repoRoot = repoRoot; + this.projectPath = projectPath; + } + + // Method alterted from VersionFinder.cs to find the metadata of a given package + private async Task FindPackageMetadataAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken) + { + string? currentDirectory = Path.GetDirectoryName(projectPath); + string CurrentDirectory = currentDirectory ?? Environment.CurrentDirectory; + SourceCacheContext SourceCacheContext = new SourceCacheContext(); + PackageDownloadContext PackageDownloadContext = new PackageDownloadContext(SourceCacheContext); + ILogger Logger = NullLogger.Instance; + + IMachineWideSettings MachineWideSettings = new NuGet.CommandLine.CommandLineMachineWideSettings(); + ISettings Settings = NuGet.Configuration.Settings.LoadDefaultSettings( + CurrentDirectory, + configFileName: null, + MachineWideSettings); + + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(Settings); + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageIdentity.Id).ToHashSet(); + var sourceProvider = new PackageSourceProvider(Settings); + + ImmutableArray PackageSources = sourceProvider.LoadPackageSources() + .Where(p => p.IsEnabled) + .ToImmutableArray(); + + var sources = packageSources.Count == 0 + ? PackageSources + : PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + var message = new StringBuilder(); + message.AppendLine($"finding info url for {packageIdentity}, using package sources: {string.Join(", ", sources.Select(s => s.Name))}"); + + foreach (var source in sources) + { + message.AppendLine($" checking {source.Name}"); + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(cancellationToken); + if (feed is null) + { + message.AppendLine($" feed for {source.Name} was null"); + continue; + } + + try + { + var existsInFeed = await feed.Exists( + packageIdentity, + includeUnlisted: false, + SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + if (!existsInFeed) + { + message.AppendLine($" package {packageIdentity} does not exist in {source.Name}"); + continue; + } + } + catch (FatalProtocolException) + { + // if anything goes wrong here, the package source obviously doesn't contain the requested package + continue; + } + + var metadataResource = await sourceRepository.GetResourceAsync(cancellationToken); + var metadata = await metadataResource.GetMetadataAsync(packageIdentity, SourceCacheContext, Logger, cancellationToken); + return metadata; + } + + return null; + } + + // Method to find the best match framework of a given package's target framework availability + public static NuGetFramework FindBestMatchFramework(IEnumerable dependencySet, string targetFrameworkString) + { + // Parse the given target framework string into a NuGetFramework object + var targetFramework = NuGetFramework.ParseFolder(targetFrameworkString); + var frameworkReducer = new FrameworkReducer(); + + // Collect all target frameworks from the dependency set + var availableFrameworks = dependencySet.Select(dg => dg.TargetFramework).ToList(); + + // Return bestmatch framework + return frameworkReducer.GetNearest(targetFramework, availableFrameworks); + } + + // Method to get the dependencies of a package + public async Task> GetDependenciesAsync(PackageToUpdate package, string targetFramework, string projectDirectory) + { + if (!NuGetVersion.TryParse(package.NewVersion, out var otherVersion)) + { + return null; + } + + // Create a package identity to use for obtaining the metadata url + PackageIdentity packageIdentity = new PackageIdentity(package.PackageName, otherVersion); + + bool specific = false; + + List dependencyList = new List(); + + try + { + // Fetch package metadata URL + var metadataUrl = await FindPackageMetadataAsync(packageIdentity, CancellationToken.None); + IEnumerable dependencySet = metadataUrl?.DependencySets ?? []; + + // Get the bestMatchFramework based off the dependencies + var bestMatchFramework = FindBestMatchFramework(dependencySet, targetFramework); + + if (bestMatchFramework != null) + { + // Process the best match framework + var bestMatchGroup = dependencySet.First(dg => dg.TargetFramework == bestMatchFramework); + + foreach (var packageDependency in bestMatchGroup.Packages) + { + string version = packageDependency.VersionRange.OriginalString; + string firstVersion = null; + string SecondVersion = null; + + // Conditions to check if the version has bounds specified + if (version.StartsWith("[") && version.EndsWith("]")) + { + version = version.Trim('[', ']'); + var versions = version.Split(','); + version = versions.FirstOrDefault().Trim(); + if (versions.Length > 1) + { + SecondVersion = versions.LastOrDefault()?.Trim(); + } + specific = true; + } + else if (version.StartsWith("[") && version.EndsWith(")")) + { + version = version.Trim('[', ')'); + var versions = version.Split(','); + version = versions.FirstOrDefault().Trim(); + if (versions.Length > 1) + { + SecondVersion = versions.LastOrDefault()?.Trim(); + } + } + else if (version.StartsWith("(") && version.EndsWith("]")) + { + version = version.Trim('(', ']'); + var versions = version.Split(','); + version = versions.FirstOrDefault().Trim(); + if (versions.Length > 1) + { + SecondVersion = versions.LastOrDefault()?.Trim(); + } + } + else if (version.StartsWith("(") && version.EndsWith(")")) + { + version = version.Trim('(', ')'); + var versions = version.Split(','); + version = versions.FirstOrDefault().Trim(); + if (versions.Length > 1) + { + SecondVersion = versions.LastOrDefault()?.Trim(); + } + } + + // Store the dependency data to later add to the dependencyList + PackageToUpdate dependencyPackage = new PackageToUpdate + { + PackageName = packageDependency.Id, + CurrentVersion = version, + }; + + if (specific == true) + { + dependencyPackage.IsSpecific = true; + } + + if (SecondVersion != null) + { + dependencyPackage.SecondVersion = SecondVersion; + } + + dependencyList.Add(dependencyPackage); + } + } + else + { + Console.WriteLine("No compatible framework found."); + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP error occurred: {ex.Message}"); + } + catch (ArgumentNullException ex) + { + Console.WriteLine($"Argument is null error: {ex.ParamName}, {ex.Message}"); + } + catch (InvalidOperationException ex) + { + Console.WriteLine($"Invalid operation exception: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred: {ex.Message}"); + } + + return dependencyList; + } + + // Method AddDependency to create the relationships between a parent and child + private void AddDependency(PackageToUpdate parent, PackageToUpdate child) + { + if (!packageDependencies.ContainsKey(parent)) + { + packageDependencies[parent] = new HashSet(); + } + else if (packageDependencies[parent].Contains(child)) + { + // Remove the old child dependency if it exists + packageDependencies[parent].Remove(child); + } + + packageDependencies[parent].Add(child); + + if (!reverseDependencies.ContainsKey(child)) + { + reverseDependencies[child] = new HashSet(); + } + else if (reverseDependencies[child].Contains(parent)) + { + // Remove the old parent dependency if it exists + reverseDependencies[child].Remove(parent); + } + + reverseDependencies[child].Add(parent); + } + + // Method to get the dependencies of a package and add them as a dependency + public async Task PopulatePackageDependenciesAsync(List packages, string targetFramework, string projectDirectory) + { + // Loop through each package and get their dependencies + foreach (PackageToUpdate package in packages) + { + List dependencies = await GetDependenciesAsync(package, targetFramework, projectDirectory); + + if (dependencies == null) + { + continue; + } + + // Add each dependency based off if it exists or not + foreach (PackageToUpdate dependency in dependencies) + { + PackageToUpdate checkInExisting = packages.FirstOrDefault(p => string.Compare(p.PackageName, dependency.PackageName, StringComparison.OrdinalIgnoreCase) == 0); + if (checkInExisting != null) + { + checkInExisting.IsSpecific = dependency.IsSpecific; + AddDependency(package, checkInExisting); + } + else + { + AddDependency(package, dependency); + } + } + } + } + + // Method to get the parent packages of a given package + public HashSet GetParentPackages(PackageToUpdate package) + { + if (reverseDependencies.TryGetValue(package, out var parents)) + { + return parents; + } + + return new HashSet(); + } + + // Method to update the version of a desired package based off framework + public async Task UpdateVersion(List existingPackages, PackageToUpdate package, string targetFramework, string projectDirectory) + { + // Bool to track if the package was in the original existing list + bool inExisting = true; + + // If there is no new version to update or if the current version isn't updated + if (package.NewVersion == null) + { + return "No new version"; + } + + // If the package is already updated or needs to be updated + if (package.CurrentVersion != null) + { + if (package.CurrentVersion == package.NewVersion) + { + return "Already updated to new version"; + } + } + // Place the current version as the new version for updating purposes + else + { + package.CurrentVersion = package.NewVersion; + inExisting = false; + } + + try + { + NuGetVersion CurrentVersion = new NuGetVersion(package.CurrentVersion); + NuGetVersion newerVersion = new NuGetVersion(package.NewVersion); + + // If the CurrentVersion is less than or equal to the newerVersion, proceed with the update + if (CurrentVersion <= newerVersion) + { + string currentVersiontemp = package.CurrentVersion; + package.CurrentVersion = package.NewVersion; + + // Check if the current package has dependencies + List dependencyList = await GetDependenciesAsync(package, targetFramework, projectDirectory); + + // If there are dependencies + if (dependencyList != null) + { + foreach (PackageToUpdate dependency in dependencyList) + { + // Check if the dependency is in the existing packages + foreach (PackageToUpdate existingPackage in existingPackages) + { + // If you find the dependency + if (string.Equals(dependency.PackageName, existingPackage.PackageName, StringComparison.OrdinalIgnoreCase)) + { + NuGetVersion existingCurrentVersion = new NuGetVersion(existingPackage.CurrentVersion); + NuGetVersion dependencyCurrentVersion = new NuGetVersion(dependency.CurrentVersion); + + // Check if the existing version is less than the dependency's existing version + if (existingCurrentVersion < dependencyCurrentVersion) + { + // Create temporary copy of the current version and of the existing package + string dependencyOldVersion = existingPackage.CurrentVersion; + + // Susbtitute the current version of the existingPackage with the dependency current version + existingPackage.CurrentVersion = dependency.CurrentVersion; + + // If the family is compatible with the dependency's version, update with the dependency version + if (await AreAllParentsCompatibleAsync(existingPackages, existingPackage, targetFramework, projectDirectory) == true) + { + existingPackage.CurrentVersion = dependencyOldVersion; + string NewVersion = dependency.CurrentVersion; + existingPackage.NewVersion = dependency.CurrentVersion; + await UpdateVersion(existingPackages, existingPackage, targetFramework, projectDirectory); + } + // If not, resort to putting version back to normal and remove new version + else + { + existingPackage.CurrentVersion = dependencyOldVersion; + package.CurrentVersion = currentVersiontemp; + package.NewVersion = package.CurrentVersion; + return "Out of scope"; + } + } + } + } + + // If the dependency has brackets or parenthesis, it's a specific version + if (dependency.CurrentVersion.Contains('[') || dependency.CurrentVersion.Contains(']') || dependency.CurrentVersion.Contains('{') || dependency.CurrentVersion.Contains('}')) + { + dependency.IsSpecific = true; + } + + await UpdateVersion(existingPackages, dependency, targetFramework, projectDirectory); + } + } + + // Get the parent packages of the package and check the compatibility between its family + HashSet parentPackages = GetParentPackages(package); + + foreach (PackageToUpdate parent in parentPackages) + { + bool isCompatible = await IsCompatibleAsync(parent, package, targetFramework, projectDirectory); + + // If the parent and package are not compatible + if (!isCompatible) + { + // Attempt to find and update to a compatible version between the two + NuGetVersion compatibleVersion = await FindCompatibleVersionAsync(existingPackages, parent, package, targetFramework); + if (compatibleVersion == null) + { + return "Failed to update"; + } + + // If a version is found, update to that version + parent.NewVersion = compatibleVersion.ToString(); + await UpdateVersion(existingPackages, parent, targetFramework, projectDirectory); + } + + // If it's compatible and the package you updated wasn't in the existing package, check if the parent's dependencies version is the same as the current version + else if (isCompatible == true && inExisting == false) + { + List dependencyListParent = await GetDependenciesAsync(parent, targetFramework, projectDirectory); + + PackageToUpdate parentDependency = dependencyListParent.FirstOrDefault(p => string.Compare(p.PackageName, package.PackageName, StringComparison.OrdinalIgnoreCase) == 0); + + // If the parent's dependency current version is not the same as the current version of the package + if (parentDependency.CurrentVersion != package.CurrentVersion) + { + // Create a NugetContext instance to get the latest versions of the parent + NuGetContext nugetContext = new NuGetContext(Path.GetDirectoryName(projectPath)); + Logger logger = null; + + string currentVersionString = parent.CurrentVersion; + NuGetVersion currentVersionParent = NuGetVersion.Parse(currentVersionString); + + var result = await VersionFinder.GetVersionsAsync(parent.PackageName, currentVersionParent, nugetContext, logger, CancellationToken.None); + var versions = result.GetVersions(); + NuGetVersion latestVersion = versions.Where(v => !v.IsPrerelease).Max(); + + // Loop from the current version to the latest version, use next patch as a limit (unless there's a limit) so it doesn't look for versions that don't exist + for (NuGetVersion version = currentVersionParent; version <= latestVersion; version = NextPatch(version, versions)) + { + NuGetVersion nextPatch = NextPatch(version, versions); + + // If the next patch is the same as the currentVersioon, then the update is a Success + if (nextPatch == version) + { + return "Success"; + } + + string parentVersion = version.ToString(); + parent.NewVersion = parentVersion; + + // Check if the parent needs to be updated since the child isn't in the existing package list and the parent can update to a newer version to remove the dependency + List dependencyListParentTemp = await GetDependenciesAsync(parent, targetFramework, projectDirectory); + PackageToUpdate parentDependencyTemp = dependencyListParentTemp.FirstOrDefault(p => string.Compare(p.PackageName, package.PackageName, StringComparison.OrdinalIgnoreCase) == 0); + + // If the newer package version of the parent has the same version as the parent's previous dependency, update + if (parentDependencyTemp.CurrentVersion == package.CurrentVersion) + { + parent.NewVersion = parentVersion; + parent.CurrentVersion = null; + await UpdateVersion(existingPackages, parent, targetFramework, projectDirectory); + package.IsSpecific = true; + return "Success"; + } + } + parent.CurrentVersion = currentVersionString; + } + } + } + } + + else + { + Console.WriteLine("Current version is >= latest version"); + } + } + catch + { + return "Failed to update"; + } + + return "Success"; + } + + // Method to determine if a parent and child are compatible with their versions + public async Task IsCompatibleAsync(PackageToUpdate parent, PackageToUpdate child, string targetFramework, string projectDirectory) + { + // Get the dependencies of the parent + List dependencies = await GetDependenciesAsync(parent, targetFramework, projectDirectory); + + foreach (PackageToUpdate dependency in dependencies) + { + + // If the child is present + if (string.Equals(dependency.PackageName, child.PackageName, StringComparison.OrdinalIgnoreCase)) + { + NuGetVersion dependencyVersion = new NuGetVersion(dependency.CurrentVersion); + NuGetVersion childVersion = new NuGetVersion(child.CurrentVersion); + + // If the dependency version of the parent and the childversion is the same, or if the child version can be >= + if (dependencyVersion == childVersion || (childVersion > dependencyVersion && dependency.IsSpecific != true)) + { + return true; + } + else + { + return false; + } + } + } + + return false; + } + + // Method to update a version to the next available version for a package + public NuGetVersion NextPatch(NuGetVersion version, IEnumerable allVersions) + { + var versions = allVersions.Where(v => v > version); + + if (!versions.Any()) + { + // If there are no greater versions, return current version + return version; + } + + // Find smallest version in the versions + return versions.Min(); + } + + // Method to find a compatible version with the child for the parent to update to + public async Task FindCompatibleVersionAsync(List existingPackages, PackageToUpdate possibleParent, PackageToUpdate possibleDependency, string targetFramework) + { + string packageId = possibleParent.PackageName; + string currentVersionString = possibleParent.CurrentVersion; + NuGetVersion CurrentVersion = NuGetVersion.Parse(currentVersionString); + string currentVersionStringDependency = possibleDependency.CurrentVersion; + NuGetVersion currentVersionDependency = NuGetVersion.Parse(currentVersionStringDependency); + + // Create a NugetContext instance to get the latest versions of the parent + NuGetContext nugetContext = new NuGetContext(Path.GetDirectoryName(projectPath)); + Logger logger = null; + + var result = await VersionFinder.GetVersionsAsync(possibleParent.PackageName, CurrentVersion, nugetContext, logger, CancellationToken.None); + var versions = result.GetVersions(); + + // If there are no versions + if (versions.Length == 0) + { + return null; + } + + NuGetVersion latestVersion = versions + .Where(v => !v.IsPrerelease) + .Max(); + + // If there's a version bounds that the parent has + if (possibleParent.SecondVersion != null) + { + NuGetVersion SecondVersion = NuGetVersion.Parse(possibleParent.SecondVersion); + latestVersion = SecondVersion; + } + + // If there is no later version + if (CurrentVersion == latestVersion) + { + return null; + } + + // If the current version of the parent is less than the current version of the dependency + else if (CurrentVersion < currentVersionDependency) + { + return currentVersionDependency; + } + + // Loop from the current version to the latest version, use next patch as a limit (unless there's a limit) so it doesn't look for versions that don't exist + for (NuGetVersion version = CurrentVersion; version <= latestVersion; version = NextPatch(version, versions)) + { + possibleParent.NewVersion = version.ToString(); + + NuGetVersion nextPatch = NextPatch(version, versions); + + // If the next patch is the same as the CurrentVersion, then nothing is needed + if (nextPatch == version) + { + return nextPatch; + } + + // Check if there's compatibility with parent and dependency + if (await IsCompatibleAsync(possibleParent, possibleDependency, targetFramework, nugetContext.CurrentDirectory)) + { + // Check if parents are compatible, recursively + if (await AreAllParentsCompatibleAsync(existingPackages, possibleParent, targetFramework, nugetContext.CurrentDirectory)) + { + // If compatible, return the new version + if (Regex.IsMatch(possibleParent.NewVersion, @"[a-zA-Z]")) + { + possibleParent.IsSpecific = true; + } + return version; + } + } + } + + // If no compatible version is found, return null + return null; + } + + // Method to determine if all the parents of a given package are compatible with the parent's desired version + public async Task AreAllParentsCompatibleAsync(List existingPackages, PackageToUpdate possibleParent, string targetFramework, string projectDirectory) + { + // Get the possibleParent parentPackages + HashSet parentPackages = GetParentPackages(possibleParent); + + foreach (PackageToUpdate parent in parentPackages) + { + // Check compatibility between the possibleParent and current parent + bool isCompatible = await IsCompatibleAsync(parent, possibleParent, targetFramework, projectDirectory); + + // If the possibleParent and parent are not compatible + if (!isCompatible) + { + // Find a compatible version if possible + NuGetVersion compatibleVersion = await FindCompatibleVersionAsync(existingPackages, parent, possibleParent, targetFramework); + if (compatibleVersion == null) + { + return false; + } + + parent.NewVersion = compatibleVersion.ToString(); + await UpdateVersion(existingPackages, parent, targetFramework, projectDirectory); + } + + // Recursively check if all ancestors are compatible + if (!await AreAllParentsCompatibleAsync(existingPackages, parent, targetFramework, projectDirectory)) + { + return false; + } + } + + return true; + } + + // Method to update the existing packages with new version of the desired packages to update + public void UpdateExistingPackagesWithNewVersions(List existingPackages, List packagesToUpdate) + { + foreach (PackageToUpdate packageToUpdate in packagesToUpdate) + { + PackageToUpdate existingPackage = existingPackages.FirstOrDefault(p => string.Compare(p.PackageName, packageToUpdate.PackageName, StringComparison.OrdinalIgnoreCase) == 0); + + if (existingPackage != null) + { + existingPackage.NewVersion = packageToUpdate.NewVersion; + } + else + { + Console.WriteLine($"Package {packageToUpdate.PackageName} not found in existing packages"); + } + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index d3ea25d561..ff8e33a6d6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -13,9 +13,12 @@ using Microsoft.Build.Locator; using Microsoft.Extensions.FileSystemGlobbing; +using NuGet; using NuGet.Configuration; +using NuGet.Frameworks; using NuGet.Versioning; +using NuGetUpdater.Core.Analyze; using NuGetUpdater.Core.Utilities; namespace NuGetUpdater.Core; @@ -331,7 +334,182 @@ internal static async Task DependenciesAreCoherentAsync(string repoRoot, s } } - internal static async Task ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger) + internal static bool UseNewDependencySolver() + { + return Environment.GetEnvironmentVariable("UseNewNugetPackageResolver") == "true"; + } + + internal static async Task ResolveDependencyConflicts(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, Logger logger) + { + if (UseNewDependencySolver()) + { + return await ResolveDependencyConflictsNew(repoRoot, projectPath, targetFramework, packages, update, logger); + } + else + { + return await ResolveDependencyConflictsOld(repoRoot, projectPath, targetFramework, packages, logger); + } + } + + internal static async Task ResolveDependencyConflictsNew(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Dependency[] update, Logger logger) + { + var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_"); + PackageManager packageManager = new PackageManager(repoRoot, projectPath); + + try + { + string tempProjectPath = await CreateTempProjectAsync(tempDirectory, repoRoot, projectPath, targetFramework, packages); + var (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"restore \"{tempProjectPath}\"", workingDirectory: tempDirectory.FullName); + + // Add Dependency[] packages to List existingPackages + List existingPackages = packages + .Select(existingPackage => new PackageToUpdate + { + PackageName = existingPackage.Name, + CurrentVersion = existingPackage.Version + }) + .ToList(); + + // Add Dependency[] update to List packagesToUpdate + List packagesToUpdate = update + .Where(package => package.Version != null) + .Select(package => new PackageToUpdate + { + PackageName = package.Name, + NewVersion = package.Version.ToString() + }) + .ToList(); + + foreach (PackageToUpdate existing in existingPackages) + { + var foundPackage = packagesToUpdate.Where(p => string.Equals(p.PackageName, existing.PackageName, StringComparison.OrdinalIgnoreCase)); + if (!foundPackage.Any()) + { + existing.NewVersion = existing.CurrentVersion; + } + } + + // Create a duplicate set of existingPackages for flexible package reference addition and removal + List existingDuplicate = new List(existingPackages); + + // Bool to keep track of if anything was added to the existingDuplicate list + bool added = false; + + // If package 'isnt there, add it to the existingDuplicate list + foreach (PackageToUpdate package in packagesToUpdate) + { + if (!existingDuplicate.Any(p => string.Equals(p.PackageName, package.PackageName, StringComparison.OrdinalIgnoreCase))) + { + existingDuplicate.Add(package); + added = true; + } + } + + // If you have to use the existingDuplicate list + if (added == true) + { + // Add existing versions to existing list + packageManager.UpdateExistingPackagesWithNewVersions(existingDuplicate, packagesToUpdate); + + // Make relationships + await packageManager.PopulatePackageDependenciesAsync(existingDuplicate, targetFramework, Path.GetDirectoryName(projectPath)); + + // Update all to new versions + foreach (var package in existingDuplicate) + { + string updateResult = await packageManager.UpdateVersion(existingDuplicate, package, targetFramework, Path.GetDirectoryName(projectPath)); + } + } + + // Editing existing list because nothing was added to existingDuplicate + else + { + // Add existing versions to existing list + packageManager.UpdateExistingPackagesWithNewVersions(existingPackages, packagesToUpdate); + + // Make relationships + await packageManager.PopulatePackageDependenciesAsync(existingPackages, targetFramework, Path.GetDirectoryName(projectPath)); + + // Update all to new versions + foreach (var package in existingPackages) + { + string updateResult = await packageManager.UpdateVersion(existingPackages, package, targetFramework, Path.GetDirectoryName(projectPath)); + } + } + + // Make new list to remove and differentiate between existingDuplicate and existingPackages lists + List packagesToRemove = existingDuplicate + .Where(existingPackageDupe => !existingPackages.Contains(existingPackageDupe) && existingPackageDupe.IsSpecific == true) + .ToList(); + + foreach (PackageToUpdate package in packagesToRemove) + { + existingDuplicate.Remove(package); + } + + if (existingDuplicate != null) + { + existingPackages = existingDuplicate; + } + + // Convert back to Dependency [], use NewVersion if available, otherwise use CurrentVersion + List candidatePackages = existingPackages + .Select(package => new Dependency( + package.PackageName, + package.NewVersion ?? package.CurrentVersion, + DependencyType.Unknown, + null, + null, + false, + false, + false, + false, + false + )) + .ToList(); + + // Return as array + Dependency[] candidatePackagesArray = candidatePackages.ToArray(); + + var targetFrameworks = new NuGetFramework[] { NuGetFramework.Parse(targetFramework) }; + + var resolveProjectPath = projectPath; + + if (!Path.IsPathRooted(resolveProjectPath) || !File.Exists(resolveProjectPath)) + { + resolveProjectPath = Path.GetFullPath(Path.Join(repoRoot, resolveProjectPath)); + } + + NuGetContext nugetContext = new NuGetContext(Path.GetDirectoryName(resolveProjectPath)); + + // Target framework compatibility check + foreach (var package in candidatePackages) + { + if (!NuGetVersion.TryParse(package.Version, out var nuGetVersion)) + { + // If version is not valid, return original packages and revert + return packages; + } + + var packageIdentity = new NuGet.Packaging.Core.PackageIdentity(package.Name, nuGetVersion); + + bool isNewPackageCompatible = await CompatibilityChecker.CheckAsync(packageIdentity, targetFrameworks.ToImmutableArray(), nugetContext, logger, CancellationToken.None); + if (!isNewPackageCompatible) + { + // If the package target framework is not compatible, return original packages and revert + return packages; + } + } + + return candidatePackagesArray; + } + finally + { + tempDirectory.Delete(recursive: true); + } + } + + internal static async Task ResolveDependencyConflictsOld(string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger logger) { var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-coherence_"); try @@ -359,22 +537,22 @@ internal static async Task DependenciesAreCoherentAsync(string repoRoot, s Dictionary> badPackagesAndCandidateVersionsDictionary = new(StringComparer.OrdinalIgnoreCase); // and for each of those packages, find all versions greater than the one that's currently installed - foreach ((string packageName, NuGetVersion packageVersion) in badPackagesAndVersions) + foreach ((string PackageName, NuGetVersion packageVersion) in badPackagesAndVersions) { // this command dumps a JSON object with all versions of the specified package from all package sources - (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"package search {packageName} --exact-match --format json", workingDirectory: tempDirectory.FullName); + (exitCode, stdOut, stdErr) = await ProcessEx.RunAsync("dotnet", $"package search {PackageName} --exact-match --format json", workingDirectory: tempDirectory.FullName); if (exitCode != 0) { continue; } // ensure collection exists - if (!badPackagesAndCandidateVersionsDictionary.ContainsKey(packageName)) + if (!badPackagesAndCandidateVersionsDictionary.ContainsKey(PackageName)) { - badPackagesAndCandidateVersionsDictionary.Add(packageName, new HashSet()); + badPackagesAndCandidateVersionsDictionary.Add(PackageName, new HashSet()); } - HashSet foundVersions = badPackagesAndCandidateVersionsDictionary[packageName]; + HashSet foundVersions = badPackagesAndCandidateVersionsDictionary[PackageName]; var json = JsonHelper.ParseNode(stdOut); if (json?["searchResult"] is JsonArray searchResults) @@ -605,9 +783,9 @@ internal static async Task GetAllPackageDependenciesAsync( .Where(match => match.Success) .Select(match => { - var packageName = match.Groups["PackageName"].Value; - var isTransitive = !topLevelPackagesNames.Contains(packageName); - return new Dependency(packageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive); + var PackageName = match.Groups["PackageName"].Value; + var isTransitive = !topLevelPackagesNames.Contains(PackageName); + return new Dependency(PackageName, match.Groups["PackageVersion"].Value, DependencyType.Unknown, TargetFrameworks: tfms, IsTransitive: isTransitive); }) .ToArray(); diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index 1992777ff5..271b495e46 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -242,7 +242,12 @@ def self.run_nuget_updater_tool(repo_root:, proj_path:, dependency:, is_transiti puts "running NuGet updater:\n" + command NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do - output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + env = {} + env["UseNewNugetPackageResolver"] = "true" if Dependabot::Experiments.enabled?(:nuget_dependency_solver) + output = SharedHelpers.run_shell_command(command, + allow_unsafe_shell_command: true, + fingerprint: fingerprint, + env: env) puts output result_contents = File.read(update_result_file_path)