1from __future__ import annotations
2
3import os
4import sys
5
6
7def glibc_version_string() -> str | None:
8 "Returns glibc version string, or None if not using glibc."
9 return glibc_version_string_confstr() or glibc_version_string_ctypes()
10
11
12def glibc_version_string_confstr() -> str | None:
13 "Primary implementation of glibc_version_string using os.confstr."
14 # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely
15 # to be broken or missing. This strategy is used in the standard library
16 # platform module:
17 # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183
18 if sys.platform == "win32":
19 return None
20 try:
21 gnu_libc_version = os.confstr("CS_GNU_LIBC_VERSION")
22 if gnu_libc_version is None:
23 return None
24 # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17":
25 _, version = gnu_libc_version.split()
26 except (AttributeError, OSError, ValueError):
27 # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)...
28 return None
29 return version
30
31
32def glibc_version_string_ctypes() -> str | None:
33 "Fallback implementation of glibc_version_string using ctypes."
34
35 try:
36 import ctypes
37 except ImportError:
38 return None
39
40 # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
41 # manpage says, "If filename is NULL, then the returned handle is for the
42 # main program". This way we can let the linker do the work to figure out
43 # which libc our process is actually using.
44 #
45 # We must also handle the special case where the executable is not a
46 # dynamically linked executable. This can occur when using musl libc,
47 # for example. In this situation, dlopen() will error, leading to an
48 # OSError. Interestingly, at least in the case of musl, there is no
49 # errno set on the OSError. The single string argument used to construct
50 # OSError comes from libc itself and is therefore not portable to
51 # hard code here. In any case, failure to call dlopen() means we
52 # can't proceed, so we bail on our attempt.
53 try:
54 process_namespace = ctypes.CDLL(None)
55 except OSError:
56 return None
57
58 try:
59 gnu_get_libc_version = process_namespace.gnu_get_libc_version
60 except AttributeError:
61 # Symbol doesn't exist -> therefore, we are not linked to
62 # glibc.
63 return None
64
65 # Call gnu_get_libc_version, which returns a string like "2.5"
66 gnu_get_libc_version.restype = ctypes.c_char_p
67 version_str: str = gnu_get_libc_version()
68 # py2 / py3 compatibility:
69 if not isinstance(version_str, str):
70 version_str = version_str.decode("ascii")
71
72 return version_str
73
74
75# platform.libc_ver regularly returns completely nonsensical glibc
76# versions. E.g. on my computer, platform says:
77#
78# ~$ python2.7 -c 'import platform; print(platform.libc_ver())'
79# ('glibc', '2.7')
80# ~$ python3.5 -c 'import platform; print(platform.libc_ver())'
81# ('glibc', '2.9')
82#
83# But the truth is:
84#
85# ~$ ldd --version
86# ldd (Debian GLIBC 2.22-11) 2.22
87#
88# This is unfortunate, because it means that the linehaul data on libc
89# versions that was generated by pip 8.1.2 and earlier is useless and
90# misleading. Solution: instead of using platform, use our code that actually
91# works.
92def libc_ver() -> tuple[str, str]:
93 """Try to determine the glibc version
94
95 Returns a tuple of strings (lib, version) which default to empty strings
96 in case the lookup fails.
97 """
98 glibc_version = glibc_version_string()
99 if glibc_version is None:
100 return ("", "")
101 else:
102 return ("glibc", glibc_version)