From 5ced90e140e8be2f610f62677fc89f99430d60d8 Mon Sep 17 00:00:00 2001 From: Ralf Gommers Date: Tue, 15 Jul 2025 15:19:39 +0200 Subject: [PATCH 1/2] ENH: implement support for build-details.json (PEP 739) --- mesonpy/__init__.py | 29 +++++++++++++++++++--- mesonpy/_tags.py | 60 ++++++++++++++++++++++++++++++++++++--------- tests/test_tags.py | 12 ++++++++- tests/test_wheel.py | 2 +- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 3281bb1d..38d7fd02 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -340,6 +340,7 @@ class _WheelBuilder(): _manifest: Dict[str, List[_Entry]] _limited_api: bool _allow_windows_shared_libs: bool + _build_details: mesonpy._tags.BuildDetails | None @property def _has_internal_libs(self) -> bool: @@ -370,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: @@ -834,6 +835,24 @@ def __init__( ''') self._meson_native_file.write_text(native_file_data, encoding='utf-8') + # Starting with version 1.10, Meson can consume a `build-detail.json` + # file following the specification is PEP 739 to obtain required + # information to build extension modules without having to run the + # interpreter. The path to the `build-details.json` can be specified + # passing the with the `-Dpython.build_config=` option to `meson + # setup`. Extract the value passed to this option and use the details + # in the `build-details.json` file to compute the wheel tag. + self._build_details: mesonpy._tags.BuildDetails | None = None + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument('-D', action='append', default=[]) + args, _ = parser.parse_known_args(self._meson_args['setup']) + for arg in reversed(args.D): + name, value = arg.split('=', 1) + if name == 'python.build_config': + with open(value, 'r', encoding='utf8') as f: + self._build_details = json.load(f) + break + # 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 @@ -1151,13 +1170,15 @@ 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._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) + builder = _EditableWheelBuilder( + self._metadata, self._manifest, self._limited_api, self._allow_windows_shared_libs, 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 00c86100..ceccdf8f 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -9,6 +9,27 @@ import struct import sys import sysconfig +import typing + + +if typing.TYPE_CHECKING: # pragma: no cover + from typing import TypedDict + + class _Abi(TypedDict): + extension_suffix: str + + class _ImplementationVersion(TypedDict): + major: int + minor: int + + class _Implementation(TypedDict): + name: str + version: _ImplementationVersion + + class BuildDetails(TypedDict): + abi: _Abi + implementation: _Implementation + platform: str # https://peps.python.org/pep-0425/#python-tag @@ -24,14 +45,20 @@ _32_BIT_INTERPRETER = struct.calcsize('P') == 4 -def get_interpreter_tag() -> str: - name = sys.implementation.name +def get_interpreter_tag(build_details: BuildDetails | None = None) -> str: + if build_details is None: + name = sys.implementation.name + major, minor = sys.version_info[:2] + else: + name = build_details['implementation']['name'] + _v = build_details['implementation']['version'] + major = _v['major'] + minor = _v['minor'] name = INTERPRETERS.get(name, name) - version = sys.version_info - return f'{name}{version[0]}{version[1]}' + return f'{name}{major}{minor}' -def get_abi_tag() -> str: +def get_abi_tag(build_details: BuildDetails | None = None) -> str: # The best solution to obtain the Python ABI is to parse the # $SOABI or $EXT_SUFFIX sysconfig variables as defined in PEP-314. @@ -39,7 +66,11 @@ def get_abi_tag() -> str: # Using $EXT_SUFFIX will not break when PyPy will fix this. # See https://foss.heptapod.net/pypy/pypy/-/issues/3816 and # https://github.com/pypa/packaging/pull/607. - empty, abi, ext = str(sysconfig.get_config_var('EXT_SUFFIX')).split('.') + if build_details is None: + ext_suffix = str(sysconfig.get_config_var('EXT_SUFFIX')) + else: + ext_suffix = build_details['abi']['extension_suffix'] + empty, abi, ext = ext_suffix.split('.') # The packaging module initially based his understanding of the # $SOABI variable on the inconsistent value reported by PyPy, and @@ -147,8 +178,12 @@ 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: BuildDetails | None = None) -> str: + if build_details is None: + platform = sysconfig.get_platform() + else: + platform = build_details['platform'] + if platform.startswith('macosx'): return _get_macosx_platform_tag() if platform.startswith('ios'): @@ -163,10 +198,11 @@ def get_platform_tag() -> str: class Tag: - def __init__(self, interpreter: str | None = None, abi: str | None = None, platform: str | None = 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: str | None = None, abi: str | None = None, platform: str | None = None, + build_details: BuildDetails | None = 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}' diff --git a/tests/test_tags.py b/tests/test_tags.py index 88972d11..1d2c68b9 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 @@ -106,7 +107,7 @@ def wheel_builder_test_factory(content, pure=True, limited_api=False): manifest = defaultdict(list) for key, value in content.items(): manifest[key] = [mesonpy._Entry(pathlib.Path(x), os.path.join('build', x)) for x in value] - return mesonpy._WheelBuilder(None, manifest, limited_api, False) + return mesonpy._WheelBuilder(None, manifest, limited_api, False, None) def test_tag_empty_wheel(): @@ -149,3 +150,12 @@ 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}' + + +def test_build_details(): + try: + with open(os.path.join(sysconfig.get_path('stdlib'), 'build-details.json'), encoding='utf8') as f: + build_details = json.load(f) + except FileNotFoundError: + return pytest.skip('build-details.json not found') + assert str(mesonpy._tags.Tag()) == str(mesonpy._tags.Tag(build_details=build_details)) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 2fdf7a5a..7f24eae0 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -255,7 +255,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, None) assert set(builder._top_level_modules) == { 'file', 'package', From 6b9e33e0d153a4ed282430e12763603e74c1fe90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 6 Jul 2026 16:20:14 +0200 Subject: [PATCH 2/2] Disable 32-bit interpreter hack when using build-details.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Michał Górny --- mesonpy/_tags.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py index ceccdf8f..b4b19086 100644 --- a/mesonpy/_tags.py +++ b/mesonpy/_tags.py @@ -91,7 +91,7 @@ def get_abi_tag(build_details: BuildDetails | None = None) -> str: return abi.replace('.', '_').replace('-', '_') -def _get_macosx_platform_tag() -> str: +def _get_macosx_platform_tag(build_details: BuildDetails | None = None) -> str: ver, _, arch = platform.mac_ver() # Override the architecture with the one provided in the @@ -151,7 +151,9 @@ def _get_macosx_platform_tag() -> str: # the patch level. Reset the patch level to zero. minor = 0 - if _32_BIT_INTERPRETER: + # When using build-details.json, the platform recorded should be correct + # per the bitness of the interpreter. + if build_details is None and _32_BIT_INTERPRETER: # 32-bit Python running on a 64-bit kernel. if arch == 'ppc64': arch = 'ppc' @@ -185,10 +187,12 @@ def get_platform_tag(build_details: BuildDetails | None = None) -> str: platform = build_details['platform'] if platform.startswith('macosx'): - return _get_macosx_platform_tag() + return _get_macosx_platform_tag(build_details) if platform.startswith('ios'): return _get_ios_platform_tag() - if _32_BIT_INTERPRETER: + # When using build-details.json, the platform recorded should be correct + # per the bitness of the interpreter. + if build_details is None and _32_BIT_INTERPRETER: # 32-bit Python running on a 64-bit kernel. if platform == 'linux-x86_64': return 'linux_i686'