Skip to content

Where abicheck Fits in the Python Build Ecosystem

Status: Proposed — vision/roadmap doc, same genre as the SciPy / Scientific-Python Roadmap but focused on the packaging/build-tooling ecosystem (scikit-build-core, cibuildwheel, rattler-build, pybind11, nanobind) rather than the scientific stack itself. Nothing here is implemented; it is recorded per the pattern in backlog.md § "Other deferred roadmap items". No registry entries exist yet — turning any section below into real work means adding a UC-* entry to usecase-registry.yaml and a plan file first, per plans/index.md.

Origin: external roadmap review (feedback captured and reorganized below), 2026-07.

Relationship to the SciPy roadmap: three proposals below overlap with that doc rather than being independently re-derived — release-matrix / support-set delta (its §4), automatic previous-release resolution (its §6), and the generic evidence-provider interface (its "Recommended architecture"). Each is described canonically there; this doc states only what's specific to the build-ecosystem framing and points back rather than restating the design.


The positioning statement

abicheck should be the release-compatibility layer for compiled Python distributions — not another build backend, wheel repairer, or package builder.

A native Python release pipeline runs through several tools that each answer a narrow question. abicheck should answer the one none of them do: did this release break existing consumers, or narrow the set of environments it claims to support?

Layer Main question What abicheck adds
scikit-build-core / meson-python / setuptools / maturin Can the build backend produce a valid wheel? Did the generated native/Python interface change incompatibly?
pybind11 / nanobind / Cython Can C++ types and functions be exposed to Python? Are binding runtime identities and cross-module type contracts still compatible?
cibuildwheel Can the project build and import across the interpreter × platform × arch matrix? Did any matrix cell lose API, ABI, dependency, or deployment compatibility?
auditwheel / delocate / delvewheel Is the artifact portable, with dependencies bundled correctly? Did the repaired artifact regress relative to the prior release?
abi3audit Is an abi3 claim internally consistent? Did the release change CPython ABI, Python API, native ABI, or support floors — abi3 or not?
rattler-build / conda-build / conda-forge Can the package be built, solved, installed, and pinned? Do declared pins and run_exports agree with the binary evidence?

PEP 517 deliberately defines a small interface for producing wheels/sdists and has no generic post-build compatibility-audit hook — a good reason not to force abicheck deep into every build backend. It should integrate primarily at the artifact and CI layers, with optional build-system helpers for richer evidence, mirroring how abi3audit plugged into cibuildwheel's audit stage rather than into every backend individually.

What abicheck already offers this ecosystem

This is not all speculative — enough exists today to start real integrations without waiting on new architecture. (Contrast with the SciPy roadmap, most of whose gaps are genuinely unimplemented; several of the ecosystem gaps below are narrower.)

Ready now:

  • Wheel and conda-package comparison. abicheck compare accepts wheel, .conda (v2 zip), legacy conda .tar.bz2, RPM, Deb, and plain archive inputs (abicheck/package.py), and discovers/matches contained native binaries.
  • Uniform CPython-extension recognition. abicheck already treats extension modules produced by Cython, pybind11, nanobind, or hand-written C uniformly as CPython extensions (abicheck/python_ext.py): it inspects their imported CPython C-API symbols and can audit an explicit --abi3/Limited-API floor (abicheck/stable_abi.py, G14). That matters because an extension module normally exports little beyond PyInit_<module> — an exported-symbol diff alone would see almost nothing.
  • Python API comparison via .pyi. When a type stub sits next to the extension, abicheck statically diffs functions, classes, methods, parameters, defaults, and annotations (abicheck/python_api.py, G23). This is the layer that actually describes the Python API a particular module exposes — the binding framework's own headers describe the framework, not the generated API.
  • Multi-extension / bundled-library analysis. The bundle layer (abicheck/bundle.py, ADR-023) detects unresolved sibling imports, provider changes, and cross-library signature drift across a wheel containing several extension modules plus vendored shared libraries.

Partially ready:

  • NumPy C-API evidence. abicheck detects _ARRAY_API/_UFUNC_API consumption and recovers the compiled NPY_TARGET_VERSION (a string literal NumPy's own _import_array() shim embeds — reliably recoverable by a rodata scan). The raw NPY_ABI_VERSION/NPY_API_VERSION hex constants would need disassembly to recover and are deliberately out of scope, same reasoning as the header-AST extractor (G26).
  • Wheel deployment claims. Linux GLIBC_*/GLIBCXX_*/CXXABI_* floor checks, musllinux/glibc contradictions, macOS deployment targets, and wheel-tag/architecture mismatches are implemented (G10, G27). Windows runtime requirements, CPU-ISA baselines, the full platform-library closure policy, and CLI auto-derivation from the compared wheel's own tag remain planned.

Material gaps (none has a registry entry yet):

  1. A first-class pybind11/nanobind binding-ABI provider (§ below).
  2. Automatic resolution of the matching previous PyPI/conda artifact — canonical design in the SciPy roadmap's §6; a resolver built for one should serve both, so this doc adds only the conda-forge/rattler-build angle (gap 4) on top.
  3. Release-matrix matching and a support-set delta, rather than only per-wheel verdicts — canonical design in the SciPy roadmap's §4; this doc's cibuildwheel matrix-aggregation use (below) is the same mechanism applied outside the scientific stack, not a second design.
  4. Verification of conda run_exports/pins against binary evidence.
  5. Cython .pxd/__pyx_capi__ support — already planned as G25.

Integration with scikit-build-core

scikit-build-core is a natural first target: it is the common CMake-oriented backend for pybind11 and nanobind projects, and its own docs explicitly delegate redistributable-wheel repair to auditwheel/delocate/delvewheel — the same "not my job" boundary abicheck should respect for build backends in general.

Two levels of integration, both optional:

A. A CMake helper for fast PR-time feedback, run after the extension target links, that locates the built .so/.dylib/.pyd, optionally compares it against a committed baseline snapshot, and emits JSON/JUnit for CTest/CI. This gives target-level signal without making abicheck part of scikit-build-core's implementation.

B. An optional build-evidence manifest in wheel metadata. scikit-build-core exposes ${SKBUILD_METADATA_DIR}, installed into the wheel's .dist-info during the actual build (not the metadata-only hook, since CMake doesn't run there) — a clean place for an optional *.dist-info/abicheck.json recording binding framework/version, runtime ABI identity, Python ABI tag, Limited-API floor, free-threaded status, and toolchain facts. Such a manifest must never be trusted blindly — abicheck should verify its fields against the binary wherever possible and report manifest-vs-binary contradictions as findings in their own right.

The final compatibility check must not live only in build-time CMake output, because repair tools rename/bundle dependencies, rewrite RPATHs, and retag wheels after the build runs. So: target check for fast dev feedback, final-wheel check as the authoritative release gate. The same target-vs-final-artifact split applies unchanged to classic scikit-build, Meson, and setuptools extensions.

Integration with cibuildwheel

This is probably the highest-leverage near-term integration. cibuildwheel has a first-class audit stage (post-repair, {wheel}/{abi3_wheel} placeholders) that already defaults to abi3audit for abi3 wheels — exactly where a single-artifact abicheck audit belongs alongside it, not in place of it:

build → repair (auditwheel/delocate/delvewheel) → abi3audit → abicheck audit → install tests

abi3audit stays specialized to abi3 internal-consistency checks. abicheck's addition is everything abi3audit doesn't cover: inter-release CPython ABI/Python-API changes, native-library changes, NumPy targeting, deployment floors, bundled-library topology, and policy aggregation — via the existing abicheck compare <previous-wheel> <new-wheel> --format json, wrapped in a project's own script to select the matching baseline until automatic resolution (gap 2 above) exists.

The missing piece is above the per-wheel level: after all cibuildwheel jobs finish, a matrix aggregation step over the complete old/new wheelhouses could answer questions a single-wheel audit structurally cannot — did a platform disappear, does one wheel omit an extension module present everywhere else, did only one OS raise its floor, does the free-threaded wheel expose a different Python API. This is not a new design: it is the support-set-delta mechanism the SciPy roadmap's §4 already specifies (dropped/unchanged/added environments), run over cibuildwheel's wheelhouse directories instead of SciPy's own release artifacts. Building it once against §4's model and pointing both consumers at it avoids two drifting implementations of the same delta.

Integration with rattler-build and conda-forge

Two distinct opportunities:

A. Testing the actual package artifact. rattler-build recipes support script tests and Python import checks; abicheck can run as a package test or as a post-build CI step. Since conda packages often split runtime, headers, and debug info across outputs, the strongest comparison should accept the runtime package plus its matching devel/debug outputs — abicheck already documents and uses that package shape.

B. Verifying run_exports and pins against observed evidence. This is the more differentiated opportunity and has no registry entry; it also depends on the same conda-forge-artifact resolver the SciPy roadmap's §6 already specifies (platform, arch, Python ABI, build variant, content-hash caching) — nothing new needed there beyond what §6 already lists. Today, run_exports and global pinnings are declared policy; abicheck could add observed evidence — e.g. a recipe pinning libfoo >=4.1,<4.2 when binary comparison shows 4.1→4.8 stays ABI-compatible (pin is unnecessarily tight) or pinning >=4.1,<5 when comparison shows a break at 4.9 (pin is dangerously loose). Conda-forge's pybind11-abi metapackage — a global pin for packages exchanging native pybind11 types across modules — is a concrete case where abicheck could verify that a package declaring it actually contains matching, mutually-compatible pybind11 modules, and flag packages that share native types without declaring it.

The binding-ABI opportunity: pybind11 and nanobind

A generic ABI checker sees almost nothing in a typical pybind11/nanobind module — just PyInit__core. The compatibility surface that actually matters lives elsewhere: the Python functions/classes created at module init (already covered by the .pyi diff above), the binding framework's internal ABI/internals identity, and whether that identity is shared, per-module, or domain-scoped across the extensions in one process.

  • pybind11 builds an internals key from its internals version and platform ABI identity; globally-registered C++ classes can pass instances between modules only when that identity matches (compatible pybind11 version, compiler, and C++ stdlib configuration). pybind11 3.0 bumped this relative to 2.13 and recommends rebuilding all participating extensions together.
  • nanobind maintains a separate ABI version from its semantic version, exposed internally via abi_tag() — covering internal data-structure version, compiler/platform ABI, C++ stdlib ABI, debug/release, stable-ABI mode, and free-threaded mode. NB_DOMAIN can deliberately scope type-sharing to a named group of extensions. nanobind also ships a stub generator, which feeds directly into abicheck's existing .pyi diff.

A wheel bundling pkg/_core.so (pybind11 3.x) and pkg/_geometry.so (pybind11 2.13) can have both modules import fine in isolation while cross-module passage of bound C++ objects silently breaks — exactly the kind of release failure import tests and wheel tags don't express.

Severity here is inherently contextual, not a flat "ABI changed" call: a binding-internals change in one isolated extension that exchanges no native objects is deployment/interop risk at most; several extensions sharing globally-registered types, or a known downstream consumer of those types, is a real break; deliberately module_local/domain-scoped types may be unaffected entirely. Treating every binding-framework version bump as a uniform ABI break would produce exactly the false-positive pattern abicheck's own FP-rate gate (scripts/check_fp_rate.py) is built to catch.

Proposed provider shape, rather than scattering framework-specific logic through the ELF/PE/Mach-O parsers: BindingAbiProvider should be one more implementation of the SurfaceProvider interface the SciPy roadmap's "Recommended architecture" section already proposes (identify/collect/compare/ coverage, alongside its own CythonApiProvider/NumPyCapiProvider), not a separately-invented interface — the two docs should not end up specifying two incompatible plugin shapes for the same evidence-provider concept. Its collect() would produce a normalized surface (framework + version, runtime ABI identity, domain, stable/free-threaded flags, C++ runtime/toolchain facts, cross-module type-visibility scope) from binary evidence first, an optional build-emitted manifest second (verified against the binary), and the existing .pyi/embedded-signature path last for the Python-level API itself. Adopting SurfaceProvider is a materially larger step than the current evidence-tier model and should be evaluated against ADR-032 (the existing extractor-plugin interface) and ADR-034 (non-C-ABI frontend scope) before either provider is designed — the same caution the SciPy roadmap gives its own proposal.

What abicheck should not try to be

Repeating the boundary that motivates all of the above: not a build backend (scikit-build-core's job), not matrix orchestration (cibuildwheel's job), not repair (auditwheel/delocate/delvewheel's job), not abi3-specific linting (abi3audit's job — keep both tools, don't replace it), not conda package construction (rattler-build's job), not a binding generator (pybind11's/ nanobind's job). The differentiated role is the compatibility-policy engine none of those tools share with each other today.

Suggested implementation order

  1. Artifact-layer plumbing that already mostly works. Document and test the existing abicheck compare <old> <new> flow against real wheel and conda pairs as a cibuildwheel audit-stage step and a rattler-build post-build test — no new code, just recipes/examples, closest to the "Phase 0" framing in the original review.
  2. BindingAbiProvider for pybind11/nanobind (needs a UC-* registry entry + plan file) — the single highest-leverage new capability, since it is invisible to every other tool in the pipeline.
  3. Release-matrix / support-set delta — build against the SciPy roadmap's §4 design, not a parallel one; cibuildwheel's matrix aggregation and SciPy's own release comparison should be two callers of the same mechanism.
  4. Automatic previous-artifact resolution (PyPI/conda-forge, per that roadmap's §6) and run_exports/pin verification — the pieces that turn this from "a tool you invoke" into "a check that runs itself."

Relationship to existing work

Idea above Closest existing plan/ADR Relationship
Wheel/conda package comparison abicheck/package.py, ADR-006 Already implemented; this doc proposes wiring it into cibuildwheel/rattler-build recipes, not new extraction code.
CPython-extension recognition, abi3 audit G14 Already implemented (python_ext.py, stable_abi.py); already treats Cython/pybind11/nanobind/C uniformly at the "is this a CPython extension" level.
.pyi Python API diff G23 Already implemented; nanobind's stub generator and a scikit-build-core manifest are both natural new inputs to this existing diff, not a new diff engine.
Bundle / multi-extension analysis ADR-023 Already implemented; the binding-ABI provider below would add a framework-identity dimension bundle analysis doesn't currently carry.
NumPy C-API evidence G26 Already partial, per that plan's own status note; unchanged by this doc.
Wheel deployment-claim verification G10, G27 Already partial; unchanged by this doc.
pybind11/nanobind BindingAbiProvider ADR-032 (plugin interface), ADR-034 (non-C-ABI frontend scope), SciPy roadmap's SurfaceProvider New provider; no registry entry yet. Should implement the SciPy roadmap's SurfaceProvider interface, not a separately-invented one — same interface, sibling to its CythonApiProvider/NumPyCapiProvider.
Release-matrix / support-set delta G2 (build matrix), ADR-002, SciPy roadmap §4 Canonical design lives in the SciPy roadmap's §4; this doc's cibuildwheel matrix-aggregation use is the same mechanism, not a second implementation.
Automatic previous-artifact resolution SciPy roadmap §6 Canonical design lives there (name/platform/arch/CPython-ABI/variant matching, content-hash caching); a resolver built for one should serve both PyPI and conda-forge lookups.
conda run_exports/pin verification none yet New; would need the same artifact-resolution/caching layer as the item above.
cibuildwheel audit-stage / matrix aggregation none yet New CLI/CI surface; the per-wheel half needs no new code (existing compare), the matrix-aggregation half does.

As with the SciPy roadmap, the next step for turning any row above into real work is a UC-* entry in usecase-registry.yaml with status: planned and a docs/contribute/plans/gNN-*.md plan file following the standard template (Problem · Goal & acceptance criteria · Design · Files & surfaces · Tests · Example fixtures · Effort & risk · Out of scope), per plans/index.md.