Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_vendor/packaging/_manylinux.py: 27%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

128 statements  

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}"