Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,36 @@ mistaken for a safe patch upgrade.

## [Unreleased]

### Fixed

- `tags`: `riscv64` and `loongarch64` targets now claim the same manylinux
series as every other non-x86 architecture — floored at glibc 2.17, with the
`manylinux2014_<arch>` legacy alias — matching pypa/packaging.

These two architectures were inherited from uv's floor table, which floors
them at glibc 2.31 and 2.36 respectively and records no legacy alias for
either. Because a target below its floor claims no manylinux tag **at all**
rather than a shortened list, the effect was not marginal: a `loongarch64`
host on glibc 2.35 (Loongnix, Debian — a real configuration) was offered 42
tags where packaging 26.2 computes 582, so no manylinux wheel whatsoever.
`riscv64` on glibc 2.28 gave 42 against 393. Both figures now match packaging
exactly.

Both causes had to be fixed together: raising the floor alone leaves the alias
missing, because packaging's legacy map is keyed by glibc version *alone* and
is therefore architecture-independent.

Callers that compile a target for either architecture will now accept
manylinux wheels they previously rejected. That is the point — this package
answers "is this wheel installable on the declared target", and being narrower
than pip means rejecting wheels pip would install. For a mirror or an offline
bundle that shows up as silently omitted content the client cannot then
obtain.

`TestLinux_NarrowNonX86Floors`, which pinned the old narrow output, is
replaced by `TestLinux_NonX86FloorsAreUniform`, a parity assertion against
`aarch64` that holds as the manylinux series grows.

## [0.8.0] - 2026-08-19

