Skip to content

Commit

Permalink
ENH: do not rely on packaging module for generating wheel tags
Browse files Browse the repository at this point in the history
The generated tags are tested checking against what the packaging
module generates. This should ensure that the two implementations will
not diverge.
  • Loading branch information
dnicolodi committed Nov 12, 2022
1 parent e1995df commit 1d81d62
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 44 deletions.
1 change: 1 addition & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ py.install_sources(
'mesonpy/__init__.py',
'mesonpy/_compat.py',
'mesonpy/_elf.py',
'mesonpy/_tags.py',
'mesonpy/_util.py',
subdir: 'mesonpy',
)
44 changes: 5 additions & 39 deletions mesonpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
Union
)

import packaging.tags


if sys.version_info < (3, 11):
import tomli as tomllib
Expand All @@ -46,6 +44,7 @@

import mesonpy._compat
import mesonpy._elf
import mesonpy._tags
import mesonpy._util

from mesonpy._compat import Iterator, Path
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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:
Expand Down
119 changes: 119 additions & 0 deletions mesonpy/_tags.py
Original file line number Diff line number Diff line change
@@ -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}'
19 changes: 19 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import os.path
import pathlib
import re
import shutil
import tempfile

Expand All @@ -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'


Expand Down
22 changes: 21 additions & 1 deletion tests/test_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()})
Expand Down
11 changes: 7 additions & 4 deletions tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 1d81d62

Please sign in to comment.