Skip to content

Commit b03ac0d

Browse files
committed
ENH: delegate computing wheel tags to the packaging module
Use the wheel contents only to determine whether the wheel contains python ABI dependent modules or other platform dependent code. Fixes mesonbuild#142, fixes mesonbuild#189, fixes mesonbuild#190.
1 parent e8afceb commit b03ac0d

File tree

6 files changed

+171
-405
lines changed

6 files changed

+171
-405
lines changed

meson.build

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ py.install_sources(
1111
'mesonpy/__init__.py',
1212
'mesonpy/_compat.py',
1313
'mesonpy/_elf.py',
14-
'mesonpy/_tags.py',
1514
'mesonpy/_util.py',
1615
subdir: 'mesonpy',
1716
)

mesonpy/__init__.py

+92-167
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import collections
1414
import contextlib
1515
import functools
16+
import importlib.machinery
1617
import io
1718
import itertools
1819
import json
@@ -35,6 +36,8 @@
3536
Union
3637
)
3738

39+
import packaging.tags
40+
3841

3942
if sys.version_info < (3, 11):
4043
import tomli as tomllib
@@ -43,10 +46,9 @@
4346

4447
import mesonpy._compat
4548
import mesonpy._elf
46-
import mesonpy._tags
4749
import mesonpy._util
4850

49-
from mesonpy._compat import Collection, Iterator, Mapping, Path
51+
from mesonpy._compat import Iterator, Path
5052

5153

5254
if typing.TYPE_CHECKING: # pragma: no cover
@@ -102,9 +104,33 @@ def _init_colors() -> Dict[str, str]:
102104
_STYLES = _init_colors() # holds the color values, should be _COLORS or _NO_COLORS
103105

104106

105-
_LINUX_NATIVE_MODULE_REGEX = re.compile(r'^(?P<name>.+)\.(?P<tag>.+)\.so$')
106-
_WINDOWS_NATIVE_MODULE_REGEX = re.compile(r'^(?P<name>.+)\.(?P<tag>.+)\.pyd$')
107-
_STABLE_ABI_TAG_REGEX = re.compile(r'^abi(?P<abi_number>[0-9]+)$')
107+
_EXTENSION_SUFFIXES = frozenset(importlib.machinery.EXTENSION_SUFFIXES)
108+
_EXTENSION_SUFFIX_REGEX = re.compile(r'^\.(?:(?P<abi>[^.]+)\.)?(?:so|pyd)$')
109+
assert all(re.match(_EXTENSION_SUFFIX_REGEX, x) for x in _EXTENSION_SUFFIXES)
110+
111+
112+
def _adjust_manylinux_tag(platform: str) -> str:
113+
# The packaging module generates overly specific platforms tags on
114+
# Linux. The platforms tags on Linux evolved over time. Relax
115+
# the platform tags to maintain compatibility with old wheel
116+
# installation tools. The relaxed platform tags match the ones
117+
# generated by the wheel package.
118+
# https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
119+
return re.sub(r'^manylinux(1|2010|2014|_\d+_\d+)_(.*)$', r'linux_\2', platform)
120+
121+
122+
def _adjust_darwin_tag(platform: str) -> str:
123+
# Override the macOS version if one is provided via the
124+
# MACOS_DEPLOYMENT_TARGET environment variable. Return it
125+
# unchanged otherwise.
126+
try:
127+
version = tuple(map(int, os.environ.get('MACOS_DEPLOYMENT_TARGET', '').split('.')))[:2]
128+
except ValueError:
129+
version = None
130+
if version is not None:
131+
# str() to silence mypy
132+
platform = str(next(packaging.tags.mac_platforms(version)))
133+
return platform
108134

109135

110136
def _showwarning(
@@ -179,6 +205,11 @@ def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
179205
def _has_internal_libs(self) -> bool:
180206
return bool(self._wheel_files['mesonpy-libs'])
181207

208+
@property
209+
def _has_extension_modules(self) -> bool:
210+
# Assume that all code installed in {platlib} is Python ABI dependent.
211+
return bool(self._wheel_files['platlib'])
212+
182213
@property
183214
def basename(self) -> str:
184215
"""Normalized wheel name and version (eg. meson_python-1.0.0)."""
@@ -187,14 +218,34 @@ def basename(self) -> str:
187218
version=self._project.version,
188219
)
189220

