Skip to content

Fix issues around crates enabling abi3 and abi3t features#3226

Open
ngoldbaum wants to merge 4 commits into
PyO3:mainfrom
ngoldbaum:fix-mixed-abi3-abi3t
Open

Fix issues around crates enabling abi3 and abi3t features#3226
ngoldbaum wants to merge 4 commits into
PyO3:mainfrom
ngoldbaum:fix-mixed-abi3-abi3t

Conversation

@ngoldbaum

Copy link
Copy Markdown
Contributor

This fixes issues reported by @2bndy5 over at PyO3/maturin-action#368 (comment).

Unfortunately, I completely skipped adding tests here for crates that mix abi3 and abi3t features and introduced some pretty serious bugs.

This PR makes it structurally impossible to generate abi3.abi3t wheels targeting ABIs before Python 3.15. Stable ABI selection now happens after interpreter resolution and chooses at most one stable ABI family per build: abi3t only when a compatible CPython 3.15+ interpreter is present, otherwise abi3 when available. Interpreters that do not support the selected stable ABI family fall back to version-specific wheels, e.g. CPython 3.14t builds cp314-cp314t instead of cp314-abi3.abi3t.

Updates the docs I added for abi3t support to describe the actual behavior of Maturin. I considered making it possible to build two stable ABI wheels in the same call using a GIL-enabled interpreter but decided that required too many internal code changes to change the stable_abi member of the PyO3 bridge struct from an Option wrapping a single value into a vec! that might contain multiple values.

The bulk of the PR is new tests, including new unit tests and integration tests. The current python versions used in CI capture interesting behaviors in this test: Python 3.9 should produce a py38-abi3 wheel, Python 3.15 and 3.15t should produce an abi3t wheel.

@messense

Copy link
Copy Markdown
Member

PyPy tests are failing.

@ngoldbaum

Copy link
Copy Markdown
Contributor Author

Looks like pypy is fixed in the latest commit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes stable-ABI (abi3/abi3t) selection bugs when crates enable both feature families, by deferring selection until after interpreter resolution and ensuring maturin emits at most one stable-ABI wheel per build (with version-specific fallback wheels for non-matching interpreters). It also adds new unit/integration tests and a fixture crate to cover mixed abi3/abi3t configurations, and updates the user guide to reflect the actual behavior.

Changes:

  • Move stable-ABI selection to after interpreter resolution and introduce per-interpreter stable-ABI compatibility (stable_abi_for_interpreter) to drive wheel/tag decisions.
  • Update wheel tag generation and PyO3 config-file generation to support “one stable ABI + fallbacks” behavior cleanly.
  • Add new fixture crate plus unit/integration tests for mixed abi3/abi3t builds; update documentation for abi3t behavior and limitations.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/run/integration.rs Adds integration test entry points for combined abi3/abi3t wheel selection.
tests/common/other.rs Implements the new integration test helper that builds the fixture and asserts produced wheel tags.
test-crates/pyo3-abi3-and-abi3t/src/lib.rs New PyO3 fixture module used to exercise mixed abi3/abi3t feature sets.
test-crates/pyo3-abi3-and-abi3t/README.md Documents the purpose of the new fixture crate.
test-crates/pyo3-abi3-and-abi3t/pyproject.toml Adds Python packaging metadata for the fixture crate.
test-crates/pyo3-abi3-and-abi3t/Cargo.toml Defines mixed abi3/abi3t feature families used by the tests.
test-crates/pyo3-abi3-and-abi3t/Cargo.lock Locks dependencies for the new fixture crate.
src/python_interpreter/resolver.rs Adjusts interpreter resolution behavior for stable ABI detection (abi3-specific fixed version handling).
src/python_interpreter/mod.rs Adds explicit abi3/abi3t capability checks and routes has_stable_api by stable-ABI kind.
src/python_interpreter/config.rs Refactors PyO3 config generation to take Option<StableAbi> and correctly encode target ABI/version.
src/compile.rs Updates PyO3 env/config-file logic to support “forced target_abi config” for stable-ABI builds where appropriate.
src/build_orchestrator.rs Updates tag computation and stable-ABI wheel building to emit one stable-ABI wheel plus version-specific fallbacks.
src/build_options.rs Adds unit tests covering mixed abi3/abi3t bridge detection and post-resolution stable-ABI selection.
src/build_context/builder.rs Ensures stable-ABI upgrade/selection always happens after interpreter resolution (and respects CLI feature overrides).
src/bridge/mod.rs Introduces ABI3T_MINIMUM_PYTHON_MINOR and adds stable_abi_for_interpreter helper for per-interpreter stable-ABI decisions.
src/bridge/detection.rs Updates stable-ABI detection/selection to consider resolved interpreters and pick a single stable-ABI family.
guide/src/bindings.md Updates documentation to describe the “one stable ABI per build” behavior and fallback wheels.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread guide/src/bindings.md Outdated
Comment thread guide/src/bindings.md Outdated
Comment thread guide/src/bindings.md
Comment on lines +30 to +31
maturin build --interpreter python3.10
maturin build --interpreter python3.15t

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had problems with this --interpreter value/format. The example in question simply does not work for Windows builds. I found it worked better (cross-platform -- windows-latest, ubuntu-latest, macos-15-intel, macos-latest) when I used that same version syntax passed to the actions/setup-python action:

