diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ccb9ce3ec28..6686774ca41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -667,7 +667,7 @@ jobs: - uses: actions/checkout@v6.0.2 - uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version: "3.15-dev" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} diff --git a/Architecture.md b/Architecture.md index 5afa9de693e..a78c0bff9c5 100644 --- a/Architecture.md +++ b/Architecture.md @@ -46,7 +46,7 @@ In the [`pyo3-ffi`] crate, there is lots of conditional compilation such as `#[c `#[cfg(Py_3_8)]`, and `#[cfg(PyPy)]`. `Py_LIMITED_API` corresponds to `#define Py_LIMITED_API` macro in Python/C API. With `Py_LIMITED_API`, we can build a Python-version-agnostic binary called an -[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3). +[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3abi3t). `Py_3_8` means that the API is available from Python >= 3.8. There are also `Py_3_9`, `Py_3_10`, and so on. `PyPy` means that the API definition is for PyPy. diff --git a/Cargo.toml b/Cargo.toml index 4cc7f4245d0..181d18ea139 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,6 +125,11 @@ abi3-py313 = ["abi3-py314", "pyo3-ffi/abi3-py313"] abi3-py314 = ["abi3-py315", "pyo3-ffi/abi3-py314"] abi3-py315 = ["abi3", "pyo3-ffi/abi3-py315"] +abi3t = ["pyo3-ffi/abi3t"] + +# With abi3t, we can manually set the minimum Python version. +abi3t-py315 = ["abi3t", "pyo3-ffi/abi3t-py315"] + # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 82b5ea836c6..7964b68220b 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -28,28 +28,57 @@ An example output of doing this is shown below: ```console $ PYO3_PRINT_CONFIG=1 cargo build - Compiling pyo3 v0.14.1 (/home/david/dev/pyo3) -error: failed to run custom build command for `pyo3 v0.14.1 (/home/david/dev/pyo3)` + Compiling pyo3-ffi v0.28.3 (/Users/goldbaum/Documents/pyo3/pyo3-ffi) +error: failed to run custom build command for `pyo3-ffi v0.28.3 (/Users/goldbaum/Documents/pyo3/pyo3-ffi)` Caused by: - process didn't exit successfully: `/home/david/dev/pyo3/target/debug/build/pyo3-7a8cf4fe22e959b7/build-script-build` (exit status: 101) + process didn't exit successfully: `/Users/goldbaum/Documents/pyo3/target/debug/build/pyo3-ffi-120b26b17564bf98/build-script-build` (exit status: 101) --- stdout + cargo:rustc-check-cfg=cfg(Py_LIMITED_API) + cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED) + cargo:rustc-check-cfg=cfg(PyPy) + cargo:rustc-check-cfg=cfg(GraalPy) + cargo:rustc-check-cfg=cfg(RustPython) + cargo:rustc-check-cfg=cfg(py_sys_config, values("Py_DEBUG", "Py_REF_DEBUG", "Py_TRACE_REFS", "COUNT_ALLOCS")) + cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool) + cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool) + cargo:rustc-check-cfg=cfg(Py_3_8) + cargo:rustc-check-cfg=cfg(Py_3_9) + cargo:rustc-check-cfg=cfg(Py_3_10) + cargo:rustc-check-cfg=cfg(Py_3_11) + cargo:rustc-check-cfg=cfg(Py_3_12) + cargo:rustc-check-cfg=cfg(Py_3_13) + cargo:rustc-check-cfg=cfg(Py_3_14) + cargo:rustc-check-cfg=cfg(Py_3_15) + cargo:rustc-check-cfg=cfg(Py_3_16) + cargo:rustc-check-cfg=cfg(pyo3_dll, values("python3", "python3_d", "python3t", "python3t_d", "python38", "python38_d", "python39", "python39_d", "python310", "python310_d", "python311", "python311_d", "python312", "python312_d", "python313", "python313_d", "python313t", "python313t_d", "python314", "python314_d", "python314t", "python314t_d", "python315", "python315_d", "python315t", "python315t_d", "python316", "python316_d", "python316t", "python316t_d", "libpypy3.11-c")) + cargo:rerun-if-env-changed=PYO3_CONFIG_FILE cargo:rerun-if-env-changed=PYO3_CROSS cargo:rerun-if-env-changed=PYO3_CROSS_LIB_DIR cargo:rerun-if-env-changed=PYO3_CROSS_PYTHON_VERSION + cargo:rerun-if-env-changed=PYO3_CROSS_PYTHON_IMPLEMENTATION + cargo:rerun-if-env-changed=PYO3_NO_PYTHON + cargo:rerun-if-env-changed=PYO3_ENVIRONMENT_SIGNATURE + cargo:rerun-if-env-changed=PYO3_PYTHON + cargo:rerun-if-env-changed=VIRTUAL_ENV + cargo:rerun-if-env-changed=CONDA_PREFIX + cargo:rerun-if-env-changed=PATH cargo:rerun-if-env-changed=PYO3_PRINT_CONFIG -- PYO3_PRINT_CONFIG=1 is set, printing configuration and halting compile -- implementation=CPython - version=3.8 + version=3.14 shared=true - abi3=false - lib_name=python3.8 - lib_dir=/usr/lib - executable=/usr/bin/python + target_abi=CPython-free_threaded-3.14 + lib_name=python3.14t + lib_dir=/Users/goldbaum/.pyenv/versions/3.14.4t/lib + executable=/Users/goldbaum/.pyenv/versions/3.14.4t/bin/python pointer_width=64 - build_flags= + build_flags=Py_GIL_DISABLED + python_framework_prefix= suppress_build_script_link_lines=false + + note: unset the PYO3_PRINT_CONFIG environment variable and retry to compile with the above config ``` The `PYO3_ENVIRONMENT_SIGNATURE` environment variable can be used to trigger rebuilds when its value changes, it has no other effect. @@ -74,7 +103,8 @@ The PyO3 ecosystem has two packaging tools, [`maturin`] and [`setuptools-rust`], PyO3 has some functionality for configuring projects when building Python extension modules: - The `PYO3_BUILD_EXTENSION_MODULE` environment variable, which must be set when building Python extension modules. `maturin` and `setuptools-rust` set this automatically. -- The `abi3` Cargo feature and its version-specific `abi3-pyXY` companions, which are used to opt-in to the limited Python API in order to support multiple Python versions in a single wheel. +- The `abi3` and `abi3t` Cargo feature and their version-specific `abi3-pyXY` and `abi3t-pyXY` companions, which are used to opt-in to the limited Python API and a flavor of stable ABI in order to support multiple Python versions in a single wheel. + The free-threaded build of CPython cannot load `abi3` wheels but both builds can load `abi3t` wheels. This section describes the packaging tools before describing how to build manually without them. It then proceeds with an explanation of the `PYO3_BUILD_EXTENSION_MODULE` environment variable. @@ -212,43 +242,56 @@ This should only be set when building a library for distribution. > > Projects are encouraged to migrate off the feature, as it caused [major development pain](faq.md#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror) due to the lack of linking. -### `Py_LIMITED_API`/`abi3` +### `Py_LIMITED_API`/`abi3`/`abi3t` By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. -This is also known as `abi3`. +The ABI defined by PEP 384 is called `abi3`, since it's stable for all Python 3.X releases. +In 2026, the steering council approved [PEP 803](https://www.python.org/dev/peps/pep-0803/)) which defines a new stable ABI, `abi3t`, that can be used on the free-threaded build of CPython. +Extensions built using the `abit3` ABI are importable on both the GIL-enabled and free-threaded builds of any CPython version newer than the target version. +So, `abi3t` extensions built for the Python 3.15 limited API will be importable on Python 3.16, 3.17, and all future Python 3.X versions. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3-and-abi3t-builds) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. -It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +It's also possible to design your extension module such that you can distribute `abi3` or `abi3t` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. -There are three steps involved in making use of `abi3` when building Python packages as wheels: +There are three steps involved in targeting `abi3` or `abi3t` when building Python packages as wheels: -1. Enable the `abi3` feature in `pyo3`. +1. Enable the `abi3` and/or `abi3t` feature in `pyo3`. This ensures `pyo3` only calls Python C-API functions which are part of the stable API, and on Windows also ensures that the project links against the correct shared object (no special behavior is required on other platforms): ```toml [dependencies] - pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3"] } + pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3", "abi3t"] } ``` -2. Ensure that the built shared objects are correctly marked as `abi3`. + Enabling both the `"abi3"` and `"abi3t"` features will produce an abi3 extension if the host interpreter is a GIL-enabled interpreter and an abi3t extension with a host free-threaded interpreter. + If you always want to produce `abi3t` wheels, you can enable just the `"abi3t"` feature, which will produce extensions targeting `abi3t` if the host interpreter is Python 3.15 or newer, but will produce version-specific extensions otherwise. + If you only enable the `"abi3"` feature, you will produce `abi3` extensions if the host interpreter is a GIL-enabled build of CPython and version-specific extensions otherwise. + + We suggest enabling both features and using multiple python interpreters to build several wheels. + You can build an `abi3` wheel for your minimum supported Python version, a `cp314t` version-specific wheel using a free-threaded Python 3.14 interpreter, and an `abi3t` wheel using a Python 3.15 interpreter. + This will produce wheels targeting all versions of CPython that PyO3 supports. + +2. Ensure that the built shared objects are correctly marked as `abi3` and `abi3t`. This is accomplished by telling your build system that you're using the limited API. - [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels. + [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels and [`maturin`] >= 1.14 supports `abi3t` wheels. - See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) for more. + See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) [for](https://github.com/PyO3/maturin/pull/3113) more. -3. Ensure that the `.whl` is correctly marked as `abi3`. +3. Ensure that the `.whl` is correctly marked as `abi3` or `abi3t`. For projects using `setuptools`, this is accomplished by passing `--py-limited-api=cp3x` (where `x` is the minimum Python version supported by the wheel, e.g. `--py-limited-api=cp35` for Python 3.5) to `setup.py bdist_wheel`. -#### Minimum Python version for `abi3` +#### Minimum Python version for `abi3` and `abi3t` builds Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py38`, `abi3-py39`, `abi3-py310` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py38` feature, your wheel can be used on all Python 3 versions from Python 3.8 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp38-abi3-manylinux2020_x86_64.whl`. +Similarly, there is an `abi3t-py315` feature and future PyO3 versions will offer `abi3t-py316` and so on. + As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. @@ -256,16 +299,17 @@ PyO3 is only able to link your extension module to abi3 version up to and includ E.g., if you set `abi3-py39` and try to compile the crate with a host of Python 3.8, the build will fail. > [!NOTE] -> If you set more that one of these `abi3` version feature flags the lowest version always wins. For example, with both `abi3-py38` and `abi3-py39` set, PyO3 would build a wheel which supports Python 3.8 and up. +> If you set more that one of these `abi3` version feature flags the lowest version always wins. +> For example, with both `abi3-py38` and `abi3-py39` set, PyO3 would build a wheel which supports Python 3.8 and up. -#### Building `abi3` extension modules without a Python interpreter +#### Building stable ABI extension modules without a Python interpreter As an advanced feature, you can build a PyO3 wheel without calling Python interpreter with the environment variable `PYO3_NO_PYTHON` set. -Also, if the build host Python interpreter is not found or is too old or otherwise unusable, PyO3 will still attempt to compile `abi3` extension modules after displaying a warning message. +Also, if the build host Python interpreter is not found or is too old or otherwise unusable, PyO3 will still attempt to compile stable ABI extension modules after displaying a warning message. #### Missing features -Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for `abi3`. +Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for either `abi3` or `abi3t`. These are: - `#[pyo3(text_signature = "...")]` does not work on classes until Python 3.10 or greater. diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index 97d22780625..9f9aa996826 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -25,7 +25,7 @@ fn function_only_supported_on_python_3_8_and_up() {} fn function_only_supported_before_python_3_8() {} #[cfg(not(Py_LIMITED_API))] -fn function_incompatible_with_abi3_feature() {} +fn function_incompatible_with_stable_abi_feature() {} ``` The following sections first show how to add these `#[cfg]` flags to your build process, and then cover some common patterns flags in a little more detail. @@ -72,12 +72,19 @@ There are similar options `Py_3_9`, `Py_3_10`, `Py_3_11` and so on for each mino This `#[cfg]` marks code that will only be present on Python versions before (but not including) Python 3.8. +```text +#[cfg(Py_GIL_DISABLED)] +``` + +This `#[cfg]` marks code that will only be present for free-threaded builds of CPython. +You might use this in situations where the GIL provides thread safety but the free-threaded build needs extra synchronization logic. + ```text #[cfg(not(Py_LIMITED_API))] ``` -This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). -This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. +This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` or `abi3t` features are not enabled). +This might be useful if you want to ship your extension module as an `abi3` or `abi3t` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. ```text #[cfg(any(Py_3_9, not(Py_LIMITED_API)))] @@ -86,6 +93,13 @@ This might be useful if you want to ship your extension module as an `abi3` whee This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version. +```text +#[cfg(all(Py_GIL_DISABLED, Py_LIMITED_API))] +``` + +This marks code which is available only for the free-threaded `abi3t` stable ABI. +You might use this if you need some sort of custom locking implementation in code that makes strong assumptions about the GIL providing thread safety. + ```text #[cfg(PyPy)] ``` @@ -94,7 +108,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` or `abi3t` features, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension module will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. diff --git a/guide/src/features.md b/guide/src/features.md index 2b9ee9c4024..bff8d03c945 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -5,21 +5,36 @@ This chapter of the guide provides detail on each of them. By default, only the `macros` feature is enabled. -## Features for extension module authors +## Features to select the target ABI -### `extension-module` +Each version of CPython exposes an ABI corresponding to the C API that is particular to that version. +Extensions built for this "version-specific" ABI can only be imported by Python interpreters with the same major version, since the Python C API and ABI are subject to change in every major release. +Python also supports a [limited API](https://docs.python.org/3/c-api/stable.html#limited-c-api) and [stable ABI](https://docs.python.org/3/c-api/stable.html#stable-abi). +Extensions targeting the stable ABI defined by Python 3.X can be imported by any subsequent Python version newer than Python 3.X, but not older versions. -Deprecated, users should remove this feature and upgrade to `maturin >= 1.9.4` or `setuptools-rust >= 1.12`. +There are two "flavors" of stable ABI: `abi3`, supported on Python 3.2 and newer but *not* the free-threaded builds and `abi3t`, supported on Python 3.15 and newer for both the GIL-enabled and free-threaded builds of Python. +PyO3 supports building extensions targeting both flavors of stable ABI. -See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. +When PyO3 finds a "host" python interpreter and no `abi3` or `abi3t` feature is active, it will generate extensions targetting the version-specific ABI for the host Python version. +For example, when ``pip`` or ``uv`` installs a Python package that includes Rust dependencies that depend on PyO3, the host interpreter is the interpreter running ``pip`` or the interpreter from the activated ``uv`` environment. + +To build for the stable ABIs, you must activate an `abi3` and/or an `abi3t` feature. +You can simultaneously activate both features. +This will produce an `abi3` extension on the GIL-enabled and a version-specific extension on Python 3.14 and an `abi3t` extension on Python 3.15 and newer. ### `abi3` -This feature is used when building Python extension modules to create wheels which are compatible with multiple Python versions. +This feature is used when building Python extension modules to create wheels which are compatible with multiple Python versions but not the free-threaded build. It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) to be forwards-compatible with future Python versions. -See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. +As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. + +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. +However, see the description of the `abi3-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. + +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3abi3t) section for further detail. ### The `abi3-pyXY` features @@ -27,13 +42,39 @@ See the [building and distribution](building-and-distribution.md#py_limited_apia These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. -See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. +They can also be used without any host interpreter at all to cross-compile. -### `generate-import-lib` +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) section for further detail. -This feature is deprecated and has no effect. -PyO3 now uses Rust's `raw-dylib` linking feature to link against the Python DLL on Windows, eliminating the need for import library (`.lib`) files entirely. -Cross-compiling for Windows targets works without any additional setup. +### `abi3t` + +This feature is used when building Python extension modules to create wheels which are compatible with Python 3.15 or newer, targeting both the free-threaded and GIL-enabled build with a single extension module. + +It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) and [PEP 803](https://www.python.org/dev/peps/pep-0803/) to be forwards-compatible with future Python versions and be importable by the free-threaded and GIL-enabled interpreter. + +Since no particular "target" Python version is implied by this feature, it will generate an `abi3` extension targeting the stable ABI defined by the "host" Python interpreter. +As such, this feature requires a host Python interpreter is present and cannot be used for cross-compilation. + +If you are using this feature to distribute extensions, you should ensure your minimum supported Python version is installed to do the build. +However, see the description of the `abi3t-pyXY` feature if you would like to generate extensions targeting an older Python version using a recent Python interpreter. + +Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. + +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3abi3t) section for further detail. + +### The `abi3t-pyXY` features + +(`abi3t-py315`) + +These features are extensions of the `abi3t` feature to specify the exact minimum Python version which the multiple-version-wheel will support. + +They can be used to target versions of the ABI defined by Python versions released before the "host" python interpreter. +They can also be used without any host interpreter at all to cross-compile. + +Activating an `abi3t` and targeting Python 3.14 and older is a no-op: the build will produce an extension but it will not be an `abi3t` extension, since `abi3t` is unsupported before Python 3.15. + +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3-and-abi3t-builds) section for further detail. ## Features for embedding Python in Rust @@ -88,7 +129,8 @@ These macros require a number of dependencies which may not be needed by users w Disabling this feature enables faster builds for those users, as these dependencies will not be built if this feature is disabled. > [!NOTE] -> This feature is enabled by default. To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. +> This feature is enabled by default. +> To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. ### `multiple-pymethods` @@ -269,3 +311,17 @@ Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversion ### `uuid` Adds a dependency on [uuid](https://docs.rs/uuid) and enables conversions into its [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) type. + +## Deprecated features for extension module authors + +### `extension-module` + +Deprecated, users should remove this feature and upgrade to `maturin >= 1.9.4` or `setuptools-rust >= 1.12`. + +See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. + +### `generate-import-lib` + +This feature is deprecated and has no effect. +PyO3 now uses Rust's `raw-dylib` linking feature to link against the Python DLL on Windows, eliminating the need for import library (`.lib`) files entirely. +Cross-compiling for Windows targets works without any additional setup. diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 90a49a988a7..48f54d05532 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -154,10 +154,13 @@ mod foo { } } +# #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None)) } +# #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] +# fn main() -> () {} ``` If `append_to_inittab` cannot be used due to constraints in the program, an alternative is to create a module using [`PyModule::new`] and insert it manually into `sys.modules`: diff --git a/newsfragments/5807.added.md b/newsfragments/5807.added.md new file mode 100644 index 00000000000..f01b275abdc --- /dev/null +++ b/newsfragments/5807.added.md @@ -0,0 +1,16 @@ +* Support for the new PEP 803 abi3t ABI. +* New `abi3t` and `abi3t-py315` features to enable abi3t builds. These features + work analogously to the `abi3` and `abi3-py3XX` features but are no-ops on + Python 3.14 and older and prudce abi3t builds on Python 3.15. +* A `PythonAbi` struct which wraps a `PythonImplementation`, `PythonVersion`, + and a new `PythonAbiKind` enum. The `PythonAbiKind` enum can itself be one of + two enums: `PythonAbiKind::StableAbi` or `PythonAbiKind::VersionSpecific`. The + former can be `StableAbi::Abi3` or `StabieAbi::Abi3t` and the latter can be + `VersionSpecific::FreeThreaded` or `VersionSpecific::GilEnabled`. Together + these types enable fine-grained descriptions of the target ABI for an + extension build. +* A `PythonAbiBuilder` struct for constructing new `PythonAbi` instances. +* A new `InterpreterConfig::target_abi` function for accessing details about the + target ABI for a build. +* A new `InterpreterConfigBuilder::target_abi` function for setting the target + ABI for a newly constructed `InterpreterConfig`. diff --git a/newsfragments/5807.changed.md b/newsfragments/5807.changed.md new file mode 100644 index 00000000000..c1d2837d1a4 --- /dev/null +++ b/newsfragments/5807.changed.md @@ -0,0 +1,6 @@ +* The boolean `abi3` field of `pyo3_build_config::impl_::InterpreterConfig` is now + deprecated and has been replaced by a new `target_abi` accessor function, which returns a `PythonAbi` + struct. +* The InterpreterConfig::from_interpreter function now accepts a second + `stable_abi_version` argument. The old behavior is the same as passing + `None`. diff --git a/noxfile.py b/noxfile.py index c0c12b0b84c..71c6a906cd3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -94,6 +94,9 @@ def _supported_interpreter_versions( PY_VERSIONS = _supported_interpreter_versions("cpython") ABI3_PY_VERSIONS = [p for p in PY_VERSIONS if not p.endswith("t")] +ABI3T_PY_VERSIONS = [ + p for p in PY_VERSIONS if p.endswith("t") and int(p.split(".")[1].strip("t")) > 14 +] PYPY_VERSIONS = _supported_interpreter_versions("pypy") @@ -132,17 +135,14 @@ def test_rust(session: nox.Session): # We need to pass the feature set to the test command # so that it can be used in the test code # (e.g. for `#[cfg(feature = "abi3-py38")]`) - if feature_set and "abi3" in feature_set and FREE_THREADED_BUILD: - # free-threaded builds don't support abi3 yet - continue - _run_cargo_test(session, features=feature_set, extra_flags=flags) - if ( feature_set and "abi3" in feature_set + and "abi3t" not in feature_set and "full" in feature_set and sys.version_info >= (3, 9) + and sys.implementation.name != "pypy" ): # run abi3-py38 tests to check abi3 forward compatibility _run_cargo_test( @@ -151,6 +151,20 @@ def test_rust(session: nox.Session): extra_flags=flags, ) + if ( + feature_set + and "abi3t" in feature_set + and "full" in feature_set + and sys.version_info >= (3, 16) + ): + # run abi3t-py315 tests to check for abi3t forward + # compatibility + _run_cargo_test( + session, + features=feature_set.replace("abi3t", "abi3t-py315"), + extra_flags=flags, + ) + @nox.session(name="test-py", venv_backend="none") def test_py(session: nox.Session) -> None: @@ -227,10 +241,15 @@ def clippy(session: nox.Session) -> bool: session.error("one or more jobs failed") -def _clippy(session: nox.Session, *, env: Dict[str, str] = None) -> bool: +def _clippy( + session: nox.Session, + *, + env: Dict[str, str] = None, + version: Optional[Tuple[int, int]], +) -> bool: success = True env = env or os.environ - for feature_set in _get_feature_sets(): + for feature_set in _get_feature_sets(version): try: _run_cargo( session, @@ -290,9 +309,11 @@ def codspeed(session: nox.Session) -> bool: def clippy_all(session: nox.Session) -> None: success = True - def _clippy_with_config(env: Dict[str, str]) -> None: + def _clippy_with_config( + env: Dict[str, str], version: Optional[Tuple[int, int]] + ) -> None: nonlocal success - success &= _clippy(session, env=env) + success &= _clippy(session, env=env, version=version) _for_all_version_configs(session, _clippy_with_config) success &= _clippy_additional_workspaces(session) @@ -305,7 +326,7 @@ def _clippy_with_config(env: Dict[str, str]) -> None: def check_all(session: nox.Session) -> None: success = True - def _check(env: Dict[str, str]) -> None: + def _check(env: Dict[str, str], version: Tuple[int, int]) -> None: nonlocal success for feature_set in _get_feature_sets(): try: @@ -1197,8 +1218,9 @@ def test_version_limits(session: nox.Session): with _config_file() as config_file: env["PYO3_CONFIG_FILE"] = config_file.name - assert "3.6" not in PY_VERSIONS - config_file.set("CPython", "3.6") + # Oldest-support Python version - 1 should error + assert "3.7" not in PY_VERSIONS + config_file.set("CPython", "3.7") _run_cargo(session, "check", env=env, expect_error=True) # We allow building with our max version + 1, to support @@ -1219,6 +1241,7 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.16") env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) + del env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] # we only support 3.11 PyPy assert "3.10" not in PYPY_VERSIONS @@ -1233,6 +1256,31 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.14t") _run_cargo(session, "check", env=env) + # 3.15t is PyO3's maximum version of free-threaded Python + config_file.set("CPython", "3.15t") + _run_cargo(session, "check", env=env) + + # 3.16t should build with abi3t forward compatibility + config_file.set("CPython", "3.16t") + env["PYO3_USE_ABI3T_FORWARD_COMPATIBILITY"] = "1" + _run_cargo(session, "check", env=env) + del env["PYO3_USE_ABI3T_FORWARD_COMPATIBILITY"] + + # 3.17t isn't supported + config_file.set("CPython", "3.17t") + _run_cargo(session, "check", env=env, expect_error=True) + + # 3.17t CPython should build if abi3t is explicitly requested + _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) + + # abi3t builds should succeed on Python versions that do not support abi3t + config_file.set("CPython", "3.14") + _run_cargo(session, "check", "--features=pyo3/abi3t", env=env) + + # the build should also succeed if both abi3 and abi3t features are enabled + config_file.set("CPython", "3.14") + _run_cargo(session, "check", "--features=pyo3/abi3t,pyo3/abi3", env=env) + # attempt to build with latest version and check that abi3 version # configured matches the feature max_minor_version = max(int(v.split(".")[1]) for v in ABI3_PY_VERSIONS) @@ -1254,7 +1302,7 @@ def test_version_limits(session: nox.Session): # "An abi3-py3* feature must be specified when compiling without a Python # interpreter." # - # then `ABI3_MAX_MINOR` in `pyo3-build-config/src/impl_.rs` is probably outdated. + # then `STABLE_ABI_MAX_MINOR` in `pyo3-build-config/src/impl_.rs` is probably outdated. assert f"version=3.{max_minor_version}" in stderr, ( f"Expected to see version=3.{max_minor_version}, got: \n\n{stderr}" ) @@ -1274,6 +1322,9 @@ def _check_raw_dylib_macro(session: nox.Session): if minor >= 13: expected_dlls.add(f"python3{minor}t") expected_dlls.add(f"python3{minor}t_d") + if minor >= 15: + expected_dlls.add("python3t") + expected_dlls.add("python3t_d") # PyPy DLL names (libpypy3.X-c.dll) pypy_min, pypy_max = _parse_supported_interpreter_version("pypy") @@ -1418,6 +1469,10 @@ def check_feature_powerset(session: nox.Session): f"abi3-py3{ver.split('.')[1]}" for ver in ABI3_PY_VERSIONS } + EXPECTED_ABI3T_FEATURES = { + f"abi3t-py3{ver.split('.')[1].strip('t')}" for ver in ABI3T_PY_VERSIONS + } + EXCLUDED_FROM_FULL = { "nightly", "extension-module", @@ -1431,20 +1486,37 @@ def check_feature_powerset(session: nox.Session): features = cargo_toml["features"] full_feature = set(features["full"]) - abi3_features = {feature for feature in features if feature.startswith("abi3")} + abi3_features = { + feature + for feature in features + if feature.startswith("abi3") and not feature.startswith("abi3t") + } abi3_version_features = abi3_features - {"abi3"} - unexpected_abi3_features = abi3_version_features - EXPECTED_ABI3_FEATURES - if unexpected_abi3_features: + abi3t_features = {feature for feature in features if feature.startswith("abi3t")} + abi3t_version_features = abi3t_features - {"abi3t"} + + unexpected_stable_abi_features = ( + abi3_version_features - EXPECTED_ABI3_FEATURES - EXPECTED_ABI3T_FEATURES + ) + if unexpected_stable_abi_features: session.error( - f"unexpected `abi3` features found in Cargo.toml: {unexpected_abi3_features}" + f"unexpected `abi3` or `abi3t` features found in Cargo.toml: {unexpected_stable_abi_features}" ) missing_abi3_features = EXPECTED_ABI3_FEATURES - abi3_version_features if missing_abi3_features: session.error(f"missing `abi3` features in Cargo.toml: {missing_abi3_features}") - expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features + missing_abi3t_features = EXPECTED_ABI3T_FEATURES - abi3t_version_features + if missing_abi3t_features: + session.error( + f"missing `abi3t` features in Cargo.toml: {missing_abi3t_features}" + ) + + expected_full_feature = ( + features.keys() - EXCLUDED_FROM_FULL - abi3_features - abi3t_features + ) uncovered_features = expected_full_feature - full_feature if uncovered_features: @@ -1473,6 +1545,7 @@ def check_feature_powerset(session: nox.Session): features_to_skip = [ *(EXCLUDED_FROM_FULL), *abi3_version_features, + *abi3t_version_features, ] # deny warnings @@ -1485,17 +1558,18 @@ def check_feature_powerset(session: nox.Session): subcommand = "minimal-versions" comma_join = ",".join - _run_cargo( - session, - subcommand, - "--feature-powerset", - '--optional-deps=""', - f'--skip="{comma_join(features_to_skip)}"', - *(f"--group-features={comma_join(group)}" for group in features_to_group), - "check", - "--all-targets", - env=env, - ) + for abi_name in ["abi3", "abi3t"]: + _run_cargo( + session, + subcommand, + "--feature-powerset", + '--optional-deps=""', + f'--skip="{comma_join(features_to_skip + [abi_name])}"', + *(f"--group-features={comma_join(group)}" for group in features_to_group), + "check", + "--all-targets", + env=env, + ) @nox.session(name="update-ui-tests", venv_backend="none") @@ -1598,8 +1672,13 @@ def _get_rust_default_target() -> str: @lru_cache() -def _get_feature_sets() -> Tuple[Optional[str], ...]: +def _get_feature_sets( + version: Optional[Tuple[int, int]] = None, +) -> Tuple[Optional[str], ...]: """Returns feature sets to use for Rust jobs""" + if version is None: + version = sys.version_info[:2] + cargo_target = os.getenv("CARGO_BUILD_TARGET", "") features = "full" @@ -1611,6 +1690,24 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: if is_rust_nightly(): features += ",nightly" + if FREE_THREADED_BUILD: + if version >= (3, 15): + return (None, "abi3t", features, f"abi3t,{features}") + else: + return (None, features) + + # do fewer abi3t builds? + # TODO re-enable Windows abi3t builds once 3.15.0b2 is released. + # See https://github.com/python/cpython/issues/149887 + if version >= (3, 15) and sys.platform != "win32": + return ( + None, + "abi3", + "abi3t", + features, + f"abi3,{features}", + f"abi3t,{features}", + ) return (None, "abi3", features, f"abi3,{features}") @@ -1730,7 +1827,8 @@ def _for_all_version_configs( def _job_with_config(implementation, version): session.log(f"{implementation} {version}") config_file.set(implementation, version) - job(env) + major_minor = tuple(map(int, version.strip("t").split("."))) + job(env, major_minor) for version in PY_VERSIONS: _job_with_config("CPython", version) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 261e02180d1..43754fe9b4d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -34,6 +34,11 @@ pub(crate) const MAXIMUM_SUPPORTED_VERSION_PYPY: PythonVersion = PythonVersion { minor: 11, }; +pub(crate) const MINIMUM_SUPPORTED_VERSION_ABI3T: PythonVersion = PythonVersion { + major: 3, + minor: 15, +}; + /// GraalPy may implement the same CPython version over multiple releases. const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { major: 25, @@ -41,7 +46,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -pub(crate) const ABI3_MAX_MINOR: u8 = 15; +pub(crate) const STABLE_ABI_MAX_MINOR: u8 = 15; #[cfg(test)] thread_local! { @@ -78,8 +83,31 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +fn sanitize_stable_abi_version( + stable_abi_version: Option, + version: PythonVersion, +) -> Result { + if let Some(min_version) = stable_abi_version { + ensure!( + min_version <= version, + "cannot set a minimum Python version {} higher than the interpreter version {} \ + (the minimum Python version is implied by the abi3-py3{} feature)", + min_version, + version, + min_version.minor + ); + Ok(min_version) + } else { + Ok(version) + } +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// +/// The version and implementation fields correspond to the interpreter +/// used to host a build. These need not be the same as the implementation and +/// version fields set for the build target in the `target_abi` field. +/// /// Usually this is queried directly from the Python interpreter, or overridden using the /// `PYO3_CONFIG_FILE` environment variable. /// @@ -87,7 +115,7 @@ pub fn target_triple_from_env() -> Triple { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// The Python implementation flavor. + /// The host Python implementation flavor. /// /// Serialized to `implementation`. #[deprecated( @@ -96,7 +124,7 @@ pub struct InterpreterConfig { )] pub implementation: PythonImplementation, - /// Python `X.Y` version. e.g. `3.9`. + /// The host Python `X.Y` version. e.g. `3.9`. /// /// Serialized to `version`. #[deprecated( @@ -114,13 +142,10 @@ pub struct InterpreterConfig { )] pub shared: bool, - /// Whether linking against the stable/limited Python 3 API. - /// + target_abi: PythonAbi, + /// Serialized to `abi3`. - #[deprecated( - since = "0.29.0", - note = "please use `.abi3()` getter or `InterpreterConfigBuilder` instead" - )] + #[deprecated(since = "0.29.0", note = "please match against target_abi instead")] pub abi3: bool, /// The name of the link library defining Python. @@ -242,11 +267,19 @@ impl InterpreterConfig { self.shared } + /// The ABI to use for the compilation target. + /// See the documentation for the PythonAbi enum for more details. + /// + /// Serialized to `target_abi`. + pub fn target_abi(&self) -> PythonAbi { + self.target_abi + } + /// Whether linking against the stable/limited Python 3 API. /// - /// Serialized to `abi3`. + /// Not serialized, see the target_abi instead. pub fn abi3(&self) -> bool { - self.abi3 + matches!(self.target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) } /// The name of the link library defining Python. @@ -315,39 +348,49 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.target_abi.version() >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version().minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version().minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.implementation { + match self.target_abi.implementation() { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !self.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + match self.target_abi.kind() { + PythonAbiKind::Stable(kind) => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if kind == StableAbi::Abi3t { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + } + } + PythonAbiKind::VersionSpecific(kind) => match kind { + GilUsed::FreeThreaded => { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + } + GilUsed::GilEnabled => {} + }, } - for flag in &self.build_flags.0 { match flag { - BuildFlag::Py_GIL_DISABLED => { - out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()) - } + // already handled by target ABI logic above + BuildFlag::Py_GIL_DISABLED => continue, flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), } } - out } - fn from_interpreter(interpreter: impl AsRef) -> Result { + fn from_interpreter( + interpreter: impl AsRef, + stable_abi_version: Option, + ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. from __future__ import print_function @@ -445,8 +488,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let abi3 = is_abi3(); - let implementation = map["implementation"].parse()?; let gil_disabled = match map["gil_disabled"].as_str() { @@ -456,28 +497,25 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; + let target_abi = + PythonAbi::from_build_env(implementation, version, stable_abi_version, gil_disabled)?; + let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - version, - implementation, - abi3, + target_abi, map["mingw"].as_str() == "True", // This is the best heuristic currently available to detect debug build // on Windows from sysconfig - e.g. ext_suffix may be // `_d.cp312-win_amd64.pyd` for 3.12 debug build map["ext_suffix"].starts_with("_d."), - gil_disabled, )? } else { default_lib_name_unix( - version, - implementation, - abi3, + target_abi, cygwin, map.get("ld_version").map(String::as_str), - gil_disabled, )? }; @@ -497,17 +535,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - let builder = InterpreterConfigBuilder::new(implementation, version) - .abi3(abi3) + InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi) .shared(shared) .lib_name(lib_name) .lib_dir(lib_dir) .executable(map["executable"].clone()) .pointer_width(calcsize_pointer * 8) .build_flags(BuildFlags::from_interpreter(interpreter)?) - .python_framework_prefix(python_framework_prefix); - - builder.finalize() + .python_framework_prefix(python_framework_prefix) + .finalize() } /// Generate from parsed sysconfigdata file @@ -553,30 +590,22 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let abi3 = is_abi3(); - let lib_name = Some(default_lib_name_unix( - version, - implementation, - abi3, - cygwin, - sysconfigdata.get_value("LDVERSION"), - gil_disabled, - )?); - let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") - .map(|bytes_width: u32| bytes_width * 8) - .ok(); + let target_abi = PythonAbi::from_build_env(implementation, version, None, gil_disabled)?; + let lib_name = + default_lib_name_unix(target_abi, cygwin, sysconfigdata.get_value("LDVERSION"))?; + let pointer_width = + parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - let builder = InterpreterConfigBuilder::new(implementation, version) - .abi3(abi3) + InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi) .shared(shared || framework) .pointer_width(pointer_width) .lib_name(lib_name) .lib_dir(lib_dir) .python_framework_prefix(python_framework_prefix) - .build_flags(build_flags); - - builder.finalize() + .build_flags(build_flags) + .finalize() } /// Import an externally-provided config file. @@ -594,25 +623,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); let mut config = InterpreterConfig::from_path(path) - .context("failed to parse contents of PYO3_CONFIG_FILE")?; - // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 - // feature. - // - // TODO: abi3 is a property of the build mode, not the interpreter. Should this be - // removed from `InterpreterConfig`? - config.abi3 |= is_abi3(); - config.fixup_for_abi3_version(get_abi3_version())?; + .context("failed to parse contents of PYO3_CONFIG_FILE")? + .apply_build_env()?; // For config files which don't apply a lib name, apply a default which we can use // for linking. if config.lib_name.is_none() { - config.lib_name = Some(default_lib_name_for_target( - config.version, - config.implementation, - config.abi3, - config.is_free_threaded(), - target, - )); + config.lib_name = Some(default_lib_name_for_target(config.target_abi, target)); } Ok(config) @@ -658,13 +675,15 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut implementation = None; let mut version = None; let mut shared = None; + let mut target_abi = None; + // deprecated in the struct but we still allow it to support old config files let mut abi3 = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; let mut pointer_width = None; let mut build_flags: Option = None; - let mut suppress_build_script_link_lines = None; + let mut suppress_build_script_link_lines: Option = None; let mut extra_build_script_lines = vec![]; let mut python_framework_prefix = None; @@ -683,6 +702,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "implementation" => parse_value!(implementation, value), "version" => parse_value!(version, value), "shared" => parse_value!(shared, value), + "target_abi" => parse_value!(target_abi, value), "abi3" => parse_value!(abi3, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), @@ -702,17 +722,39 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let abi3 = abi3.unwrap_or(false); - let build_flags = build_flags.unwrap_or_default(); + let flags_contains_free_threaded = if let Some(ref flags) = build_flags { + flags.0.contains(&BuildFlag::Py_GIL_DISABLED) + } else { + false + }; + let target_abi = if let Some(target_abi) = target_abi { + ensure!( + abi3.is_none(), + "Invalid config that sets both target_abi and abi3." + ); + target_abi + } else if flags_contains_free_threaded { + // This fires even if is_abi3() is True for backward compatibility reasons + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .finalize()? + } else if abi3 == Some(true) { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .finalize()? + } else { + PythonAbiBuilder::new(implementation, version).finalize()? + }; let builder = InterpreterConfigBuilder::new(implementation, version) - .abi3(abi3) + .target_abi(target_abi) .shared(shared.unwrap_or(true)) .lib_name(lib_name) .lib_dir(lib_dir) .executable(executable) .pointer_width(pointer_width) - .build_flags(build_flags) + .build_flags(build_flags.unwrap_or_default()) .suppress_build_script_link_lines(suppress_build_script_link_lines.unwrap_or(false)) .extra_build_script_lines(extra_build_script_lines) .python_framework_prefix(python_framework_prefix); @@ -768,7 +810,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_line!(implementation)?; write_line!(version)?; write_line!(shared)?; - write_line!(abi3)?; + write_line!(target_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -816,37 +858,247 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } pub fn is_free_threaded(&self) -> bool { - self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) + self.target_abi.kind().is_free_threaded() + } + + fn apply_build_env(mut self) -> Result { + self.target_abi = PythonAbi::from_build_env( + self.implementation, + self.version, + exact_stable_abi_version(get_abi3_version().or(get_abi3t_version())), + self.target_abi.kind().is_free_threaded(), + )?; + Ok(self) + } +} + +#[cfg_attr(test, derive(Debug))] +pub struct PythonAbiBuilder { + implementation: PythonImplementation, + version: PythonVersion, + kind: Option, +} + +impl PythonAbiBuilder { + pub fn new(implementation: PythonImplementation, version: PythonVersion) -> PythonAbiBuilder { + PythonAbiBuilder { + implementation, + version, + kind: None, + } + } + + pub fn stable_abi(self, kind: StableAbi) -> PythonAbiBuilder { + let mut build_version = self.version; + if self.version.minor > STABLE_ABI_MAX_MINOR { + warn!("Automatically falling back to {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + build_version.minor = STABLE_ABI_MAX_MINOR; + } + + PythonAbiBuilder { + kind: Some(PythonAbiKind::Stable(kind)), + version: build_version, + ..self + } + } + + pub fn free_threaded(self) -> PythonAbiBuilder { + PythonAbiBuilder { + kind: Some(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), + ..self + } } - /// Updates configured ABI to build for to the requested abi3 version - /// This is a no-op for platforms where abi3 is not supported - fn fixup_for_abi3_version(&mut self, abi3_version: Option) -> Result<()> { - // PyPy, GraalPy, and the free-threaded build don't support abi3; don't adjust the version - if self.implementation.is_pypy() - || self.implementation.is_graalpy() - || self.is_free_threaded() + pub fn finalize(self) -> Result { + // default to GIL-enabled version-specific ABI + let kind = self + .kind + .unwrap_or(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)); + if matches!(kind, PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) + && self.version + < (PythonVersion { + major: 3, + minor: 13, + }) { - return Ok(()); + bail!( + "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {}", self.version + ) } + Ok(PythonAbi { + implementation: self.implementation, + kind, + version: self.version, + }) + } +} - if let Some(version) = abi3_version { - ensure!( - version <= self.version, - "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", - version, - self.version, - version.minor, - ); +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub struct PythonAbi { + implementation: PythonImplementation, + kind: PythonAbiKind, + version: PythonVersion, +} + +impl Display for PythonAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}-{}", self.implementation, self.kind, self.version) + } +} + +impl FromStr for PythonAbi { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + let mut parts = value.splitn(3, '-'); + Ok(PythonAbi { + implementation: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + kind: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + version: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + }) + } +} + +impl PythonAbi { + pub fn from_build_env( + implementation: PythonImplementation, + version: PythonVersion, + stable_abi_version: Option, + gil_disabled: bool, + ) -> Result { + let builder = PythonAbiBuilder { + implementation, + version: sanitize_stable_abi_version(stable_abi_version, version)?, + kind: None, + }; + let builder = if get_abi3_version().is_some() && !gil_disabled { + builder.stable_abi(StableAbi::Abi3) + } else if get_abi3t_version().is_some() && version >= MINIMUM_SUPPORTED_VERSION_ABI3T { + builder.stable_abi(StableAbi::Abi3t) + } else if gil_disabled { + builder.free_threaded() + } else { + builder + }; + builder.finalize() + } + + /// The Python implementation flavor. + /// + /// Serialized to `implementation`. + pub fn implementation(&self) -> PythonImplementation { + self.implementation + } + + /// The ABI flavor + /// + /// Serialized to `kind` + pub fn kind(&self) -> PythonAbiKind { + self.kind + } + + /// Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub fn version(&self) -> PythonVersion { + self.version + } +} + +/// The "kind" of ABI. +/// +/// Either a variety of stable ABI or a GIL-enabled or free-threaded +/// version-specific ABI. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum PythonAbiKind { + /// One of the stable ABIs, which supports multiple Python versions + Stable(StableAbi), + /// Version specific ABI, which is different on the free-threaded build + VersionSpecific(GilUsed), +} + +impl Display for PythonAbiKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), + PythonAbiKind::VersionSpecific(gil_used) => { + write!(f, "{gil_used}") + } + } + } +} + +impl FromStr for PythonAbiKind { + type Err = crate::errors::Error; - self.version = version; - } else if is_abi3() && self.version.minor > ABI3_MAX_MINOR { - warn!("Automatically falling back to abi3-py3{ABI3_MAX_MINOR} because current Python is higher than the maximum supported"); - self.version.minor = ABI3_MAX_MINOR; + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), + "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), + "free_threaded" => Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), + "gil_enabled" => Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)), + _ => Err(format!("Unrecognized ABI name: {value}").into()), } + } +} - Ok(()) +impl PythonAbiKind { + pub fn is_free_threaded(self) -> bool { + match self { + PythonAbiKind::VersionSpecific(gil_disabled) => gil_disabled == GilUsed::FreeThreaded, + PythonAbiKind::Stable(StableAbi::Abi3) => false, + PythonAbiKind::Stable(StableAbi::Abi3t) => true, + } + } +} + +/// The variety of stable ABI +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum StableAbi { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// The free-threaded stable ABI, supporting Python 3.15 and up + Abi3t, +} + +impl Display for StableAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StableAbi::Abi3 => write!(f, "abi3"), + StableAbi::Abi3t => write!(f, "abi3t"), + } + } +} + +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum GilUsed { + /// The original PyObject layout + GilEnabled, + /// The free-threaded PyObject layout + FreeThreaded, +} + +impl Display for GilUsed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GilUsed::GilEnabled => write!(f, "gil_enabled"), + GilUsed::FreeThreaded => write!(f, "free_threaded"), + } } } @@ -855,7 +1107,7 @@ pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, shared: bool, - abi3: bool, + target_abi: Option, lib_name: Option, lib_dir: Option, executable: Option, @@ -875,7 +1127,7 @@ impl InterpreterConfigBuilder { implementation, version, shared: true, - abi3: false, + target_abi: None, lib_name: None, lib_dir: None, executable: None, @@ -887,9 +1139,33 @@ impl InterpreterConfigBuilder { } } - pub fn abi3(mut self, abi3: bool) -> InterpreterConfigBuilder { - self.abi3 = abi3; - self + pub fn target_abi(self, target_abi: PythonAbi) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + target_abi: Some(target_abi), + ..self + } + } + + pub fn stable_abi(self, kind: StableAbi) -> InterpreterConfigBuilder { + let implementation = self.implementation; + let version = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .stable_abi(kind) + .finalize() + // Cannot panic + .unwrap(), + ) + } + + pub fn free_threaded(self) -> Result { + let implementation = self.implementation; + let version = self.version; + Ok(self.target_abi( + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .finalize()?, + )) } pub fn lib_name(mut self, lib_name: impl Into>) -> InterpreterConfigBuilder { @@ -950,6 +1226,45 @@ impl InterpreterConfigBuilder { } pub fn finalize(self) -> Result { + let mut build_flags = self.build_flags.clone(); + let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); + let target_abi = match (self.target_abi, py_gil_disabled) { + // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific. + (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize()?, + // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded. + (None, true) => PythonAbiBuilder::new(self.implementation, self.version) + .free_threaded() + .finalize()?, + // Target ABI set, no Py_GIL_DISABLED: use as-is. + (Some(target_abi), false) => target_abi, + // Target ABI set + Py_GIL_DISABLED: reconcile. + (Some(target_abi), true) => match target_abi.kind() { + // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded + // interpreters, so for backward compatibility fall back to a free-threaded + // version-specific build. + PythonAbiKind::Stable(StableAbi::Abi3) => { + let new_abi = + PythonAbiBuilder::new(target_abi.implementation(), target_abi.version()) + .free_threaded() + .finalize()?; + warn!( + "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \ + falling back to a version-specific free-threaded build" + ); + new_abi + } + // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory. + PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!( + "build_flags contains Py_GIL_DISABLED but target_abi \ + '{target_abi}' is not free-threaded" + ), + // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)). + _ => target_abi, + }, + }; + if target_abi.kind().is_free_threaded() { + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } #[expect( deprecated, reason = "constructing an InterpreterConfig directly, need to write to fields" @@ -958,12 +1273,13 @@ impl InterpreterConfigBuilder { implementation: self.implementation, version: self.version, shared: self.shared, - abi3: self.abi3, + target_abi, + abi3: matches!(target_abi.kind(), PythonAbiKind::Stable(StableAbi::Abi3)), lib_name: self.lib_name, lib_dir: self.lib_dir, executable: self.executable, pointer_width: self.pointer_width, - build_flags: self.build_flags, + build_flags, suppress_build_script_link_lines: self.suppress_build_script_link_lines, extra_build_script_lines: self.extra_build_script_lines, python_framework_prefix: self.python_framework_prefix, @@ -1046,10 +1362,6 @@ impl PythonImplementation { self == PythonImplementation::PyPy } - fn is_graalpy(self) -> bool { - self == PythonImplementation::GraalPy - } - fn from_soabi(soabi: &str) -> Result { if soabi.starts_with("pypy") { Ok(PythonImplementation::PyPy) @@ -1095,21 +1407,50 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } -/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. +/// The target stable ABI version. /// -/// Must be called from a PyO3 crate build script. -fn is_abi3() -> bool { - cargo_env_var("CARGO_FEATURE_ABI3").is_some() - || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") +/// For abi3(t)-py* builds a specific version is chosen, otherwise the builds is +/// for the stable ABI exposed by the host python interpreter version. +#[derive(Debug, Copy, Clone)] +pub enum StableAbiVersion { + Current, + Target(PythonVersion), } /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. /// -/// Must be called from a PyO3 crate build script. -pub fn get_abi3_version() -> Option { - let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR) +/// Must be called from a PyO3 crate build script. Returns None if an `abi3-py*` +/// feature is activated that is unsupported or if no `abi3-py3*` feature is +/// active. +pub fn get_abi3_version() -> Option { + let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some()); - minor_version.map(|minor| PythonVersion { major: 3, minor }) + minor_version.map_or( + if cargo_env_var("CARGO_FEATURE_ABI3").is_some() { + Some(StableAbiVersion::Current) + } else { + None + }, + |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })), + ) +} + +/// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. +/// +/// Must be called from a PyO3 crate build script. Returns None if an `abi3t-py*` +/// feature is activated that is unsupported or if no `abi3t-py3*` feature is +/// active. +pub fn get_abi3t_version() -> Option { + let minor_version = (MINIMUM_SUPPORTED_VERSION_ABI3T.minor..=STABLE_ABI_MAX_MINOR) + .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3T_PY3{i}")).is_some()); + minor_version.map_or( + if cargo_env_var("CARGO_FEATURE_ABI3T").is_some() { + Some(StableAbiVersion::Current) + } else { + None + }, + |minor| Some(StableAbiVersion::Target(PythonVersion { major: 3, minor })), + ) } /// Checks if the `extension-module` feature is enabled for the PyO3 crate. @@ -1786,6 +2127,13 @@ fn cross_compile_from_sysconfigdata( } } +fn exact_stable_abi_version(version: Option) -> Option { + version.and_then(|v| match v { + StableAbiVersion::Current => None, + StableAbiVersion::Target(inner) => Some(inner), + }) +} + /// Generates "default" cross compilation information for the target. /// /// This should work for most CPython extension modules when targeting @@ -1795,70 +2143,78 @@ fn cross_compile_from_sysconfigdata( fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result { let version = cross_compile_config .version - .or_else(get_abi3_version) + .or_else(|| exact_stable_abi_version(get_abi3_version())) + .or_else(|| exact_stable_abi_version(get_abi3t_version())) .ok_or_else(|| format!( - "PYO3_CROSS_PYTHON_VERSION or an abi3-py3* feature must be specified \ + "PYO3_CROSS_PYTHON_VERSION or either an abi3-py3* or abi3t-py3* feature must be specified \ when cross-compiling and PYO3_CROSS_LIB_DIR is not set.\n\ = help: see the PyO3 user guide for more information: https://pyo3.rs/v{}/building-and-distribution.html#cross-compiling", env!("CARGO_PKG_VERSION") ) )?; - let abi3 = is_abi3(); let implementation = cross_compile_config .implementation .unwrap_or(PythonImplementation::CPython); - let gil_disabled: bool = cross_compile_config.abiflags.as_deref() == Some("t"); - - let lib_name = default_lib_name_for_target( - version, - implementation, - abi3, - gil_disabled, - &cross_compile_config.target, - ); + let gil_disabled = cross_compile_config.abiflags.as_deref() == Some("t"); - let builder = InterpreterConfigBuilder::new(implementation, version) - .abi3(abi3) - .lib_name(lib_name) - .lib_dir(cross_compile_config.lib_dir_string()); + let target_abi = + PythonAbi::from_build_env(implementation, version, Some(version), gil_disabled)?; + + let lib_name = default_lib_name_for_target(target_abi, &cross_compile_config.target); - builder.finalize() + let lib_dir = cross_compile_config.lib_dir_string(); + + InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi) + .lib_name(lib_name) + .lib_dir(lib_dir) + .finalize() } -/// Generates "default" interpreter configuration when compiling "abi3" extensions +/// Generates "default" interpreter configuration when compiling stable ABI extensions /// without a working Python interpreter. /// -/// `version` specifies the minimum supported Stable ABI CPython version. +/// `abi3_version` or `abi3t_version` specifies the minimum supported Stable ABI +/// CPython version and which stable ABI to target. /// /// This should work for most CPython extension modules when compiling on /// Windows, macOS and Linux. /// /// Must be called from a PyO3 crate build script. -fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result { - // FIXME: PyPy & GraalPy do not support the Stable ABI. - let implementation = PythonImplementation::CPython; - let abi3 = true; - - let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows( - version, - implementation, - abi3, - false, - false, - false, - )?) +fn default_stable_abi_config( + host: &Triple, + abi3_version: Option, + abi3t_version: Option, +) -> Result { + if abi3_version.is_none() && abi3t_version.is_none() { + bail!("Neither abi3 or abi3t features are enabled") + } + let (stable_abi, version) = if let Some(version) = abi3_version { + (StableAbi::Abi3, version) + } else if let Some(version) = abi3t_version { + (StableAbi::Abi3t, version) } else { - None + unreachable!(); }; - let builder = InterpreterConfigBuilder::new(implementation, version) - .abi3(abi3) - .lib_name(lib_name); + if stable_abi == StableAbi::Abi3t && version < MINIMUM_SUPPORTED_VERSION_ABI3T { + bail!("Cannot target an abi3t version below {MINIMUM_SUPPORTED_VERSION_ABI3T}") + } - builder.finalize() + // FIXME: PyPy & GraalPy do not support the Stable ABI. + let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version) + .stable_abi(stable_abi) + .finalize()?; + let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) + .target_abi(target_abi); + if host.operating_system == OperatingSystem::Windows { + builder.lib_name(default_lib_name_windows(target_abi, false, false)?) + } else { + builder + } + .finalize() } /// Detects the cross compilation target interpreter configuration from all @@ -1890,107 +2246,108 @@ fn load_cross_compile_config( } // These contains only the limited ABI symbols. -const WINDOWS_ABI3_LIB_NAME: &str = "python3"; -const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d"; +const WINDOWS_STABLE_ABI_LIB_NAME: &str = "python3"; +const WINDOWS_STABLE_ABI_DEBUG_LIB_NAME: &str = "python3_d"; /// Generates the default library name for the target platform. -fn default_lib_name_for_target( - version: PythonVersion, - implementation: PythonImplementation, - abi3: bool, - gil_disabled: bool, - target: &Triple, -) -> String { +#[allow(dead_code)] +fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String { if target.operating_system == OperatingSystem::Windows { - default_lib_name_windows(version, implementation, abi3, false, false, gil_disabled).unwrap() + default_lib_name_windows(abi, false, false).unwrap() } else { default_lib_name_unix( - version, - implementation, - abi3, + abi, target.operating_system == OperatingSystem::Cygwin, None, - gil_disabled, ) .unwrap() } } -fn default_lib_name_windows( - version: PythonVersion, - implementation: PythonImplementation, - abi3: bool, - mingw: bool, - debug: bool, - gil_disabled: bool, -) -> Result { - if implementation.is_pypy() { +fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result { + if abi.implementation.is_pypy() { // PyPy on Windows ships `libpypy3.X-c.dll` (e.g. `libpypy3.11-c.dll`), // not CPython's `pythonXY.dll`. With raw-dylib linking we need the real // DLL name rather than the import-library alias. - Ok(format!("libpypy{}.{}-c", version.major, version.minor)) - } else if debug && version < PythonVersion::PY310 { + Ok(format!( + "libpypy{}.{}-c", + abi.version.major, abi.version.minor + )) + } else if debug && abi.version < PythonVersion::PY310 { // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 - Ok(format!("python{}{}_d", version.major, version.minor)) - } else if abi3 && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) { - if debug { - Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) + } else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) + || abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) + { + let mut lib_name = if debug { + WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned() } else { - Ok(WINDOWS_ABI3_LIB_NAME.to_owned()) + WINDOWS_STABLE_ABI_LIB_NAME.to_owned() + }; + if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { + lib_name = lib_name.replace("python3", "python3t"); } + Ok(lib_name) } else if mingw { ensure!( - !gil_disabled, + !abi.kind.is_free_threaded(), "MinGW free-threaded builds are not currently tested or supported" ); // https://packages.msys2.org/base/mingw-w64-python - Ok(format!("python{}.{}", version.major, version.minor)) - } else if gil_disabled { + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) + } else if abi.kind().is_free_threaded() { #[expect(deprecated, reason = "using constant internally")] { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + ensure!(abi.version() >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); } if debug { - Ok(format!("python{}{}t_d", version.major, version.minor)) + Ok(format!( + "python{}{}t_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}t", version.major, version.minor)) + Ok(format!("python{}{}t", abi.version.major, abi.version.minor)) } } else if debug { - Ok(format!("python{}{}_d", version.major, version.minor)) + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}", version.major, version.minor)) + Ok(format!("python{}{}", abi.version.major, abi.version.minor)) } } -fn default_lib_name_unix( - version: PythonVersion, - implementation: PythonImplementation, - abi3: bool, - cygwin: bool, - ld_version: Option<&str>, - gil_disabled: bool, -) -> Result { - match implementation { +fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) -> Result { + match abi.implementation { PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && abi3 { + if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) { Ok("python3".to_string()) - } else if gil_disabled { + } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { + Ok("python3t".to_string()) + } else if abi.kind.is_free_threaded() { #[expect(deprecated, reason = "using constant internally")] { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); } - Ok(format!("python{}.{}t", version.major, version.minor)) + Ok(format!( + "python{}.{}t", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}.{}", version.major, version.minor)) + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) } } }, PythonImplementation::PyPy => match ld_version { Some(ld_version) => Ok(format!("pypy{ld_version}-c")), - None => Ok(format!("pypy{}.{}-c", version.major, version.minor)), + None => Ok(format!("pypy{}.{}-c", abi.version.major, abi.version.minor)), }, PythonImplementation::GraalPy => Ok("python-native".to_string()), @@ -2116,11 +2473,11 @@ pub fn find_interpreter() -> Result { /// Locates and extracts the build host Python interpreter configuration. /// /// Lowers the configured Python version to `abi3_version` if required. -fn get_host_interpreter(abi3_version: Option) -> Result { +fn get_host_interpreter(stable_abi_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let mut interpreter_config = InterpreterConfig::from_interpreter(interpreter_path)?; - interpreter_config.fixup_for_abi3_version(abi3_version)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -2132,9 +2489,7 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_to(&Triple::host(), target)? { - let mut interpreter_config = load_cross_compile_config(cross_config)?; - interpreter_config.fixup_for_abi3_version(get_abi3_version())?; - Some(interpreter_config) + Some(load_cross_compile_config(cross_config)?.apply_build_env()?) } else { None }; @@ -2146,30 +2501,31 @@ pub fn make_cross_compile_config(target: &Triple) -> Result Result { let host = Triple::host(); let abi3_version = get_abi3_version(); + let abi3t_version = get_abi3t_version(); // See if we can safely skip the Python interpreter configuration detection. - // Unix "abi3" extension modules can usually be built without any interpreter. - let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); + // Unix stable ABI extension modules can usually be built without any interpreter. + let need_interpreter = + (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host); if have_python_interpreter() { - match get_host_interpreter(abi3_version) { + match get_host_interpreter(exact_stable_abi_version(abi3_version.or(abi3t_version))) { Ok(interpreter_config) => return Ok(interpreter_config), // Bail if the interpreter configuration is required to build. Err(e) if need_interpreter => return Err(e), _ => { - // Fall back to the "abi3" defaults just as if `PYO3_NO_PYTHON` + // Fall back to the stable ABI just as if `PYO3_NO_PYTHON` // environment variable was set. warn!("Compiling without a working Python interpreter."); } } - } else { - ensure!( - abi3_version.is_some(), - "An abi3-py3* feature must be specified when compiling without a Python interpreter." - ); - }; + } - let interpreter_config = default_abi3_config(&host, abi3_version.unwrap())?; + let interpreter_config = default_stable_abi_config( + &host, + exact_stable_abi_version(abi3_version), + exact_stable_abi_version(abi3t_version), + )?; Ok(interpreter_config) } @@ -2220,7 +2576,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .pointer_width(32) .executable("executable".to_string()) .lib_dir("lib_name".to_string()) @@ -2258,7 +2614,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .pointer_width(32) .executable("executable".to_string()) .lib_name("lib_name".to_string()) @@ -2302,20 +2658,128 @@ mod tests { } #[test] - fn build_flags_default() { - assert_eq!(BuildFlags::default(), BuildFlags::new()); + fn test_config_file_invalid_keys() { + assert!( + InterpreterConfig::from_reader("version=3.14\ntarget_abi=foo-bar-baz".as_bytes()) + .is_err() + ); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-bar-baz".as_bytes() + ) + .is_err()); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-abi3-baz".as_bytes() + ) + .is_err()); } #[test] - fn build_flags_from_sysconfigdata() { - let mut sysconfigdata = Sysconfigdata::new(); - + fn gil_disabled_config_file_corner_cases() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + // Legacy: build_flags=Py_GIL_DISABLED with no target_abi infers free-threaded. assert_eq!( - BuildFlags::from_sysconfigdata(&sysconfigdata).0, - HashSet::new() - ); - - for flag in &BuildFlags::ALL { + InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + .unwrap() + ); + // Canonical: target_abi=free_threaded. + assert_eq!( + InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-free_threaded-3.13".as_bytes() + ) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + .unwrap() + ); + // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. + assert!(InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-gil_enabled-3.13\nbuild_flags=Py_GIL_DISABLED" + .as_bytes() + ) + .is_err()); + // build_flags=Py_GIL_DISABLED on a builder without target_abi is ok + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(InterpreterConfigBuilder::new(implementation, version) + .build_flags(flags) + .finalize() + .unwrap() + .target_abi + .kind + .is_free_threaded()); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .build_flags(flags) + .finalize() + .is_err() + ); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .stable_abi(StableAbi::Abi3) + .build_flags(flags) + .finalize() + .is_err() + ); + + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY38) + .free_threaded() + .is_err() + ); + } + + #[test] + fn abi3_from_old_config_file() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nabi3=true".as_bytes()).unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap() + ); + } + + #[test] + fn test_target_abi_and_abi3() { + assert!(InterpreterConfig::from_reader( + "version=3.13\nabi3=true\ntarget_abi=CPython-abi3-3.13".as_bytes() + ) + .unwrap_err() + .to_string() + .contains("Invalid config"),); + } + + #[test] + fn build_flags_default() { + assert_eq!(BuildFlags::default(), BuildFlags::new()); + } + + #[test] + fn build_flags_from_sysconfigdata() { + let mut sysconfigdata = Sysconfigdata::new(); + + assert_eq!( + BuildFlags::from_sysconfigdata(&sysconfigdata).0, + HashSet::new() + ); + + for flag in &BuildFlags::ALL { sysconfigdata.insert(flag.to_string(), "0".into()); } @@ -2450,29 +2914,87 @@ mod tests { #[test] fn windows_hardcoded_abi3_compile() { let host = triple!("x86_64-pc-windows-msvc"); - let min_version = "3.8".parse().unwrap(); - let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .lib_name("python3".to_string()) .finalize() .unwrap(); - assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); + assert_eq!( + default_stable_abi_config(&host, Some(version), None).unwrap(), + config + ); + } + + #[test] + fn windows_hardcoded_abi3t_compile() { + let host = triple!("x86_64-pc-windows-msvc"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion { + major: 3, + minor: 15, + }; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .lib_name("python3t".to_string()) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, None, Some(version)).unwrap(), + config + ); } #[test] fn unix_hardcoded_abi3_compile() { let host = triple!("x86_64-unknown-linux-gnu"); - let min_version = "3.9".parse().unwrap(); let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .finalize() .unwrap(); - assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); + assert_eq!( + default_stable_abi_config(&host, Some(version), None).unwrap(), + config + ); + } + + #[test] + fn unix_hardcoded_abi3t_compile() { + let host = triple!("x86_64-unknown-linux-gnu"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion { + major: 3, + minor: 15, + }; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, None, Some(version)).unwrap(), + config + ); + } + + #[test] + fn default_stable_abi_config_corner_cases() { + let host = triple!("x86_64-unknown-linux-gnu"); + let py315 = Some("3.15".parse().unwrap()); + let py39 = Some("3.9".parse().unwrap()); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(); + assert_eq!( + default_stable_abi_config(&host, py39, py315).unwrap(), + config + ); + assert!(default_stable_abi_config(&host, None, py39).is_err()); } #[test] @@ -2579,34 +3101,30 @@ mod tests { #[test] fn default_lib_name_windows() { - use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize() + .unwrap(), false, false, ) .unwrap(), "python39", ); - assert!(super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, - false, - false, - true, - ) - .is_err()); + // free-threaded Python 3.9 builds should be impossible + assert!( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded() + .finalize() + .is_err() + ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - true, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, false, ) @@ -2615,34 +3133,33 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize() + .unwrap(), true, false, - false, ) .unwrap(), "python3.9", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), true, false, - false, ) .unwrap(), "python3", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - PyPy, - true, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, false, ) @@ -2651,13 +3168,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - false, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, false, ) @@ -2666,142 +3180,135 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, true, - false, ) .unwrap(), - "python39_d", + "python3_d", ); // abi3 debug builds on windows use version-specific lib on 3.9 and older // to workaround https://github.com/python/cpython/issues/101614 assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, true, - false, ) .unwrap(), "python39_d", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 10 - }, - CPython, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), false, true, - false, ) .unwrap(), "python3_d", ); - // Python versions older than 3.13 don't support gil_disabled - assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - false, - false, - false, - true, - ) - .is_err()); // mingw and free-threading are incompatible (until someone adds support) assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .finalize() + .unwrap(), true, false, - true, ) .is_err()); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .finalize() + .unwrap(), false, false, - false, - true, ) .unwrap(), "python313t", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - true, // abi3 true should not affect the free-threaded lib name - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .finalize() + .unwrap(), false, true, ) .unwrap(), - "python313t", + "python313t_d", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15 + } + ) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(), false, false, - true, + ) + .unwrap(), + "python3t", + ); + assert_eq!( + super::default_lib_name_windows( + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15 + } + ) + .stable_abi(StableAbi::Abi3t) + .finalize() + .unwrap(), + false, true, ) .unwrap(), - "python313t_d", + "python3t_d", ); } #[test] fn default_lib_name_unix() { - use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 8 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize() + .unwrap(), false, None, - false ) .unwrap(), "python3.8", ); assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize() + .unwrap(), false, None, - false ) .unwrap(), "python3.9", @@ -2809,12 +3316,11 @@ mod tests { // Can use ldversion to override for CPython assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize() + .unwrap(), false, Some("3.8d"), - false ) .unwrap(), "python3.8d", @@ -2823,15 +3329,11 @@ mod tests { // PyPy 3.11 includes ldversion assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .finalize() + .unwrap(), false, None, - false ) .unwrap(), "pypy3.11-c", @@ -2839,12 +3341,11 @@ mod tests { assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - PyPy, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .finalize() + .unwrap(), false, Some("3.11d"), - false ) .unwrap(), "pypy3.11d-c", @@ -2853,50 +3354,63 @@ mod tests { // free-threading adds a t suffix assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .finalize() + .unwrap(), false, None, - true ) .unwrap(), "python3.13t", ); - // 3.12 and older are incompatible with gil_disabled - assert!(super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - false, - false, - None, - true, - ) - .is_err()); // cygwin abi3 links to unversioned libpython assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), true, None, - false ) .unwrap(), "python3", ); } + #[test] + fn abi_builder_error_paths() { + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded() + .finalize(); + + assert!(builder.is_err()); + assert!(builder.unwrap_err().to_string().contains("Cannot target")); + + assert_eq!( + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 16, + }, + ) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap() + .version + .minor, + STABLE_ABI_MAX_MINOR + ); + + assert!("invalid".parse::().is_err()); + assert!("CPython-invalid".parse::().is_err()); + assert!("CPython-free_threaded-invalid" + .parse::() + .is_err()); + } + #[test] fn parse_cross_python_version() { let env_vars = CrossCompileEnvVars { @@ -2949,37 +3463,45 @@ mod tests { } #[test] - fn interpreter_version_reduced_to_abi3() { - let builder = InterpreterConfigBuilder::new( - PythonImplementation::CPython, - // Make this greater than the target abi3 version to reduce to below - PythonVersion::PY39, - ) - .abi3(true); - - let mut config = builder.finalize().unwrap(); - - config - .fixup_for_abi3_version(Some(PythonVersion::PY38)) + fn target_abi3_version_different_from_host() { + let implementation = PythonImplementation::CPython; + let host_version = PythonVersion::PY39; + let target_version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, host_version) + .target_abi( + PythonAbiBuilder::new(implementation, target_version) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(), + ) + .finalize() .unwrap(); - assert_eq!(config.version(), PythonVersion::PY38); + assert_eq!(config.target_abi.version(), target_version); + assert_eq!(config.version, host_version); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let builder = - InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .abi3(true); + if !have_python_interpreter() { + return; + } - let mut config = builder.finalize().unwrap(); + let interpreter = get_host_interpreter(Some(PythonVersion { + major: 3, + minor: 45, + })); + assert!(interpreter.unwrap_err().to_string().contains( + "cannot set a minimum Python version 3.45 higher than the interpreter version" + )); - assert!(config - .fixup_for_abi3_version(Some(PythonVersion::PY39)) - .unwrap_err() - .to_string() - .contains( - "cannot set a minimum Python version 3.9 higher than the interpreter version 3.8" - )); + let host_version = get_host_interpreter(None).unwrap().version; + if host_version >= PythonVersion::PY313 { + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version(), + PythonVersion::PY313 + ); + } } #[test] @@ -3006,7 +3528,7 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), - abiflags: if interpreter_config.is_free_threaded() { + abiflags: if interpreter_config.target_abi.kind().is_free_threaded() { Some("t".into()) } else { None @@ -3030,6 +3552,7 @@ mod tests { parsed_config.python_framework_prefix = None; } + assert_eq!(parsed_config.implementation, PythonImplementation::CPython); assert_eq!( parsed_config, InterpreterConfigBuilder::new( @@ -3195,7 +3718,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .finalize() .unwrap(); @@ -3209,7 +3732,7 @@ mod tests { ); let interpreter_config = InterpreterConfigBuilder::new(PythonImplementation::PyPy, version) - .abi3(true) + .stable_abi(StableAbi::Abi3) .finalize() .unwrap(); assert_eq!( @@ -3221,18 +3744,41 @@ mod tests { "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); + + let interpreter_config = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 15, + }, + ) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(); + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), + "cargo:rustc-cfg=Py_3_10".to_owned(), + "cargo:rustc-cfg=Py_3_11".to_owned(), + "cargo:rustc-cfg=Py_3_12".to_owned(), + "cargo:rustc-cfg=Py_3_13".to_owned(), + "cargo:rustc-cfg=Py_3_14".to_owned(), + "cargo:rustc-cfg=Py_3_15".to_owned(), + "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), + ] + ); } #[test] fn test_build_script_outputs_gil_disabled() { - let mut build_flags = BuildFlags::default(); - build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - let implementation = PythonImplementation::CPython; - let version = PythonVersion::PY313; - let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .build_flags(build_flags) - .finalize() - .unwrap(); + let interpreter_config = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3247,6 +3793,81 @@ mod tests { ); } + #[test] + fn test_interpreter_config_builder_gil_disabled_flag() { + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let config = builder + .stable_abi(StableAbi::Abi3) + .build_flags(flags) + .finalize() + .unwrap(); + // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) + assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + + // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3) + // produces the same result as the previous ordering. + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let config = builder + .build_flags(flags) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(); + assert!(config.target_abi.kind() == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + + // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and + // is rejected at finalize regardless of the order in which the setters were called. + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); + let target_abi = PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ) + .finalize() + .unwrap(); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .target_abi(target_abi) + .build_flags(flags) + .finalize() + .is_err()); + + let builder = InterpreterConfigBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 14, + }, + ); + let config = builder.free_threaded().unwrap().finalize().unwrap(); + assert!(config.target_abi.kind().is_free_threaded()); + assert!(config.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED)); + } + #[test] fn test_build_script_outputs_debug() { let mut build_flags = BuildFlags::default(); @@ -3308,45 +3929,55 @@ mod tests { major: 3, minor: 13, }; + let cpy39 = PythonAbiBuilder::new(cpython, py39).finalize().unwrap(); + let pypy311 = PythonAbiBuilder::new(pypy, py311).finalize().unwrap(); + let cpy313t = PythonAbiBuilder::new(cpython, py313) + .free_threaded() + .finalize() + .unwrap(); + let cpy313_abi3 = PythonAbiBuilder::new(cpython, py313) + .stable_abi(StableAbi::Abi3) + .finalize() + .unwrap(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); - let lib_name = default_lib_name_for_target(py39, cpython, false, false, &unix); + let lib_name = default_lib_name_for_target(cpy39, &unix); assert_eq!(lib_name, "python3.9"); - let lib_name = default_lib_name_for_target(py39, cpython, false, false, &win_x64); + let lib_name = default_lib_name_for_target(cpy39, &win_x64); assert_eq!(lib_name, "python39"); - let lib_name = default_lib_name_for_target(py39, cpython, false, false, &win_arm64); + let lib_name = default_lib_name_for_target(cpy39, &win_arm64); assert_eq!(lib_name, "python39"); // PyPy - let lib_name = default_lib_name_for_target(py311, pypy, false, false, &unix); + let lib_name = default_lib_name_for_target(pypy311, &unix); assert_eq!(lib_name, "pypy3.11-c"); - let lib_name = default_lib_name_for_target(py311, pypy, false, false, &win_x64); + let lib_name = default_lib_name_for_target(pypy311, &win_x64); assert_eq!(lib_name, "libpypy3.11-c"); // Free-threaded - let lib_name = default_lib_name_for_target(py313, cpython, false, true, &unix); + let lib_name = default_lib_name_for_target(cpy313t, &unix); assert_eq!(lib_name, "python3.13t"); - let lib_name = default_lib_name_for_target(py313, cpython, false, true, &win_x64); + let lib_name = default_lib_name_for_target(cpy313t, &win_x64); assert_eq!(lib_name, "python313t"); - let lib_name = default_lib_name_for_target(py313, cpython, false, true, &win_arm64); + let lib_name = default_lib_name_for_target(cpy313t, &win_arm64); assert_eq!(lib_name, "python313t"); // abi3 - let lib_name = default_lib_name_for_target(py313, cpython, true, false, &unix); + let lib_name = default_lib_name_for_target(cpy313_abi3, &unix); assert_eq!(lib_name, "python3.13"); - let lib_name = default_lib_name_for_target(py313, cpython, true, false, &win_x64); + let lib_name = default_lib_name_for_target(cpy313_abi3, &win_x64); assert_eq!(lib_name, "python3"); - let lib_name = default_lib_name_for_target(py313, cpython, true, false, &win_arm64); + let lib_name = default_lib_name_for_target(cpy313_abi3, &win_arm64); assert_eq!(lib_name, "python3"); } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 6a06a4f968d..be241fbc8b7 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -14,8 +14,8 @@ use std::{env, process::Command, str::FromStr, sync::LazyLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonImplementation, - PythonVersion, Triple, + CrossCompileConfig, GilUsed, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonAbiBuilder, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, Triple, }; use target_lexicon::OperatingSystem; @@ -29,7 +29,7 @@ use target_lexicon::OperatingSystem; /// | Flag | Description | /// | ---- | ----------- | /// | `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]`, `#[cfg(Py_3_11)]`, ... | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_8)]` marks code which can run on Python 3.8 **and newer**. There is one attribute for each Python version currently supported by PyO3. | -/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | +/// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` or abi3t feature enabled. | /// | `#[cfg(Py_GIL_DISABLED)]` | This marks code which is run on the free-threaded interpreter. | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | /// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | @@ -204,13 +204,18 @@ pub fn print_expected_cfgs() { // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); } // pyo3_dll cfg for raw-dylib linking on Windows - let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()]; - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + let mut dll_names = vec![ + "python3".to_string(), + "python3_d".to_string(), + "python3t".to_string(), + "python3t_d".to_string(), + ]; + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { dll_names.push(format!("python3{i}")); dll_names.push(format!("python3{i}_d")); if i >= 13 { @@ -249,6 +254,7 @@ pub mod pyo3_build_script_impl { } pub use crate::impl_::{ cargo_env_var, env_var, is_linking_libpython_for_target, target_triple_from_env, + InterpreterConfig, PythonAbi, PythonAbiKind, PythonVersion, StableAbi, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -303,13 +309,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.implementation() { + let implementation = match interpreter_config.target_abi().implementation() { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = interpreter_config.version(); + let version = &interpreter_config.target_abi().version(); let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 0546793f07b..aa89aa52cb8 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -44,7 +44,9 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().version() < PY_3_15 && struct_name == "PyBytesWriter" { + if pyo3_build_config::get().target_abi().version() < PY_3_15 + && struct_name == "PyBytesWriter" + { // PyBytesWriter was added in Python 3.15 continue; } @@ -167,7 +169,9 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream if struct_name == "PyMemberDef" { // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); - } else if struct_name == "PyObject" && pyo3_build_config::get().version() >= PY_3_12 { + } else if struct_name == "PyObject" + && pyo3_build_config::get().target_abi().version() >= PY_3_12 + { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly all_fields.remove("__bindgen_anon_1"); @@ -184,7 +188,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().version() >= PY_3_12) + let bindgen_field_ident = if (pyo3_build_config::get().target_abi().version() >= PY_3_12) && struct_name == "PyObject" && field_name == "ob_refcnt" { diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 6d6b0949d46..5a8cbd6dc47 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -25,6 +25,9 @@ extension-module = ["pyo3-build-config/extension-module"] # Use the Python limited API. See https://www.python.org/dev/peps/pep-0384/ for more. abi3 = [] +# Use the Free-threaded Python limited API. See https://www.python.org/dev/peps/pep-0803/ for more. +abi3t = [] + # With abi3, we can manually set the minimum Python version. abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] @@ -35,6 +38,8 @@ abi3-py313 = ["abi3-py314"] abi3-py314 = ["abi3-py315"] abi3-py315 = ["abi3"] +abi3t-py315 = ["abi3t"] + # deprecated: no longer needed, raw-dylib is used instead generate-import-lib = ["pyo3-build-config/generate-import-lib"] diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 582d13faf47..bc7a0746137 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -5,7 +5,7 @@ use pyo3_build_config::{ resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, MaximumVersionExceeded, }, - warn, InterpreterConfig, PythonImplementation, PythonVersion, + warn, InterpreterConfig, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, }; /// Minimum Python version PyO3 supports. @@ -45,9 +45,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.implementation() { + match interpreter_config.target_abi().implementation() { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; + let interp_version = interpreter_config.target_abi().version(); ensure!( interpreter_config.version() >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", @@ -58,20 +59,37 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { major: versions.max.major, minor: versions.max.minor + 1, }; - if interpreter_config.version() == v_plus_1 { + if interp_version == v_plus_1 { warn!( "Using experimental support for the Python {}.{} ABI. \ Build artifacts may not be compatible with the final release of CPython, \ so do not distribute them.", v_plus_1.major, v_plus_1.minor, ); - } else if interpreter_config.version() > v_plus_1 { + } else if interp_version > v_plus_1 { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); - if interpreter_config.is_free_threaded() { - error.add_help( - "the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", - ); - return Err(error.finish().into()); + let major = interp_version.major; + let minor = interp_version.minor; + let py_3_15 = PythonVersion { + major: 3, + minor: 15, + }; + if interpreter_config.target_abi().kind().is_free_threaded() { + if (PythonVersion { major, minor }) >= py_3_15 { + if env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY") + .is_none_or(|os_str| os_str != "1") + { + error.add_help( + "set PYO3_USE_ABI3T_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the free-threaded stable ABI" + ); + return Err(error.finish().into()); + } + } else { + error.add_help(format!( + "the free-threaded build of CPython {major}.{minor} does not support the limited API so this check cannot be suppressed.", + ).as_str()); + return Err(error.finish().into()); + } } if env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_none_or(|os_str| os_str != "1") @@ -81,29 +99,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.is_free_threaded() { + if interpreter_config.target_abi().kind().is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.version() >= min_free_threaded_version, + interpreter_config.target_abi().version() >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.version(), + interpreter_config.target_abi().version(), ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.version() >= versions.min, + interpreter_config.target_abi().version() >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version(), + interpreter_config.target_abi().version(), versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version() > versions.max { + if interpreter_config.target_abi().version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -111,13 +129,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.version() >= versions.min, + interpreter_config.target_abi().version() >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version(), + interpreter_config.target_abi().version(), versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version() > versions.max { + if interpreter_config.target_abi().version() > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -125,21 +143,27 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if interpreter_config.abi3() { - match interpreter_config.implementation() { - PythonImplementation::CPython => { - if interpreter_config.is_free_threaded() { - warn!( - "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi().kind() { + match interpreter_config.target_abi().implementation() { + PythonImplementation::CPython => match abi { + StableAbi::Abi3t => { + ensure!( + interpreter_config.target_abi().version() + >= PythonVersion { + major: 3, + minor: 15 + }, + "Abi3t builds are not supported on CPython targets before Python 3.15" ) } - } + StableAbi::Abi3 => {} + }, PythonImplementation::PyPy => warn!( - "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ - See https://github.com/pypy/pypy/issues/3397 for more information." + "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ + See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( - "GraalPy does not support abi3 so the build artifacts will be version-specific." + "GraalPy does not support {abi} so the build artifacts will be version-specific." ), PythonImplementation::RustPython => {} } diff --git a/pyo3-ffi/src/impl_/macros.rs b/pyo3-ffi/src/impl_/macros.rs index 8984f4aae97..5fc50dda2df 100644 --- a/pyo3-ffi/src/impl_/macros.rs +++ b/pyo3-ffi/src/impl_/macros.rs @@ -306,6 +306,8 @@ macro_rules! extern_libpython { extern_libpython!(@impl $abi { $($body)* } // abi3 "python3", "python3_d", + // abi3t + "python3t", "python3t_d", // Python 3.8 - 3.15 "python38", "python38_d", "python39", "python39_d", diff --git a/pyo3-ffi/src/impl_/mod.rs b/pyo3-ffi/src/impl_/mod.rs index df759d25207..02e08e6425c 100644 --- a/pyo3-ffi/src/impl_/mod.rs +++ b/pyo3-ffi/src/impl_/mod.rs @@ -1,4 +1,4 @@ -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] mod atomic_c_ulong { pub struct GetAtomicCULong(); @@ -17,6 +17,6 @@ mod atomic_c_ulong { } /// Typedef for an atomic integer to match the platform-dependent c_ulong type. -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] #[doc(hidden)] pub type AtomicCULong = atomic_c_ulong::TYPE; diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 447b705f6fd..9971556cffc 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -157,7 +157,9 @@ const _PyABIInfo_DEFAULT_FLAG_STABLE: u16 = 0; // skipped PyABIInfo_DEFAULT_ABI_VERSION: depends on Py_VERSION_HEX -#[cfg(all(Py_3_15, Py_GIL_DISABLED))] +#[cfg(all(Py_3_15, all(Py_LIMITED_API, Py_GIL_DISABLED)))] +const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADING_AGNOSTIC; +#[cfg(all(Py_3_15, Py_GIL_DISABLED, not(Py_LIMITED_API)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_FREETHREADED; #[cfg(all(Py_3_15, not(Py_GIL_DISABLED)))] const _PyABIInfo_DEFAULT_FLAG_FT: u16 = PyABIInfo_GIL; diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 03b811d286e..22379e18766 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,3 +1,4 @@ +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; @@ -58,6 +59,7 @@ extern_libpython! { pub static mut PyModuleDef_Type: PyTypeObject; } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] pub struct PyModuleDef_Base { pub ob_base: PyObject, @@ -67,6 +69,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -127,6 +130,7 @@ extern_libpython! { pub fn PyModule_GetToken(module: *mut PyObject, result: *mut *mut c_void) -> c_int; } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, @@ -140,3 +144,7 @@ pub struct PyModuleDef { pub m_clear: Option, pub m_free: Option, } + +// from pytypedefs.h +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] +opaque_struct!(pub PyModuleDef); diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index d3ab41757d4..d07afcc2475 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,15 +1,14 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; -#[cfg(Py_GIL_DISABLED)] -use crate::refcount; -#[cfg(Py_GIL_DISABLED)] -use crate::PyMutex; #[cfg(Py_3_15)] use crate::PySlot; +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] +use crate::{refcount, PyMutex}; use core::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; use core::mem; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use core::sync::atomic::{AtomicIsize, AtomicU32}; +// from pytypedefs.h #[cfg(Py_LIMITED_API)] opaque_struct!(pub PyTypeObject); @@ -93,6 +92,7 @@ const _PyObject_MIN_ALIGNMENT: usize = 4; // not currently possible to use constant variables with repr(align()), see // https://github.com/rust-lang/rust/issues/52840 +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[cfg_attr(not(all(Py_3_15, Py_GIL_DISABLED)), repr(C))] #[cfg_attr(all(Py_3_15, Py_GIL_DISABLED), repr(C, align(4)))] #[derive(Debug)] @@ -118,8 +118,10 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] const _: () = assert!(core::mem::align_of::() >= _PyObject_MIN_ALIGNMENT); +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[allow( clippy::declare_interior_mutable_const, reason = "contains atomic refcount on free-threaded builds" @@ -150,10 +152,15 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { ob_type: core::ptr::null_mut(), }; +// from pytypedefs.h +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] +opaque_struct!(pub PyObject); + // skipped _Py_UNOWNED_TID // skipped _PyObject_CAST +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] #[derive(Debug)] pub struct PyVarObject { @@ -165,6 +172,10 @@ pub struct PyVarObject { pub _ob_size_graalpy: Py_ssize_t, } +// from pytypedefs.h +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] +opaque_struct!(pub PyVarObject); + // skipped private _PyVarObject_CAST #[inline] @@ -221,6 +232,7 @@ extern_libpython! { pub static mut PyBool_Type: PyTypeObject; } +#[cfg(not(all(Py_LIMITED_API, Py_3_15)))] #[inline] #[cfg(not(RustPython))] pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index 232d1d6b2d2..4da10969b4a 100644 --- a/pyo3-ffi/src/refcount.rs +++ b/pyo3-ffi/src/refcount.rs @@ -11,7 +11,7 @@ use core::ffi::c_uint; #[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] use core::ffi::c_ulong; use core::ptr; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API)))] use core::sync::atomic::Ordering::Relaxed; #[cfg(all(Py_3_14, not(Py_3_15)))] @@ -116,6 +116,7 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { } } +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[cfg(Py_3_12)] #[inline(always)] unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 253064f27d9..bffe4715105 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1442,6 +1442,7 @@ pub trait ExtractPyClassWithClone: generic_pyclass::Sealed {} #[cfg(test)] #[cfg(feature = "macros")] mod tests { + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] use crate::pycell::impl_::PyClassObjectContents; use super::*; @@ -1470,17 +1471,23 @@ mod tests { Some(PyMethodDefType::StructMember(member)) => { assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[repr(C)] struct ExpectedLayout { ob_base: ffi::PyObject, contents: PyClassObjectContents, } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] assert_eq!( member.offset, (offset_of!(ExpectedLayout, contents) + offset_of!(FrozenClass, value)) as ffi::Py_ssize_t ); + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] assert_eq!(member.flags, ffi::Py_READONLY); + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + // ABI3T builds set other flags besides READONLY + assert_eq!(member.flags & ffi::Py_READONLY, ffi::Py_READONLY); } _ => panic!("Expected a StructMember"), } @@ -1592,17 +1599,17 @@ mod tests { // SAFETY: def.doc originated from a CStr assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc"); assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX); - #[allow(irrefutable_let_patterns)] - let PyObjectOffset::Absolute(contents_offset) = - ::Layout::CONTENTS_OFFSET - else { - panic!() + #[allow(clippy::infallible_destructuring_match)] + let contents_offset = match ::Layout::CONTENTS_OFFSET { + PyObjectOffset::Absolute(contents_offset) => contents_offset, + #[cfg(Py_3_12)] + PyObjectOffset::Relative(contents_offset) => contents_offset, }; assert_eq!( def.offset, contents_offset + FIELD_OFFSET as ffi::Py_ssize_t ); - assert_eq!(def.flags, ffi::Py_READONLY); + assert_eq!(def.flags & ffi::Py_READONLY, ffi::Py_READONLY); } #[test] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index d3d6e5f10d9..e8bfc714db0 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -34,15 +34,20 @@ use crate::prelude::PyTypeMethods; use crate::{ ffi, impl_::pyfunction::PyFunctionDef, - sync::PyOnceLock, - types::{any::PyAnyMethods, dict::PyDictMethods, PyDict, PyModule, PyModuleMethods}, - Bound, Py, PyAny, PyClass, PyResult, PyTypeInfo, Python, + types::{PyModule, PyModuleMethods}, + Bound, PyClass, PyResult, PyTypeInfo, }; use crate::{ffi_ptr_ext::FfiPtrExt, PyErr}; +use crate::{ + sync::PyOnceLock, + types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, + Py, PyAny, Python, +}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { // wrapped in UnsafeCell so that Rust compiler treats this as interior mutability + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] ffi_def: UnsafeCell, #[cfg(Py_3_15)] name: &'static CStr, @@ -72,7 +77,7 @@ impl ModuleDef { ) -> Self { // This is only used in PyO3 for append_to_inittab on Python 3.15 and newer. // There could also be other tools that need the legacy init hook. - // Opaque PyObject builds won't be able to use this. + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, @@ -86,6 +91,7 @@ impl ModuleDef { m_free: None, }; + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] let ffi_def = UnsafeCell::new(ffi::PyModuleDef { m_name: name.as_ptr(), m_doc: doc.as_ptr(), @@ -96,6 +102,7 @@ impl ModuleDef { }); ModuleDef { + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] ffi_def, #[cfg(Py_3_15)] name, @@ -115,7 +122,12 @@ impl ModuleDef { } pub fn init_multi_phase(&'static self) -> *mut ffi::PyObject { - unsafe { ffi::PyModuleDef_Init(self.ffi_def.get()) } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + unsafe { + ffi::PyModuleDef_Init(self.ffi_def.get()) + } + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + panic!("Legacy module initialization cannot work under abi3t. Use the PyModExport slots-based initialization hook instead."); } /// Builds a module object directly. Used for [`#[pymodule]`][crate::pymodule] submodules. @@ -575,6 +587,7 @@ mod tests { let module_def: ModuleDef = ModuleDef::new(NAME, DOC, &SLOTS); + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] unsafe { assert_eq!((*module_def.ffi_def.get()).m_slots, SLOTS.0.get().cast()); } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index fa538734648..2475acd937f 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -637,6 +637,9 @@ mod tests { #[pyclass(crate = "crate", extends = ImmutableChildOfImmutableBase, frozen)] struct ImmutableChildOfImmutableChildOfImmutableBase; + #[pyclass(crate = "crate", subclass)] + struct BaseWithoutData; + #[pyclass(crate = "crate", subclass)] struct BaseWithData(#[allow(unused)] u64); @@ -648,13 +651,32 @@ mod tests { #[test] fn test_inherited_size() { - let base_size = PyStaticClassObject::::BASIC_SIZE; - assert!(base_size > 0); // negative indicates variable sized - assert_eq!( - base_size, - PyStaticClassObject::::BASIC_SIZE - ); - assert!(base_size < PyStaticClassObject::::BASIC_SIZE); + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + type ClassObject = PyVariableClassObject; + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + type ClassObject = PyStaticClassObject; + + let base_without_data_size = ClassObject::::BASIC_SIZE; + let base_with_data_size = ClassObject::::BASIC_SIZE; + let child_without_data_size = ClassObject::::BASIC_SIZE; + let child_with_data_size = ClassObject::::BASIC_SIZE; + #[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] + { + assert!(base_without_data_size < 0); // negative indicates variable sized + assert!(base_with_data_size < base_without_data_size); + assert_eq!(child_without_data_size, 0); + assert_eq!( + base_with_data_size - base_without_data_size, + child_with_data_size + ); + } + #[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] + { + assert!(base_without_data_size > 0); + assert!(base_with_data_size > base_without_data_size); + assert_eq!(base_with_data_size, child_without_data_size); + assert!(base_with_data_size < child_with_data_size); + } } fn assert_mutable>() {} diff --git a/src/sync/critical_section.rs b/src/sync/critical_section.rs index a0bc96509e4..9ef0d6c2dad 100644 --- a/src/sync/critical_section.rs +++ b/src/sync/critical_section.rs @@ -46,10 +46,10 @@ use crate::{types::PyAny, Bound}; #[cfg(all(Py_3_14, not(Py_LIMITED_API)))] use core::cell::UnsafeCell; -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] struct CSGuard(crate::ffi::PyCriticalSection); -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] impl Drop for CSGuard { fn drop(&mut self) { unsafe { @@ -58,10 +58,10 @@ impl Drop for CSGuard { } } -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] struct CS2Guard(crate::ffi::PyCriticalSection2); -#[cfg(Py_GIL_DISABLED)] +#[cfg(all(Py_GIL_DISABLED, any(not(Py_LIMITED_API), not(Py_3_15), Py_3_15)))] impl Drop for CS2Guard { fn drop(&mut self) { unsafe { diff --git a/src/types/any.rs b/src/types/any.rs index fc3a3332b1f..cdb0e3f5dd5 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,7 +4,8 @@ use crate::conversion::{FromPyObject, IntoPyObject}; use crate::err::{error_on_minusone, PyErr, PyResult}; use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::impl_::pycell::PyStaticClassObject; +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] +use crate::impl_::pycell::{PyClassObjectBase, PyStaticClassObject}; use crate::instance::Bound; use crate::internal::get_slot::TP_DESCR_GET; use crate::py_result_ext::PyResultExt; @@ -67,15 +68,24 @@ pyobject_native_type_info!( ); pyobject_native_type_sized!(PyAny, ffi::PyObject); -// We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API`. +// We could use pyobject_subclassable_native_type for all builds here, but for +// now only on opaque PyObject builds to not introduce unintended behavior +// changes on older Python releases. +// +// The difference is that pyobject_subclassable_native_type will use variable +// object size for inheritance rather than PyStaticClassObject +#[cfg(not(all(Py_LIMITED_API, Py_GIL_DISABLED)))] impl crate::impl_::pyclass::PyClassBaseType for PyAny { - type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; + type LayoutAsBase = PyClassObjectBase; type BaseNativeType = PyAny; type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; type Layout = PyStaticClassObject; } +#[cfg(all(Py_LIMITED_API, Py_GIL_DISABLED))] +pyobject_subclassable_native_type!(PyAny, ffi::PyObject); + /// This trait represents the Python APIs which are usable on all Python objects. /// /// It is recommended you import this trait via `use pyo3::prelude::*` rather than diff --git a/src/types/list.rs b/src/types/list.rs index 6d02039da93..b3bf7ff12e7 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -699,7 +699,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn fold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -715,7 +715,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), feature = "nightly"))] fn try_fold(&mut self, init: B, mut f: F) -> R where Self: Sized, @@ -732,7 +732,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn all(&mut self, mut f: F) -> bool where Self: Sized, @@ -749,7 +749,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn any(&mut self, mut f: F) -> bool where Self: Sized, @@ -766,7 +766,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn find

(&mut self, mut predicate: P) -> Option where Self: Sized, @@ -783,7 +783,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn find_map(&mut self, mut f: F) -> Option where Self: Sized, @@ -800,7 +800,7 @@ impl<'py> Iterator for BoundListIterator<'py> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn position

(&mut self, mut predicate: P) -> Option where Self: Sized, @@ -872,7 +872,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), not(feature = "nightly")))] fn rfold(mut self, init: B, mut f: F) -> B where Self: Sized, @@ -888,7 +888,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { } #[inline] - #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + #[cfg(all(Py_GIL_DISABLED, not(Py_LIMITED_API), feature = "nightly"))] fn try_rfold(&mut self, init: B, mut f: F) -> R where Self: Sized, diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 1ff7b5d698b..20b968fa2dc 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -20,7 +20,7 @@ mod module_mod_with_functions { use super::foo; } -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(any(PyPy, GraalPy, all(Py_LIMITED_API, Py_GIL_DISABLED))))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab;