Skip to content

Correctly identify transitive package references#5397

Closed
ViktorHofer wants to merge 2 commits intoNuGet:devfrom
ViktorHofer:TransitivePackageReferencesMixedWithProjectReferences
Closed

Correctly identify transitive package references#5397
ViktorHofer wants to merge 2 commits intoNuGet:devfrom
ViktorHofer:TransitivePackageReferencesMixedWithProjectReferences

Conversation

@ViktorHofer
Copy link
Contributor

@ViktorHofer ViktorHofer commented Sep 3, 2023

Bug

Fixes NuGet/Home#10368

Regression? Last working version: Never worked before

Description

When a transitive package dependency has the same identity as a direct project dependency that is differenced by a different inner build (TFM), the dependency is incorrectly resolved as a project.

The resolver logic invoked by the dependency walker only knows about which projects are referenced but not by which TFM. To correctly isolate inner builds from each other and avoid incorrectly promoting a project dependency, pass the nearest project dependencies to the dependency resolving logic.

PR Checklist

  • PR has a meaningful title

  • PR has a linked issue.

  • Described changes

  • Tests

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

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

Fixes NuGet/Home#10368

When a transitive package dependency has the same identity as a direct
project dependency that is differenced by a different inner build (TFM),
the dependency is incorrectly resolved as a project.

The resolver logic invoked by the dependency walker only knows about which
projects are referenced but not by which TFM. To correctly isolate inner
builds from each other and avoid incorrectly promoting a project
dependency, pass the nearest project dependencies to the dependency
resolving logic.
Copy link
Member

@zivkan zivkan left a comment

Choose a reason for hiding this comment

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

I'm still trying to wrap my head around the original issue and the fix, to ensure I can't foresee unintended consequences from side effects. But if I understand correctly, say I have Contoso.Utilities as a transitive package, if I clone the repo and add Contoso.Utilities.csproj to my solution, then the build will use that project's dll? I can make changes to Contoso.Utilities and test with my app, without neeing to publish packages or changing dependency versions?

If so, that's very cool, and brings us closer to what we had with project.json back in the dnx/.NET Core 1.0 preview days.

@ViktorHofer
Copy link
Contributor Author

ViktorHofer commented Sep 6, 2023

I'm still trying to wrap my head around the original issue and the fix, to ensure I can't foresee unintended consequences from side effects. But if I understand correctly, say I have Contoso.Utilities as a transitive package, if I clone the repo and add Contoso.Utilities.csproj to my solution, then the build will use that project's dll? I can make changes to Contoso.Utilities and test with my app, without neeing to publish packages or changing dependency versions?

Here's my example project as a zip: tfmnugetoverlap.zip. Do a dotnet restore tfmnugetoverlap\main\main.csproj to repro the restore failure.

I'm really not a fan of optional types in public APIs. I prefer a new overload, which would also prevent a breaking ABI change. A new overload would also keep the convention of keeping the CancellationToken as the last parameter.

Agreed. Since I opened the PR I also explored another solution that would do the following instead:

The general problem is that the PackageSpecReferenceDependencyProvider which gets added to the restore context isn't aware of target frameworks / inner builds:

private readonly Dictionary<string, ExternalProjectReference> _externalProjectsByPath
= new Dictionary<string, ExternalProjectReference>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ExternalProjectReference> _externalProjectsByUniqueName
= new Dictionary<string, ExternalProjectReference>(StringComparer.OrdinalIgnoreCase);

Because of that, the individual TFM restore operations consider all project dependencies, even the ones that conditionally are excluded for the current framework. I experimented with updating PackageSpecReferenceDependencyProvider to encode which projects apply to which framework. The challenging part is calculating the TFM aware dependency tree though. NuGet / msbuild creates a dgspec.json file which lists all the dependencies but the TFM selection doesn't happen until NuGet actually resolves the dependency. So building that dependency tree upfront might cause unintended side-effects which I'm not too excited to make given that this code base is unfamiliar to me.

So all in all, I guess the current approach (passing the actual dependencies in to the resolver logic vs updating the PackageSpecReferenceDependencyProvider to be TFM aware and only return the matching dependencies) is fine.