### Breaking
Expand Down
21 changes: 0 additions & 21 deletions tags/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,6 @@
// with macOS 11, so as a *declared* target that combination can only be a
// mistake.
//
// - riscv64 and loongarch64 targets get NARROWER manylinux coverage than
// packaging, and the gap is large. Two independent causes, both in
// manylinuxFloor: this package floors those architectures at glibc 2.31 and
// 2.36 respectively (uv's values) where packaging floors every non-x86
// architecture at 2.17; and it records no legacy alias for them, where
// packaging's legacy map is keyed by glibc version ALONE and is therefore
// architecture-independent, so pip does emit manylinux2014_riscv64 and
// manylinux2014_loongarch64. Fixing only the floor number would still not
// produce the alias.
//
// Because a target below its floor yields no manylinux tag at all rather
// than a shortened list, the effect is not marginal. Measured against
// packaging 26.2 for a CPython 3.12 target, in order: loongarch64 on glibc
// 2.35 gives packaging 582 tags and this package 42, missing 540;
// riscv64 on glibc 2.28 gives 393 against 42. glibc 2.35 loongarch64 is a
// real configuration (Loongnix, Debian), and such a host would be offered no
// manylinux wheel whatsoever. The current behavior is pinned by
// TestLinux_NarrowNonX86Floors so that changing the floors is a deliberate
// test update; those tags are not otherwise covered by the golden fixtures,
// since a generated fixture would record packaging's wider answer.
//
// - The exact ABI of a pre-3.3 CPython target always carries the UCS-4 "u"
// flag, e.g. cp27mu, whatever OS the target names.
//
Expand Down
53 changes: 28 additions & 25 deletions tags/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,20 +321,26 @@ type legacyManylinuxAlias struct {

// manylinuxFloor is the oldest glibc version (major, minor) a given
// architecture's manylinux tags may claim, plus any legacy aliases that
// architecture supports. Values are uv's (github.com/astral-sh/uv) floor table,
// which matches pypa/packaging for x86_64/i686/aarch64/armv7l/ppc64/ppc64le/
// s390x but floors riscv64 at 2.31 and loongarch64 at 2.36.
// architecture supports. Every non-x86 architecture floors at glibc 2.17 and
// carries the manylinux2014 alias, matching pypa/packaging.
//
// ⚠️ Those last two are NARROWER than pypa/packaging 26.2, which floors every
// non-x86 architecture at glibc 2.17 and does list both in its _ALLOWED_ARCHS
// (verified against the installed 26.2; an earlier version of this comment
// claimed packaging did not recognize them at all, which is no longer true, if
// it ever was). So a riscv64 or loongarch64 target here declines manylinux tags
// between 2.17 and its floor that pip on the same host would accept. That is a
// real divergence on real architectures, inherited from #18632, and is
// deliberately left alone here rather than changed as a drive-by: it is the
// opposite direction from the cross-major fix below and deserves its own
// decision. Tracked for follow-up.
// This table began as uv's (github.com/astral-sh/uv) floor table, which agrees
// with packaging for x86_64/i686/aarch64/armv7l/ppc64/ppc64le/s390x but floors
// riscv64 at 2.31 and loongarch64 at 2.36 and records no legacy alias for
// either. Those two entries have been widened to packaging's values, because a
// target below its floor claims no manylinux tag AT ALL rather than a shortened
// list: a loongarch64 host on glibc 2.35 (Loongnix, Debian -- a real
// configuration) was offered 42 tags where pip computes 582, so no manylinux
// wheel whatsoever. Both fixes are needed together; the floor number alone
// leaves the alias missing, since packaging's legacy map is keyed by glibc
// version ALONE and is therefore architecture-independent.
//
// Direction matters for the consumers: this package answers "is this wheel
// installable on the declared target", and the thing that ultimately installs
// is pip. Being NARROWER than pip rejects wheels pip would accept, which for a
// mirror or an offline bundle means silently omitting content the client then
// cannot get. Being wider only costs bytes. Where the two upstreams disagree,
// follow the one pip uses.
//
// Rule (Global Constraints): a target declaring glibc (2, m) accepts a
// manylinux_2_y tag iff m >= y, down to this floor.
Expand All @@ -348,16 +354,13 @@ var manylinuxFloor = map[string]struct {
"i686": {2, 5, []legacyManylinuxAlias{
{"manylinux1", 2, 5}, {"manylinux2010", 2, 12}, {"manylinux2014", 2, 17},
}},
"aarch64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"armv7l": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"ppc64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"ppc64le": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"s390x": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"riscv64": {2, 31, nil},
// loongarch64's floor is uv's 2.36. pypa/packaging 26.2 does recognize this
// architecture -- it is in _ALLOWED_ARCHS -- but floors it at 2.17 like
// every other non-x86 arch; see the divergence note above.
"loongarch64": {2, 36, nil},
"aarch64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"armv7l": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"ppc64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"ppc64le": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"s390x": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"riscv64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
"loongarch64": {2, 17, []legacyManylinuxAlias{{"manylinux2014", 2, 17}}},
}

// linuxPlatformTags builds the ordered platform-tag list for a linux
Expand Down Expand Up @@ -429,8 +432,8 @@ type glibcVersion struct {
// will stop.
//
// Within the floor's own major the walk bottoms out at that architecture's floor
// minor from manylinuxFloor -- 2.5 on x86_64/i686, 2.17 on most others, but 2.31
// on riscv64 and 2.36 on loongarch64; any other major goes down to <major>_0.
// minor from manylinuxFloor -- 2.5 on x86_64/i686, 2.17 on every other
// architecture; any other major goes down to <major>_0.
//
// So "below the floor" has two different answers, and conflating them is how this
// function's narrower predecessor looked correct:
Expand Down
90 changes: 44 additions & 46 deletions tags/target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,56 +199,54 @@ func TestLinux_CrossGlibcMajor(t *testing.T) {
})
}

