codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import collections4import contextlib5import functools6import os7import re8import sys9import warnings10from typing import Generator, Iterator, NamedTuple, Sequence11 12from ._elffile import EIClass, EIData, ELFFile, EMachine13 14EF_ARM_ABIMASK = 0xFF00000015EF_ARM_ABI_VER5 = 0x0500000016EF_ARM_ABI_FLOAT_HARD = 0x0000040017 18_ALLOWED_ARCHS = {19 "x86_64",20 "aarch64",21 "ppc64",22 "ppc64le",23 "s390x",24 "loongarch64",25 "riscv64",26}27 28 29# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`30# as the type for `path` until then.31@contextlib.contextmanager32def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:33 try:34 with open(path, "rb") as f:35 yield ELFFile(f)36 except (OSError, TypeError, ValueError):37 yield None38 39 40def _is_linux_armhf(executable: str) -> bool:41 # hard-float ABI can be detected from the ELF header of the running42 # process43 # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf44 with _parse_elf(executable) as f:45 return (46 f is not None47 and f.capacity == EIClass.C3248 and f.encoding == EIData.Lsb49 and f.machine == EMachine.Arm50 and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER551 and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD52 )53 54 55def _is_linux_i686(executable: str) -> bool:56 with _parse_elf(executable) as f:57 return (58 f is not None59 and f.capacity == EIClass.C3260 and f.encoding == EIData.Lsb61 and f.machine == EMachine.I38662 )63 64 65def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:66 if "armv7l" in archs:67 return _is_linux_armhf(executable)68 if "i686" in archs:69 return _is_linux_i686(executable)70 return any(arch in _ALLOWED_ARCHS for arch in archs)71 72 73# If glibc ever changes its major version, we need to know what the last74# minor version was, so we can build the complete list of all versions.75# For now, guess what the highest minor version might be, assume it will76# be 50 for testing. Once this actually happens, update the dictionary77# with the actual value.78_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)79 80 81class _GLibCVersion(NamedTuple):82 major: int83 minor: int84 85 86def _glibc_version_string_confstr() -> str | None:87 """88 Primary implementation of glibc_version_string using os.confstr.89 """90 # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely91 # to be broken or missing. This strategy is used in the standard library92 # platform module.93 # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L18394 try:95 # Should be a string like "glibc 2.17".96 version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")97 assert version_string is not None98 _, version = version_string.rsplit()99 except (AssertionError, AttributeError, OSError, ValueError):100 # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...101 return None102 return version103 104 105def _glibc_version_string_ctypes() -> str | None:106 """107 Fallback implementation of glibc_version_string using ctypes.108 """109 try:110 import ctypes # noqa: PLC0415111 except ImportError:112 return None113 114 # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen115 # manpage says, "If filename is NULL, then the returned handle is for the116 # main program". This way we can let the linker do the work to figure out117 # which libc our process is actually using.118 #119 # We must also handle the special case where the executable is not a120 # dynamically linked executable. This can occur when using musl libc,121 # for example. In this situation, dlopen() will error, leading to an122 # OSError. Interestingly, at least in the case of musl, there is no123 # errno set on the OSError. The single string argument used to construct124 # OSError comes from libc itself and is therefore not portable to125 # hard code here. In any case, failure to call dlopen() means we126 # can proceed, so we bail on our attempt.127 try:128 process_namespace = ctypes.CDLL(None)129 except OSError:130 return None131 132 try:133 gnu_get_libc_version = process_namespace.gnu_get_libc_version134 except AttributeError:135 # Symbol doesn't exist -> therefore, we are not linked to136 # glibc.137 return None138 139 # Call gnu_get_libc_version, which returns a string like "2.5"140 gnu_get_libc_version.restype = ctypes.c_char_p141 version_str: str = gnu_get_libc_version()142 # py2 / py3 compatibility:143 if not isinstance(version_str, str):144 version_str = version_str.decode("ascii")145 146 return version_str147 148 149def _glibc_version_string() -> str | None:150 """Returns glibc version string, or None if not using glibc."""151 return _glibc_version_string_confstr() or _glibc_version_string_ctypes()152 153 154def _parse_glibc_version(version_str: str) -> _GLibCVersion:155 """Parse glibc version.156 157 We use a regexp instead of str.split because we want to discard any158 random junk that might come after the minor version -- this might happen159 in patched/forked versions of glibc (e.g. Linaro's version of glibc160 uses version strings like "2.20-2014.11"). See gh-3588.161 """162 m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str)163 if not m:164 warnings.warn(165 f"Expected glibc version with 2 components major.minor, got: {version_str}",166 RuntimeWarning,167 stacklevel=2,168 )169 return _GLibCVersion(-1, -1)170 return _GLibCVersion(int(m.group("major")), int(m.group("minor")))171 172 173@functools.lru_cache174def _get_glibc_version() -> _GLibCVersion:175 version_str = _glibc_version_string()176 if version_str is None:177 return _GLibCVersion(-1, -1)178 return _parse_glibc_version(version_str)179 180 181# From PEP 513, PEP 600182def _is_compatible(arch: str, version: _GLibCVersion) -> bool:183 sys_glibc = _get_glibc_version()184 if sys_glibc < version:185 return False186 # Check for presence of _manylinux module.187 try:188 import _manylinux # noqa: PLC0415189 except ImportError:190 return True191 if hasattr(_manylinux, "manylinux_compatible"):192 result = _manylinux.manylinux_compatible(version[0], version[1], arch)193 if result is not None:194 return bool(result)195 return True196 if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):197 return bool(_manylinux.manylinux1_compatible)198 if version == _GLibCVersion(2, 12) and hasattr(199 _manylinux, "manylinux2010_compatible"200 ):201 return bool(_manylinux.manylinux2010_compatible)202 if version == _GLibCVersion(2, 17) and hasattr(203 _manylinux, "manylinux2014_compatible"204 ):205 return bool(_manylinux.manylinux2014_compatible)206 return True207 208 209_LEGACY_MANYLINUX_MAP: dict[_GLibCVersion, str] = {210 # CentOS 7 w/ glibc 2.17 (PEP 599)211 _GLibCVersion(2, 17): "manylinux2014",212 # CentOS 6 w/ glibc 2.12 (PEP 571)213 _GLibCVersion(2, 12): "manylinux2010",214 # CentOS 5 w/ glibc 2.5 (PEP 513)215 _GLibCVersion(2, 5): "manylinux1",216}217 218 219def platform_tags(archs: Sequence[str]) -> Iterator[str]:220 """Generate manylinux tags compatible to the current platform.221 222 :param archs: Sequence of compatible architectures.223 The first one shall be the closest to the actual architecture and be the part of224 platform tag after the ``linux_`` prefix, e.g. ``x86_64``.225 The ``linux_`` prefix is assumed as a prerequisite for the current platform to226 be manylinux-compatible.227 228 :returns: An iterator of compatible manylinux tags.229 """230 if not _have_compatible_abi(sys.executable, archs):231 return232 # Oldest glibc to be supported regardless of architecture is (2, 17).233 too_old_glibc2 = _GLibCVersion(2, 16)234 if set(archs) & {"x86_64", "i686"}:235 # On x86/i686 also oldest glibc to be supported is (2, 5).236 too_old_glibc2 = _GLibCVersion(2, 4)237 current_glibc = _GLibCVersion(*_get_glibc_version())238 glibc_max_list = [current_glibc]239 # We can assume compatibility across glibc major versions.240 # https://sourceware.org/bugzilla/show_bug.cgi?id=24636241 #242 # Build a list of maximum glibc versions so that we can243 # output the canonical list of all glibc from current_glibc244 # down to too_old_glibc2, including all intermediary versions.245 for glibc_major in range(current_glibc.major - 1, 1, -1):246 glibc_minor = _LAST_GLIBC_MINOR[glibc_major]247 glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor))248 for arch in archs:249 for glibc_max in glibc_max_list:250 if glibc_max.major == too_old_glibc2.major:251 min_minor = too_old_glibc2.minor252 else:253 # For other glibc major versions oldest supported is (x, 0).254 min_minor = -1255 for glibc_minor in range(glibc_max.minor, min_minor, -1):256 glibc_version = _GLibCVersion(glibc_max.major, glibc_minor)257 if _is_compatible(arch, glibc_version):258 yield "manylinux_{}_{}_{}".format(*glibc_version, arch)259 260 # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags.261 if legacy_tag := _LEGACY_MANYLINUX_MAP.get(glibc_version):262 yield f"{legacy_tag}_{arch}"263 