@ViktorHofer ViktorHofer marked this pull request as ready for review September 7, 2023 15:20
@ViktorHofer ViktorHofer requested a review from a team as a code owner September 7, 2023 15:20
@nkolev92
Copy link
Member

nkolev92 commented Sep 8, 2023

Thanks for the contribution @ViktorHofer! I know this must have been a lot of work.

The general problem is that the PackageSpecReferenceDependencyProvider which gets added to the restore context isn't aware of target frameworks / inner builds:

Can you elaborate on this?
It has methods that take a framework, so it can do the proper compat selection.

public Library GetLibrary(LibraryRange libraryRange, NuGetFramework targetFramework)

For example, I recently did work there: df7d995.

The packagespecreferenceprovider should be called in the context of calling a dependency provider.
When the package spec reference provider is asked for certain dependencies it is asked with a framework: https://github.com/NuGet/NuGet.Client/blob/40648c3cb3de37cf62b194accdc8e7117b36245f/src/NuGet.Core/NuGet.DependencyResolver.Core/ResolverUtility.cs#L161C38-L161C59.

You are probably onto something, but with the reasoning given here, I can't confirm it's the right thing yet.

When a transitive package dependency has the same identity as a direct project dependency that is differenced by a different inner build (TFM), the dependency is incorrectly resolved as a project.

What does same identity mean for you here? Same ID? Same ID/Version?

@nkolev92
Copy link
Member

nkolev92 commented Sep 8, 2023

fwiw, I started writing a test so I can debug the behavior.
Here's the code.

