Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Central transitive dependencies should be considered only for root nodes #4611

Merged

Conversation

marcin-krystianc
Copy link
Contributor

Bug

Fixes: NuGet/Home#11760

Regression? Last working version: -

Description

After this change, the DependencyWalker will check whether transitive dependency eclipses or downgrades the current library only when that transitive dependency is at the top-level node.

With this change, the example from the linked issue fails with downgrade errors (GOOD):

dotnet.exe restore --force
  Determining projects to restore...
  The project D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\Lib1\Lib1.csproj is using CentralPackageVersionManagement, a NuGet preview feature.
  The project D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\App\App.csproj is using CentralPackageVersionManagement, a NuGet preview feature.
D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\App\App.csproj : error NU1605: Detected package downgrade: Newtonsoft.Json from 12.0.2 to 12.0.1. Reference the package directly from the project to select a different version.  [D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\Solution.sln]
D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\App\App.csproj : error NU1605:  App -> Lib1 -> Lib2 -> Newtonsoft.Json (>= 12.0.2)  [D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\Solution.sln]
D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\App\App.csproj : error NU1605:  App -> Newtonsoft.Json (>= 12.0.1) [D:\workspace\TestSolutions\MissingDowngradeCentralisedLib\Solution.sln]

PR Checklist

  • PR has a meaningful title

  • PR has a linked issue.

  • Described changes

  • Tests

    • [x Automated tests added
    • OR
    • Test exception
    • OR
    • N/A
  • Documentation

    • Documentation PR or issue filled
    • OR
    • N/A

@marcin-krystianc marcin-krystianc requested a review from a team as a code owner May 4, 2022 11:41
@ghost ghost added the Community PRs created by someone not in the NuGet team label May 4, 2022
@@ -843,7 +843,7 @@ private static List<CentralTransitiveDependencyGroup> ReadProjectFileTransitiveD
NuGetFramework framework = NuGetFramework.Parse(frameworkPropertyName);
var dependencies = new List<LibraryDependency>();

JsonPackageSpecReader.ReadCentralTransitveDependencyGroup(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just fixing a typographic mistake in an internal method.

@jeffkl jeffkl self-assigned this May 4, 2022
@jeffkl jeffkl force-pushed the dev-marcink-20220504-centraltransitive branch from a28d352 to 1612530 Compare May 4, 2022 13:12
Copy link
Contributor

@jeffkl jeffkl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, looks good with one small comment. I had to rebase your branch and force push so be sure to pull it next time you work on it.

…ndencyWalker.cs

Co-authored-by: Jeff Kluge <jeffkl@microsoft.com>
@marcin-krystianc
Copy link
Contributor Author

@jeffkl 1612530 has some test failures. Are these legitimate issues or just a flakiness?

@jeffkl
Copy link
Contributor

jeffkl commented May 5, 2022

@jeffkl 1612530 has some test failures. Are these legitimate issues or just a flakiness?

Yeah sorry we're having some troubles with CI today, I'll work on it.

Copy link
Member

@nkolev92 nkolev92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay.

Your explanation seems good! It's been a while since we've looked at this.

A few qs about the test.

I wouldn't be the worst idea to define an end to end test as well, as the dependency walker tests rely on a lot of configuration.

Example test is ExecuteAsync_WithDowngradesInPrunedSubgraph_DoesNotRaiseNU1605, it's not CPM, but other in the same class are RestoreCommand_CentralVersion_ErrorWhenDependenciesHaveVersion.

var context = new TestRemoteWalkContext();
var provider = new DependencyProvider();
provider.Package("A", "1.0")
.DependsOn("B", "1.0")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think B needs to be centrally managed here, as B is a top level to A, which effectively means all it's direct deps need to be centrally managed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B is a project here so that is why it is not "centrally managed" as any package would be.


provider.Package("B", "1.0")
.DependsOn("C", "1.0")
.DependsOn("D", "3.0", LibraryDependencyTarget.Package, versionCentrallyManaged: true, libraryDependencyReferenceType: LibraryDependencyReferenceType.None);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be centrally managed?
In the context of A, this isn't centrally managed, it's just a dependency of B.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B is a project that uses CPM, so the package D dependency should be centrally managed.

Copy link
Member

@nkolev92 nkolev92 Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but not in the context of A.

A and B are restored separately.
When A is getting restored, A doesn't care about central management configured by B, it just cares about the declared dependencies (and their versions) from B.

This is tricky, as it seems like we don't really recognize the concept of transitively pinned for project 1 but not pinned for project 2.

var provider = new DependencyProvider();
provider.Package("A", "1.0")
.DependsOn("B", "1.0")
.DependsOn("D", "1.0", LibraryDependencyTarget.Package, versionCentrallyManaged: true, libraryDependencyReferenceType: LibraryDependencyReferenceType.None);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we another another level of dependencies here?

If A is the root node, and it's centrally managed, then all it's dependencies need to be centrally managed and are direct.

…nsitive' into dev-marcink-20220504-centraltransitive

# Conflicts:
#	src/NuGet.Core/NuGet.DependencyResolver.Core/Remote/RemoteDependencyWalker.cs
#	test/NuGet.Core.Tests/NuGet.DependencyResolver.Core.Tests/RemoteDependencyWalkerTests.cs
@marcin-krystianc
Copy link
Contributor Author

A few qs about the test.

I think that all these questions are due to confusion about which dependency is a project and which is a package. I've updated the test so now it should be clearer. Please have another look.

It wouldn't be the worst idea to define an end to end test as well, as the dependency walker tests rely on a lot of configuration.

I implemented ExecuteAsync_TransitiveDependenciesFromNonRootLibraries_AreIgnored - it has smililrar configuration complexity to the walker tests though 😞

nkolev92
nkolev92 previously approved these changes Jun 16, 2022
Copy link
Member

@nkolev92 nkolev92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution @marcin-krystianc and apologies for the delays.


provider.Package("B", "1.0")
.DependsOn("C", "1.0")
.DependsOn("D", "3.0", LibraryDependencyTarget.Package, versionCentrallyManaged: true, libraryDependencyReferenceType: LibraryDependencyReferenceType.None);
Copy link
Member

@nkolev92 nkolev92 Jun 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but not in the context of A.

A and B are restored separately.
When A is getting restored, A doesn't care about central management configured by B, it just cares about the declared dependencies (and their versions) from B.

This is tricky, as it seems like we don't really recognize the concept of transitively pinned for project 1 but not pinned for project 2.

@nkolev92 nkolev92 self-requested a review June 16, 2022 00:37
@nkolev92 nkolev92 dismissed their stale review June 16, 2022 00:38

Meant to comment.

@nkolev92
Copy link
Member

Seems like our CI hook is broken.

cc @zivkan

@zivkan
Copy link
Member

zivkan commented Jun 20, 2022

github webhooks are (were?) having an outage: https://www.githubstatus.com/incidents/gvzcw6sd0xxb

@nkolev92
Copy link
Member

Can you rebase again @marcin-krystianc

e9637ed didn't make it in your rebase.

@nkolev92
Copy link
Member

Basically NuGet.CommandLine.Test.NuGetPushCommandTest.PushCommand_InvalidInput_V2HttpSource(invalidInput: "https://invalid-2a0358f1-88f2-48c0-b68a-bb150cac00"...)

is failing.

Copy link
Member

@nkolev92 nkolev92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

I have a hunch about some other potential bugs we may have with the transitive walking.

I'll ping everyone on the details if it indeed does turn out to be a bug.

@nkolev92
Copy link
Member

So the thing I was worried about is that if a package is transitively specified in a child project, the parent project gets a library dependency that's centrally managed.

Turns out it doesn't hurt transitive pinning, but I wonder if it could mess with it in a different way.

This is the test I used (I basically tweaked the test being added in this change).

/// <summary>
        /// A 1.0 -> D 3.0 (Central transitive) ->  E 1.0.0
        ///       -> B 1.0 -> D 2.0.0 (declared)
        ///       -> B 1.0 -> E 2.0.0
        /// </summary>
        [Fact]
        public async Task RestoreNetCore_DependenciesFromRootProjectThatArePinned_ShouldBeIgnored()
        {
            // Arrange
            using var pathContext = new SimpleTestPathContext();

            // Set up solution, project, and packages
            var solution = new SimpleTestSolutionContext(pathContext.SolutionRoot);

            var projectB = CreateProject(pathContext, "B");
            var projectA = CreateProject(pathContext, "A", projectB);

            var packageD200 = new SimpleTestPackageContext("D", "2.0.0");
            var packageD300 = new SimpleTestPackageContext("D", "3.0.0");
            var packageE100 = new SimpleTestPackageContext("E", "1.0.0");
            var packageE200 = new SimpleTestPackageContext("E", "2.0.0");
            packageD300.Dependencies.Add(packageE100);

            await SimpleTestPackageUtility.CreateFolderFeedV3Async(
                pathContext.PackageSource,
                PackageSaveMode.Defaultv3,
                packageD200,
                packageD300,
                packageE100,
                packageE200
            );

            solution.Projects.Add(projectA);
            solution.Projects.Add(projectB);
            solution.Create(pathContext.SolutionRoot);

            AddPackageReferenceToProject(projectB);

            CreateDirectoryPackagesPropsWithVersionForPackageD(pathContext, projectA, "3.0.0");
            CreateDirectoryPackagesPropsWithVersionForPackageDAndE(pathContext, projectB, "2.0.0");

            var args = new string[] {
                    "restore",
                    solution.SolutionPath,
                    "-Verbosity",
                    "detailed",
                };

            // Act
            var r = CommandRunner.Run(
                Util.GetNuGetExePath(),
                pathContext.WorkingDirectory.Path,
                string.Join(" ", args),
                waitForExit: true);

            // Assert
            r.Success.Should().Be(true, because: r.AllOutput);

            var assetsFile = projectA.AssetsFile;

            assetsFile.Libraries.Should().HaveCount(3); // B, D & E.
            var eLibrary = assetsFile.Libraries.Single(e => e.Name.Equals("E"));
            eLibrary.Version.Should().Be(NuGetVersion.Parse("1.0.0"));
            // Local methods
            void CreateDirectoryPackagesPropsWithVersionForPackageD(SimpleTestPathContext pathContext, SimpleTestProjectContext projectContext, string version)
            {
                var directoryPackagesPropsContent =
                    @$"<Project>
                            <ItemGroup>
                                <PackageVersion Include=""D"" Version=""{version}"" />
                            </ItemGroup>
                        </Project>";
                var directoryName = Path.GetDirectoryName(projectContext.ProjectPath);
                File.WriteAllText(Path.Combine(directoryName, $"Directory.Packages.Props"), directoryPackagesPropsContent);
            }

            void CreateDirectoryPackagesPropsWithVersionForPackageDAndE(SimpleTestPathContext pathContext, SimpleTestProjectContext projectContext, string version)
            {
                var directoryPackagesPropsContent =
                    @$"<Project>
                            <ItemGroup>
                                <PackageVersion Include=""D"" Version=""{version}"" />
                                <PackageVersion Include=""E"" Version=""2.0.0"" />
                            </ItemGroup>
                        </Project>";
                var directoryName = Path.GetDirectoryName(projectContext.ProjectPath);
                File.WriteAllText(Path.Combine(directoryName, $"Directory.Packages.Props"), directoryPackagesPropsContent);
            }

            SimpleTestProjectContext CreateProject(SimpleTestPathContext pathContext, string name, SimpleTestProjectContext referencedProject = null)
            {
                var projectContext = SimpleTestProjectContext.CreateNETCoreWithSDK(
                    name,
                    pathContext.SolutionRoot,
                    "net472");

                projectContext.Properties.Add("ManagePackageVersionsCentrally", "true");
                projectContext.Properties.Add("CentralPackageTransitivePinningEnabled", "true");

                if (referencedProject != null)
                    projectContext.AddProjectToAllFrameworks(referencedProject);

                return projectContext;
            }

            void AddPackageReferenceToProject(SimpleTestProjectContext project)
            {
                var xml = project.GetXML();

                ProjectFileUtils.AddItem(
                    xml,
                    "PackageReference",
                    "D",
                    NuGetFramework.AnyFramework,
                    new Dictionary<string, string>(),
                    new Dictionary<string, string>());

                xml.Save(project.ProjectPath);
            }
        }

@marcin-krystianc
Copy link
Contributor Author

@nkolev92 I've merged dev branch into mine.

I have a hunch about some other potential bugs we may have with the transitive walking.

Yeah, it is quite messy if you start considering graphs in which different projects have a different sets of central package versions and both use transitive pinning. That is definitely not a common scenario anyway.
I hope that my change removes all the ambiguity. The transitive pining information is going to be respected only for the entry project and ignored for other projects, so it is much simpler to reason about it.
This change also enables some performance improvements in the graph walking algorithm when transitive pinning is enabled  (we can do some dictionary lookups instead of walking the entire graph).

@nkolev92 nkolev92 requested a review from jeffkl June 23, 2022 17:36
@ghost ghost added the Status:No recent activity No recent activity. label Jul 23, 2022
@ghost
Copy link

ghost commented Jul 23, 2022

This PR has been automatically marked as stale because it has no activity for 30 days. It will be closed if no further activity occurs within another 60 days of this comment. If it is closed, you may reopen it anytime when you're ready again, as long as you don't delete the branch.

@nkolev92
Copy link
Member

@jeffkl's out and he'll handle it when back.
We expect this change to go into 7.0.100 of the SDK, 6.4 of NuGet, 17.4 of VS.

It was pretty late in the game for 6.3 so we didn't feel comfortable merging.

@ghost ghost removed the Status:No recent activity No recent activity. label Jul 25, 2022
Copy link
Contributor

@jeffkl jeffkl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @marcin-krystianc !

@jeffkl jeffkl merged commit bae08de into NuGet:dev Aug 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community PRs created by someone not in the NuGet team
Projects
None yet
5 participants