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