Skip to content

Commit 541d090

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 541d090

File tree

4 files changed

+45
-334
lines changed

4 files changed

+45
-334
lines changed

mesonpy/__init__.py

+38-165
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 Collection, Iterator, Path
5052

5153

5254
if typing.TYPE_CHECKING: # pragma: no cover
@@ -102,9 +104,17 @@ 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)$')
109+
110+
111+
def _extension_abi_tag(path: pathlib.Path) -> str:
112+
"""Extract the PEP 3149 ABI tag from the extension file path, if any is present."""
113+
# The file path cannot contain a dot.
114+
name, suffix = path.name.split('.', 1)
115+
if suffix not in _EXTENSION_SUFFIXES:
116+
raise ValueError('Extension module "{}" not compatible with Python interpreter.'.format(str(path)))
117+
return _EXTENSION_SUFFIX_REGEX.match(suffix).group('abi')
108118

109119

110120
def _showwarning(
@@ -179,6 +189,10 @@ def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
179189
def _has_internal_libs(self) -> bool:
180190
return bool(self._wheel_files['mesonpy-libs'])
181191

192+
@property
193+
def _has_extension_modules(self) -> bool:
194+
return bool(self._wheel_files['platlib'])
195+
182196
@property
183197
def basename(self) -> str:
184198
"""Normalized wheel name and version (eg. meson_python-1.0.0)."""
@@ -187,14 +201,24 @@ def basename(self) -> str:
187201
version=self._project.version,
188202
)
189203

204+
@property
205+
def tags(self) -> str:
206+
"""Wheel tags."""
207+
if self.is_pure:
208+
return 'py3-none-any'
209+
tags = next(packaging.tags.sys_tags())
210+
if not self._has_extension_modules:
211+
tags.abi = 'none'
212+
elif self._use_stable_abi:
213+
tags.abi = 'abi3'
214+
return tags
215+
190216
@property
191217
def name(self) -> str:
192218
"""Wheel name, this includes the basename and tags."""
193-
return '{basename}-{python_tag}-{abi_tag}-{platform_tag}'.format(
219+
return '{basename}-{tags}'.format(
194220
basename=self.basename,
195-
python_tag=self.python_tag,
196-
abi_tag=self.abi_tag,
197-
platform_tag=self.platform_tag,
221+
tags=str(self.tags),
198222
)
199223

200224
@property
@@ -229,7 +253,7 @@ def wheel(self) -> bytes: # noqa: F811
229253
Tag: {tags}
230254
''').strip().format(
231255
is_purelib='true' if self.is_pure else 'false',
232-
tags=f'{self.python_tag}-{self.abi_tag}-{self.platform_tag}',
256+
tags=self.tags,
233257
).encode()
234258

235259
@property
@@ -267,166 +291,15 @@ def _debian_python(self) -> bool:
267291
except ModuleNotFoundError:
268292
return False
269293

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-
284-
@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')
336-
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')
365-
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-
378294
def _file_list_repr(self, files: Collection[str], prefix: str = '\t\t', max_count: int = 3) -> str:
379295
if len(files) > max_count:
380296
files = list(itertools.islice(files, max_count)) + [f'(... +{len(files)}))']
381297
return ''.join(f'{prefix}- {file}\n' for file in files)
382298

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.
399-
"""
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
299+
@cached_property
300+
def _use_stable_abi(self) -> bool:
301+
"""Determine wether the package is compatible with the stabe ABI."""
302+
return all(_extension_abi_tag(path) == 'abi3' for path, _ in self._wheel_files['platlib'])
430303

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