// TestLinux_NarrowNonX86Floors pins the riscv64/loongarch64 manylinux
// divergence documented in doc.go. It asserts THIS PACKAGE'S current, narrower
// output rather than upstream's, which is why it is hand-written instead of a
// generated golden fixture: a fixture from packaging would record the wider
// answer and fail.
// TestLinux_NonX86FloorsAreUniform asserts that every non-x86 architecture
// claims the same manylinux series as every other, i.e. floors at glibc 2.17
// and carries the manylinux2014 alias, matching pypa/packaging.
//
// It exists so that changing manylinuxFloor is a deliberate test update rather
// than silent drift, and so the size of the gap is written down somewhere
// executable. Measured against packaging 26.2 for these same targets, in order:
// loongarch64/glibc 2.35 gives packaging 582 tags to our 42, and
// riscv64/glibc 2.28 gives 393 to our 42.
// riscv64 and loongarch64 used to be the exceptions: inherited from uv's floor
// table, they floored at 2.31 and 2.36 with no legacy alias. Because a target
// below its floor claims no manylinux tag AT ALL rather than a shortened list,
// that was not marginal -- a loongarch64 host on glibc 2.35 got 42 tags where
// packaging computes 582, so no manylinux wheel whatsoever.
//
// If you are here because you widened the floors to 2.17 and this test failed:
// that is the intended interaction. Update the expectations, and add the legacy
// alias too -- packaging's legacy map is keyed by glibc version alone, so
// manylinux2014_<arch> applies to these architectures despite manylinuxFloor
// recording no alias for them.
func TestLinux_NarrowNonX86Floors(t *testing.T) {
for _, tc := range []struct {
arch string
libcMajor, libcMinor int
upstreamTagCount int
}{
{"loongarch64", 2, 35, 582},
{"riscv64", 2, 28, 393},
} {
t.Run(tc.arch, func(t *testing.T) {
m, err := Target{
Implementation: "cp", PyMajor: 3, PyMinor: 12,
OS: "linux", Arch: tc.arch,
Libc: "glibc", LibcMajor: tc.libcMajor, LibcMinor: tc.libcMinor,
}.Compile()
require.NoError(t, err)
ss := tagStrings(m.Tags())
// This is written as a PARITY assertion against a reference architecture rather
// than as a hardcoded tag list on purpose: it is the actual invariant, and it
// keeps holding when the manylinux series is extended. Re-narrowing any single
// architecture breaks it, which is what the old pinning test was for.
func TestLinux_NonX86FloorsAreUniform(t *testing.T) {
// Platform tags for arch at a fixed glibc, with the architecture suffix
// normalized away so two architectures' series can be compared directly.
seriesFor := func(t *testing.T, arch string) []string {
t.Helper()
m, err := Target{
Implementation: "cp", PyMajor: 3, PyMinor: 12,
OS: "linux", Arch: arch,
Libc: "glibc", LibcMajor: 2, LibcMinor: 35,
}.Compile()
require.NoError(t, err)

var out []string
for _, tag := range m.Tags() {
out = append(out, strings.ReplaceAll(tag.Platform, arch, "<arch>"))
}
return out
}

// aarch64 was never affected, so it is the reference for what a non-x86
// architecture should claim.
want := seriesFor(t, "aarch64")

// Below its floor this target claims no manylinux tag at all -- not
// a shortened list -- so the only platform tag left is the bare one.
for _, s := range ss {
assert.NotContains(t, s, "manylinux",
"%s glibc %d.%d is below its floor and must claim no manylinux tag",
tc.arch, tc.libcMajor, tc.libcMinor)
}
assert.Contains(t, ss, "cp312-cp312-linux_"+tc.arch)
// Guard the reference itself, so a regression there cannot make the parity
// assertions below vacuously pass.
require.Contains(t, want, "manylinux_2_17_<arch>",
"reference architecture must reach the 2.17 floor")
require.Contains(t, want, "manylinux2014_<arch>",
"reference architecture must carry the legacy alias")

// The magnitude of the divergence, recorded executably. 42 is the
// bare-linux-only tag count; upstreamTagCount is what packaging 26.2
// produces for the same target.
assert.Len(t, ss, 42)
assert.Less(t, len(ss), tc.upstreamTagCount,
"this package is narrower than packaging here, by design but not by preference")
for _, arch := range []string{"riscv64", "loongarch64", "armv7l", "ppc64", "ppc64le", "s390x"} {
t.Run(arch, func(t *testing.T) {
assert.Equal(t, want, seriesFor(t, arch),
"%s must claim the same manylinux series as aarch64", arch)
})
}
}
Expand Down