1313import collections
1414import contextlib
1515import functools
16+ import importlib .machinery
1617import io
1718import itertools
1819import json
3536 Union
3637)
3738
39+ import packaging .tags
40+
3841
3942if sys .version_info < (3 , 11 ):
4043 import tomli as tomllib
4346
4447import mesonpy ._compat
4548import mesonpy ._elf
46- import mesonpy ._tags
4749import mesonpy ._util
4850
49- from mesonpy ._compat import Collection , Iterator , Mapping , Path
51+ from mesonpy ._compat import Iterator , Path
5052
5153
5254if 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
110136def _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- + '\t and\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- + '\t and\n '
427- + self ._file_list_repr (files )
428- )
429- return selected_tag
345+ f'Extension module { str (path )!r} not compatible with Python interpreter. '
346+ f'Filename suffix { suffix !r} not in { set (_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."""
0 commit comments