diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index aaddde31..31c3113c 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[mesonpy._tags.BuildDetailsDict] = 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 + # 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 + # 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,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) + 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) + 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/_compat.py b/mesonpy/_compat.py index 8d22c166..81460c1f 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 9c09fe5a..1547f39e 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,11 +47,17 @@ _32_BIT_INTERPRETER = struct.calcsize('P') == 4 -def get_interpreter_tag() -> str: - name = sys.implementation.name +def get_interpreter_tag(build_details: Optional[BuildDetailsDict] = 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_config_var(name: str, default: Union[str, int, None] = None) -> Union[str, int, None]: @@ -53,7 +77,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[BuildDetailsDict] = 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 +91,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 +207,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[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() if platform.startswith('ios'): @@ -194,10 +223,11 @@ 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[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) 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 62aa6193..b57120b1 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(): @@ -102,7 +113,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(): @@ -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)) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index e3c93559..fa3f5594 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',