]>
Commit | Line | Data |
---|---|---|
e0df8241 JR |
1 | # This file is dual licensed under the terms of the Apache License, Version |
2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository | |
3 | # for complete details. | |
4 | ||
5 | import logging | |
6 | import platform | |
7 | import sys | |
8 | import sysconfig | |
9 | from importlib.machinery import EXTENSION_SUFFIXES | |
10 | from typing import ( | |
11 | Dict, | |
12 | FrozenSet, | |
13 | Iterable, | |
14 | Iterator, | |
15 | List, | |
16 | Optional, | |
17 | Sequence, | |
18 | Tuple, | |
19 | Union, | |
20 | cast, | |
21 | ) | |
22 | ||
23 | from . import _manylinux, _musllinux | |
24 | ||
25 | logger = logging.getLogger(__name__) | |
26 | ||
27 | PythonVersion = Sequence[int] | |
28 | MacVersion = Tuple[int, int] | |
29 | ||
30 | INTERPRETER_SHORT_NAMES: Dict[str, str] = { | |
31 | "python": "py", # Generic. | |
32 | "cpython": "cp", | |
33 | "pypy": "pp", | |
34 | "ironpython": "ip", | |
35 | "jython": "jy", | |
36 | } | |
37 | ||
38 | ||
39 | _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 | |
40 | ||
41 | ||
42 | class Tag: | |
43 | """ | |
44 | A representation of the tag triple for a wheel. | |
45 | ||
46 | Instances are considered immutable and thus are hashable. Equality checking | |
47 | is also supported. | |
48 | """ | |
49 | ||
50 | __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] | |
51 | ||
52 | def __init__(self, interpreter: str, abi: str, platform: str) -> None: | |
53 | self._interpreter = interpreter.lower() | |
54 | self._abi = abi.lower() | |
55 | self._platform = platform.lower() | |
56 | # The __hash__ of every single element in a Set[Tag] will be evaluated each time | |
57 | # that a set calls its `.disjoint()` method, which may be called hundreds of | |
58 | # times when scanning a page of links for packages with tags matching that | |
59 | # Set[Tag]. Pre-computing the value here produces significant speedups for | |
60 | # downstream consumers. | |
61 | self._hash = hash((self._interpreter, self._abi, self._platform)) | |
62 | ||
63 | @property | |
64 | def interpreter(self) -> str: | |
65 | return self._interpreter | |
66 | ||
67 | @property | |
68 | def abi(self) -> str: | |
69 | return self._abi | |
70 | ||
71 | @property | |
72 | def platform(self) -> str: | |
73 | return self._platform | |
74 | ||
75 | def __eq__(self, other: object) -> bool: | |
76 | if not isinstance(other, Tag): | |
77 | return NotImplemented | |
78 | ||
79 | return ( | |
80 | (self._hash == other._hash) # Short-circuit ASAP for perf reasons. | |
81 | and (self._platform == other._platform) | |
82 | and (self._abi == other._abi) | |
83 | and (self._interpreter == other._interpreter) | |
84 | ) | |
85 | ||
86 | def __hash__(self) -> int: | |
87 | return self._hash | |
88 | ||
89 | def __str__(self) -> str: | |
90 | return f"{self._interpreter}-{self._abi}-{self._platform}" | |
91 | ||
92 | def __repr__(self) -> str: | |
93 | return f"<{self} @ {id(self)}>" | |
94 | ||
95 | ||
96 | def parse_tag(tag: str) -> FrozenSet[Tag]: | |
97 | """ | |
98 | Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. | |
99 | ||
100 | Returning a set is required due to the possibility that the tag is a | |
101 | compressed tag set. | |
102 | """ | |
103 | tags = set() | |
104 | interpreters, abis, platforms = tag.split("-") | |
105 | for interpreter in interpreters.split("."): | |
106 | for abi in abis.split("."): | |
107 | for platform_ in platforms.split("."): | |
108 | tags.add(Tag(interpreter, abi, platform_)) | |
109 | return frozenset(tags) | |
110 | ||
111 | ||
112 | def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: | |
113 | value = sysconfig.get_config_var(name) | |
114 | if value is None and warn: | |
115 | logger.debug( | |
116 | "Config variable '%s' is unset, Python ABI tag may be incorrect", name | |
117 | ) | |
118 | return value | |
119 | ||
120 | ||
121 | def _normalize_string(string: str) -> str: | |
122 | return string.replace(".", "_").replace("-", "_") | |
123 | ||
124 | ||
125 | def _abi3_applies(python_version: PythonVersion) -> bool: | |
126 | """ | |
127 | Determine if the Python version supports abi3. | |
128 | ||
129 | PEP 384 was first implemented in Python 3.2. | |
130 | """ | |
131 | return len(python_version) > 1 and tuple(python_version) >= (3, 2) | |
132 | ||
133 | ||
134 | def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: | |
135 | py_version = tuple(py_version) # To allow for version comparison. | |
136 | abis = [] | |
137 | version = _version_nodot(py_version[:2]) | |
138 | debug = pymalloc = ucs4 = "" | |
139 | with_debug = _get_config_var("Py_DEBUG", warn) | |
140 | has_refcount = hasattr(sys, "gettotalrefcount") | |
141 | # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled | |
142 | # extension modules is the best option. | |
143 | # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 | |
144 | has_ext = "_d.pyd" in EXTENSION_SUFFIXES | |
145 | if with_debug or (with_debug is None and (has_refcount or has_ext)): | |
146 | debug = "d" | |
147 | if py_version < (3, 8): | |
148 | with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) | |
149 | if with_pymalloc or with_pymalloc is None: | |
150 | pymalloc = "m" | |
151 | if py_version < (3, 3): | |
152 | unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) | |
153 | if unicode_size == 4 or ( | |
154 | unicode_size is None and sys.maxunicode == 0x10FFFF | |
155 | ): | |
156 | ucs4 = "u" | |
157 | elif debug: | |
158 | # Debug builds can also load "normal" extension modules. | |
159 | # We can also assume no UCS-4 or pymalloc requirement. | |
160 | abis.append(f"cp{version}") | |
161 | abis.insert( | |
162 | 0, | |
163 | "cp{version}{debug}{pymalloc}{ucs4}".format( | |
164 | version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 | |
165 | ), | |
166 | ) | |
167 | return abis | |
168 | ||
169 | ||
170 | def cpython_tags( | |
171 | python_version: Optional[PythonVersion] = None, | |
172 | abis: Optional[Iterable[str]] = None, | |
173 | platforms: Optional[Iterable[str]] = None, | |
174 | *, | |
175 | warn: bool = False, | |
176 | ) -> Iterator[Tag]: | |
177 | """ | |
178 | Yields the tags for a CPython interpreter. | |
179 | ||
180 | The tags consist of: | |
181 | - cp<python_version>-<abi>-<platform> | |
182 | - cp<python_version>-abi3-<platform> | |
183 | - cp<python_version>-none-<platform> | |
184 | - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. | |
185 | ||
186 | If python_version only specifies a major version then user-provided ABIs and | |
187 | the 'none' ABItag will be used. | |
188 | ||
189 | If 'abi3' or 'none' are specified in 'abis' then they will be yielded at | |
190 | their normal position and not at the beginning. | |
191 | """ | |
192 | if not python_version: | |
193 | python_version = sys.version_info[:2] | |
194 | ||
195 | interpreter = f"cp{_version_nodot(python_version[:2])}" | |
196 | ||
197 | if abis is None: | |
198 | if len(python_version) > 1: | |
199 | abis = _cpython_abis(python_version, warn) | |
200 | else: | |
201 | abis = [] | |
202 | abis = list(abis) | |
203 | # 'abi3' and 'none' are explicitly handled later. | |
204 | for explicit_abi in ("abi3", "none"): | |
205 | try: | |
206 | abis.remove(explicit_abi) | |
207 | except ValueError: | |
208 | pass | |
209 | ||
210 | platforms = list(platforms or platform_tags()) | |
211 | for abi in abis: | |
212 | for platform_ in platforms: | |
213 | yield Tag(interpreter, abi, platform_) | |
214 | if _abi3_applies(python_version): | |
215 | yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) | |
216 | yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) | |
217 | ||
218 | if _abi3_applies(python_version): | |
219 | for minor_version in range(python_version[1] - 1, 1, -1): | |
220 | for platform_ in platforms: | |
221 | interpreter = "cp{version}".format( | |
222 | version=_version_nodot((python_version[0], minor_version)) | |
223 | ) | |
224 | yield Tag(interpreter, "abi3", platform_) | |
225 | ||
226 | ||
227 | def _generic_abi() -> Iterator[str]: | |
228 | abi = sysconfig.get_config_var("SOABI") | |
229 | if abi: | |
230 | yield _normalize_string(abi) | |
231 | ||
232 | ||
233 | def generic_tags( | |
234 | interpreter: Optional[str] = None, | |
235 | abis: Optional[Iterable[str]] = None, | |
236 | platforms: Optional[Iterable[str]] = None, | |
237 | *, | |
238 | warn: bool = False, | |
239 | ) -> Iterator[Tag]: | |
240 | """ | |
241 | Yields the tags for a generic interpreter. | |
242 | ||
243 | The tags consist of: | |
244 | - <interpreter>-<abi>-<platform> | |
245 | ||
246 | The "none" ABI will be added if it was not explicitly provided. | |
247 | """ | |
248 | if not interpreter: | |
249 | interp_name = interpreter_name() | |
250 | interp_version = interpreter_version(warn=warn) | |
251 | interpreter = "".join([interp_name, interp_version]) | |
252 | if abis is None: | |
253 | abis = _generic_abi() | |
254 | platforms = list(platforms or platform_tags()) | |
255 | abis = list(abis) | |
256 | if "none" not in abis: | |
257 | abis.append("none") | |
258 | for abi in abis: | |
259 | for platform_ in platforms: | |
260 | yield Tag(interpreter, abi, platform_) | |
261 | ||
262 | ||
263 | def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: | |
264 | """ | |
265 | Yields Python versions in descending order. | |
266 | ||
267 | After the latest version, the major-only version will be yielded, and then | |
268 | all previous versions of that major version. | |
269 | """ | |
270 | if len(py_version) > 1: | |
271 | yield f"py{_version_nodot(py_version[:2])}" | |
272 | yield f"py{py_version[0]}" | |
273 | if len(py_version) > 1: | |
274 | for minor in range(py_version[1] - 1, -1, -1): | |
275 | yield f"py{_version_nodot((py_version[0], minor))}" | |
276 | ||
277 | ||
278 | def compatible_tags( | |
279 | python_version: Optional[PythonVersion] = None, | |
280 | interpreter: Optional[str] = None, | |
281 | platforms: Optional[Iterable[str]] = None, | |
282 | ) -> Iterator[Tag]: | |
283 | """ | |
284 | Yields the sequence of tags that are compatible with a specific version of Python. | |
285 | ||
286 | The tags consist of: | |
287 | - py*-none-<platform> | |
288 | - <interpreter>-none-any # ... if `interpreter` is provided. | |
289 | - py*-none-any | |
290 | """ | |
291 | if not python_version: | |
292 | python_version = sys.version_info[:2] | |
293 | platforms = list(platforms or platform_tags()) | |
294 | for version in _py_interpreter_range(python_version): | |
295 | for platform_ in platforms: | |
296 | yield Tag(version, "none", platform_) | |
297 | if interpreter: | |
298 | yield Tag(interpreter, "none", "any") | |
299 | for version in _py_interpreter_range(python_version): | |
300 | yield Tag(version, "none", "any") | |
301 | ||
302 | ||
303 | def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: | |
304 | if not is_32bit: | |
305 | return arch | |
306 | ||
307 | if arch.startswith("ppc"): | |
308 | return "ppc" | |
309 | ||
310 | return "i386" | |
311 | ||
312 | ||
313 | def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: | |
314 | formats = [cpu_arch] | |
315 | if cpu_arch == "x86_64": | |
316 | if version < (10, 4): | |
317 | return [] | |
318 | formats.extend(["intel", "fat64", "fat32"]) | |
319 | ||
320 | elif cpu_arch == "i386": | |
321 | if version < (10, 4): | |
322 | return [] | |
323 | formats.extend(["intel", "fat32", "fat"]) | |
324 | ||
325 | elif cpu_arch == "ppc64": | |
326 | # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? | |
327 | if version > (10, 5) or version < (10, 4): | |
328 | return [] | |
329 | formats.append("fat64") | |
330 | ||
331 | elif cpu_arch == "ppc": | |
332 | if version > (10, 6): | |
333 | return [] | |
334 | formats.extend(["fat32", "fat"]) | |
335 | ||
336 | if cpu_arch in {"arm64", "x86_64"}: | |
337 | formats.append("universal2") | |
338 | ||
339 | if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: | |
340 | formats.append("universal") | |
341 | ||
342 | return formats | |
343 | ||
344 | ||
345 | def mac_platforms( | |
346 | version: Optional[MacVersion] = None, arch: Optional[str] = None | |
347 | ) -> Iterator[str]: | |
348 | """ | |
349 | Yields the platform tags for a macOS system. | |
350 | ||
351 | The `version` parameter is a two-item tuple specifying the macOS version to | |
352 | generate platform tags for. The `arch` parameter is the CPU architecture to | |
353 | generate platform tags for. Both parameters default to the appropriate value | |
354 | for the current system. | |
355 | """ | |
356 | version_str, _, cpu_arch = platform.mac_ver() | |
357 | if version is None: | |
358 | version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) | |
359 | else: | |
360 | version = version | |
361 | if arch is None: | |
362 | arch = _mac_arch(cpu_arch) | |
363 | else: | |
364 | arch = arch | |
365 | ||
366 | if (10, 0) <= version and version < (11, 0): | |
367 | # Prior to Mac OS 11, each yearly release of Mac OS bumped the | |
368 | # "minor" version number. The major version was always 10. | |
369 | for minor_version in range(version[1], -1, -1): | |
370 | compat_version = 10, minor_version | |
371 | binary_formats = _mac_binary_formats(compat_version, arch) | |
372 | for binary_format in binary_formats: | |
373 | yield "macosx_{major}_{minor}_{binary_format}".format( | |
374 | major=10, minor=minor_version, binary_format=binary_format | |
375 | ) | |
376 | ||
377 | if version >= (11, 0): | |
378 | # Starting with Mac OS 11, each yearly release bumps the major version | |
379 | # number. The minor versions are now the midyear updates. | |
380 | for major_version in range(version[0], 10, -1): | |
381 | compat_version = major_version, 0 | |
382 | binary_formats = _mac_binary_formats(compat_version, arch) | |
383 | for binary_format in binary_formats: | |
384 | yield "macosx_{major}_{minor}_{binary_format}".format( | |
385 | major=major_version, minor=0, binary_format=binary_format | |
386 | ) | |
387 | ||
388 | if version >= (11, 0): | |
389 | # Mac OS 11 on x86_64 is compatible with binaries from previous releases. | |
390 | # Arm64 support was introduced in 11.0, so no Arm binaries from previous | |
391 | # releases exist. | |
392 | # | |
393 | # However, the "universal2" binary format can have a | |
394 | # macOS version earlier than 11.0 when the x86_64 part of the binary supports | |
395 | # that version of macOS. | |
396 | if arch == "x86_64": | |
397 | for minor_version in range(16, 3, -1): | |
398 | compat_version = 10, minor_version | |
399 | binary_formats = _mac_binary_formats(compat_version, arch) | |
400 | for binary_format in binary_formats: | |
401 | yield "macosx_{major}_{minor}_{binary_format}".format( | |
402 | major=compat_version[0], | |
403 | minor=compat_version[1], | |
404 | binary_format=binary_format, | |
405 | ) | |
406 | else: | |
407 | for minor_version in range(16, 3, -1): | |
408 | compat_version = 10, minor_version | |
409 | binary_format = "universal2" | |
410 | yield "macosx_{major}_{minor}_{binary_format}".format( | |
411 | major=compat_version[0], | |
412 | minor=compat_version[1], | |
413 | binary_format=binary_format, | |
414 | ) | |
415 | ||
416 | ||
417 | def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: | |
418 | linux = _normalize_string(sysconfig.get_platform()) | |
419 | if is_32bit: | |
420 | if linux == "linux_x86_64": | |
421 | linux = "linux_i686" | |
422 | elif linux == "linux_aarch64": | |
423 | linux = "linux_armv7l" | |
424 | _, arch = linux.split("_", 1) | |
425 | yield from _manylinux.platform_tags(linux, arch) | |
426 | yield from _musllinux.platform_tags(arch) | |
427 | yield linux | |
428 | ||
429 | ||
430 | def _generic_platforms() -> Iterator[str]: | |
431 | yield _normalize_string(sysconfig.get_platform()) | |
432 | ||
433 | ||
434 | def platform_tags() -> Iterator[str]: | |
435 | """ | |
436 | Provides the platform tags for this installation. | |
437 | """ | |
438 | if platform.system() == "Darwin": | |
439 | return mac_platforms() | |
440 | elif platform.system() == "Linux": | |
441 | return _linux_platforms() | |
442 | else: | |
443 | return _generic_platforms() | |
444 | ||
445 | ||
446 | def interpreter_name() -> str: | |
447 | """ | |
448 | Returns the name of the running interpreter. | |
449 | """ | |
450 | name = sys.implementation.name | |
451 | return INTERPRETER_SHORT_NAMES.get(name) or name | |
452 | ||
453 | ||
454 | def interpreter_version(*, warn: bool = False) -> str: | |
455 | """ | |
456 | Returns the version of the running interpreter. | |
457 | """ | |
458 | version = _get_config_var("py_version_nodot", warn=warn) | |
459 | if version: | |
460 | version = str(version) | |
461 | else: | |
462 | version = _version_nodot(sys.version_info[:2]) | |
463 | return version | |
464 | ||
465 | ||
466 | def _version_nodot(version: PythonVersion) -> str: | |
467 | return "".join(map(str, version)) | |
468 | ||
469 | ||
470 | def sys_tags(*, warn: bool = False) -> Iterator[Tag]: | |
471 | """ | |
472 | Returns the sequence of tag triples for the running interpreter. | |
473 | ||
474 | The order of the sequence corresponds to priority order for the | |
475 | interpreter, from most to least important. | |
476 | """ | |
477 | ||
478 | interp_name = interpreter_name() | |
479 | if interp_name == "cp": | |
480 | yield from cpython_tags(warn=warn) | |
481 | else: | |
482 | yield from generic_tags() | |
483 | ||
484 | if interp_name == "pp": | |
485 | yield from compatible_tags(interpreter="pp3") | |
486 | else: | |
487 | yield from compatible_tags() |