Skip to content

Commit c24880b

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, mesonbuild#189, mesonbuild#190.
1 parent 71c6a3a commit c24880b

File tree

6 files changed

+63
-411
lines changed

6 files changed

+63
-411
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

+56-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,30 @@ def basename(self) -> str:
187193
version=self._project.version,
188194
)
189195

196+
@property
197+
def tags(self) -> packaging.tags.Tag:
198+
"""Wheel tags."""
199+
if self.is_pure:
200+
return packaging.tags.Tag('py3', 'none', 'any')
201+
# Get the most specific tag for the Python interpreter.
202+
tag = next(packaging.tags.sys_tags())
203+
if not self._has_extension_modules:
204+
# The wheel has platform dependent code (is not pure) but
205+
# does not contain any extension module (does not
206+
# distribute any file in {platlib}) thus use generic
207+
# implementation and ABI tags.
208+
return packaging.tags.Tag('py3', 'none', tag.platform)
209+
elif self._use_stable_abi:
210+
# All distributed extension modules use the stable ABI.
211+
return packaging.tags.Tag(tag.interpreter, 'abi3', tag.platform)
212+
return tag
213+
190214
@property
191215
def name(self) -> str:
192216
"""Wheel name, this includes the basename and tags."""
193-
return '{basename}-{python_tag}-{abi_tag}-{platform_tag}'.format(
217+
return '{basename}-{tags}'.format(
194218
basename=self.basename,
195-
python_tag=self.python_tag,
196-
abi_tag=self.abi_tag,
197-
platform_tag=self.platform_tag,
219+
tags=self.tags,
198220
)
199221

200222
@property
@@ -229,7 +251,7 @@ def wheel(self) -> bytes: # noqa: F811
229251
Tag: {tags}
230252
''').strip().format(
231253
is_purelib='true' if self.is_pure else 'false',
232-
tags=f'{self.python_tag}-{self.abi_tag}-{self.platform_tag}',
254+
tags=self.tags,
233255
).encode()
234256

235257
@property
@@ -267,166 +289,34 @@ def _debian_python(self) -> bool:
267289
except ModuleNotFoundError:
268290
return False
269291

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-
284292
@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')
293+
def _use_stable_abi(self) -> bool:
294+
"""Determine whether the package is compatible with the stabe ABI.
336295
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')
296+
Examine all files installed in {platlib} that look like
297+
extension modules (extension .pyd on Windows and .so on other
298+
platforms) and return True only if all of them have a PEP 3149
299+
filename tag indicating that they ase the stable Python ABI.
365300
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.
301+
All files that look like extension modules are verified to
302+
have a file name compatibel with what is expected by the
303+
Python interpreter. An exception is raised otherwise.
304+
305+
Other files are ignored.
399306
"""
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
307+
abis = []
308+
309+
for path, src in self._wheel_files['platlib']:
310+
if os.name == 'nt' and path.suffix == '.pyd' or path.suffix == '.so':
311+
name, suffix = path.name.split('.', 1)
312+
if suffix not in _EXTENSION_SUFFIXES:
313+
raise ValueError('Extension module "{}" not compatible with Python interpreter.'.format(path))
314+
match = _EXTENSION_SUFFIX_REGEX.match(suffix)
315+
if match is None:
316+
raise ValueError('Suffix "{}" for extension module "{}" not understood.'.format(suffix, path))
317+
abis.append(match.group('abi'))
318+
319+
return all(abi == 'abi3' for abi in abis)
430320

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