Skip to content
Open
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
39 changes: 24 additions & 15 deletions src/auditwheel/lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,24 +334,24 @@ def load_ld_paths(
dict containing library paths to search

"""
ldpaths: dict[str, list[str]] = {"conf": [], "env": [], "interp": []}
ldpaths: dict[str, list[str]] = {
"conf": [],
"auditwheel": [],
"env": [],
"interp": [],
}

auditwheel_ld_library_path = os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH")
if auditwheel_ld_library_path:
ldpaths["auditwheel"] = parse_ld_paths(auditwheel_ld_library_path, path="")

ld_library_path = os.environ.get("LD_LIBRARY_PATH")
if root != "/" and ld_library_path is not None:
log.warning("ignoring LD_LIBRARY_PATH due to ROOT usage")
ld_library_path = None

# Load up $AUDITWHEEL_LD_LIBRARY_PATH and $LD_LIBRARY_PATH
env_ldpath = ":".join(
filter(None, (os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH"), ld_library_path)),
)

if env_ldpath:
# TODO: If this contains $ORIGIN, we probably have to parse this
# on a per-ELF basis so it can get turned into the right thing.
# don't pass root: in case root != "/", only AUDITWHEEL_LD_LIBRARY_PATH is checked
# it shall already contain fully resolved paths
ldpaths["env"] = parse_ld_paths(env_ldpath, path="")
if ld_library_path:
ldpaths["env"] = parse_ld_paths(ld_library_path, path="")

if libc == Libc.MUSL:
# from https://git.musl-libc.org/cgit/musl/tree/ldso
Expand Down Expand Up @@ -389,7 +389,8 @@ def ld_paths_from_arg(args_ldpaths: str | None) -> dict[str, list[str]] | None:

return {
"conf": parse_ld_paths(args_ldpaths),
"env": parse_ld_paths(os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH", "")),
"auditwheel": parse_ld_paths(os.environ.get("AUDITWHEEL_LD_LIBRARY_PATH", "")),
"env": [],
"interp": [],
}

Expand Down Expand Up @@ -587,12 +588,20 @@ def ldd(

assert ldpaths is not None # noqa: S101

# On recursive calls, ldpaths["rpath"] and ldpaths["runpath"] are the
# top-level ELF's paths propagated for dependency resolution, while rpaths
# and runpaths belong to the current ELF. On the first call they are the
# same lists, so omit the inherited copies to avoid searching duplicates.
inherited_rpaths = [] if _first else ldpaths["rpath"]
inherited_runpaths = [] if _first else ldpaths["runpath"]

all_ldpaths = (
ldpaths["rpath"]
inherited_rpaths
+ rpaths
+ ldpaths.get("auditwheel", [])
+ runpaths
+ ldpaths["env"]
+ ldpaths["runpath"]
+ inherited_runpaths
+ ldpaths["conf"]
+ ldpaths["interp"]
)
Expand Down
109 changes: 104 additions & 5 deletions tests/unit/test_lddtree.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import pytest

from auditwheel import lddtree
from auditwheel.architecture import Architecture
from auditwheel.lddtree import LIBPYTHON_RE, ld_paths_from_arg, ldd, parse_ld_paths
from auditwheel.lddtree import LIBPYTHON_RE, ld_paths_from_arg, ldd, load_ld_paths, parse_ld_paths
from auditwheel.libc import Libc
from auditwheel.tools import zip2dir

Expand Down Expand Up @@ -114,13 +115,13 @@ def test_parse_ld_paths_origin(origin):
[
(None, "", None),
(None, str(HERE.parent), None),
("", "", {"conf": [], "env": [], "interp": []}),
(str(HERE), "", {"conf": [str(HERE)], "env": [], "interp": []}),
("", str(HERE), {"conf": [], "env": [str(HERE)], "interp": []}),
("", "", {"conf": [], "auditwheel": [], "env": [], "interp": []}),
(str(HERE), "", {"conf": [str(HERE)], "auditwheel": [], "env": [], "interp": []}),
("", str(HERE), {"conf": [], "auditwheel": [str(HERE)], "env": [], "interp": []}),
(
str(HERE),
str(HERE.parent),
{"conf": [str(HERE)], "env": [str(HERE.parent)], "interp": []},
{"conf": [str(HERE)], "auditwheel": [str(HERE.parent)], "env": [], "interp": []},
),
],
)
Expand All @@ -144,3 +145,101 @@ def test_libc_no_detect_musl_cp310(tmp_path: Path) -> None:
assert result.rpath == ()
assert result.runpath == ()
assert not result.libraries


def test_load_ld_paths_root_scenario(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""root != '/' should ignore standard LD_LIBRARY_PATH but retain AUDITWHEEL_LD_LIBRARY_PATH."""
auditwheel_dir = tmp_path / "auditwheel"
auditwheel_dir.mkdir()
ld_dir = tmp_path / "ld_library_path"
ld_dir.mkdir()

monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(auditwheel_dir))
monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(ld_dir))

# root is non-root
fake_root = tmp_path / "fake_root"
fake_root.mkdir()

res = load_ld_paths(Libc.GLIBC, root=str(fake_root))
assert res["auditwheel"] == [str(auditwheel_dir)]
assert res["env"] == []


def test_runpath_vs_auditwheel_and_ld_library_path_precedence(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Verify precedence order: AUDITWHEEL_LD_LIBRARY_PATH > RUNPATH > LD_LIBRARY_PATH."""
wheel = BUNDLED_WHEELS / "testzlib-0.0.1-cp310-cp310-linux_x86_64.whl"
so = tmp_path / "testzlib.cpython-310-x86_64-linux-gnu.so"
zip2dir(wheel, tmp_path)

# Prepare directories with fake libz.so.1 (use valid ELF bytes from testzlib so)
valid_elf_bytes = so.read_bytes()

dir_auditwheel = tmp_path / "dir_auditwheel"
dir_auditwheel.mkdir()
(dir_auditwheel / "libz.so.1").write_bytes(valid_elf_bytes)

dir_runpath = tmp_path / "dir_runpath"
dir_runpath.mkdir()
(dir_runpath / "libz.so.1").write_bytes(valid_elf_bytes)

dir_ld_lib = tmp_path / "dir_ld_lib"
dir_ld_lib.mkdir()
(dir_ld_lib / "libz.so.1").write_bytes(valid_elf_bytes)

orig_iter_segments = lddtree.ELFFile.iter_segments

def mocked_iter_segments(self):
for seg in orig_iter_segments(self):
if seg.header.p_type == "PT_DYNAMIC":
orig_iter_tags = getattr(seg, "iter_tags") # noqa: B009

def mocked_iter_tags(tags_fn=orig_iter_tags):
yield from tags_fn()
entry = type("Entry", (), {"d_tag": "DT_RUNPATH"})()
yield type(
"MockTag",
(),
{"entry": entry, "runpath": str(dir_runpath)},
)()

seg.iter_tags = mocked_iter_tags # type: ignore[attr-defined]
yield seg

monkeypatch.setattr(lddtree.ELFFile, "iter_segments", mocked_iter_segments)

# 1. RUNPATH vs normal LD_LIBRARY_PATH (PR #4 historical behavior: RUNPATH must win)
lddtree.load_ld_paths.cache_clear()
monkeypatch.delenv("AUDITWHEEL_LD_LIBRARY_PATH", raising=False)
monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(dir_ld_lib))
res1 = ldd(so)
assert res1.libraries["libz.so.1"].path == str(dir_runpath / "libz.so.1")

# 2. RUNPATH vs AUDITWHEEL_LD_LIBRARY_PATH (Issue #737: AUDITWHEEL must win)
lddtree.load_ld_paths.cache_clear()
monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(dir_auditwheel))
monkeypatch.delenv("LD_LIBRARY_PATH", raising=False)
res2 = ldd(so)
assert res2.libraries["libz.so.1"].path == str(dir_auditwheel / "libz.so.1")

# 3. Triple precedence: AUDITWHEEL_LD_LIBRARY_PATH > RUNPATH > LD_LIBRARY_PATH
lddtree.load_ld_paths.cache_clear()
monkeypatch.setitem(os.environ, "AUDITWHEEL_LD_LIBRARY_PATH", str(dir_auditwheel))
monkeypatch.setitem(os.environ, "LD_LIBRARY_PATH", str(dir_ld_lib))
res3 = ldd(so)
assert res3.libraries["libz.so.1"].path == str(dir_auditwheel / "libz.so.1")

# 4. Backward compatibility: custom ldpaths dict without 'auditwheel' key
# Ensure no KeyError is raised and original search behavior is preserved
custom_ldpaths = {
"rpath": [],
"runpath": [],
"conf": [str(dir_runpath)],
"env": [str(dir_ld_lib)],
"interp": [],
}
res4 = ldd(so, ldpaths=custom_ldpaths)
assert res4.libraries["libz.so.1"].path == str(dir_runpath / "libz.so.1")