G27 — wheel tag / deployment-claim verification¶
Registry: UC-TC-wheel-deployment-claims (partial)
Effort: L · Risk: low
Origin: SciPy / Scientific-Python Roadmap §3.
Generalizes G10 (Linux glibc floor only) across
platforms and claim types; reuses G13's
architecture-guard machinery and G12's
hardening-flag capture.
Status note (delivered scope): the Linux GLIBCXX/CXXABI floor
extension, the musllinux glibc-dependency check, the macOS
deployment-target check, the wheel-tag architecture-mismatch check, and a
narrowly-scoped RPATH-portability / vendored-dependency-closure check pair
are done (all reachable via the same --env-matrix/runtime_floors
declared-constraint mechanism G10 already shipped for the plain GLIBC
case — see usecase-registry.yaml's next_steps for the full breakdown).
The RPATH/closure pair deliberately does not attempt the general "is this
dependency permitted by the wheel's platform tag" question (that needs a
real per-manylinux/musllinux-tag allowed-SONAME policy, out of scope here —
see "Out of scope"); it only catches internally-inconsistent evidence
(an absolute RPATH/RUNPATH entry; a vendored-looking dependency with no
bundling mechanism at all). Windows, CPU-ISA-baseline, the full closure
policy, and end-to-end CLI auto-derivation from a compared wheel's own
filename tag (still requires an explicit --env-matrix today, for every
check including G10's original GLIBC one) remain planned — see "Out of
scope" below and the registry entry.
Problem¶
A wheel's filename tag and package metadata make explicit promises about where its binaries will run. G10 (Linux manylinux glibc floor) is now done. The same class of "claim vs. binary evidence" mismatch exists — largely unchecked — across every platform a scientific-Python wheel matrix targets:
- Linux: manylinux tag vs. required
GLIBC_*; musllinux compatibility;GLIBCXX_*/CXXABI_*floor; RPATH/RUNPATH correctness; dependencies outside the permittedmanylinux/musllinuxwheel closure. - macOS: wheel deployment target (
MACOSX_DEPLOYMENT_TARGETembedded in the wheel tag) vs. the Mach-OLC_VERSION_MIN_MACOSX/LC_BUILD_VERSIONminimum-OS load command; SDK symbol availability; architecture consistency (x86_64vsarm64vsuniversal2); OpenBLAS vs. Accelerate backend identification. - Windows: architecture/subsystem consistency; UCRT/MSVC runtime requirements; accidental dependency on a developer-machine DLL not present on a clean target; MinGW vs. MSVC runtime transitions.
- Cross-platform: CPU ISA baseline (SSE/AVX/AVX2/AVX-512 on x86,
NEON/SVE feature requirements on ARM) vs. the wheel tag's implied baseline;
wheel tag vs. the binary's actual recorded architecture (already partly
covered by G13's
e_machine/EI_CLASSguard for arbitrary binary pairs, but not tied to the wheel tag's claim specifically); unexpected OpenMP runtime additions; changed security-hardening properties (G12's detectors exist, but aren't yet cross-checked against a wheel's platform claim).
An accidental AVX2 instruction, an understated macOS deployment target, or a too-new glibc symbol makes an otherwise API-compatible wheel simply fail to load on part of its advertised install base — a deployment-tier break with no native-ABI signal at all.
Goal & acceptance criteria¶
- [x] Parse the wheel filename tag (PEP 425/600 platform tags:
manylinux_2_28_x86_64,musllinux_1_2_aarch64,macosx_11_0_arm64, …) into a structured floor value —parse_musllinux_floor/parse_macos_deployment_target_flooralongside G10'sparse_manylinux_glibc_floor(abicheck/package.py).win_amd64tag parsing (Windows) is not yet done — no Windows evidence-vs-claim check exists yet to consume it. - [x] For each binary, extract the corresponding evidence:
GLIBC_*/GLIBCXX_*/CXXABI_*version-need floor (Linux, extends G10's mechanism toGLIBCXX/CXXABI),LC_VERSION_MIN_MACOSX/LC_BUILD_VERSION(macOS). PE machine type/UCRT-MSVC-runtime import set (Windows) and the CPU ISA baseline from disassembled dispatch/feature-detection sections remain planned. - [x] Compare claim vs. evidence and emit deployment-
RISK/BREAKINGfindings on mismatch, each classified per the rootCLAUDE.mdfour-step procedure — G10'splatform_baseline_floor_raisedkind now also covers theGLIBCXX/CXXABIcase, plus five new siblings:musllinux_glibc_dependency_detected,macos_deployment_target_raised,wheel_tag_architecture_mismatch(the wheel-tag-claim counterpart to G13'self_machine_changed/macho_cpu_type_changed, which compare two arbitrary binaries rather than a binary against its own wheel's filename promise),wheel_rpath_not_portable(a non-$ORIGIN-relative RPATH/RUNPATH entry), andwheel_closure_dependency_violation(a vendored/hash-suffixed dependency — G9'sstrip_vendor_hashpattern — with no$ORIGIN-relative RPATH/RUNPATH to find it).windows_runtime_requirement_addedremains planned. - [x] A within-claim binary (evidence at or below the tag's promised floor)
stays clean on all new checks (unit-tested in
tests/test_environment_drift.py/tests/test_diff_wheel_deployment.py). - [x] Musllinux and macOS checks are genuinely new coverage, not just glibc
generalized in name:
check_musllinux_glibc_dependencyparses ELFversions_requiredfor any glibc-flavoured tag (a presence check, no numeric floor — musl has no version-floor concept at all), andcheck_macos_deployment_target_floorreads the actual Mach-Omin_os_versionfield. Windows (PE import table) is not yet implemented, so that specific "genuinely new, not just renamed" bar is unverified for the Windows case.
Design¶
- A
wheel_tag.py(or an addition toabicheck/package.py) parses PEP 425/600 wheel filename tags into a structuredWheelPlatformClaim. - Per-platform evidence extractors reuse existing metadata modules
(
elf_metadata.py,macho_metadata.py,pe_metadata.py) — this plan adds the claim-vs-evidence comparison, not new binary parsing where those modules already expose the needed fields (e.g. Mach-O load commands are likely already parsed for other purposes; confirm before adding a second extractor). diff_versioning.pygains theGLIBCXX/CXXABIfloor comparison alongside G10'sGLIBCfloor (same mechanism, wider version-prefix set).- A new
diff_wheel_deployment.py(or an addition to thecompare-releasewheel-matching pass) runs the claim-vs-evidence checks once G10 lands the Linux half, so this plan can start Linux-only if G10 isn't done yet and fold in as siblings. - CPU ISA baseline detection is the highest-uncertainty piece — start with
the disassembly-free case (dispatch-table symbol names like
_avx2/_sse42suffixes, common in NumPy/SciPy's runtime CPU dispatch) and treat true instruction-level scanning as a stretch goal, not an acceptance-criteria blocker.
Files & surfaces¶
abicheck/package.py(wheel tag parsing),abicheck/diff_versioning.py(glibc/GLIBCXX/CXXABI floor, extending G10),abicheck/macho_metadata.py/abicheck/pe_metadata.py(deployment-target / runtime-requirement fields if not already exposed), a newabicheck/diff_wheel_deployment.py(claim-vs-evidence comparison across all platforms),abicheck/checker_policy.pyabicheck/change_registry.py(new kinds).
Tests¶
- Unit: wheel-tag parser on real-world tag strings (manylinux variants, musllinux, macOS universal2, Windows) including malformed/unrecognized tags (must degrade to "no claim checked", not crash).
- Per-platform claim-vs-evidence cases: Linux glibc/GLIBCXX floor exceeded, macOS deployment target raised, Windows UCRT requirement added — each clean-below-floor and breaking-above-floor.
examples/pairs withground_truth.jsonentries for at least the macOS deployment-target case (the SciPy roadmap's north-star example: "macOS OpenBLAS deployment target raised: 10.14 → 12.3").
Example fixtures¶
Two .whl fixtures tagged macosx_10_14_x86_64 where the second version's
Mach-O binaries actually carry LC_BUILD_VERSION minos 12.3 — ground
truth: macos_deployment_target_raised (RISK), surfaced even though the
Python/native API is otherwise unchanged.
Effort & risk¶
L — mostly comparison logic and metadata extraction over binary formats abicheck already parses; the new per-platform evidence extraction (Mach-O load commands, PE runtime imports) is incremental, not a new frontend. Low risk: each platform's claim format (PEP 425/600, Mach-O load commands, PE headers) is well-specified and stable; the CPU-ISA-baseline sub-goal is the one open-ended piece and is scoped as best-effort, not a hard blocker.
Out of scope¶
True instruction-level CPU ISA disassembly (the dispatch-symbol-name
heuristic is the acceptance bar; full disassembly is a stretch goal, not
required); OpenMP-runtime-collision detection at the process level (that is
runtime behavior, not a static wheel-claim check); conda package
run_exports/pinning verification (a separate, package-manager-specific
mechanism — see the "one-command PyPI/conda compare" item in the
SciPy roadmap,
not yet gap-plan-ified).
A general "is this DT_NEEDED entry permitted by the wheel's platform
tag" wheel-closure check — replicating auditwheel's own versioned
per-manylinux-tag allowed-SONAME policy JSON — is also out of scope for
this increment: an incomplete allowlist risks false positives on
legitimately-present system libraries this project doesn't enumerate,
which this codebase's FP-rate gate (scripts/check_fp_rate.py, baseline
0) treats as a real regression, not an acceptable cost of coverage. The
narrower wheel_closure_dependency_violation check delivered here sidesteps
that risk entirely by keying off internal evidence (G9's vendored-hash
naming convention plus the absence of any $ORIGIN-relative RPATH/RUNPATH
entry) rather than an external allowlist — it only fires on a class of
binary that cannot possibly load, not on an ordinary-but-unenumerated
system dependency. macOS's own RPATH/@loader_path/@rpath portability
conventions (a different, more involved mechanism than Linux's $ORIGIN)
are also deferred — check_wheel_rpath_not_portable/
check_wheel_closure_dependency_violation are ELF/Linux only.