Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions src/auditwheel/patcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
Expand All @@ -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()
)
30 changes: 27 additions & 3 deletions tests/integration/test_manylinux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 11 additions & 5 deletions tests/unit/test_elfpatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@

@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()


@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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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"
Expand All @@ -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"),
Expand All @@ -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)
Expand All @@ -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 = [
Expand All @@ -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)
Expand All @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions tests/unit/test_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down
Loading