Skip to content

Commit f2d86c4

Browse files
committed
Fetch unavailable libraries directly from pypi
If a requested library is not available in one of the repositories, usually the channels "libraries_cache", fetch the information directly from PyPi. The core-feature is to automatically discover suitable versions of a library, even when the Python host changes or gets compiled differently. Vendors [packaging](https://github.com/pypa/packaging).
1 parent 6743158 commit f2d86c4

File tree

18 files changed

+5780
-7
lines changed

18 files changed

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

0 commit comments

Comments
 (0)