221+
@property
222+
def tag(self) -> packaging.tags.Tag:
223+
"""Wheel tags."""
224+
if self.is_pure:
225+
return packaging.tags.Tag('py3', 'none', 'any')
226+
# Get the most specific tag for the Python interpreter.
227+
tag = next(packaging.tags.sys_tags())
228+
if tag.platform.startswith('manylinux'):
229+
tag = packaging.tags.Tag(tag.interpreter, tag.abi, _adjust_manylinux_tag(tag.platform))
230+
elif tag.platform.startswith('darwin'):
231+
tag = packaging.tags.Tag(tag.interpreter, tag.abi, _adjust_darwin_tag(tag.platform))
232+
if not self._has_extension_modules:
233+
# The wheel has platform dependent code (is not pure) but
234+
# does not contain any extension module (does not
235+
# distribute any file in {platlib}) thus use generic
236+
# implementation and ABI tags.
237+
return packaging.tags.Tag('py3', 'none', tag.platform)
238+
if self._stable_abi:
239+
# All distributed extension modules use the stable ABI.
240+
return packaging.tags.Tag(tag.interpreter, self._stable_abi, tag.platform)
241+
return tag
242+
190243
@property
191244
def name(self) -> str:
192-
"""Wheel name, this includes the basename and tags."""
193-
return '{basename}-{python_tag}-{abi_tag}-{platform_tag}'.format(
245+
"""Wheel name, this includes the basename and tag."""
246+
return '{basename}-{tag}'.format(
194247
basename=self.basename,
195-
python_tag=self.python_tag,
196-
abi_tag=self.abi_tag,
197-
platform_tag=self.platform_tag,
248+
tag=self.tag,
198249
)
199250

200251
@property
@@ -226,10 +277,10 @@ def wheel(self) -> bytes: # noqa: F811
226277
Wheel-Version: 1.0
227278
Generator: meson
228279
Root-Is-Purelib: {is_purelib}
229-
Tag: {tags}
280+
Tag: {tag}
230281
''').strip().format(
231282
is_purelib='true' if self.is_pure else 'false',
232-
tags=f'{self.python_tag}-{self.abi_tag}-{self.platform_tag}',
283+
tag=self.tag,
233284
).encode()
234285

235286
@property
@@ -267,166 +318,40 @@ def _debian_python(self) -> bool:
267318
except ModuleNotFoundError:
268319
return False
269320

270-
@property
271-
def python_tag(self) -> str:
272-
selected_tag = self._select_abi_tag()
273-
if selected_tag and selected_tag.python:
274-
return selected_tag.python
275-
return 'py3'
276-
277-
@property
278-
def abi_tag(self) -> str:
279-
selected_tag = self._select_abi_tag()
280-
if selected_tag:
281-
return selected_tag.abi
282-
return 'none'
283-
284321
@cached_property
285-
def platform_tag(self) -> str:
286-
if self.is_pure:
287-
return 'any'
288-
# XXX: Choose the sysconfig platform here and let something like auditwheel
289-
# fix it later if there are system dependencies (eg. replace it with a manylinux tag)
290-
platform_ = sysconfig.get_platform()
291-
parts = platform_.split('-')
292-
if parts[0] == 'macosx':
293-
target = os.environ.get('MACOSX_DEPLOYMENT_TARGET')
294-
if target:
295-
print(
296-
'{yellow}MACOSX_DEPLOYMENT_TARGET is set so we are setting the '
297-
'platform tag to {target}{reset}'.format(target=target, **_STYLES)
298-
)
299-
parts[1] = target
300-
else:
301-
# If no target macOS version is specified fallback to
302-
# platform.mac_ver() instead of sysconfig.get_platform() as the
303-
# latter specifies the target macOS version Python was built
304-
# against.
305-
parts[1] = platform.mac_ver()[0]
306-
if parts[1] >= '11':
307-
# Only pick up the major version, which changed from 10.X
308-
# to X.0 from macOS 11 onwards. See
309-
# https://github.com/mesonbuild/meson-python/issues/160
310-
parts[1] = parts[1].split('.')[0]
311-
312-
if parts[1] in ('11', '12'):
313-
# Workaround for bug where pypa/packaging does not consider macOS
314-
# tags without minor versions valid. Some Python flavors (Homebrew
315-
# for example) on macOS started to do this in version 11, and
316-
# pypa/packaging should handle things correctly from version 13 and
317-
# forward, so we will add a 0 minor version to MacOS 11 and 12.
318-
# https://github.com/mesonbuild/meson-python/issues/91
319-
# https://github.com/pypa/packaging/issues/578
320-
parts[1] += '.0'
321-
322-
platform_ = '-'.join(parts)
323-
elif parts[0] == 'linux' and parts[1] == 'x86_64' and sys.maxsize == 0x7fffffff:
324-
# 32-bit Python running on an x86_64 host
325-
# https://github.com/mesonbuild/meson-python/issues/123
326-
parts[1] = 'i686'
327-
platform_ = '-'.join(parts)
328-
return platform_.replace('-', '_').replace('.', '_')
329-
330-
def _calculate_file_abi_tag_heuristic_windows(self, filename: str) -> Optional[mesonpy._tags.Tag]:
331-
"""Try to calculate the Windows tag from the Python extension file name."""
332-
match = _WINDOWS_NATIVE_MODULE_REGEX.match(filename)
333-
if not match:
334-
return None
335-
tag = match.group('tag')
322+
def _stable_abi(self) -> Optional[str]:
323+
"""Determine stabe ABI compatibility.
336324
337-
try:
338-
return mesonpy._tags.StableABITag(tag)
339-
except ValueError:
340-
return mesonpy._tags.InterpreterTag(tag)
341-
342-
def _calculate_file_abi_tag_heuristic_posix(self, filename: str) -> Optional[mesonpy._tags.Tag]:
343-
"""Try to calculate the Posix tag from the Python extension file name."""
344-
# sysconfig is not guaranted to export SHLIB_SUFFIX but let's be
345-
# preventive and check its value to make sure it matches our expectations
346-
try:
347-
extension = sysconfig.get_config_vars().get('SHLIB_SUFFIX', '.so')
348-
if extension != '.so':
349-
raise NotImplementedError(
350-
f"We don't currently support the {extension} extension. "
351-
'Please report this to https://github.com/mesonbuild/mesonpy/issues '
352-
'and include information about your operating system.'
353-
)
354-
except KeyError:
355-
warnings.warn(
356-
'sysconfig does not export SHLIB_SUFFIX, so we are unable to '
357-
'perform the sanity check regarding the extension suffix. '
358-
'Please report this to https://github.com/mesonbuild/mesonpy/issues '
359-
'and include the output of `python -m sysconfig`.'
360-
)
361-
match = _LINUX_NATIVE_MODULE_REGEX.match(filename)
362-
if not match: # this file does not appear to be a native module
363-
return None
364-
tag = match.group('tag')
325+
Examine all files installed in {platlib} that look like
326+
extension modules (extension .pyd on Windows and .so on other
327+
platforms) and, if they all share the same PEP 3149 filename
328+
stable ABI tag, return it.
365329
366-
try:
367-
return mesonpy._tags.StableABITag(tag)
368-
except ValueError:
369-
return mesonpy._tags.InterpreterTag(tag)
370-
371-
def _calculate_file_abi_tag_heuristic(self, filename: str) -> Optional[mesonpy._tags.Tag]:
372-
"""Try to calculate the ABI tag from the Python extension file name."""
373-
if os.name == 'nt':
374-
return self._calculate_file_abi_tag_heuristic_windows(filename)
375-
# everything else *should* follow the POSIX way, at least to my knowledge
376-
return self._calculate_file_abi_tag_heuristic_posix(filename)
377-
378-
def _file_list_repr(self, files: Collection[str], prefix: str = '\t\t', max_count: int = 3) -> str:
379-
if len(files) > max_count:
380-
files = list(itertools.islice(files, max_count)) + [f'(... +{len(files)}))']
381-
return ''.join(f'{prefix}- {file}\n' for file in files)
382-
383-
def _files_by_tag(self) -> Mapping[mesonpy._tags.Tag, Collection[str]]:
384-
"""Map files into ABI tags."""
385-
files_by_tag: Dict[mesonpy._tags.Tag, List[str]] = collections.defaultdict(list)
386-
387-
for _, file in self._wheel_files['platlib']:
388-
# if in platlib, calculate the ABI tag
389-
tag = self._calculate_file_abi_tag_heuristic(file)
390-
if tag:
391-
files_by_tag[tag].append(file)
392-
393-
return files_by_tag
394-
395-
def _select_abi_tag(self) -> Optional[mesonpy._tags.Tag]: # noqa: C901
396-
"""Given a list of ABI tags, selects the most specific one.
397-
398-
Raises an error if there are incompatible tags.
330+
All files that look like extension modules are verified to
331+
have a file name compatibel with what is expected by the
332+
Python interpreter. An exception is raised otherwise.
333+
334+
Other files are ignored.
399335
"""
400-
# Possibilities:
401-
# - interpreter specific (cpython/pypy/etc, version)
402-
# - stable abi (abiX)
403-
tags = self._files_by_tag()
404-
selected_tag = None
405-
for tag, files in tags.items():
406-
# no selected tag yet, let's assign this one
407-
if not selected_tag:
408-
selected_tag = tag
409-
# interpreter tag
410-
elif isinstance(tag, mesonpy._tags.InterpreterTag):
411-
if tag != selected_tag:
412-
if isinstance(selected_tag, mesonpy._tags.InterpreterTag):
413-
raise ValueError(
414-
'Found files with incompatible ABI tags:\n'
415-
+ self._file_list_repr(tags[selected_tag])
416-
+ '\tand\n'
417-
+ self._file_list_repr(files)
418-
)
419-
selected_tag = tag
420-
# stable ABI
421-
elif isinstance(tag, mesonpy._tags.StableABITag):
422-
if isinstance(selected_tag, mesonpy._tags.StableABITag) and tag != selected_tag:
336+
abis = []
337+
338+
for path, src in self._wheel_files['platlib']:
339+
if os.name == 'nt' and path.suffix == '.pyd' or path.suffix == '.so':
340+
match = re.match(r'^[^.]+(.*)$', path.name)
341+
assert match is not None
342+
suffix = match.group(1)
343+
if suffix not in _EXTENSION_SUFFIXES:
423344
raise ValueError(
424-
'Found files with incompatible ABI tags:\n'
425-
+ self._file_list_repr(tags[selected_tag])
426-
+ '\tand\n'
427-
+ self._file_list_repr(files)
428-
)
429-
return selected_tag
345+
f'Extension module {path!r} not compatible with Python interpreter. '
346+
f'Recognized extension suffixes are: {", ".join(repr(x) for x in _EXTENSION_SUFFIXES)}.')
347+
match = _EXTENSION_SUFFIX_REGEX.match(suffix)
348+
assert match is not None
349+
abis.append(match.group('abi'))
350+
351+
stable = [x for x in abis if x and re.match(r'abi\d+', x)]
352+
if len(stable) > 0 and len(stable) == len(abis) and all(x == stable[0] for x in stable[1:]):
353+
return stable[0]
354+
return None
430355

431356
def _is_native(self, file: Union[str, pathlib.Path]) -> bool:
432357
"""Check if file is a native file."""

mesonpy/_compat.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,9 @@
1010

1111

1212
if sys.version_info >= (3, 9):
13-
from collections.abc import (
14-
Collection, Iterable, Iterator, Mapping, Sequence
15-
)
13+
from collections.abc import Collection, Iterable, Iterator, Sequence
1614
else:
17-
from typing import Collection, Iterable, Iterator, Mapping, Sequence
15+
from typing import Collection, Iterable, Iterator, Sequence
1816

1917

2018
if sys.version_info >= (3, 8):
@@ -41,7 +39,6 @@ def is_relative_to(path: pathlib.Path, other: Union[pathlib.Path, str]) -> bool:
4139
'Iterable',
4240
'Iterator',
4341
'Literal',
44-
'Mapping',
4542
'Path',
4643
'Sequence',
4744
]

0 commit comments

Comments
 (0)