Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,13 @@ def _stable_abi(self) -> Optional[str]:
# not use the stable ABI filename suffix and wheels should not
# be tagged with the abi3 tag.
if self._limited_api and '__pypy__' not in sys.builtin_module_names:
# On free-threaded Python 3.15.0b2+, we expect to be
# building 'abi3t' wheels for the time being. In the future
# we will want an option to target 'abi3t' from GIL-enabled
# Python too.
abi3t = bool(sysconfig.get_config_var('Py_GIL_DISABLED')) and sys.version_info >= (3, 15)
expected_abi = 'abi3t' if abi3t else 'abi3'

# Verify stable ABI compatibility: examine files installed
# in {platlib} that look like extension modules, and raise
# an exception if any of them has a Python version
Expand All @@ -434,11 +441,11 @@ def _stable_abi(self) -> Optional[str]:
match = _EXTENSION_SUFFIX_REGEX.match(entry.dst.name)
if match:
abi = match.group('abi')
if abi is not None and abi != 'abi3':
if abi is not None and abi != expected_abi:
raise BuildError(
f'The package declares compatibility with Python limited API but extension '
f'module {os.fspath(entry.dst)!r} is tagged for a specific Python version.')
return 'abi3'
return 'abi3.abi3t' if abi3t else 'abi3'
return None

def _install_path(self, wheel_file: mesonpy._wheelfile.WheelFile, origin: Path, destination: pathlib.Path) -> None:
Expand Down Expand Up @@ -874,10 +881,10 @@ def __init__(
if not allow_limited_api:
self._limited_api = False

if self._limited_api and bool(sysconfig.get_config_var('Py_GIL_DISABLED')):
if self._limited_api and bool(sysconfig.get_config_var('Py_GIL_DISABLED')) and sys.version_info < (3, 15):
raise BuildError(
'The package targets Python\'s Limited API, which is not supported by free-threaded CPython. '
'The "python.allow_limited_api" Meson build option may be used to override the package default.')
'The package targets Python\'s Limited API, which is not supported by free-threaded CPython before version '
'3.15. The "python.allow_limited_api" Meson build option may be used to override the package default.')

# Shared library support on Windows requires collaboration
# from the package, make sure the developers acknowledge this.
Expand Down
14 changes: 14 additions & 0 deletions tests/packages/limited-api-ft/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2023-2026 The meson-python developers
Comment thread
rgommers marked this conversation as resolved.
#
# SPDX-License-Identifier: MIT

project('limited-api-ft', 'c', version: '1.0.0', meson_version: '>= 1.3')

py = import('python').find_installation(pure: false)

py.extension_module(
'module',
'module.c',
limited_api: '3.15',
install: true,
)
33 changes: 33 additions & 0 deletions tests/packages/limited-api-ft/module.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-FileCopyrightText: 2023-2026 The meson-python developers
//
// SPDX-License-Identifier: MIT

#include <Python.h>

static PyObject* add(PyObject *self, PyObject *args) {
int a, b;

if (!PyArg_ParseTuple(args, "ii", &a, &b))
return NULL;

return PyLong_FromLong(a + b);
}

static PyMethodDef methods[] = {
{"add", add, METH_VARARGS, NULL},
{NULL, NULL, 0, NULL},
};

PyABIInfo_VAR(abi_info);

static PySlot module_slots[] = {
PySlot_STATIC_DATA(Py_mod_name, "module"),
PySlot_STATIC_DATA(Py_mod_methods, methods),
PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
PySlot_DATA(Py_mod_abi, &abi_info),
PySlot_END,
};

PyMODEXPORT_FUNC PyModExport_module(void) {
return module_slots;
}
10 changes: 10 additions & 0 deletions tests/packages/limited-api-ft/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2023-2026 The meson-python developers
#
# SPDX-License-Identifier: MIT

[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']

[tool.meson-python]
limited-api = true
2 changes: 1 addition & 1 deletion tests/packages/limited-api/module.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ static PyMethodDef methods[] = {

static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"plat",
"module",
NULL,
-1,
methods,
Expand Down
11 changes: 10 additions & 1 deletion tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import mesonpy._tags

from .conftest import adjust_packaging_platform_tag
from .test_wheel import NOGIL_BUILD


# Test against the wheel tag generated by packaging module.
Expand All @@ -29,6 +30,9 @@


def get_abi3_suffix():
# EXTENSION_SUFFIXES are ordered in preference order, and more specific ABI is preferred.
# On free-threaded 3.15+, this will match ".abi3t" as the only supported stable ABI.
# On GIL-enabled 3.15+, it will match plain ".abi3" first, since that ABI is more specific.
for suffix in importlib.machinery.EXTENSION_SUFFIXES:
if '.abi3' in suffix: # Unix
return suffix
Expand Down Expand Up @@ -130,7 +134,12 @@ def test_tag_stable_abi():
'platlib': [f'extension{ABI3SUFFIX}'],
}, limited_api=True)
# PyPy does not support the stable ABI.
abi = 'abi3' if '__pypy__' not in sys.builtin_module_names else ABI
if '__pypy__' in sys.builtin_module_names:
abi = ABI
elif NOGIL_BUILD and sys.version_info >= (3, 15):
abi = 'abi3.abi3t'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think that, if this works, it does by chance. The fake data passed to the wheel builder uses ABI3SUFFIX which is defined at the top of this module. That that resolves to abi3t because of the orders in which the suffixes are listed in importlib.machinery.EXTENSION_SUFFIXES. Is the order guaranteed or should we try to make this more robust? If the order is guaranteed, I would still add a comment in the get_abi3_suffix() explaining what it is expected and why it works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A comment would be great. My understanding is that it is guaranteed - and Meson relies on it as well, which came up in the PEP 803 discussions. (that said, future-proofing might be wise).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is guaranteed, CPython is driving module loading on it. For GIL-enabled, it's:

>>> importlib.machinery.EXTENSION_SUFFIXES
['.cpython-315-x86_64-linux-gnu.so', '.abi3.so', '.abi3t.so', '.so']

It's ordered from most specific (i.e. presumably most performant) to least specific, to .so legacy fallback. I'll add a comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Doesn't this mean that get_abi3_suffix() on Python 3.15t returns .abi3.so and this test fails because when building on Python 3.15t the extension modules should be suffixed with .abi3t.so?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Doesn't this mean that get_abi3_suffix() on Python 3.15t returns .abi3.so and this test fails because when building on Python 3.15t the extension modules should be suffixed with .abi3t.so?

Nope, EXTENSION_SUFFIXES is different on 3.15t:

Python 3.15.0b2 free-threading build (main, Jun  4 2026, 14:23:26) [Clang 21.0.0 (clang-2100.1.1.101)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import importlib.machinery
>>> importlib.machinery.EXTENSION_SUFFIXES
['.cpython-315t-darwin.so', '.abi3t.so', '.so']

This is actually explicitly specified in PEP 803: https://peps.python.org/pep-0803/#the-abi3t-wheel-and-filename-tags

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

And yes, abi3.so is included on 3.14t and 3.13t and that's probably a bug but it was only noticed in the discussion around PEP 803 after 3.14t came out. Arguably we probably should have fixed this in the lead-up to 3.13t but no one noticed. I have no idea if removing .abi3.so from EXTENSION_SUFFIXES on 3.14t would have fallout.

else:
abi = 'abi3'
assert str(builder.tag) == f'{INTERPRETER}-{abi}-{PLATFORM}'


Expand Down
11 changes: 11 additions & 0 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,14 @@ def test_cmake_subproject(wheel_cmake_subproject):
f'.cmake_subproject.mesonpy.libs/libcmaketest{LIB_SUFFIX}',
f'cmakesubproject{EXT_SUFFIX}',
}


# Requires Meson 1.3.0, see https://github.com/mesonbuild/meson/pull/11745.
@pytest.mark.skipif(MESON_VERSION < (1, 2, 99), reason='meson too old')
@pytest.mark.skipif(sys.version_info < (3, 15), reason='Requires PEP 820 API, present since Python 3.15')
def test_limited_api_ft(wheel_limited_api_ft):
artifact = wheel.wheelfile.WheelFile(wheel_limited_api_ft)
name = artifact.parsed_filename
assert name.group('pyver') == INTERPRETER
assert name.group('abi') == 'abi3.abi3t' if NOGIL_BUILD else 'abi3'
assert name.group('plat') == PLATFORM
Loading