13
13
import collections
14
14
import contextlib
15
15
import functools
16
+ import importlib .machinery
16
17
import io
17
18
import itertools
18
19
import json
35
36
Union
36
37
)
37
38
39
+ import packaging .tags
40
+
38
41
39
42
if sys .version_info < (3 , 11 ):
40
43
import tomli as tomllib
43
46
44
47
import mesonpy ._compat
45
48
import mesonpy ._elf
46
- import mesonpy ._tags
47
49
import mesonpy ._util
48
50
49
- from mesonpy ._compat import Collection , Iterator , Mapping , Path
51
+ from mesonpy ._compat import Iterator , Path
50
52
51
53
52
54
if typing .TYPE_CHECKING : # pragma: no cover
@@ -102,9 +104,32 @@ def _init_colors() -> Dict[str, str]:
102
104
_STYLES = _init_colors () # holds the color values, should be _COLORS or _NO_COLORS
103
105
104
106
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 _adjust_manylinux_tag (platform : str ) -> str :
112
+ # The packaging module generates overly specific platforms tags on
113
+ # Linux. The platforms tags on Linux evolved over time. Relax
114
+ # the platform tags to maintain compatibility with old wheel
115
+ # installation tools. The relaxed platform tags match the ones
116
+ # generated by the wheel package.
117
+ # https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/
118
+ return re .sub (r'^manylinux(1|2010|2014|_\d+_\d+)_(.*)$' , r'linux_\2' , platform )
119
+
120
+
121
+ def _adjust_darwin_tag (platform : str ) -> str :
122
+ # Override the macOS version if one is provided via the
123
+ # MACOS_DEPLOYMENT_TARGET environment variable. Return it
124
+ # unchanged otherwise.
125
+ try :
126
+ version = tuple (map (int , os .environ .get ('MACOS_DEPLOYMENT_TARGET' , '' ).split ('.' )))[:2 ]
127
+ except ValueError :
128
+ version = None
129
+ if version is not None :
130
+ # str() to silence mypy
131
+ platform = str (next (packaging .tags .mac_platforms (version )))
132
+ return platform
108
133
109
134
110
135
def _showwarning (
@@ -179,6 +204,11 @@ def _wheel_files(self) -> DefaultDict[str, List[Tuple[pathlib.Path, str]]]:
179
204
def _has_internal_libs (self ) -> bool :
180
205
return bool (self ._wheel_files ['mesonpy-libs' ])
181
206
207
+ @property
208
+ def _has_extension_modules (self ) -> bool :
209
+ # Assume that all code installed in {platlib} is Python ABI dependent.
210
+ return bool (self ._wheel_files ['platlib' ])
211
+
182
212
@property
183
213
def basename (self ) -> str :
184
214
"""Normalized wheel name and version (eg. meson_python-1.0.0)."""
@@ -187,14 +217,34 @@ def basename(self) -> str:
187
217
version = self ._project .version ,
188
218
)
189
219
220
+ @property
221
+ def tag (self ) -> packaging .tags .Tag :
222
+ """Wheel tags."""
223
+ if self .is_pure :
224
+ return packaging .tags .Tag ('py3' , 'none' , 'any' )
225
+ # Get the most specific tag for the Python interpreter.
226
+ tag = next (packaging .tags .sys_tags ())
227
+ if tag .platform .startswith ('manylinux' ):
228
+ tag = packaging .tags .Tag (tag .interpreter , tag .abi , _adjust_manylinux_tag (tag .platform ))
229
+ elif tag .platform .startswith ('darwin' ):
230
+ tag = packaging .tags .Tag (tag .interpreter , tag .abi , _adjust_darwin_tag (tag .platform ))
231
+ if not self ._has_extension_modules :
232
+ # The wheel has platform dependent code (is not pure) but
233
+ # does not contain any extension module (does not
234
+ # distribute any file in {platlib}) thus use generic
235
+ # implementation and ABI tags.
236
+ return packaging .tags .Tag ('py3' , 'none' , tag .platform )
237
+ if self ._stable_abi :
238
+ # All distributed extension modules use the stable ABI.
239
+ return packaging .tags .Tag (tag .interpreter , self ._stable_abi , tag .platform )
240
+ return tag
241
+
190
242
@property
191
243
def name (self ) -> str :
192
- """Wheel name, this includes the basename and tags ."""
193
- return '{basename}-{python_tag}-{abi_tag}-{platform_tag }' .format (
244
+ """Wheel name, this includes the basename and tag ."""
245
+ return '{basename}-{tag }' .format (
194
246
basename = self .basename ,
195
- python_tag = self .python_tag ,
196
- abi_tag = self .abi_tag ,
197
- platform_tag = self .platform_tag ,
247
+ tag = self .tag ,
198
248
)
199
249
200
250
@property
@@ -226,10 +276,10 @@ def wheel(self) -> bytes: # noqa: F811
226
276
Wheel-Version: 1.0
227
277
Generator: meson
228
278
Root-Is-Purelib: {is_purelib}
229
- Tag: {tags }
279
+ Tag: {tag }
230
280
''' ).strip ().format (
231
281
is_purelib = 'true' if self .is_pure else 'false' ,
232
- tags = f' { self .python_tag } - { self . abi_tag } - { self . platform_tag } ' ,
282
+ tag = self .tag ,
233
283
).encode ()
234
284
235
285
@property
@@ -267,166 +317,38 @@ def _debian_python(self) -> bool:
267
317
except ModuleNotFoundError :
268
318
return False
269
319
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
320
@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' )
321
+ def _stable_abi (self ) -> Optional [str ]:
322
+ """Determine stabe ABI compatibility.
336
323
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' )
324
+ Examine all files installed in {platlib} that look like
325
+ extension modules (extension .pyd on Windows and .so on other
326
+ platforms) and, if they all share the same PEP 3149 filename
327
+ stable ABI tag, return it.
365
328
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.
329
+ All files that look like extension modules are verified to
330
+ have a file name compatibel with what is expected by the
331
+ Python interpreter. An exception is raised otherwise.
332
+
333
+ Other files are ignored.
399
334
"""
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 :
423
- 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
335
+ abis = []
336
+
337
+ for path , src in self ._wheel_files ['platlib' ]:
338
+ if os .name == 'nt' and path .suffix == '.pyd' or path .suffix == '.so' :
339
+ name , suffix = path .name .split ('.' , 1 )
340
+ if suffix not in _EXTENSION_SUFFIXES :
341
+ raise ValueError ('Extension module "{}" not compatible with Python interpreter.' .format (path ))
342
+ match = _EXTENSION_SUFFIX_REGEX .match (suffix )
343
+ if match is None :
344
+ raise ValueError ('Extension module "{}" suffix not understood.' .format (path ))
345
+ abis .append (match .group ('abi' ) or '' )
346
+
347
+ stable = [x for x in abis if re .match (r'abi\d+' , x )]
348
+ if len (stable ) > 0 and len (stable ) == len (abis ) and all (x == stable [0 ] for x in stable [1 :]):
349
+ return stable [0 ]
350
+ return None
351
+
430
352
431
353
def _is_native (self , file : Union [str , pathlib .Path ]) -> bool :
432
354
"""Check if file is a native file."""
0 commit comments