diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 7325d23f1..3ca554af0 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -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 @@ -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: @@ -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. diff --git a/tests/packages/limited-api-ft/meson.build b/tests/packages/limited-api-ft/meson.build new file mode 100644 index 000000000..297bf16f2 --- /dev/null +++ b/tests/packages/limited-api-ft/meson.build @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2023-2026 The meson-python developers +# +# 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, +) diff --git a/tests/packages/limited-api-ft/module.c b/tests/packages/limited-api-ft/module.c new file mode 100644 index 000000000..98032e8f8 --- /dev/null +++ b/tests/packages/limited-api-ft/module.c @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023-2026 The meson-python developers +// +// SPDX-License-Identifier: MIT + +#include + +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; +} diff --git a/tests/packages/limited-api-ft/pyproject.toml b/tests/packages/limited-api-ft/pyproject.toml new file mode 100644 index 000000000..26631d4e1 --- /dev/null +++ b/tests/packages/limited-api-ft/pyproject.toml @@ -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 diff --git a/tests/packages/limited-api/module.c b/tests/packages/limited-api/module.c index e53b2f13a..c641b4066 100644 --- a/tests/packages/limited-api/module.c +++ b/tests/packages/limited-api/module.c @@ -20,7 +20,7 @@ static PyMethodDef methods[] = { static struct PyModuleDef module = { PyModuleDef_HEAD_INIT, - "plat", + "module", NULL, -1, methods, diff --git a/tests/test_tags.py b/tests/test_tags.py index d2bbd057b..29640b394 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -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. @@ -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 @@ -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' + else: + abi = 'abi3' assert str(builder.tag) == f'{INTERPRETER}-{abi}-{PLATFORM}' diff --git a/tests/test_wheel.py b/tests/test_wheel.py index e3c935599..1f40224e8 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -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