strategy:
  matrix:
    # note, '3.11' is min available build on `windows-11-arm` runner
    include:
      - python-version: '3.10'
        pyo3-feature: 'pyo3/abi3-py310'
      - python-version: '3.15t'
        pyo3-feature: 'pyo3/abi3t-py315'
steps:
  - uses: actions/checkout@v6
  - uses: actions/setup-python@v7
    with:
      python-version: ${{ matrix.python-version }}
      # 3.15 is not stable yet, so ...
      allow-prereleases: ${{ startsWith(matrix.python-version, '3.15') }}
  - uses: pyo3/maturin-action@v1
    with:
      args: >-
        --release
        --out dist
        --interpreter ${{ matrix.python-version }}
        --features ${{ matrix.pyo3-feature }}

FYI, passing the ${{ steps.python-setup.outputs.python-path }} is a bad idea for Linux builds (if pyo3/maturin-action manylinux input is not 'off').

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have those problems using the version of Maturin from this PR or the released version?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the CI logs...

Installing 'maturin' from tag 'v1.14.0'

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So then I think this PR will fix things for you, there is Windows CI running on this PR and the invocation in the docs is what the integration test I added here does.

@2bndy5 2bndy5 Jun 17, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the windows CI logs show:

python_version=$(echo 3.15t-dev | sed -e s/-dev//)
echo "PYTHON_VERSION=$python_version" >> "${GITHUB_ENV}"

where that output is

python_version=3.15t
echo PYTHON_VERSION=3.15t

and then it is passed to maturin build:

cargo run build -i $PYTHON_VERSION -m test-crates/pyo3-pure/Cargo.toml

This coincides with my observation and seems contrary to the example in question.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see what you're getting at now. Yes, the CI over here doesn't set allow-prereleases but I don't think that matters for the question you have? The maturin integration tests don't know about setup-python or its naming conventions.

So the place to handle the problem you're having is probably over in maturin-action. Maybe in the PR I have open over there?

PyO3/maturin-action#453

Also with this PR merged you won't need to explicitly pass in abi3 and abi3t features at build time anymore. You can just enable both features, like in the test crates in this PR.

I'm going to bed now so I won't deal with subsequent replies here until tomorrow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't think there was any pre-processing in pyo3/maturin-action's args input. I'll look at that PR too.

Also with this PR merged you won't need to explicitly pass in abi3 and abi3t features at build time anymore. You can just enable both features, like in the test crates in this PR.

The way this patch reads, I think I still need to specify each feature separately if I want a abi3 wheel and a abi3t wheel. My understanding is that it prevents building abi3t wheels for any version of python prior to 3.15t.

Anyway, I bid you well-deserved sweet dreams! Thanks again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this patch reads, I think I still need to specify each feature separately if I want a abi3 wheel and a abi3t wheel. My understanding is that it prevents building abi3t wheels for any version of python prior to 3.15t.

One last comment:

PyO3 is perfectly happy to let you enable both features.

If you enable, say abi3-py310 and abi3t-py315, the wheel you get out of maturin depends on the Python version used to build the wheel:

  • 3.14 or older Gil-enabled build -> abi3 wheel targeting the Python 3.10 limited API
  • 3.14t -> cp314t wheel
  • 3.15 or newer, both builds -> abi3.abi3t wheel targeting the 3.15 limited API

The idea is you get a working build no matter what, with abi3t preferred and falling back to abi3 on the GIL-enabled build where abi3t can't work and a version-specific wheel on 3.14t whhere neither stable ABI is supported.

Please try it yourself by checking out this PR locally, building and installing it in your Python environment, and trying it with a test crate you come up with or one of the test crates I added in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants