]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
1 | import collections |
2 | import contextlib | |
3 | import functools | |
4 | import os | |
5 | import re | |
6 | import sys | |
7 | import warnings | |
8 | from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple | |
9 | ||
10 | from ._elffile import EIClass, EIData, ELFFile, EMachine | |
11 | ||
12 | EF_ARM_ABIMASK = 0xFF000000 | |
13 | EF_ARM_ABI_VER5 = 0x05000000 | |
14 | EF_ARM_ABI_FLOAT_HARD = 0x00000400 | |
15 | ||
16 | ||
17 | # `os.PathLike` not a generic type until Python 3.9, so sticking with `str` | |
18 | # as the type for `path` until then. | |
19 | @contextlib.contextmanager | |
20 | def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: | |
21 | try: | |
22 | with open(path, "rb") as f: | |
23 | yield ELFFile(f) | |
24 | except (OSError, TypeError, ValueError): | |
25 | yield None | |
26 | ||
27 | ||
28 | def _is_linux_armhf(executable: str) -> bool: | |
29 | # hard-float ABI can be detected from the ELF header of the running | |
30 | # process | |
31 | # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf | |
32 | with _parse_elf(executable) as f: | |
33 | return ( | |
34 | f is not None | |
35 | and f.capacity == EIClass.C32 | |
36 | and f.encoding == EIData.Lsb | |
37 | and f.machine == EMachine.Arm | |
38 | and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 | |
39 | and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD | |
40 | ) | |
41 | ||
42 | ||
43 | def _is_linux_i686(executable: str) -> bool: | |
44 | with _parse_elf(executable) as f: | |
45 | return ( | |
46 | f is not None | |
47 | and f.capacity == EIClass.C32 | |
48 | and f.encoding == EIData.Lsb | |
49 | and f.machine == EMachine.I386 | |
50 | ) | |
51 | ||
52 | ||
53 | def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: | |
54 | if "armv7l" in archs: | |
55 | return _is_linux_armhf(executable) | |
56 | if "i686" in archs: | |
57 | return _is_linux_i686(executable) | |
58 | allowed_archs = {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x", "loongarch64"} | |
59 | return any(arch in allowed_archs for arch in archs) | |
60 | ||
61 | ||
62 | # If glibc ever changes its major version, we need to know what the last | |
63 | # minor version was, so we can build the complete list of all versions. | |
64 | # For now, guess what the highest minor version might be, assume it will | |
65 | # be 50 for testing. Once this actually happens, update the dictionary | |
66 | # with the actual value. | |
67 | _LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) | |
68 | ||
69 | ||
70 | class _GLibCVersion(NamedTuple): | |
71 | major: int | |
72 | minor: int | |
73 | ||
74 | ||
75 | def _glibc_version_string_confstr() -> Optional[str]: | |
76 | """ | |
77 | Primary implementation of glibc_version_string using os.confstr. | |
78 | """ | |
79 | # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely | |
80 | # to be broken or missing. This strategy is used in the standard library | |
81 | # platform module. | |
82 | # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 | |
83 | try: | |
84 | # Should be a string like "glibc 2.17". | |
85 | version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION") | |
86 | assert version_string is not None | |
87 | _, version = version_string.rsplit() | |
88 | except (AssertionError, AttributeError, OSError, ValueError): | |
89 | # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... | |
90 | return None | |
91 | return version | |
92 | ||
93 | ||
94 | def _glibc_version_string_ctypes() -> Optional[str]: | |
95 | """ | |
96 | Fallback implementation of glibc_version_string using ctypes. | |
97 | """ | |
98 | try: | |
99 | import ctypes | |
100 | except ImportError: | |
101 | return None | |
102 | ||
103 | # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen | |
104 | # manpage says, "If filename is NULL, then the returned handle is for the | |
105 | # main program". This way we can let the linker do the work to figure out | |
106 | # which libc our process is actually using. | |
107 | # | |
108 | # We must also handle the special case where the executable is not a | |
109 | # dynamically linked executable. This can occur when using musl libc, | |
110 | # for example. In this situation, dlopen() will error, leading to an | |
111 | # OSError. Interestingly, at least in the case of musl, there is no | |
112 | # errno set on the OSError. The single string argument used to construct | |
113 | # OSError comes from libc itself and is therefore not portable to | |
114 | # hard code here. In any case, failure to call dlopen() means we | |
115 | # can proceed, so we bail on our attempt. | |
116 | try: | |
117 | process_namespace = ctypes.CDLL(None) | |
118 | except OSError: | |
119 | return None | |
120 | ||
121 | try: | |
122 | gnu_get_libc_version = process_namespace.gnu_get_libc_version | |
123 | except AttributeError: | |
124 | # Symbol doesn't exist -> therefore, we are not linked to | |
125 | # glibc. | |
126 | return None | |
127 | ||
128 | # Call gnu_get_libc_version, which returns a string like "2.5" | |
129 | gnu_get_libc_version.restype = ctypes.c_char_p | |
130 | version_str: str = gnu_get_libc_version() | |
131 | # py2 / py3 compatibility: | |
132 | if not isinstance(version_str, str): | |
133 | version_str = version_str.decode("ascii") | |
134 | ||
135 | return version_str | |
136 | ||
137 | ||
138 | def _glibc_version_string() -> Optional[str]: | |
139 | """Returns glibc version string, or None if not using glibc.""" | |
140 | return _glibc_version_string_confstr() or _glibc_version_string_ctypes() | |
141 | ||
142 | ||
143 | def _parse_glibc_version(version_str: str) -> Tuple[int, int]: | |
144 | """Parse glibc version. | |
145 | ||
146 | We use a regexp instead of str.split because we want to discard any | |
147 | random junk that might come after the minor version -- this might happen | |
148 | in patched/forked versions of glibc (e.g. Linaro's version of glibc | |
149 | uses version strings like "2.20-2014.11"). See gh-3588. | |
150 | """ | |
151 | m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) | |
152 | if not m: | |
153 | warnings.warn( | |
154 | f"Expected glibc version with 2 components major.minor," | |
155 | f" got: {version_str}", | |
156 | RuntimeWarning, | |
157 | ) | |
158 | return -1, -1 | |
159 | return int(m.group("major")), int(m.group("minor")) | |
160 | ||
161 | ||
162 | @functools.lru_cache() | |
163 | def _get_glibc_version() -> Tuple[int, int]: | |
164 | version_str = _glibc_version_string() | |
165 | if version_str is None: | |
166 | return (-1, -1) | |
167 | return _parse_glibc_version(version_str) | |
168 | ||
169 | ||
170 | # From PEP 513, PEP 600 | |
171 | def _is_compatible(arch: str, version: _GLibCVersion) -> bool: | |
172 | sys_glibc = _get_glibc_version() | |
173 | if sys_glibc < version: | |
174 | return False | |
175 | # Check for presence of _manylinux module. | |
176 | try: | |
177 | import _manylinux # noqa | |
178 | except ImportError: | |
179 | return True | |
180 | if hasattr(_manylinux, "manylinux_compatible"): | |
181 | result = _manylinux.manylinux_compatible(version[0], version[1], arch) | |
182 | if result is not None: | |
183 | return bool(result) | |
184 | return True | |
185 | if version == _GLibCVersion(2, 5): | |
186 | if hasattr(_manylinux, "manylinux1_compatible"): | |
187 | return bool(_manylinux.manylinux1_compatible) | |
188 | if version == _GLibCVersion(2, 12): | |
189 | if hasattr(_manylinux, "manylinux2010_compatible"): | |
190 | return bool(_manylinux.manylinux2010_compatible) | |
191 | if version == _GLibCVersion(2, 17): | |
192 | if hasattr(_manylinux, "manylinux2014_compatible"): | |
193 | return bool(_manylinux.manylinux2014_compatible) | |
194 | return True | |
195 | ||
196 | ||
197 | _LEGACY_MANYLINUX_MAP = { | |
198 | # CentOS 7 w/ glibc 2.17 (PEP 599) | |
199 | (2, 17): "manylinux2014", | |
200 | # CentOS 6 w/ glibc 2.12 (PEP 571) | |
201 | (2, 12): "manylinux2010", | |
202 | # CentOS 5 w/ glibc 2.5 (PEP 513) | |
203 | (2, 5): "manylinux1", | |
204 | } | |
205 | ||
206 | ||
207 | def platform_tags(archs: Sequence[str]) -> Iterator[str]: | |
208 | """Generate manylinux tags compatible to the current platform. | |
209 | ||
210 | :param archs: Sequence of compatible architectures. | |
211 | The first one shall be the closest to the actual architecture and be the part of | |
212 | platform tag after the ``linux_`` prefix, e.g. ``x86_64``. | |
213 | The ``linux_`` prefix is assumed as a prerequisite for the current platform to | |
214 | be manylinux-compatible. | |
215 | ||
216 | :returns: An iterator of compatible manylinux tags. | |
217 | """ | |
218 | if not _have_compatible_abi(sys.executable, archs): | |
219 | return | |
220 | # Oldest glibc to be supported regardless of architecture is (2, 17). | |
221 | too_old_glibc2 = _GLibCVersion(2, 16) | |
222 | if set(archs) & {"x86_64", "i686"}: | |
223 | # On x86/i686 also oldest glibc to be supported is (2, 5). | |
224 | too_old_glibc2 = _GLibCVersion(2, 4) | |
225 | current_glibc = _GLibCVersion(*_get_glibc_version()) | |
226 | glibc_max_list = [current_glibc] | |
227 | # We can assume compatibility across glibc major versions. | |
228 | # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 | |
229 | # | |
230 | # Build a list of maximum glibc versions so that we can | |
231 | # output the canonical list of all glibc from current_glibc | |
232 | # down to too_old_glibc2, including all intermediary versions. | |
233 | for glibc_major in range(current_glibc.major - 1, 1, -1): | |
234 | glibc_minor = _LAST_GLIBC_MINOR[glibc_major] | |
235 | glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) | |
236 | for arch in archs: | |
237 | for glibc_max in glibc_max_list: | |
238 | if glibc_max.major == too_old_glibc2.major: | |
239 | min_minor = too_old_glibc2.minor | |
240 | else: | |
241 | # For other glibc major versions oldest supported is (x, 0). | |
242 | min_minor = -1 | |
243 | for glibc_minor in range(glibc_max.minor, min_minor, -1): | |
244 | glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) | |
245 | tag = "manylinux_{}_{}".format(*glibc_version) | |
246 | if _is_compatible(arch, glibc_version): | |
247 | yield f"{tag}_{arch}" | |
248 | # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. | |
249 | if glibc_version in _LEGACY_MANYLINUX_MAP: | |
250 | legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] | |
251 | if _is_compatible(arch, glibc_version): | |
252 | yield f"{legacy_tag}_{arch}" |