diff --git a/src/auditwheel/patcher.py b/src/auditwheel/patcher.py index 2f6bef35..ebdb4a05 100644 --- a/src/auditwheel/patcher.py +++ b/src/auditwheel/patcher.py @@ -27,35 +27,39 @@ def get_rpath(self, file_name: Path) -> str: raise NotImplementedError -def _verify_patchelf() -> None: - """This function looks for the ``patchelf`` external binary in the PATH, +def _verify_patchelf(*, allow_lief: bool) -> str: + """This function looks for the ``lief-patchelf`` or ``patchelf`` external binary in the PATH, checks for the required version, and throws an exception if a proper version can't be found. Otherwise, silence is golden """ - if not which("patchelf"): - msg = "Cannot find required utility `patchelf` in PATH" - raise ValueError(msg) - try: - version = check_output(["patchelf", "--version"]).decode("utf-8") - except CalledProcessError: - msg = "Could not call `patchelf` binary" - raise ValueError(msg) from None - - m = re.match(r"patchelf\s+(\d+(.\d+)?)", version) - if m and tuple(int(x) for x in m.group(1).split(".")) >= (0, 14): - return - msg = f"patchelf {version} found. auditwheel repair requires patchelf >= 0.14." - raise ValueError(msg) + result = which("lief-patchelf") if allow_lief else None + if result is None: + result = which("patchelf") + if not result: + msg = "Cannot find required utility `patchelf` in PATH" + raise ValueError(msg) + try: + version = check_output([result, "--version"]).decode("utf-8") + except CalledProcessError: + msg = "Could not call `patchelf` binary" + raise ValueError(msg) from None + + m = re.match(r"patchelf\s+(\d+(.\d+)?)", version) + if not (m and tuple(int(x) for x in m.group(1).split(".")) >= (0, 14)): + msg = f"patchelf {version} found. auditwheel repair requires patchelf >= 0.14." + raise ValueError(msg) + + return result class Patchelf(ElfPatcher): - def __init__(self) -> None: - _verify_patchelf() + def __init__(self, *, allow_lief: bool = True) -> None: + self.patchelf_path = _verify_patchelf(allow_lief=allow_lief) def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> None: check_call( [ - "patchelf", + self.patchelf_path, *chain.from_iterable(("--replace-needed", *pair) for pair in old_new_pairs), file_name, ], @@ -64,18 +68,20 @@ def replace_needed(self, file_name: Path, *old_new_pairs: tuple[str, str]) -> No def remove_needed(self, file_name: Path, *sonames: str) -> None: check_call( [ - "patchelf", + self.patchelf_path, *chain.from_iterable(("--remove-needed", soname) for soname in sonames), file_name, ], ) def set_soname(self, file_name: Path, new_so_name: str) -> None: - check_call(["patchelf", "--set-soname", new_so_name, file_name]) + check_call([self.patchelf_path, "--set-soname", new_so_name, file_name]) def set_rpath(self, file_name: Path, rpath: str) -> None: - check_call(["patchelf", "--remove-rpath", file_name]) - check_call(["patchelf", "--force-rpath", "--set-rpath", rpath, file_name]) + check_call([self.patchelf_path, "--remove-rpath", file_name]) + check_call([self.patchelf_path, "--force-rpath", "--set-rpath", rpath, file_name]) def get_rpath(self, file_name: Path) -> str: - return check_output(["patchelf", "--print-rpath", file_name]).decode("utf-8").strip() + return ( + check_output([self.patchelf_path, "--print-rpath", file_name]).decode("utf-8").strip() + ) diff --git a/tests/integration/test_manylinux.py b/tests/integration/test_manylinux.py index a70cd612..da097bb1 100644 --- a/tests/integration/test_manylinux.py +++ b/tests/integration/test_manylinux.py @@ -82,11 +82,11 @@ DEVTOOLSET = { "manylinux_2_12": "devtoolset-8", "manylinux_2_17": "devtoolset-10", - "manylinux_2_28": "gcc-toolset-14", + "manylinux_2_28": "gcc-toolset-15", "manylinux_2_31": "devtoolset-not-present", - "manylinux_2_34": "gcc-toolset-14", + "manylinux_2_34": "gcc-toolset-15", "manylinux_2_35": "devtoolset-not-present", - "manylinux_2_39": "devtoolset-not-present", + "manylinux_2_39": "gcc-toolset-15", "musllinux_1_2": "devtoolset-not-present", } PATH_DIRS = [ @@ -1005,6 +1005,30 @@ def any_manylinux_img(self, request): "pip install -U pip setuptools 'coverage[toml]>=7.13'", "pip install -U -e /auditwheel_src", ] + if policy in {"manylinux_2_28", "manylinux_2_34", "manylinux_2_39"}: + commands.append( + "dnf install -y " + "gcc-toolset-15-binutils gcc-toolset-15-gcc gcc-toolset-15-gcc-c++ " + "gcc-toolset-15-gcc-gfortran gcc-toolset-15-libatomic-devel", + ) + lief_patchelf_file = { + "aarch64": "lief-tools-aarch64-unknown-linux-gnu.zip", + "i686": "lief-tools-i686-unknown-linux-gnu.zip", + "x86_64": "lief-tools-x86_64-unknown-linux-gnu.zip", + }.get(PLATFORM) + if lief_patchelf_file: + lief_patchelf_url = "https://github.com/lief-project/LIEF/releases/download" + lief_patchelf_url = f"{lief_patchelf_url}/0.17.6/{lief_patchelf_file}" + commands.extend( + ( + "pipx uninstall patchelf", + f"curl -fsSLo /tmp/lief-tools.zip {lief_patchelf_url}", + "bash -c 'cd /tmp && unzip /tmp/lief-tools.zip'", + "mv -f /tmp/bin/lief-patchelf /usr/local/bin/", + "chmod +x /usr/local/bin/lief-patchelf", + ), + ) + if policy in {"manylinux_2_31", "manylinux_2_35"}: commands.append("apt-get update -yqq") with tmp_docker_image(base, commands, env) as img_id: diff --git a/tests/unit/test_elfpatcher.py b/tests/unit/test_elfpatcher.py index 556489a1..b257269c 100644 --- a/tests/unit/test_elfpatcher.py +++ b/tests/unit/test_elfpatcher.py @@ -11,7 +11,7 @@ @patch("auditwheel.patcher.which") def test_patchelf_unavailable(which): - which.return_value = False + which.return_value = None with pytest.raises(ValueError, match="Cannot find required utility"): Patchelf() @@ -19,10 +19,10 @@ def test_patchelf_unavailable(which): @patch("auditwheel.patcher.which") @patch("auditwheel.patcher.check_output") def test_patchelf_check_output_fail(check_output, which): - which.return_value = True + which.return_value = "patchelf" check_output.side_effect = CalledProcessError(1, "patchelf --version") with pytest.raises(ValueError, match="Could not call"): - Patchelf() + Patchelf(allow_lief=False) @patch("auditwheel.patcher.which") @@ -31,7 +31,7 @@ def test_patchelf_check_output_fail(check_output, which): def test_patchelf_version_check(check_output, which, version): which.return_value = True check_output.return_value.decode.return_value = f"patchelf {version}" - Patchelf() + Patchelf(allow_lief=False) @patch("auditwheel.patcher.which") @@ -41,7 +41,7 @@ def test_patchelf_version_check_fail(check_output, which, version): which.return_value = True check_output.return_value.decode.return_value = f"patchelf {version}" with pytest.raises(ValueError, match=f"patchelf {version} found"): - Patchelf() + Patchelf(allow_lief=False) @patch("auditwheel.patcher._verify_patchelf") @@ -52,6 +52,7 @@ class TestPatchElf: def test_replace_needed_one(self, check_call, _0, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") soname_old = "TEST_OLD" soname_new = "TEST_NEW" @@ -62,6 +63,7 @@ def test_replace_needed_one(self, check_call, _0, _1): # noqa: PT019 def test_replace_needed_multple(self, check_call, _0, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") replacements = [ ("TEST_OLD1", "TEST_NEW1"), @@ -81,6 +83,7 @@ def test_replace_needed_multple(self, check_call, _0, _1): # noqa: PT019 def test_set_soname(self, check_call, _0, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") soname_new = "TEST_NEW" patcher.set_soname(filename, soname_new) @@ -90,6 +93,7 @@ def test_set_soname(self, check_call, _0, _1): # noqa: PT019 def test_set_rpath(self, check_call, _0, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") patcher.set_rpath(filename, "$ORIGIN/.lib") check_call_expected_args = [ @@ -103,6 +107,7 @@ def test_set_rpath(self, check_call, _0, _1): # noqa: PT019 def test_get_rpath(self, _0, check_output, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") check_output.return_value = b"existing_rpath" result = patcher.get_rpath(filename) @@ -113,6 +118,7 @@ def test_get_rpath(self, _0, check_output, _1): # noqa: PT019 def test_remove_needed(self, check_call, _0, _1): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" filename = Path("test.so") soname_1 = "TEST_REM_1" soname_2 = "TEST_REM_2" diff --git a/tests/unit/test_repair.py b/tests/unit/test_repair.py index 2a0df9d4..65976e7b 100644 --- a/tests/unit/test_repair.py +++ b/tests/unit/test_repair.py @@ -13,6 +13,7 @@ class TestRepair: def test_append_rpath(self, check_call, check_output, _): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" # When a library has an existing RPATH entry within wheel_dir existing_rpath = b"$ORIGIN/.existinglibdir" check_output.return_value = existing_rpath @@ -42,6 +43,7 @@ def test_append_rpath(self, check_call, check_output, _): # noqa: PT019 def test_append_rpath_reject_outside_wheel(self, check_call, check_output, _): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" # When a library has an existing RPATH entry outside wheel_dir existing_rpath = b"/outside/wheel/dir" check_output.return_value = existing_rpath @@ -71,6 +73,7 @@ def test_append_rpath_reject_outside_wheel(self, check_call, check_output, _): def test_append_rpath_ignore_duplicates(self, check_call, check_output, _): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" # When a library has an existing RPATH entry and we try and append it again existing_rpath = b"$ORIGIN" check_output.return_value = existing_rpath @@ -94,6 +97,7 @@ def test_append_rpath_ignore_duplicates(self, check_call, check_output, _): # n def test_append_rpath_ignore_relative(self, check_call, check_output, _): # noqa: PT019 patcher = Patchelf() + patcher.patchelf_path = "patchelf" # When a library has an existing RPATH entry but it cannot be resolved # to an absolute path, it is eliminated existing_rpath = b"not/absolute"