-
Notifications
You must be signed in to change notification settings - Fork 252
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
NU1608 warning is not evaluating prerelease versions combined with exclusive upper bounds properly #6434
Comments
I think what it's doing is correct. //cc |
When you consider the reason for putting an upper bound on a version range, I totally disagree. If you're following SemVer, you're trying to avoid breaking changes. Picking up a prerelease version of a new major completely defeats the point of putting the upper bound there. Moving to version 2 of PackageB means I have breaking changes. That means that that 2.0.0-beta1 also has breaking changes. If I've declared that PackageA does not work with 2.0 of PackageB, then I want consumers of PackageA to see a warning letting them know that they're violating the constraint. |
SemVer only declares the ordering. As I mentioned earlier, so 2.0.0 > 2.0.0.-beta1 So the way NuGet implements the ranges is correct. The way to go here would be something like marking the upper bound as "2.0.0-0" as that'd be the lowest possible prerelease version of 2.0.0. @emgarten can correct me if I'm wrong here. |
Basically, think of it this way. 1.0.0 < 2.0.0-beta1 <2.0.0 And you're saying 1.0.0 and higher works, and lower than 2.0.0 works |
Setting the range to
While I understand what you're saying, and from a general sorting order you are correct, I feel like falling back on that to explain this behavior is largely missing the point of what specifying version ranges is for. If I was specifying an inclusive upper bound, then I totally think that prerelease versions of 2.0.0 should be included. But if I've specified an exclusive upper bound, then I'm declaring that the major version of the package should never be 2. |
I get that this is confusing since you intend to limit the version to Ranges in NuGet have always worked this way and I think it would be a major breaking change to the most basic part of NuGet to change this behavior. I am not sure that this could or should be changed. |
Then that means there are a whole bunch of people out there handling version ranges on packages incorrectly. I suspect this behavior would be quite the surprise to most people, in fact. I totally get the whole "can't make breaking changes" thing, but this definitely feels like a bug. At least the |
I disagree with saying that this is a bug. It's definitely not the most intuitive thing, I agree, but having a value (2.0.0-beta1) mean one thing as an exact version, but another thing in a version range will be even more confusing. |
may "technically" not be a bug. but it is so outside the majority of peoples understanding and usage that it should change. |
So without changing anything in the existing behavior, how to specify |
I think this may be a regression. IIRC, earlier versions of NuGet (before But more importantly, that is the desired behaviour. No-one will ever want to specify "use any 1.x versions, but no 2.x versions, unless they are pre-release". That scenario is plain silly. Note that this is not a conversation about SemVer. SemVer is doing it's own thing and communicating the version info correctly. This is about how to use SemVer compliant version numbers in the context of version ranges, and the only sensible thing to do is to exclude any pre-release versions of the exclusive upper bound. |
Nuget documentation states:
That to me means that this is a bug. |
SemVer talks about ordering but I don't think it talks specifically about how to implement ranges. I.e. it says 2.0.0-beta1 < 2.0.0 but it doesn't say something like [1.0.0, 2.0.0) should be defined as 1.0.0 <= x < 2.0.0. That's Nuget's implementation of a specific syntax, isn't it? For reference, npm has special handling of pre-releases: https://docs.npmjs.com/misc/semver |
@nkolev92 and I talked this through. I also told him that I'd like to understand how we treat a few cases: @nkolev92 will try to squeeze some more investigation/analysis to this sprint or soon thereafter. |
I did some digging on this. At a low level VersionRange/VersionSpec behavior has never changed, What has changed is that stable packages now allow pre-release dependencies in some additional scenarios. Here is an example:
For packages.config scenario one does not allow 2.0.0-alpha, purely because it doesn't allow any pre-release packages in the graph by default since A 1.0.0 is stable. In scenario two when the parent package is pre-release, or if the user opts in to pre-release packages, or if a higher level package is being installed that is pre-release then pre-release is allowed everywhere and 2.0.0-alpha will be added. Example of this in NuGet 2.2.0:
For PackageReference stable packages have always been able to resolve pre-release dependencies by default. Which is likely the source of confusion here around how this works and what is allowed. History on allowing pre-release dependencies
packages.config
SummaryThis is very painful, but I do not see this as a regression. Since version ranges have always worked this way in NuGet changing it would be a major breaking change. A better way to expression version ranges would be to put them in terms of the lower bound only like npm. Example: |
Does this mean that the implementation was always different from the documentation? |
The documentation mentioned by @mauroservienti was a recent addition meant to explain that |
Please add this to the docs until this issue is proper resolved! |
it looks like the (publish from AppVeyor) - https://ci.appveyor.com/project/nlog/nlog-framework-logging/build/1.0.634
|
Since even the official documentation now links to this old issue, I will amend the workaround proposed here. Specifying an open range like But for now, we have to. <PackageVersion Include="Abc" Version="[2.0.0, 3.0.0-0)" NoWarn="NU5104" /> When using central package management, the |
NOTE: I noticed this works in VS, but not with dotnet CLI. I will update this comment as soon as I find out why and fix it. And again for future readers: I ended up adding a target in my root This is the main part: <Target Name="__PinMajorVersionsOnPackageReference" BeforeTargets="ResolvePackageAssets">
<ItemGroup>
<PackageReference Update="@(PackageReference)"
Version="[%(Version), $([System.String]::Copy('%(Version)').Substring(0, $([System.String]::Copy('%(Version)').IndexOf('.')))).999.999]"
Condition="'$([System.String]::Copy(`%(Version)`).Contains(`[`))' == 'false' AND
'$([System.String]::Copy(`%(Version)`).Contains(`(`))' == 'false' AND
'$([System.String]::Copy(`%(Version)`).Contains(`*`))' == 'false'" />
</ItemGroup>
</Target> A full It works by replacing all specific versions (those that do not themselves define a range or have wildcards in them) with a version range with the specific version as inclusive minimum and some reasonably high minor version within the same major version as the inclusive maximum. It will effectively replace I chose an inclusive upper bound instead of the exclusive |
To solve this issue in a backward compatible way, I suggest to support a syntax like |
There really is not a single developer on earth and beyond that would want to match version |
True, but it would fix an even bigger confusion. In reality, introducing this "huge" breaking change actually would only fix things! |
I just created a proposal for a warning to help guide customers to use a syntax that achieves the desired outcome, without causing breaking behaviour (well, as long as you don't count customers who use TreatWarningsAsErrors as breaking): #12423. Feel free to add a 👍 reaction to the new issue to upvote it and increase its prioritization. |
I was digging into how other ecosystems approach this and found that Node's
The empty line here meaning that the provided version doesn't match the range. Now, rust: use semver::{BuildMetadata, Prerelease, Version, VersionReq};
fn main() {
let req = VersionReq::parse(">=1.0.0, <2.0.0").unwrap();
// Check whether this requirement matches version 2.0.0-alpha.1 (no)
let version = Version {
major: 2,
minor: 0,
patch: 0,
pre: Prerelease::new("alpha.1").unwrap(),
build: BuildMetadata::EMPTY,
};
assert!(req.matches(&version)); // this will fail
// Check whether it matches 1.3.0 (yes it does)
let version = Version::parse("1.3.0").unwrap();
assert!(req.matches(&version));
}
So neither of these ecosystems behave the same as we do currently. I dug a bit further and both seem to point to section 5 of the supplemental
To my reading, this suggests that since the range Am I reading this wrong? Happy to be told so! |
Good find @baronfel ! For what it's worth, that ranges spec was created in 2020, so much more recent than NuGet's implementation (I can find NuGet's VersionRange.cs was copied from another repo in 2014, but I can't find the repo where it was copied from, to figure out how long that file has existed), and it's not linked to on https://semver.org, which makes it harder to discover. But if we change behaviour and it breaks people, at least we have something to point to as justification. For what it's worth, there have been a small number of times in my time in NuGet where changes that seemed safe enough, and a good idea benefiting most customers, but broke a team working on a major product, and I think once it even got escalated though to high level management. So I fully understand my team mates hesitation in introducing breaking changes. If we make the change and it breaks the entire Office build system for example, or some major customer, I wouldn't want to be the engineer who added the breaking change in the first place. |
That's not the complete behavior. Packages on server:
A 1.0.0 declares a [1.0.0, 2.0.0) dependency on B. If the project declares: A 1.0.0 as a dependency, restore will fail with NU1103. #6434 (comment) has a more detailed explanation, but prerelease versions will be allowed only when some other part of the graph specifically wants those prerelease versions. |
I'm not sure I follow that example. [Fact]
public async Task Test()
{
// Arrange
using var pathContext = new SimpleTestPathContext();
var logger = new TestLogger();
var projectName = "TestProject";
var projectPath = Path.Combine(pathContext.SolutionRoot, projectName);
var sources = new List<PackageSource> { new PackageSource(pathContext.PackageSource) };
// packages in the project.
var project1Json = @"
{
""version"": ""1.0.0"",
""frameworks"": {
""net472"": {
""dependencies"": {
""A"": ""[5.0.0,6.0.0)"",
""B"": ""[1.0.0,2.0.0)""
}
}
}
}";
var A500 = new SimpleTestPackageContext("A", "5.0.0");
A500.Dependencies.Add(new SimpleTestPackageContext("B", "[2.0.0,3.0.0)"));
// Packages on the server.
await SimpleTestPackageUtility.CreateFolderFeedV3Async(
pathContext.PackageSource,
PackageSaveMode.Defaultv3,
A500,
new SimpleTestPackageContext("A", "5.0.0-rc.1"),
new SimpleTestPackageContext("A", "5.1.0-rc.1"),
new SimpleTestPackageContext("A", "5.1.0"),
new SimpleTestPackageContext("A", "6.0.0-rc.1"),
new SimpleTestPackageContext("A", "6.0.0"),
new SimpleTestPackageContext("B", "1.0.0"),
new SimpleTestPackageContext("B", "2.0.0-rc.1"),
new SimpleTestPackageContext("B", "2.0.0"),
new SimpleTestPackageContext("B", "2.0.1"),
new SimpleTestPackageContext("B", "3.0.0")
);
// set up the project
var spec = JsonPackageSpecReader.GetPackageSpec(project1Json, projectName, Path.Combine(projectPath, $"{projectName}.json")).WithTestRestoreMetadata(); var request = new TestRestoreRequest(spec, sources, pathContext.UserPackagesFolder, logger)
{
LockFilePath = Path.Combine(projectPath, "project.assets.json"),
ProjectStyle = ProjectStyle.PackageReference
};
var command = new RestoreCommand(request);
// Act
var result = await command.ExecuteAsync();
// Assert
result.Success.Should().BeTrue(because: logger.ShowMessages());
result.LogMessages.Should().HaveCount(1);
result.LogMessages.Single().Code.Should().Be(NuGetLogCode.NU1605); // Downgrade because the project specified a direct dependency to B.
var targets = result.LockFile.Targets.Single();
targets.Libraries.Should().HaveCount(2);
targets.Libraries.First().Name.Should().Be("A");
targets.Libraries.First().Version.Should().Be(NuGetVersion.Parse("5.0.0"));
targets.Libraries.Last().Name.Should().Be("B");
targets.Libraries.Last().Version.Should().Be(NuGetVersion.Parse("1.0.0"));
} As long as no one in the graph specifies a prerelease dependency, no prerelease dependencies will selected. That's why https://learn.microsoft.com/en-us/nuget/reference/errors-and-warnings/nu1103 happens. |
I'm sorry, I made a typo and corrected that comment. There will be no conflict because of the range behavior that allows the prerelease of the next major to be selected that (due to semver) likely has a breaking API change thus cannot compile or even worse compile but have breaking runtime behavior. I've run into this behavior several times now over the year and it really is unwanted. Its not something developers can fix as they are dependent on the package maintainer. 100% of package maintainer really don't want to state they are compatible with a pre-release version of the next major even when that version hasn't been released. What package maintainers want to express is... HEY, I'm compatible with version 4.1+ when they specify Take this comment: Which links to: You really think that a developer that states the following wants to express they are compatible with 13.0 pre-release packages? <PackageReference Include="Newtonsoft.Json" Version="[12.0,13.0)" /> According to your team that is a maintainer error and they should have expressed it as I'll rephrase my earlier comment where I stated that introducing this "breaking change" would actually only benefit all developers as it actually would only fix things! |
So when will this "massive breaking change" that will only fix things finally be resolved? |
The NU1608 warning that was added in NuGet/NuGet.Client#1678 that displays when a package is outside of a dependency constraint is not working correctly when prerelease versions are combined with exclusive upper bounds.
The scenario:
1.0.0
has a dependency on PackageB defined as[1.0.0, 2.0.0)
1.0.0, 2.0.0-beta1, 2.0.0
<PackageReference Include="PackageA" Version="1.0.0" />
<PackageReference Include="PackageB" Version="2.0.0" />
In this case, an NU1608 warning is displayed as expected.
However, if the project's package references are instead:
<PackageReference Include="PackageA" Version="1.0.0" />
<PackageReference Include="PackageB" Version="2.0.0-beta1" />
No warning is displayed.
Given that PackageA's constraint on PackageB is
[1.0.0, 2.0.0)
, any package with a major version > 1 should fall outside of that version range constraint, regardless if it's a prerelease package or not.Based on that, referencing PackageB
2.0.0-beta1
should also cause an NU1608 warning.NuGet product used: VS UI
VS version: 2017 15.5.3
cc: @adamralph
The text was updated successfully, but these errors were encountered: