Repository navigation
Add initial support for abi3.abi3t tag from PEP 803 - #5193
Conversation
|
I'll do another pass next week to get the tests passing here. In the meantime, I'd appreciate comments. |
|
In order to make the diff coverage happy, I'll probably need to:
I'm going to punt on that for now but I hope to come back to that. I also think free-threaded CI coverage is probably needed independent of the PEP 803 work. |
|
Actually, on second thought, I think I might be able to use |
|
Fun! It looks like a |
|
I looked at Python 3.15 testing a bit today and ran into ronaldoussoren/pyobjc#671. Even if that isn't a problem on CI, the full pyobjc dependency tree takes quite a while to build, so any 3.15 testing will be Linux-only for now probably. It probably does make sense to add an abi3t test extension that requires Python 3.15 or newer, but I'm going to punt on that for now. Besides that issue, all code in this PR is tested. |
rgommers
left a comment
There was a problem hiding this comment.
Thanks for working on this @ngoldbaum. Overall this PR looks solid, I only found one minor issue in one of the added tests in test_build_ext.py.
| Extension( | ||
| name="extension", | ||
| sources=["extension.c"], | ||
| py_limited_api=True |
There was a problem hiding this comment.
This change looks correct and is a bug fix for the existing test, which didn't quite make sense before - I think given the test_limited_abi test name, it was intended to test the Limited API indeed.
|
I marked this ready for review and updated the PR description. @jaraco @abravalheri I'd love your opinions on this. |
Signed-off-by: Michał Górny <mgorny@quansight.com>
|
Tick the box to add this pull request to the merge queue (same as
|
|
Hi @jaraco Python 3.15rc1 is out and it would really help downstream libraries to be able to test this. |
|
Tick the box to add this pull request to the merge queue (same as
|
|
Yay, CI is green! (except the continue-on-error job #5251 addresses). |
|
@ngoldbaum I think your idea of adding and additional Building for a target ABI should be decoupled from the runtime. The new abi3 vs abi3t situation is no really different than using for example Python 3.14 to build a |
|
Before I write more code here I'd |
|
@ngoldbaum While you want for maintainer feedback, in the meantime maybe you could take a look at the following for future consideration? Please take a look at this code that decides the extension suffix: Up to Python 3.14, the code above was fine, it would return the first and only abi3 suffix, for example: $ python3.14 -c 'from importlib.machinery import EXTENSION_SUFFIXES as es; print(es)'
['.cpython-314-x86_64-linux-gnu.so', '.abi3.so', '.so']However, in Python 3.15, things look a bit different: $ python3.15 -c 'from importlib.machinery import EXTENSION_SUFFIXES as es; print(es)'
['.cpython-315-x86_64-linux-gnu.so', '.abi3-x86_64-linux-gnu.so', '.abi3.so', '.abi3t-x86_64-linux-gnu.so', '.abi3t.so', '.so']With a free-threaded Python $ python3.15t -c 'from importlib.machinery import EXTENSION_SUFFIXES as es; print(es)'
['.cpython-315t-x86_64-linux-gnu.so', '.abi3t-x86_64-linux-gnu.so', '.abi3t.so', '.so']Therefore, the |
That seems unworkable. For a free-threaded interpreter, there's only one supported value ( All build backends will do it like this I believe. |
I did not phrase things very well. I was not trying to make a point about defaults, and I understand the backward compatibility implication. Let me restate my comments this way: I'm a module author, I wrote my code such that it can build
Exactly! And that's what I was trying to point out. Currently, even with the current state of this PR, a GIL-enabled interpreter produces an This PR is mostly about fixing the wheel tagging. The extension module suffix also needs some additional work. Or maybe there is something I got fundamentally wrong, and a GIL-enabled interpreter cannot really produce an |
|
Ah, that makes perfect sense now, thanks @dalcinl.
No, you got nothing wrong. I think that's still two: there should be UX to tell the build backend to produce In |
|
Also just to clarify, I'm happy to add UI to support abi3t builds with a GIL-enabled interpreter, I just need a setuptools maintainer to give the OK and tell me what API they'd prefer. |
jameshilliard
left a comment
There was a problem hiding this comment.
A few cross-compilation recommendations from the Buildroot case. Selecting target sysconfig data does not change the interpreter executing setuptools: sys.version_info, sys.implementation, and the import loader still describe the build machine. Validation, extension naming, and wheel tagging need to consume the same target metadata.
The validation and filename/tag examples below were reproduced with isolated PR functions and real or simulated target configuration, not full cross-builds. Buildroot normally uses matching host/target Python versions, so the different-version cases are broader backend correctness tests rather than a claim about its default configuration.
For complete cross-build safety, the pre-existing runtime-derived fallbacks also need auditing. For example, _is_32bit_interpreter() should not rewrite a target platform using the build interpreter's struct.calcsize("P"); target SIZEOF_VOID_P, where available, is the relevant value. Likewise, implementation/debug fallbacks should not silently substitute host information for an explicitly configured target. These are existing limitations, not all introduced by this PR.
| raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'") | ||
|
|
||
| if sysconfig.get_config_var("Py_GIL_DISABLED"): | ||
| if sysconfig.get_config_var("Py_GIL_DISABLED") and sys.version_info < (3, 15): |
There was a problem hiding this comment.
Could this compare the configured target Python version rather than sys.version_info? Py_GIL_DISABLED can come from target sysconfig, but sys.version_info always describes the running build interpreter. Exercising this validation function with independent host/target values gives:
- Host 3.14, target 3.15t: rejects a target that supports the free-threaded stable ABI.
- Host 3.15, target 3.14t: accepts a target that does not.
For CPython POSIX builds, sysconfig.get_config_var("VERSION") comes from the selected target build data. A target-version helper could use that or equivalent explicit target metadata, with platform-aware parsing and a native-build fallback rather than silently using the host for a known cross-build. The minimum version requested by py_limited_api is a separate value.
Avoid replacing this with sysconfig.get_python_version(), py_version_short, or py_version_nodot: CPython derives those from the running interpreter too. With a real AArch64 3.14 sysconfig loaded into a 3.15 build interpreter, I get VERSION="3.14" but py_version_short="3.15" and py_version_nodot="315".
| def stable_abi_tag(impl_name, impl_version): | ||
| abi_tag = None | ||
| if impl_name == "cp" and impl_version[0] == '3': | ||
| if sysconfig.get_config_var("Py_GIL_DISABLED"): |
There was a problem hiding this comment.
Reading the target's Py_GIL_DISABLED here is appropriate for choosing the default ABI, but extension naming needs to use the same target information. The existing get_abi3_suffix() still scans the build interpreter's EXTENSION_SUFFIXES. With a GIL-enabled x86-64 3.15 build interpreter and a simulated AArch64 3.15t target, this function produces abi3.abi3t while the filename helper returns .abi3-x86_64-linux-gnu.so. That filename cannot be imported by the target.
Please keep the ABI choice consistent across these paths and derive the filename from target configuration (Py_GIL_DISABLED, SOABI_PLATFORM, and EXT_SUFFIX where supported), preserving Windows and non-CPython behavior rather than assuming every platform uses .so. #5352 addresses the target-derived filename part.
This cross-build consistency issue is separate from the optional API discussed above for explicitly requesting abi3t against a GIL-enabled target; target configuration can provide the default without making it depend on the build interpreter.
| (t.interpreter, t.abi, plat_name) for t in tags.sys_tags() | ||
| ] | ||
| assert tag in supported_tags, ( | ||
| sys_tags = (f"{t.interpreter}-{t.abi}-{plat_name}" for t in tags.sys_tags()) |
There was a problem hiding this comment.
The supported-tag check also needs target inputs. tags.sys_tags() is defined in terms of the running interpreter; its CPython path defaults to sys.version_info[:2]. Replacing only each tag's platform with plat_name does not fix the interpreter version/ABI assumptions, so a target-correct wheel can still be rejected after fixing the earlier version check.
For the CPython path, could this use the documented tags.cpython_tags(python_version=target_version, abis=target_abis, platforms=[plat_name]) interface, with all three inputs derived from the configured target? Use the target interpreter's version-specific ABI to generate its supported tags, not the wheel's requested compressed abi3.abi3t tag. Preserve separate handling for other implementations.
The nearby tags.interpreter_name() / tags.interpreter_version() calls should consume the same target description as well: they otherwise use sys.implementation and the host-derived py_version_nodot/sys.version_info. This is a pre-existing limitation in this path, but changing the validation conditional alone would leave it in place.
Reference: https://packaging.pypa.io/en/stable/tags.html#packaging.tags.cpython_tags
| "get_config_var", | ||
| lambda name: 1 if name == "Py_GIL_DISABLED" else get_config_var(name), | ||
| ) | ||
| monkeypatch.setattr(sys, "version_info", (3, 14, 5)) |
There was a problem hiding this comment.
Could we add cross-build tests that vary build-interpreter information and target sysconfig independently? This test changes the running version and target free-threaded flag together, so it cannot establish that the version check is using target data.
Useful cases are a 3.14 build interpreter with a 3.15t target, the reverse version pairing, and same-version builds where GIL mode and architecture differ. Holding the target configuration fixed while changing host metadata should leave validation, extension filenames, and wheel tags unchanged.
It would also help to check the extension filename inside the resulting wheel, including --skip-build, and keep native/Windows/non-CPython coverage. That would catch a correct-looking wheel tag paired with a host-derived extension suffix.
|
Thanks for your patience here @ngoldbaum, and thanks @dalcinl @rgommers @jameshilliard for the design discussion. Here's where I've landed, including the API answer you've been waiting on. How this relates to #5352These are two halves of one problem, and they touch disjoint source files:
An abi3t build needs both — Both already select abi3 vs abi3t from Worth recording that @dalcinl diagnosed #5351 in this thread on 2026-08-28, seven weeks before it was filed, with the same Plan1. #5352 lands first. It is a regression against released 3.15 and is behavior-preserving for native builds — comparing the current and proposed implementations directly: It diverges only when sysconfig describes a target other than the running interpreter. 2. Then rebase this PR on it, dropping For the real conflict, keep this PR's 3. Two prerequisites before this merges. The vendored The second is a The API questionShip this PR without the opt-in, as @rgommers suggested — free-threaded interpreter produces abi3t, GIL-enabled produces abi3, and the target-sysconfig default covers every cross-build once #5352 is in. That leaves only "native GIL-enabled interpreter targeting abi3t" uncovered, and it can be added compatibly later. When we do add it, it should be a separate explicit option, not Separately@jameshilliard's review raises residual host-vs-target leaks that neither PR addresses: |
Reconciles with pypa#5352, which landed the target-sysconfig rewrite of get_abi3_suffix() in build_ext.py. The two changes are complementary -- pypa#5352 fixes the extension filename, this branch fixes the wheel tag -- so the only collisions are in tests. test_bdist_wheel.py: keep both imports; stable_abi_tag from here and get_abi3_suffix from pypa#5352. test_build_ext.py: git merges this file cleanly but leaves it broken. test_stable_abi_filename patched build_ext.EXTENSION_SUFFIXES, and pypa#5352 removed that import, so monkeypatch.setattr raised AttributeError on both parametrizations. Rather than drop the test (which would orphan the check_stable_abi helper and lose native coverage), drive it from the configured target: it now asserts abi3t on a free-threaded build and abi3 otherwise, which exercises the real code path instead of a simulated suffix list. pypa#5352's test_abi3_filename_cross already covers cross-build naming for both ABIs, so the simulated suffix fixtures are redundant. Ref pypa#5193 (comment) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refreshes only `packaging` and raises its declared floor to match, using the technique from `tools/vendored.py` (uv pip install --target, pinned to the minimum supported Python, binary-only) scoped to this one dependency rather than re-vendoring everything. packaging 26.3 adds the abi3t tag from PEP 803 (pypa/packaging#1099). The vendored 26.0 could not emit it: its `_abi3_applies(python_version, threading)` returns False for free-threaded builds, so `sys_tags()` yields neither abi3 nor abi3t there. That blocks pypa#5193, whose supported -tag assertion cannot pass on a free-threaded 3.15. packaging has no runtime dependencies, so nothing else in _vendor moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary of changes
Fixes #5205
PEP 803 proposes a free-threaded stable ABI. I'm working on ecosystem support for a free-threaded stable ABI and end-to-end tests for PEP 803. PEP 803 was accepted in March and there will be support for it in Python 3.15.
This PR is set up to require a free-threaded build to produce abi3.abi3t wheels. It does this by making builds where
py_limited_api=Trueis set on the free-threaded build on 3.15 or newer produce abi3t extensions and abi3.abi3t wheels.In principle it's possible to do this on the GIL-enabled build as well but it'll require at least a new
Extensionkeyword argumentpy_target_abi3t=Truemaybe?As far as I can tell,
py_limited_apiis undocumented, so this doesn't add new docs, although I'd be happy to explore that if you'd like to see docs for this.Pull Request Checklist
newsfragments/.(See documentation for details)