Skip to content

Commit d9e7993

Browse files
committed
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#189, mesonbuild#190.
1 parent 71c6a3a commit d9e7993

File tree

5 files changed

+54
-410
lines changed

5 files changed

+54
-410
lines changed

mesonpy/__init__.py

+47-166
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,8 @@ 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(s.lstrip('.') for s in importlib.machinery.EXTENSION_SUFFIXES)
108+
_EXTENSION_SUFFIX_REGEX = re.compile(r'^(?:(?P<abi>[^.]+)\.)?(?:so|pyd)$')
108109

109110

110111
def _showwarning(
@@ -179,6 +180,11 @@ def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
179180
def _has_internal_libs(self) -> bool:
180181
return bool(self._wheel_files['mesonpy-libs'])
181182

183+
@property
184+
def _has_extension_modules(self) -> bool:
185+
# Assume that all code installed in {platlib} is Python ABI dependent.
186+
return bool(self._wheel_files['platlib'])
187+
182188
@property
183189
def basename(self) -> str:
184190
"""Normalized wheel name and version (eg. meson_python-1.0.0)."""
@@ -187,14 +193,24 @@ def basename(self) -> str:
187193
version=self._project.version,
188194
)
189195

196+
@property
197+
def tags(self) -> str:
198+
"""Wheel tags."""
199+
if self.is_pure:
200+
return packaging.tags.Tag('py3', 'none', 'any')
201+
tag = next(packaging.tags.sys_tags())
202+
if not self._has_extension_modules:
203+
return packaging.tags.Tag('py3', 'none', tag.platform)
204+
elif self._use_stable_abi:
205+
return packaging.tags.Tag(tag.interpreter, 'abi3', tag.platform)
206+
return tag
207+
190208
@property
191209
def name(self) -> str:
192210
"""Wheel name, this includes the basename and tags."""
193-
return '{basename}-{python_tag}-{abi_tag}-{platform_tag}'.format(
211+
return '{basename}-{tags}'.format(
194212
basename=self.basename,
195-
python_tag=self.python_tag,
196-
abi_tag=self.abi_tag,
197-
platform_tag=self.platform_tag,
213+
tags=str(self.tags),
198214
)
199215

200216
@property
@@ -229,7 +245,7 @@ def wheel(self) -> bytes: # noqa: F811
229245
Tag: {tags}
230246
''').strip().format(
231247
is_purelib='true' if self.is_pure else 'false',
232-
tags=f'{self.python_tag}-{self.abi_tag}-{self.platform_tag}',
248+
tags=self.tags,
233249
).encode()
234250

235251
@property
@@ -267,166 +283,31 @@ def _debian_python(self) -> bool:
267283
except ModuleNotFoundError:
268284
return False
269285

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-
284286
@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')
287+
def _use_stable_abi(self) -> bool:
288+
"""Determine wether the package is compatible with the stabe ABI.
336289
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')
290+
Examine all files installed in {platlib} that look like
291+
extension modules (extension .pyd on Windows and .so on other
292+
platforms) and return True only if all of them have a PEP 3149
293+
filename tag indicating that they ase the stable Python ABI.
365294
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.
295+
All files that look like extension modules are verified to
296+
have a file name compatibel with what is expected by the
297+
Python interpreter. An exception is raised otherwise.
298+
299+
Other files are ignored.
399300
"""
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:
423-
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
301+
abis = []
302+
303+
for path, src in self._wheel_files['platlib']:
304+
if sys.platform == 'nt' and path.suffix == '.pyd' or path.suffix == '.so':
305+
name, suffix = path.name.split('.', 1)
306+
if suffix not in _EXTENSION_SUFFIXES:
307+
raise ValueError('Extension module "{}" not compatible with Python interpreter.'.format(str(path)))
308+
abis.append(_EXTENSION_SUFFIX_REGEX.match(suffix).group('abi'))
309+
310+
return all(abi == 'abi3' for abi in abis)
430311

431312
def _is_native(self, file: Union[str, pathlib.Path]) -> bool:
432313
"""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)