[Fact] public async Task ExecuteAsync_WithConditionalProjectAndPackageReferences() {     // Arrange     using var context = new SourceCacheContext();     using var pathContext = new SimpleTestPathContext();     // Add nuget.org for the extra package.     pathContext.Settings.AddSource("nuget.org", "https://api.nuget.org/v3/index.json");      var mainProject = "main";     var mainProjectPath = Path.Combine(pathContext.SolutionRoot, mainProject);      var childProject = "System.Numerics.Vectors";     var childProjectPath = Path.Combine(pathContext.SolutionRoot, childProject);      var mainProjectJson = @"     {       ""version"": ""1.0.0"",       ""frameworks"": {         ""netstandard2.0"": {             ""imports"": [                         ""net461"",                         ""net462"",                         ""net47"",                         ""net471"",                         ""net472"",                         ""net48"",                         ""net481""                       ],             ""assetTargetFallback"": true,             ""dependencies"": {                 ""System.Memory"": {                               ""target"": ""Package"",                               ""version"": ""[4.5.5, )""                             },                   ""NETStandard.Library"": {                               ""suppressParent"": ""All"",                               ""target"": ""Package"",                               ""version"": ""[2.0.3, )"",                               ""autoReferenced"": true                             }             }         },         ""net8.0"": {             ""imports"": [                         ""net461"",                         ""net462"",                         ""net47"",                         ""net471"",                         ""net472"",                         ""net48"",                         ""net481""                       ],             ""assetTargetFallback"": true,             ""dependencies"": {             }         }       }     }";     PackageSpec systemNumericsVectorPackageSpec = ProjectTestHelpers.GetPackageSpec(childProject, pathContext.SolutionRoot, "net8.0",         useAssetTargetFallback: true, "net461\",\"net462\",\"net47\",\"net471\",\"net472\",\"net48\",\"net481");     PackageSpec mainPackageSpec = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec(mainProject, pathContext.SolutionRoot, mainProjectJson);     var settings = Settings.LoadDefaultSettings(pathContext.SolutionRoot);     mainPackageSpec.RestoreMetadata.ConfigFilePaths = settings.GetConfigFilePaths();     mainPackageSpec.RestoreMetadata.Sources = SettingsUtility.GetEnabledSources(settings).ToList();     mainPackageSpec.RestoreMetadata.FallbackFolders = SettingsUtility.GetFallbackPackageFolders(settings).ToList();     mainPackageSpec.RestoreMetadata.PackagesPath = SettingsUtility.GetGlobalPackagesFolder(settings);      mainPackageSpec.RestoreMetadata.TargetFrameworks.Single(e => e.FrameworkName.Equals(NuGetFramework.Parse("net8.0")))         .ProjectReferences.Add(new ProjectRestoreReference()     {         ProjectUniqueName = systemNumericsVectorPackageSpec.RestoreMetadata.ProjectUniqueName,         ProjectPath = systemNumericsVectorPackageSpec.RestoreMetadata.ProjectPath,     });      var logger = new TestLogger();     var request = ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, mainPackageSpec, systemNumericsVectorPackageSpec);      var restoreCommand = new RestoreCommand(request);     RestoreResult result = await restoreCommand.ExecuteAsync();     await result.CommitAsync(logger, CancellationToken.None);      result.Success.Should().BeTrue(because: logger.ShowMessages()); }

I used test\NuGet.Core.Tests\NuGet.Commands.Test\RestoreCommandTests\RestoreCommandTests.cs as the class

@nkolev92
Copy link
Member

nkolev92 commented Sep 8, 2023

I took a stab fixing the PackageSpecReferenceDependencyProvider. It basically gets asked for a library, but it ignores the version. If the version matches, then I think the project should be preferred over the package.
See equivalent for packages:

var packageVersion = packageVersions?.FindBestMatch(libraryRange.VersionRange, version => version);
if (packageVersion != null)
{
return new LibraryIdentity
{
Name = libraryRange.Name,
Version = packageVersion,
Type = LibraryType.Package
};
}

I'm not really sure if direct refs are the ones that should be checked when including a project in the graph. I think if you have an extra layer of dependencies, then you'd likely have issues, like for example, if System.Numerics vector was a transitive project reference.

My changes are in https://github.com/NuGet/NuGet.Client/tree/dev-nkolev92-10368, here's what happens when I run dotnet list package

C:\Code\Temp\tfmnugetoverlap [main]> dotnet list .\main\main.csproj package --include-transitive
Project 'main' has the following package references
   [netstandard2.0]:
   Top-level Package             Requested   Resolved
   > NETStandard.Library   (A)   [2.0.3, )   2.0.3
   > System.Memory               4.5.5       4.5.5

   Transitive Package                            Resolved
   > Microsoft.NETCore.Platforms                 1.1.0
   > System.Buffers                              4.5.1
   > System.Numerics.Vectors                     4.4.0
   > System.Runtime.CompilerServices.Unsafe      4.5.3

   [net8.0]:
   Transitive Package             Resolved
   > System.Numerics.Vectors      1.0.0

(A) : Auto-referenced package.

Now the trick is that this is a very tricky change. I'm not really sure it's the right thing just yet.

I'll let the CI run all the tests and revisit after at again when I have more time.

@aortiz-msft aortiz-msft added the Merge next release PRs that should not be merged until the dev branch targets the next release label Sep 8, 2023
@ghost ghost added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Sep 15, 2023
@ghost
Copy link

ghost commented Sep 15, 2023

This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 90 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.

@ViktorHofer
Copy link
Contributor Author

I'm on holiday now and will respond when I'm back. Thanks for taking a look so far.

@ghost ghost removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Sep 18, 2023
@ghost ghost added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Sep 25, 2023
@ghost
Copy link

ghost commented Sep 25, 2023

This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 90 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.

@ViktorHofer
Copy link
Contributor Author

Met with @nkolev92 offline and talked about different solutions to this problem. I like @nkolev92's proposal which avoids adding new public API and meets our requirements. We identified that there's still a problem in regards to choosing P2P over PackageReferences but we believe this is a super corner case that people shouldn't hit today.

Closing this PR as he will open a new one with the test and the fix.

@ghost ghost removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Oct 2, 2023
@ViktorHofer ViktorHofer closed this Oct 2, 2023
@ViktorHofer ViktorHofer deleted the TransitivePackageReferencesMixedWithProjectReferences branch October 2, 2023 19:22
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 Merge next release PRs that should not be merged until the dev branch targets the next release

Projects

None yet

4 participants