From cef5c756ae99063ca17371def23e0aad8ba90586 Mon Sep 17 00:00:00 2001 From: Jonathan Yoder Date: Thu, 27 Aug 2026 07:06:18 -0400 Subject: [PATCH] tags: floor riscv64 and loongarch64 at glibc 2.17 like every other arch (#42) Both architectures were inherited from uv's floor table, which floors them at glibc 2.31 and 2.36 and records no legacy alias for either. Because a target below its floor claims no manylinux tag at all rather than a shortened list, a loongarch64 host on glibc 2.35 was offered 42 tags where pypa/packaging 26.2 computes 582, so no manylinux wheel whatsoever. riscv64 on glibc 2.28 gave 42 against 393. Both now match packaging exactly. Raising the floor alone is not enough: packaging's legacy map is keyed by glibc version alone and is therefore architecture-independent, so manylinux2014_ applies to these architectures too. Direction matters for the consumers. This package answers whether a wheel is 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 cannot then obtain. Being wider only costs bytes. Replaces TestLinux_NarrowNonX86Floors, which pinned the old narrow output, with TestLinux_NonX86FloorsAreUniform: a parity assertion against aarch64 that keeps holding as the manylinux series grows, and that fails if any single architecture is re-narrowed. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 30 +++++++++++++++ tags/doc.go | 21 ----------- tags/generate.go | 53 +++++++++++++------------- tags/target_test.go | 90 ++++++++++++++++++++++----------------------- 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4fda7..9ea3c3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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_` 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 diff --git a/tags/doc.go b/tags/doc.go index b47e1ab..9ff6d70 100644 --- a/tags/doc.go +++ b/tags/doc.go @@ -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. // diff --git a/tags/generate.go b/tags/generate.go index ae4b680..8b6c42f 100644 --- a/tags/generate.go +++ b/tags/generate.go @@ -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. @@ -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 @@ -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 _0. +// minor from manylinuxFloor -- 2.5 on x86_64/i686, 2.17 on every other +// architecture; any other major goes down to _0. // // So "below the floor" has two different answers, and conflating them is how this // function's narrower predecessor looked correct: diff --git a/tags/target_test.go b/tags/target_test.go index 7dedd46..f15f61f 100644 --- a/tags/target_test.go +++ b/tags/target_test.go @@ -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_ 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, "")) + } + 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_", + "reference architecture must reach the 2.17 floor") + require.Contains(t, want, "manylinux2014_", + "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) }) } }