From 3ad3cc4155424499fd8923b03c2043b020c20e8f Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Tue, 15 Jul 2025 15:19:39 +0200 Subject: [PATCH 1/6] WIP: ENH: implement support for build-details.json (PEP 739) --- mesonpy/__init__.py | 25 ++++++++++++++++++++++--- mesonpy/_tags.py | 32 +++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index aaddde316..0f1a3f601 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -332,11 +332,15 @@ def __init__( manifest: Dict[str, List[Tuple[pathlib.Path, str]]], limited_api: bool, allow_windows_shared_libs: bool, + is_cross: bool, + build_details: Optional[Dict[str, str | Dict[str, Any]]] = None, ) -> None: self._metadata = metadata self._manifest = manifest self._limited_api = limited_api self._allow_windows_shared_libs = allow_windows_shared_libs + self._is_cross = is_cross + self._build_details = build_details @property def _has_internal_libs(self) -> bool: @@ -367,8 +371,8 @@ def tag(self) -> mesonpy._tags.Tag: # does not contain any extension module (does not # distribute any file in {platlib}) thus use generic # implementation and ABI tags. - return mesonpy._tags.Tag('py3', 'none', None) - return mesonpy._tags.Tag(None, self._stable_abi, None) + return mesonpy._tags.Tag('py3', 'none', None, self._build_details) + return mesonpy._tags.Tag(None, self._stable_abi, None, self._build_details) @property def name(self) -> str: @@ -779,6 +783,21 @@ def __init__( ''') self._meson_native_file.write_text(native_file_data, encoding='utf-8') + # Handle cross compilation + self._is_cross = any(s.startswith('--cross-file') for s in self._meson_args['setup']) + self._build_details = None + if self._is_cross: + # Use build-details.json (PEP 739) to determine + # platform/interpreter/abi tags, if given. + for setup_arg in self._meson_args['setup']: + if setup_arg.startswith('-Dpython.build_config='): + with open(setup_arg.split('-Dpython.build_config=')[1]) as f: + self._build_details = json.load(f) + break + else: + # TODO: warn that interpreter details may be wrong. Get platform from cross file. + pass + # reconfigure if we have a valid Meson build directory. Meson # uses the presence of the 'meson-private/coredata.dat' file # in the build directory as indication that the build @@ -1096,7 +1115,7 @@ def sdist(self, directory: Path) -> pathlib.Path: def wheel(self, directory: Path) -> pathlib.Path: """Generates a wheel in the specified directory.""" self.build() - builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs) + builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._is_cross, self._build_details) return builder.build(directory) def editable(self, directory: Path) -> pathlib.Path: diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index 9c09fe5a1..a3a994ef2 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -29,10 +29,15 @@ _32_BIT_INTERPRETER = struct.calcsize('P') == 4 -def get_interpreter_tag() -> str: - name = sys.implementation.name +def get_interpreter_tag(build_details: Optional[dict] = None) -> str: + if build_details is None: + name = sys.implementation.name + version = sys.version_info + else: + name = build_details['implementation']['name'] + _v = build_details['implementation']['version'] + version = (_v['major'], _v['minor']) name = INTERPRETERS.get(name, name) - version = sys.version_info return f'{name}{version[0]}{version[1]}' @@ -53,7 +58,12 @@ def _get_cpython_abi() -> str: return f'cp{version[0]}{version[1]}{debug}{pymalloc}' -def get_abi_tag() -> str: +def get_abi_tag(build_details: Optional[dict] = None) -> str: + if build_details is not None: + ext_suffix = build_details['abi']['extension_suffix'] + else: + ext_suffix = sysconfig.get_config_var('EXT_SUFFIX') + # The best solution to obtain the Python ABI is to parse the # $SOABI or $EXT_SUFFIX sysconfig variables as defined in PEP-314. @@ -62,7 +72,7 @@ def get_abi_tag() -> str: # See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and # https://github.com/pypa/packaging/pull/607. try: - empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.') + empty, abi, ext = str(ext_suffix).split('.') except ValueError as exc: # CPython <= 3.8.7 on Windows does not implement PEP3149 and # uses '.pyd' as $EXT_SUFFIX, which does not allow to extract @@ -178,8 +188,8 @@ def _get_ios_platform_tag() -> str: return f'ios_{version[0]}_{version[1]}_{multiarch}' -def get_platform_tag() -> str: - platform = sysconfig.get_platform() +def get_platform_tag(build_details: Optional[dict] = None) -> str: + platform = build_details['platform'] if build_details is not None else sysconfig.get_platform() if platform.startswith('macosx'): return _get_macosx_platform_tag() if platform.startswith('ios'): @@ -194,10 +204,10 @@ def get_platform_tag() -> str: class Tag: - def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None): - self.interpreter = interpreter or get_interpreter_tag() - self.abi = abi or get_abi_tag() - self.platform = platform or get_platform_tag() + def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None, build_details: Optional[dict] = None): + self.interpreter = interpreter or get_interpreter_tag(build_details) + self.abi = abi or get_abi_tag(build_details) + self.platform = platform or get_platform_tag(build_details) def __str__(self) -> str: return f'{self.interpreter}-{self.abi}-{self.platform}' From 888ad04c9fffcb6d17e7ce6fde8c09004baade61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 8 Jan 2026 16:41:00 +0100 Subject: [PATCH 2/6] Update new _WheelBuilder inits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- mesonpy/__init__.py | 2 +- tests/test_tags.py | 2 +- tests/test_wheel.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 0f1a3f601..035d5e50c 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -1121,7 +1121,7 @@ def wheel(self, directory: Path) -> pathlib.Path: def editable(self, directory: Path) -> pathlib.Path: """Generates an editable wheel in the specified directory.""" self.build() - builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs) + builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._is_cross, self._build_details) return builder.build(directory, self._source_dir, self._build_dir, self._build_command, self._editable_verbose) diff --git a/tests/test_tags.py b/tests/test_tags.py index 62aa61934..1aff4eeb0 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -102,7 +102,7 @@ def test_ios_platform_tag(monkeypatch): def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) manifest.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) - return mesonpy._WheelBuilder(None, manifest, limited_api, False) + return mesonpy._WheelBuilder(None, manifest, limited_api, False, False, None) def test_tag_empty_wheel(): diff --git a/tests/test_wheel.py b/tests/test_wheel.py index e3c935599..fa3f55948 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -265,7 +265,7 @@ def test_entrypoints(wheel_full_metadata): def test_top_level_modules(package_module_types): with mesonpy._project() as project: builder = mesonpy._EditableWheelBuilder( - project._metadata, project._manifest, project._limited_api, project._allow_windows_shared_libs) + project._metadata, project._manifest, project._limited_api, project._allow_windows_shared_libs, False, None) assert set(builder._top_level_modules) == { 'file', 'package', From b60fb11d254e3a900e935cfef8804b72ad18dd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 8 Jan 2026 16:43:48 +0100 Subject: [PATCH 3/6] Address ruff errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- mesonpy/__init__.py | 8 ++++++-- mesonpy/_tags.py | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 035d5e50c..759a3b90b 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -1115,13 +1115,17 @@ def sdist(self, directory: Path) -> pathlib.Path: def wheel(self, directory: Path) -> pathlib.Path: """Generates a wheel in the specified directory.""" self.build() - builder = _WheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._is_cross, self._build_details) + builder = _WheelBuilder( + self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, + self._is_cross, self._build_details) return builder.build(directory) def editable(self, directory: Path) -> pathlib.Path: """Generates an editable wheel in the specified directory.""" self.build() - builder = _EditableWheelBuilder(self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, self._is_cross, self._build_details) + builder = _EditableWheelBuilder( + self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, + self._is_cross, self._build_details) return builder.build(directory, self._source_dir, self._build_dir, self._build_command, self._editable_verbose) diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index a3a994ef2..35adf3237 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -204,7 +204,8 @@ def get_platform_tag(build_details: Optional[dict] = None) -> str: class Tag: - def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None, build_details: Optional[dict] = None): + def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None, + build_details: Optional[dict] = None): self.interpreter = interpreter or get_interpreter_tag(build_details) self.abi = abi or get_abi_tag(build_details) self.platform = platform or get_platform_tag(build_details) From 7a33fb4fa20c447097324a6dca96f17698703458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 8 Jan 2026 18:16:02 +0100 Subject: [PATCH 4/6] Add TypedDict for build_details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- mesonpy/__init__.py | 2 +- mesonpy/_compat.py | 5 +++-- mesonpy/_tags.py | 33 ++++++++++++++++++++++++++------- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 759a3b90b..d4223bdc7 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -333,7 +333,7 @@ def __init__( limited_api: bool, allow_windows_shared_libs: bool, is_cross: bool, - build_details: Optional[Dict[str, str | Dict[str, Any]]] = None, + build_details: Optional[mesonpy._tags.BuildDetailsDict] = None, ) -> None: self._metadata = metadata self._manifest = manifest diff --git a/mesonpy/_compat.py b/mesonpy/_compat.py index 8d22c166e..81460c1fc 100644 --- a/mesonpy/_compat.py +++ b/mesonpy/_compat.py @@ -34,9 +34,9 @@ def read_binary(package: str, resource: str) -> bytes: from typing_extensions import ParamSpec if sys.version_info >= (3, 11): - from typing import Self + from typing import Self, TypedDict else: - from typing_extensions import Self + from typing_extensions import Self, TypedDict Path = Union[str, os.PathLike] @@ -51,4 +51,5 @@ def read_binary(package: str, resource: str) -> bytes: 'ParamSpec', 'Self', 'Sequence', + 'TypedDict', ] diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index 35adf3237..1547f39e3 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -15,6 +15,24 @@ if typing.TYPE_CHECKING: # pragma: no cover from typing import Optional, Union + from mesonpy._compat import TypedDict + + class _AbiDict(TypedDict): + extension_suffix: str + + class _ImplementationVersionDict(TypedDict): + major: int + minor: int + + class _ImplementationDict(TypedDict): + name: str + version: _ImplementationVersionDict + + class BuildDetailsDict(TypedDict): + abi: _AbiDict + implementation: _ImplementationDict + platform: str + # https://peps.python.org/pep-0425/#python-tag INTERPRETERS = { @@ -29,16 +47,17 @@ _32_BIT_INTERPRETER = struct.calcsize('P') == 4 -def get_interpreter_tag(build_details: Optional[dict] = None) -> str: +def get_interpreter_tag(build_details: Optional[BuildDetailsDict] = None) -> str: if build_details is None: name = sys.implementation.name - version = sys.version_info + major, minor = sys.version_info[:2] else: name = build_details['implementation']['name'] _v = build_details['implementation']['version'] - version = (_v['major'], _v['minor']) + major = _v['major'] + minor = _v['minor'] name = INTERPRETERS.get(name, name) - return f'{name}{version[0]}{version[1]}' + return f'{name}{major}{minor}' def _get_config_var(name: str, default: Union[str, int, None] = None) -> Union[str, int, None]: @@ -58,7 +77,7 @@ def _get_cpython_abi() -> str: return f'cp{version[0]}{version[1]}{debug}{pymalloc}' -def get_abi_tag(build_details: Optional[dict] = None) -> str: +def get_abi_tag(build_details: Optional[BuildDetailsDict] = None) -> str: if build_details is not None: ext_suffix = build_details['abi']['extension_suffix'] else: @@ -188,7 +207,7 @@ def _get_ios_platform_tag() -> str: return f'ios_{version[0]}_{version[1]}_{multiarch}' -def get_platform_tag(build_details: Optional[dict] = None) -> str: +def get_platform_tag(build_details: Optional[BuildDetailsDict] = None) -> str: platform = build_details['platform'] if build_details is not None else sysconfig.get_platform() if platform.startswith('macosx'): return _get_macosx_platform_tag() @@ -205,7 +224,7 @@ def get_platform_tag(build_details: Optional[dict] = None) -> str: class Tag: def __init__(self, interpreter: Optional[str] = None, abi: Optional[str] = None, platform: Optional[str] = None, - build_details: Optional[dict] = None): + build_details: Optional[BuildDetailsDict] = None): self.interpreter = interpreter or get_interpreter_tag(build_details) self.abi = abi or get_abi_tag(build_details) self.platform = platform or get_platform_tag(build_details) From 66cf573971c78e7e426d36886e322ca5ed431535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 8 Jan 2026 18:58:22 +0100 Subject: [PATCH 5/6] Always use build-details if specified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- mesonpy/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index d4223bdc7..31c3113c3 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -786,15 +786,15 @@ def __init__( # Handle cross compilation self._is_cross = any(s.startswith('--cross-file') for s in self._meson_args['setup']) self._build_details = None - if self._is_cross: - # Use build-details.json (PEP 739) to determine - # platform/interpreter/abi tags, if given. - for setup_arg in self._meson_args['setup']: - if setup_arg.startswith('-Dpython.build_config='): - with open(setup_arg.split('-Dpython.build_config=')[1]) as f: - self._build_details = json.load(f) - break - else: + # Use build-details.json (PEP 739) to determine + # platform/interpreter/abi tags, if given. + for setup_arg in reversed(self._meson_args['setup']): + if setup_arg.startswith('-Dpython.build_config='): + with open(setup_arg.split('=', 1)[1]) as f: + self._build_details = json.load(f) + break + else: + if self._is_cross: # TODO: warn that interpreter details may be wrong. Get platform from cross file. pass From 9862d32a9836dbacc13df70f13ccd6f4b2641a84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Thu, 8 Jan 2026 19:27:56 +0100 Subject: [PATCH 6/6] Add an initial equivalence test for build-details.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- tests/test_tags.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_tags.py b/tests/test_tags.py index 1aff4eeb0..b57120b14 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: MIT import importlib.machinery +import json import os import pathlib import platform @@ -36,8 +37,18 @@ def get_abi3_suffix(): return suffix +def get_build_details_json(): + # Technically, this is only applicable to 3.14+, but we account for FileNotFoundError anyway. + try: + with open(pathlib.Path(sysconfig.get_path('stdlib')) / 'build-details.json') as f: + return json.load(f) + except FileNotFoundError: + return None + + SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') ABI3SUFFIX = get_abi3_suffix() +BUILD_DETAILS_JSON = get_build_details_json() def test_wheel_tag(): @@ -141,3 +152,8 @@ def test_tag_mixed_abi(): }, pure=False, limited_api=True) with pytest.raises(mesonpy.BuildError, match='The package declares compatibility with Python limited API but '): assert str(builder.tag) == f'{INTERPRETER}-abi3-{PLATFORM}' + + +@pytest.mark.skipif(BUILD_DETAILS_JSON is None, reason='No build-details.json for this interpreter') +def test_system_build_details(): + assert str(mesonpy._tags.Tag()) == str(mesonpy._tags.Tag(build_details=BUILD_DETAILS_JSON))