Skip to content

tags: accept newer platforms than declared, any-libc linux targets, exported arch lists - #48

Merged
jonyoder merged 4 commits into
mainfrom
feat/45-tags-gaps
Aug 27, 2026
Merged

jonyoder merged 4 commits into
mainfrom
feat/45-tags-gaps

Conversation

@jonyoder

Copy link
Copy Markdown
Collaborator

Addresses #45, items 1-3. Item 4 is withdrawn — see below.

Three additive changes to tags, one commit each.

Matcher.IsCompatibleOrNewer (the one that earns its keep)

The platform walks run downward from the declared version to a floor, so a Matcher answers "can the host I described run this wheel". A mirror or an offline bundle asks whether a durable declaration of that host should collect the wheel, and the two diverge over time: a set compiled once quietly stops matching wheels built for platforms released since. Accepting a too-new platform costs bytes; rejecting one costs availability, and for an air-gapped mirror a wheel not collected cannot be obtained at all.

Consumers currently work around this by baking their own libc and macOS constants and adding a drift test that goes red whenever PyPI publishes a new platform tag — a test that fails for reasons unrelated to whatever change is under review. This removes the need for all three.

The interpreter and ABI axes are not relaxed. Matcher now retains the set of (Interpreter, ABI) pairs it accepts on any platform, and a candidate qualifies as newer only if its pair is already in that set. Without it a cp314 wheel would ride in on a newer platform tag. Covered for the plain case and for the abi3/abi3t substitution, where a free-threaded target must not gain abi3 wheels at any platform version.

No rank is returned, deliberately: these tags fall outside the ordered set the Matcher generated, so there is no defensible priority.

parseMacosPlatformTag is new, since distro.go's parseLibcPlatformTag has no macOS counterpart. It is tested against the macosx_10_9_x86_64 shape that defeats a naive _x86_64 suffix test.

Two things the tests corrected while writing this, both worth a reviewer's eye:

  • Legacy aliases are judged by the glibc version they name, not by being old. manylinux2014 is newer than a 2.12 declaration and merely compatible with a 2.28 one. I asserted the opposite first; the rule is "requires more platform than declared", not "is recent".
  • muslMajor/muslMinor are stored separately from target.LibcMajor/Minor. My first version reused them and a test failed: comparing musllinux_2_0 against a glibc 2.28 floor reads as "older" and rejects the wheel. musl and glibc version numbers have no correspondence.

Target.CompileAnyLibc

Compile requires a Libc and platform tags are manylinux xor musllinux depending on it, so "linux x86_64, either libc" meant compiling two targets and unioning them by hand. Omitting the musl one is silent: every musllinux wheel stops matching with nothing to indicate a whole family was dropped. TestCompileAnyLibc_SingleFamilyDropsTheOther pins that failure so the helper's value is executable rather than argued.

The union is deduplicated, because the overlap is real: the bare linux_<arch> tag and the entire compatible tier are emitted by both families. glibcToMusl does not invent a glibc/musl correspondence; it returns the newest musl version this package generates, because musllinux tags are floors so the newest value accepts the widest set. A caller needing an exact musl floor should compile that target itself, and the doc says so.

Archs(os) and OSes()

Previously package-private, so a caller validating an operator-supplied os/arch string could reject a value but not report what it would have accepted, and generated documentation could not derive from the source of truth. The spellings are neither uniform nor guessable: windows uses amd64 where linux uses x86_64, macOS uses arm64 where linux uses aarch64.

Returns copies. TestArchs_ReturnsACopy pins that a caller cannot widen what Compile accepts, and TestArchs_AgreesWithValidate pins the property that motivates exporting at all: every arch reported compiles, and a value not reported does not.

Item 4 withdrawn: PyPy targets without an implementation version

The issue proposed rejecting Implementation: "pp" with no ImplMajor/ImplMinor, on the grounds that such a target silently collects no PyPy extension wheels.

I implemented it and three tests failed, one of them TestGolden/pp310_noabi_windows_amd64.json — a fixture generated by testdata/gen_goldens.py from pypa/packaging, recording that packaging produces exactly pp310-none-win_amd64 plus the compatible tier for that input. So the current behaviour is faithful to packaging, documented in Target's own doc comment, and pinned by both that fixture and TestPyPy_UnknownImplVersion.

Rejecting the input would therefore be a deliberate divergence from packaging and would delete a golden fixture, which is the opposite of this package's design posture. The behaviour is correct; the hazard belongs to the consumer, which should require an implementation version in its own declaration syntax rather than passing an under-specified target. Reverted, and I'll note it on the issue.

Verification

gofmt clean, go vet ./... clean, full module suite green including the packaging-generated golden fixtures.

🤖 Generated with Claude Code

jonyoder and others added 4 commits August 27, 2026 14:29
linuxArchs, macosArchs and windowsArchs were package-private with no
accessor, so a caller validating an operator-supplied "os/arch" string
could only construct a Target and inspect Compile's ErrUnsupportedTarget.
That is enough to decline a value and not enough to explain it: an error
message cannot list what would have been accepted, and generated
documentation cannot be derived from the source of truth.

