mirror of
https://github.com/astral-sh/setup-uv.git
synced 2026-11-05 01:35:32 +00:00
fix: resolve ~= specifiers with PEP 440 semantics (#1111)
## Summary `~=` is the PEP 440 compatible release operator, but the resolver tries `semver.validRange()` first, and semver accepts `~=` as a tilde range: | specifier | semver | PEP 440 | |---|---|---| | `~=0.9` | `>=0.9.0 <0.10.0` | `>=0.9, ==0.*` | So with the default `highest` strategy, `version: "~=0.9"` (or `required-version = "~=0.9"` picked up through `version-file`) installs the highest 0.9.x instead of the highest 0.x. The same happens with `manifest-file`, where `tc.evaluateVersions` is semver-based. This skips the semver evaluation for specifiers containing `~=`, so they are only evaluated with `@renovatebot/pep440`. No semver range legitimately uses `~=`. Notes: - Only a bare two-segment `~=X.Y` resolved incorrectly. `~=X.Y.Z` gives the same result in both, and compound specifiers like `~=0.9, !=0.9.3` were already rejected by semver and fell through to PEP 440. - `minSatisfying` gets the same check for consistency. The lower bound is the same under both interpretations, so `lowest` didn't produce wrong results before. - A single-segment `~=1` is invalid PEP 440. semver used to resolve it to 1.x; it now fails with `No version found for ~=1`, same as other invalid specifiers. ## Test plan - Added tests for `~=0.9` with the default manifest and with `manifest-file`. Both returned `0.9.26` instead of `0.10.2` before the change. - `npm run all` on Ubuntu (WSL) with Node 24.19.0: build, Biome (no fixes), packaging, and 161 tests across 15 suites pass. The rebuilt `dist/setup/index.cjs` only differs in the changed lines. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
36b7b57007
commit
80fbe0140c
@@ -146,6 +146,28 @@ describe("download-version", () => {
|
||||
expect(mockGetAllVersions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves ~= with PEP 440 compatible release semantics", async () => {
|
||||
mockGetFirstMatchingVersion.mockImplementation(
|
||||
(predicate: (version: string) => boolean) =>
|
||||
["1.0.0", "0.10.2", "0.9.26"].find(predicate),
|
||||
);
|
||||
|
||||
const version = await resolveVersion("~=0.9", undefined);
|
||||
|
||||
expect(version).toBe("0.10.2");
|
||||
});
|
||||
|
||||
it("resolves ~= with PEP 440 compatible release semantics from manifest-file", async () => {
|
||||
mockGetAllVersions.mockResolvedValue(["1.0.0", "0.10.2", "0.9.26"]);
|
||||
|
||||
const version = await resolveVersion(
|
||||
"~=0.9",
|
||||
"https://example.com/custom.ndjson",
|
||||
);
|
||||
|
||||
expect(version).toBe("0.10.2");
|
||||
});
|
||||
|
||||
it("treats == exact pins as explicit versions", async () => {
|
||||
const version = await resolveVersion("==0.9.26", undefined);
|
||||
|
||||
|
||||
+6
-3
@@ -101937,7 +101937,7 @@ async function resolveVersion(versionInput, manifestUrl, resolutionStrategy = "h
|
||||
throw new Error(`No version found for ${versionInput}`);
|
||||
}
|
||||
async function findHighestSatisfyingVersion(versionSpecifier) {
|
||||
const semverRange = semver5.validRange(versionSpecifier);
|
||||
const semverRange = isCompatibleReleaseSpecifier(versionSpecifier) ? null : semver5.validRange(versionSpecifier);
|
||||
if (semverRange !== null) {
|
||||
const semverMatch = await getFirstMatchingVersion(
|
||||
(version3) => semver5.satisfies(version3, semverRange)
|
||||
@@ -101959,8 +101959,11 @@ async function findHighestSatisfyingVersion(versionSpecifier) {
|
||||
}
|
||||
return pep440Match;
|
||||
}
|
||||
function isCompatibleReleaseSpecifier(versionSpecifier) {
|
||||
return versionSpecifier.includes("~=");
|
||||
}
|
||||
function maxSatisfying2(versions, version3) {
|
||||
const maxSemver = evaluateVersions(versions, version3);
|
||||
const maxSemver = isCompatibleReleaseSpecifier(version3) ? "" : evaluateVersions(versions, version3);
|
||||
if (maxSemver !== "") {
|
||||
debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||
return maxSemver;
|
||||
@@ -101975,7 +101978,7 @@ function maxSatisfying2(versions, version3) {
|
||||
return void 0;
|
||||
}
|
||||
function minSatisfying3(versions, version3) {
|
||||
const minSemver = semver5.minSatisfying(versions, version3);
|
||||
const minSemver = isCompatibleReleaseSpecifier(version3) ? null : semver5.minSatisfying(versions, version3);
|
||||
if (minSemver !== null) {
|
||||
debug(`Found a version that satisfies the semver range: ${minSemver}`);
|
||||
return minSemver;
|
||||
|
||||
+16
-3
@@ -169,7 +169,9 @@ async function findHighestSatisfyingVersion(
|
||||
): Promise<string | undefined> {
|
||||
// This fast path assumes uv releases are semver-monotonic: newer manifest
|
||||
// entries always have higher versions, with no lower-version backports.
|
||||
const semverRange = semver.validRange(versionSpecifier);
|
||||
const semverRange = isCompatibleReleaseSpecifier(versionSpecifier)
|
||||
? null
|
||||
: semver.validRange(versionSpecifier);
|
||||
if (semverRange !== null) {
|
||||
const semverMatch = await getFirstMatchingVersion((version) =>
|
||||
semver.satisfies(version, semverRange),
|
||||
@@ -193,11 +195,20 @@ async function findHighestSatisfyingVersion(
|
||||
return pep440Match;
|
||||
}
|
||||
|
||||
// `~=` is the PEP 440 compatible release operator. semver accepts it as a
|
||||
// tilde range with a narrower upper bound (`~=0.5` as `<0.6.0` instead of
|
||||
// `<1.0`), so it must only be evaluated as PEP 440.
|
||||
function isCompatibleReleaseSpecifier(versionSpecifier: string): boolean {
|
||||
return versionSpecifier.includes("~=");
|
||||
}
|
||||
|
||||
function maxSatisfying(
|
||||
versions: string[],
|
||||
version: string,
|
||||
): string | undefined {
|
||||
const maxSemver = tc.evaluateVersions(versions, version);
|
||||
const maxSemver = isCompatibleReleaseSpecifier(version)
|
||||
? ""
|
||||
: tc.evaluateVersions(versions, version);
|
||||
if (maxSemver !== "") {
|
||||
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
|
||||
return maxSemver;
|
||||
@@ -218,7 +229,9 @@ function minSatisfying(
|
||||
versions: string[],
|
||||
version: string,
|
||||
): string | undefined {
|
||||
const minSemver = semver.minSatisfying(versions, version);
|
||||
const minSemver = isCompatibleReleaseSpecifier(version)
|
||||
? null
|
||||
: semver.minSatisfying(versions, version);
|
||||
if (minSemver !== null) {
|
||||
core.debug(`Found a version that satisfies the semver range: ${minSemver}`);
|
||||
return minSemver;
|
||||
|
||||
Reference in New Issue
Block a user