diff --git a/meson.build b/meson.build index 7165e3dce..ce69b7d30 100644 --- a/meson.build +++ b/meson.build @@ -11,6 +11,7 @@ py.install_sources( 'mesonpy/__init__.py', 'mesonpy/_compat.py', 'mesonpy/_elf.py', + 'mesonpy/_tags.py', 'mesonpy/_util.py', subdir: 'mesonpy', ) diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index beb8cd285..1e5d1591c 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -36,8 +36,6 @@ Union ) -import packaging.tags - if sys.version_info < (3, 11): import tomli as tomllib @@ -46,6 +44,7 @@ import mesonpy._compat import mesonpy._elf +import mesonpy._tags import mesonpy._util from mesonpy._compat import Iterator, Path @@ -109,30 +108,6 @@ def _init_colors() -> Dict[str, str]: assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES) -def _adjust_manylinux_tag(platform: str) -> str: - # The packaging module generates overly specific platforms tags on - # Linux. The platforms tags on Linux evolved over time. Relax - # the platform tags to maintain compatibility with old wheel - # installation tools. The relaxed platform tags match the ones - # generated by the wheel package. - # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/ - return re.sub(r'^manylinux(1|2010|2014|_\d+_\d+)_(.*)$', r'linux_\2', platform) - - -def _adjust_darwin_tag(platform: str) -> str: - # Override the macOS version if one is provided via the - # MACOS_DEPLOYMENT_TARGET environment variable. Return it - # unchanged otherwise. - try: - version = tuple(map(int, os.environ.get('MACOS_DEPLOYMENT_TARGET', '').split('.')))[:2] - except ValueError: - version = None - if version is not None: - # str() to silence mypy - platform = str(next(packaging.tags.mac_platforms(version))) - return platform - - def _showwarning( message: Union[Warning, str], category: Type[Warning], @@ -219,26 +194,17 @@ def basename(self) -> str: ) @property - def tag(self) -> packaging.tags.Tag: + def tag(self) -> mesonpy._tags.Tag: """Wheel tags.""" if self.is_pure: - return packaging.tags.Tag('py3', 'none', 'any') - # Get the most specific tag for the Python interpreter. - tag = next(packaging.tags.sys_tags()) - if tag.platform.startswith('manylinux'): - tag = packaging.tags.Tag(tag.interpreter, tag.abi, _adjust_manylinux_tag(tag.platform)) - elif tag.platform.startswith('darwin'): - tag = packaging.tags.Tag(tag.interpreter, tag.abi, _adjust_darwin_tag(tag.platform)) + return mesonpy._tags.Tag('py3', 'none', 'any') if not self._has_extension_modules: # The wheel has platform dependent code (is not pure) but # does not contain any extension module (does not # distribute any file in {platlib}) thus use generic # implementation and ABI tags. - return packaging.tags.Tag('py3', 'none', tag.platform) - if self._stable_abi: - # All distributed extension modules use the stable ABI. - return packaging.tags.Tag(tag.interpreter, self._stable_abi, tag.platform) - return tag + return mesonpy._tags.Tag('py3', 'none', None) + return mesonpy._tags.Tag(None, self._stable_abi, None) @property def name(self) -> str: diff --git a/mesonpy/_tags.py b/mesonpy/_tags.py new file mode 100644 index 000000000..6cc7aa52d --- /dev/null +++ b/mesonpy/_tags.py @@ -0,0 +1,119 @@ +# SPDX-License-Identifier: MIT + +import os +import platform +import subprocess +import sys +import sysconfig + +from typing import Optional, Union + + +# https://peps.python.org/pep-0425/#python-tag +INTERPRETERS = { + 'python': 'py', + 'cpython': 'cp', + 'pypy': 'pp', + 'ironpython': 'ip', + 'jython': 'jy', +} + + +def get_interpreter_tag() -> str: + name = sys.implementation.name + name = INTERPRETERS.get(name, name) + if name in {'cp', 'pp'}: + version = sys.version_info + return f'{name}{version[0]}{version[1]}' + return name + + +def _get_config_var(name: str, default: Union[str, int, None] = None) -> Union[str, int, None]: + value = sysconfig.get_config_var(name) + if value is None: + return default + return value + + +def _get_cpython_abi() -> str: + version = sys.version_info + debug = pymalloc = '' + if _get_config_var('Py_DEBUG', hasattr(sys, 'gettotalrefcount')): + debug = 'd' + if sys.version_info < (3, 8) and _get_config_var('WITH_PYMALLOC', True): + pymalloc = 'm' + return f'cp{version[0]}{version[1]}{debug}{pymalloc}' + + +def get_abi_tag() -> str: + abi = sysconfig.get_config_var('SOABI') + if not abi: + # CPython on Windows does not have a SOABI sysconfig variable. + assert sys.implementation.name == 'cpython' + return _get_cpython_abi() + if abi.startswith('cpython'): + return 'cp' + abi.split('-')[1] + if abi.startswith('pypy'): + return '_'.join(abi.split('-')[:2]) + return abi.replace('.', '_').replace('-', '_') + + +def _get_macosx_platform_tag() -> str: + ver, x, arch = platform.mac_ver() + + # Override the macOS version if one is provided via the + # MACOS_DEPLOYMENT_TARGET environment variable. Return it + # unchanged otherwise. + try: + version = tuple(map(int, os.environ.get('MACOS_DEPLOYMENT_TARGET', '').split('.')))[:2] + except ValueError: + version = None + + if version is None: + version = tuple(map(int, ver.split('.')))[:2] + if version == (10, 16): + # When built against an older macOS SDK, Python will + # report macOS 10.16 instead of the real version. + ver = subprocess.check_output( + [ + sys.executable, + '-sS', + '-c', + 'import platform; print(platform.mac_ver()[0])' + ], + env={'SYSTEM_VERSION_COMPAT': '0'}, + universal_newlines=True) + version = tuple(map(int, ver.split('.')[:2])) + + major, minor = version + if major >= 11: + minor = 0 + + if sys.maxsize <= 2**32: + if arch.startswith('ppc'): + arch = 'ppc' + arch = 'i386' + + return f'macosx_{major}_{minor}_{arch}' + + +def get_platform_tag() -> str: + platform = sysconfig.get_platform() + if platform.startswith('macosx'): + return _get_macosx_platform_tag() + if sys.maxsize <= 2**32: + if platform == 'linux-x86_64': + return 'linux_i686' + if platform == 'linux-aarch64': + return 'linux_armv7l' + return platform.replace('-', '_') + + +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 __str__(self) -> str: + return f'{self.interpreter}-{self.abi}-{self.platform}' diff --git a/tests/conftest.py b/tests/conftest.py index edac18edb..f211e8a27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import os import os.path import pathlib +import re import shutil import tempfile @@ -15,6 +16,24 @@ import mesonpy +def adjust_packaging_platform_tag(platform: str) -> str: + # The packaging module generates overly specific platforms tags on + # Linux. The platforms tags on Linux evolved over time. + # meson-python uses more relaxed platform tags to maintain + # compatibility with old wheel installation tools. The relaxed + # platform tags match the ones generated by the wheel package. + # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/ + platform = re.sub(r'^manylinux(1|2010|2014|_\d+_\d+)_(.*)$', r'linux_\2', platform) + + # When built against an older macOS SDK, Python will report macOS + # 10.16 instead of the real version. Correct the tag reported by + # old packaging modules that do not contain yet a workaround. + # https://github.com/pypa/packaging/commit/67c4a2820c549070bbfc4bfbf5e2a250075048da + platform = re.sub(r'^macosx_10_16_(.*)$', r'macosx_11_0_\1', platform) + + return platform + + package_dir = pathlib.Path(__file__).parent / 'packages' diff --git a/tests/test_tags.py b/tests/test_tags.py index 902b5c53d..8a3340972 100644 --- a/tests/test_tags.py +++ b/tests/test_tags.py @@ -12,16 +12,36 @@ import pytest import mesonpy +import mesonpy._tags +from .conftest import adjust_packaging_platform_tag + +# Test against the wheel tag generated by packaging module. tag = next(packaging.tags.sys_tags()) ABI = tag.abi INTERPRETER = tag.interpreter -PLATFORM = mesonpy._adjust_manylinux_tag(tag.platform) +PLATFORM = adjust_packaging_platform_tag(tag.platform) + SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') ABI3SUFFIX = next((x for x in mesonpy._EXTENSION_SUFFIXES if '.abi3.' in x), None) +def test_wheel_tag(): + str(mesonpy._tags.Tag()) == f'{INTERPRETER}-{ABI}-{PLATFORM}' + str(mesonpy._tags.Tag(abi='abi3')) == f'{INTERPRETER}-abi3-{PLATFORM}' + + +@pytest.mark.skipif(platform.system() != 'Darwin', reason='macOS specific test') +def test_macos_platform_tag(monkeypatch): + for minor in range(9, 16): + monkeypatch.setenv('MACOS_DEPLOYMENT_TARGET', f'10.{minor}') + assert next(packaging.tags.mac_platforms((10, minor))) == mesonpy._tags.get_platform_tag() + for major in range(11, 20): + monkeypatch.setenv('MACOS_DEPLOYMENT_TARGET', f'{major}.0') + assert next(packaging.tags.mac_platforms((major, 0))) == mesonpy._tags.get_platform_tag() + + def wheel_builder_test_factory(monkeypatch, content): files = defaultdict(list) files.update({key: [(pathlib.Path(x), os.path.join('build', x)) for x in value] for key, value in content.items()}) diff --git a/tests/test_wheel.py b/tests/test_wheel.py index f23b021e2..3e9203b09 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -14,14 +14,17 @@ import mesonpy +from .conftest import adjust_packaging_platform_tag + EXT_SUFFIX = sysconfig.get_config_var('EXT_SUFFIX') INTERPRETER_VERSION = f'{sys.version_info[0]}{sys.version_info[1]}' -_tag = next(packaging.tags.sys_tags()) -ABI = _tag.abi -INTERPRETER = _tag.interpreter -PLATFORM = mesonpy._adjust_manylinux_tag(_tag.platform) +# Test against the wheel tag generated by packaging module. +tag = next(packaging.tags.sys_tags()) +ABI = tag.abi +INTERPRETER = tag.interpreter +PLATFORM = adjust_packaging_platform_tag(tag.platform) if platform.system() == 'Linux': SHARED_LIB_EXT = 'so'