diff --git a/news/6543.bugfix b/news/6543.bugfix new file mode 100644 index 00000000000..faf68532c9d --- /dev/null +++ b/news/6543.bugfix @@ -0,0 +1 @@ +Prefer ``os.confstr`` to ``ctypes`` when extracting glibc version info. diff --git a/news/6675.bugfix b/news/6675.bugfix new file mode 100644 index 00000000000..faf68532c9d --- /dev/null +++ b/news/6675.bugfix @@ -0,0 +1 @@ +Prefer ``os.confstr`` to ``ctypes`` when extracting glibc version info. diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 5bea655ebcc..aa77d9b60f8 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -import ctypes +import os import re import warnings @@ -13,6 +13,33 @@ def glibc_version_string(): # type: () -> Optional[str] "Returns glibc version string, or None if not using glibc." + return glibc_version_string_confstr() or glibc_version_string_ctypes() + + +def glibc_version_string_confstr(): + # type: () -> Optional[str] + "Primary implementation of glibc_version_string using os.confstr." + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module: + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 + try: + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17": + _, version = os.confstr("CS_GNU_LIBC_VERSION").split() + except (AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def glibc_version_string_ctypes(): + # type: () -> Optional[str] + "Fallback implementation of glibc_version_string using ctypes." + + try: + import ctypes + except ImportError: + return None # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen # manpage says, "If filename is NULL, then the returned handle is for the @@ -56,7 +83,7 @@ def check_glibc_version(version_str, required_major, minimum_minor): def have_compatible_glibc(required_major, minimum_minor): # type: (int, int) -> bool - version_str = glibc_version_string() # type: Optional[str] + version_str = glibc_version_string() if version_str is None: return False return check_glibc_version(version_str, required_major, minimum_minor) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 5833ffbfe81..34eded807db 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -26,7 +26,10 @@ ) from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated from pip._internal.utils.encoding import BOMS, auto_decode -from pip._internal.utils.glibc import check_glibc_version +from pip._internal.utils.glibc import ( + check_glibc_version, glibc_version_string, glibc_version_string_confstr, + glibc_version_string_ctypes, +) from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.misc import ( call_subprocess, egg_link_path, ensure_dir, format_command_args, @@ -668,6 +671,10 @@ def raising_mkdir(*args, **kwargs): pass +def raises(error): + raise error + + class TestGlibc(object): def test_manylinux_check_glibc_version(self): """ @@ -701,6 +708,35 @@ def test_manylinux_check_glibc_version(self): # Didn't find the warning we were expecting assert False + def test_glibc_version_string(self, monkeypatch): + monkeypatch.setattr( + os, "confstr", lambda x: "glibc 2.20", raising=False, + ) + assert glibc_version_string() == "2.20" + + def test_glibc_version_string_confstr(self, monkeypatch): + monkeypatch.setattr( + os, "confstr", lambda x: "glibc 2.20", raising=False, + ) + assert glibc_version_string_confstr() == "2.20" + + @pytest.mark.parametrize("failure", [ + lambda x: raises(ValueError), + lambda x: raises(OSError), + lambda x: "XXX", + ]) + def test_glibc_version_string_confstr_fail(self, monkeypatch, failure): + monkeypatch.setattr(os, "confstr", failure, raising=False) + assert glibc_version_string_confstr() is None + + def test_glibc_version_string_confstr_missing(self, monkeypatch): + monkeypatch.delattr(os, "confstr", raising=False) + assert glibc_version_string_confstr() is None + + def test_glibc_version_string_ctypes_missing(self, monkeypatch): + monkeypatch.setitem(sys.modules, "ctypes", None) + assert glibc_version_string_ctypes() is None + @pytest.mark.parametrize('version_info, expected', [ (None, None),