The spellings are neither uniform nor guessable, which is exactly when a
good message matters. windows spells it "amd64" where linux spells it
"x86_64"; macOS spells it "arm64" where linux spells it "aarch64".

Archs returns a copy, so a caller cannot widen what Compile accepts by
mutating it, and TestArchs_ReturnsACopy pins that. TestArchs_AgreesWith-
Validate pins the property that motivates exporting at all: every arch
Archs reports compiles, and a value it does not report does not. Without
that test the exported view could drift from validate(), which is the
failure this replaces.

OSes() ships alongside because a caller validating "os/arch" needs both
halves and would otherwise hardcode the OS list instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#45)

The platform walks run DOWNWARD from the declared version to a floor, so a
Matcher answers "can the host I described run this wheel" -- correct for an
installer. A mirror or an offline bundle asks a different question: "should
a durable declaration of that host collect this wheel". The two diverge
over time, because a set compiled once silently stops matching wheels built
for platforms released since. Accepting a too-new platform costs bytes;
rejecting one costs availability, and for an air-gapped mirror a wheel not
collected is one the client cannot obtain at all.

IsCompatibleOrNewer also accepts a same-family, same-arch platform tag
whose version is ABOVE the declared one: manylinux/musllinux by libc
version, macosx by macOS version. Windows platform tags carry no version
axis, so there it degrades to IsCompatible.

The interpreter and ABI axes are NOT relaxed. Matcher now retains the set
of (Interpreter, ABI) pairs it accepts on any platform, and a candidate
qualifies as newer only if its pair is already in that set. Without this a
cp314 wheel would ride in on a newer platform tag. Covered for the plain
case and for the abi3/abi3t substitution, where a free-threaded target must
not gain abi3 wheels at any platform version.

No rank is returned, deliberately: these tags fall outside the ordered set
the Matcher generated, so there is no defensible priority for them.

Legacy aliases are judged by the glibc version they name rather than by
being old, so manylinux2014 is newer than a 2.12 declaration and merely
compatible with a 2.28 one. I had asserted the opposite first and the test
caught it; the rule is "requires more platform than declared", not "is
recent".

parseMacosPlatformTag is new because distro.go's parseLibcPlatformTag has
no macOS counterpart. It is tested against the macosx_10_9_x86_64 shape
that defeats a naive "_x86_64" suffix test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

Compile requires a Libc for a linux Target and linuxPlatformTags emits
manylinux XOR musllinux depending on it, so a caller expressing "linux
x86_64, either libc" has to compile two Targets and union them by hand.
Omitting the musl one is silent: every musllinux wheel stops matching with
nothing to indicate that a whole wheel family was dropped.
TestCompileAnyLibc_SingleFamilyDropsTheOther pins that failure so the
helper's value is executable rather than argued in prose.

The union is deduplicated, because the overlap is real: the bare
linux_<arch> tag and the entire compatible tier are emitted by both
families. glibc tiers rank ahead of musl ones, and Rank is part of the
published contract, so that ordering is asserted.

Two things worth review attention.

muslMajor/muslMinor are stored on the Matcher separately from
target.LibcMajor/Minor rather than reusing them. My first version reused
them and TestCompileAnyLibc_NewerWorksForBothFamilies failed: comparing
musllinux_2_0 against a glibc 2.28 floor reads as "older" and rejects the
wheel. musl and glibc version numbers have no correspondence, and
collapsing them is a category error.

glibcToMusl does not try to invent a correspondence either. It returns the
newest musl version this package generates, because musllinux tags are
floors and the newest value therefore accepts the widest set. A caller
needing an exact musl floor should compile that Target itself; the doc
comment says so.

t.Libc is ignored rather than honoured, so the call cannot half-apply, and
TestCompileAnyLibc_IgnoresLibcField pins it. Validation is not bypassed:
a bad arch or a missing libc version still returns ErrUnsupportedTarget.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonyoder
jonyoder merged commit b9a950b into main Aug 27, 2026
4 checks passed
@jonyoder
jonyoder deleted the feat/45-tags-gaps branch August 27, 2026 20:21
jonyoder added a commit that referenced this pull request Aug 28, 2026
Dates [Unreleased] as 0.9.0 (2026-08-28) and adds its compare link. CHANGELOG only.

A minor bump under the 0.x policy because two changes alter observable behaviour: reqtxt now recognizes --all-releases/--only-final/--use-feature (an unrecognized option was assumed boolean, so its argument became a fabricated requirement), and a standalone --hash line is accepted rather than rejected, matching pip.

Contents since v0.8.0: tags manylinux floor fix (#43), marker EvaluateUndecidable/Variables (#47), tags IsCompatibleOrNewer/CompileAnyLibc/Archs (#48), reqtxt Source and pip-parity fixes (#49), dependency bumps (#50).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant