Skip to content

Commit c9f9349

Browse files
committed
Relocate binaries only when RELENV_BUILDENV is set.
This change ensures that binary relocation (using readelf/patchelf or otool/install_name_tool) is only attempted during pip installs when the RELENV_BUILDENV environment variable is set, preventing unnecessary processing for standard installs. It also introduces logic to prefer the relenv toolchain's `readelf` and `patchelf` binaries on Linux when available, ensuring compatibility with the specific glibc version of the environment. Tests were added to verify: - Relocation logic is skipped when RELENV_BUILDENV is missing. - Relocation logic runs when RELENV_BUILDENV is set. - Toolchain binaries are correctly selected over system defaults on Linux.
1 parent bdd4336 commit c9f9349

File tree

5 files changed

+340
-22
lines changed

5 files changed

+340
-22
lines changed

relenv/relocate.py

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
import pathlib
1212
import shutil as _shutil
1313
import subprocess as _subprocess
14+
import sys as _sys
1415
from typing import Optional
1516

1617
log = logging.getLogger(__name__)
1718

1819
os = _os
1920
shutil = _shutil
2021
subprocess = _subprocess
22+
sys = _sys
2123

2224
__all__ = [
2325
"is_macho",
@@ -70,6 +72,83 @@
7072
LC_LOAD_DYLIB = "LC_LOAD_DYLIB"
7173
LC_RPATH = "LC_RPATH"
7274

75+
# Cache for readelf binary path
76+
_READELF_BINARY: Optional[str] = None
77+
78+
# Cache for patchelf binary path
79+
_PATCHELF_BINARY: Optional[str] = None
80+
81+
82+
def _get_readelf_binary() -> str:
83+
"""
84+
Get the path to readelf binary, preferring toolchain version.
85+
86+
Returns the cached value if already computed. On Linux, prefers the
87+
toolchain's readelf over the system version. Falls back to "readelf"
88+
from PATH if toolchain is unavailable.
89+
90+
:return: Path to readelf binary
91+
:rtype: str
92+
"""
93+
global _READELF_BINARY
94+
if _READELF_BINARY is not None:
95+
return _READELF_BINARY
96+
97+
# Only Linux has the toolchain with readelf
98+
if sys.platform == "linux":
99+
try:
100+
from relenv.common import get_toolchain, get_triplet
101+
102+
toolchain = get_toolchain()
103+
if toolchain:
104+
triplet = get_triplet()
105+
toolchain_readelf = toolchain / "bin" / f"{triplet}-readelf"
106+
if toolchain_readelf.exists():
107+
_READELF_BINARY = str(toolchain_readelf)
108+
return _READELF_BINARY
109+
except Exception:
110+
# Fall through to system readelf
111+
pass
112+
113+
# Fall back to system readelf
114+
_READELF_BINARY = "readelf"
115+
return _READELF_BINARY
116+
117+
118+
def _get_patchelf_binary() -> str:
119+
"""
120+
Get the path to patchelf binary, preferring toolchain version.
121+
122+
Returns the cached value if already computed. On Linux, prefers the
123+
toolchain's patchelf over the system version. Falls back to "patchelf"
124+
from PATH if toolchain is unavailable.
125+
126+
:return: Path to patchelf binary
127+
:rtype: str
128+
"""
129+
global _PATCHELF_BINARY
130+
if _PATCHELF_BINARY is not None:
131+
return _PATCHELF_BINARY
132+
133+
# Only Linux has the toolchain with patchelf
134+
if sys.platform == "linux":
135+
try:
136+
from relenv.common import get_toolchain
137+
138+
toolchain = get_toolchain()
139+
if toolchain:
140+
toolchain_patchelf = toolchain / "bin" / "patchelf"
141+
if toolchain_patchelf.exists():
142+
_PATCHELF_BINARY = str(toolchain_patchelf)
143+
return _PATCHELF_BINARY
144+
except Exception:
145+
# Fall through to system patchelf
146+
pass
147+
148+
# Fall back to system patchelf
149+
_PATCHELF_BINARY = "patchelf"
150+
return _PATCHELF_BINARY
151+
73152

74153
def is_macho(path: str | os.PathLike[str]) -> bool:
75154
"""
@@ -192,8 +271,9 @@ def parse_rpath(path: str | os.PathLike[str]) -> list[str]:
192271
:return: The RPATH's found.
193272
:rtype: list
194273
"""
274+
readelf = _get_readelf_binary()
195275
proc = subprocess.run(
196-
["readelf", "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
276+
[readelf, "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
197277
)
198278
return parse_readelf_d(proc.stdout.decode())
199279

@@ -280,8 +360,9 @@ def remove_rpath(path: str | os.PathLike[str]) -> bool:
280360
return True
281361

282362
log.info("Remove RPATH from %s (was: %s)", path, old_rpath)
363+
patchelf = _get_patchelf_binary()
283364
proc = subprocess.run(
284-
["patchelf", "--remove-rpath", path],
365+
[patchelf, "--remove-rpath", path],
285366
stderr=subprocess.PIPE,
286367
stdout=subprocess.PIPE,
287368
)
@@ -319,8 +400,9 @@ def patch_rpath(
319400
if new_rpath not in old_rpath:
320401
patched_rpath = ":".join([new_rpath] + old_rpath)
321402
log.info("Set RPATH=%s %s", patched_rpath, path)
403+
patchelf = _get_patchelf_binary()
322404
proc = subprocess.run(
323-
["patchelf", "--force-rpath", "--set-rpath", patched_rpath, path],
405+
[patchelf, "--force-rpath", "--set-rpath", patched_rpath, path],
324406
stderr=subprocess.PIPE,
325407
stdout=subprocess.PIPE,
326408
)

relenv/runtime.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -384,25 +384,30 @@ def wrapper(
384384
direct_url,
385385
requested,
386386
)
387-
plat = pathlib.Path(scheme.platlib)
388-
rootdir = relenv_root()
389-
with open(plat / info_dir / "RECORD") as fp:
390-
for line in fp.readlines():
391-
file = plat / line.split(",", 1)[0]
392-
if not file.exists():
393-
debug(f"Relenv - File not found {file}")
394-
continue
395-
if relocate().is_elf(file):
396-
debug(f"Relenv - Found elf {file}")
397-
relocate().handle_elf(plat / file, rootdir / "lib", True, rootdir)
398-
elif relocate().is_macho(file):
399-
otool_bin = shutil.which("otool")
400-
if otool_bin:
401-
relocate().handle_macho(str(plat / file), str(rootdir), True)
402-
else:
403-
debug(
404-
"The otool command is not available, please run `xcode-select --install`"
387+
if "RELENV_BUILDENV" in os.environ:
388+
plat = pathlib.Path(scheme.platlib)
389+
rootdir = relenv_root()
390+
with open(plat / info_dir / "RECORD") as fp:
391+
for line in fp.readlines():
392+
file = plat / line.split(",", 1)[0]
393+
if not file.exists():
394+
debug(f"Relenv - File not found {file}")
395+
continue
396+
if relocate().is_elf(file):
397+
debug(f"Relenv - Found elf {file}")
398+
relocate().handle_elf(
399+
plat / file, rootdir / "lib", True, rootdir
405400
)
401+
elif relocate().is_macho(file):
402+
otool_bin = shutil.which("otool")
403+
if otool_bin:
404+
relocate().handle_macho(
405+
str(plat / file), str(rootdir), True
406+
)
407+
else:
408+
debug(
409+
"The otool command is not available, please run `xcode-select --install`"
410+
)
406411

407412
return wrapper
408413

@@ -1022,7 +1027,11 @@ def install_cargo_config() -> None:
10221027
cargo_home = dirs.data / "cargo"
10231028
triplet = common().get_triplet()
10241029

1025-
toolchain = common().get_toolchain()
1030+
try:
1031+
toolchain = common().get_toolchain()
1032+
except PermissionError:
1033+
pass
1034+
10261035
if not toolchain:
10271036
debug("Unable to set CARGO_HOME ppbt package not installed")
10281037
return

tests/test_relocate_tools.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright 2022-2026 Broadcom.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import pathlib
5+
from typing import Iterator
6+
from unittest.mock import patch
7+
8+
import pytest
9+
10+
from relenv import relocate
11+
12+
13+
@pytest.fixture(autouse=True) # type: ignore[misc]
14+
def reset_globals() -> Iterator[None]:
15+
"""Reset global caches in relocate module before and after each test."""
16+
relocate._READELF_BINARY = None
17+
relocate._PATCHELF_BINARY = None
18+
yield
19+
relocate._READELF_BINARY = None
20+
relocate._PATCHELF_BINARY = None
21+
22+
23+
def test_get_readelf_binary_toolchain_exists(tmp_path: pathlib.Path) -> None:
24+
"""Test that toolchain readelf is used when available."""
25+
toolchain_root = tmp_path / "toolchain"
26+
toolchain_root.mkdir()
27+
triplet = "x86_64-linux-gnu"
28+
29+
# Create the fake toolchain binary
30+
bin_dir = toolchain_root / "bin"
31+
bin_dir.mkdir(parents=True)
32+
toolchain_readelf = bin_dir / f"{triplet}-readelf"
33+
toolchain_readelf.touch()
34+
35+
with patch("relenv.relocate.sys.platform", "linux"):
36+
# We need to mock relenv.common.get_toolchain and get_triplet
37+
# Since they are imported inside the function, we can patch the module if it's already imported
38+
# or use patch.dict(sys.modules)
39+
40+
# Ensure relenv.common is imported so we can patch it
41+
import relenv.common # noqa: F401
42+
43+
with patch("relenv.common.get_toolchain", return_value=toolchain_root):
44+
with patch("relenv.common.get_triplet", return_value=triplet):
45+
readelf = relocate._get_readelf_binary()
46+
47+
assert readelf == str(toolchain_readelf)
48+
assert relocate._READELF_BINARY == str(toolchain_readelf)
49+
50+
51+
def test_get_readelf_binary_toolchain_missing(tmp_path: pathlib.Path) -> None:
52+
"""Test that system readelf is used when toolchain binary is missing."""
53+
toolchain_root = tmp_path / "toolchain"
54+
toolchain_root.mkdir()
55+
triplet = "x86_64-linux-gnu"
56+
57+
# Do NOT create the binary
58+
59+
with patch("relenv.relocate.sys.platform", "linux"):
60+
# Ensure relenv.common is imported so we can patch it
61+
import relenv.common # noqa: F401
62+
63+
with patch("relenv.common.get_toolchain", return_value=toolchain_root):
64+
with patch("relenv.common.get_triplet", return_value=triplet):
65+
readelf = relocate._get_readelf_binary()
66+
67+
assert readelf == "readelf"
68+
assert relocate._READELF_BINARY == "readelf"
69+
70+
71+
def test_get_readelf_binary_no_toolchain() -> None:
72+
"""Test that system readelf is used when get_toolchain returns None."""
73+
with patch("relenv.relocate.sys.platform", "linux"):
74+
# Ensure relenv.common is imported so we can patch it
75+
import relenv.common # noqa: F401
76+
77+
with patch("relenv.common.get_toolchain", return_value=None):
78+
readelf = relocate._get_readelf_binary()
79+
80+
assert readelf == "readelf"
81+
assert relocate._READELF_BINARY == "readelf"
82+
83+
84+
def test_get_readelf_binary_not_linux() -> None:
85+
"""Test that system readelf is used on non-Linux platforms."""
86+
with patch("relenv.relocate.sys.platform", "darwin"):
87+
readelf = relocate._get_readelf_binary()
88+
89+
assert readelf == "readelf"
90+
assert relocate._READELF_BINARY == "readelf"
91+
92+
93+
def test_get_patchelf_binary_toolchain_exists(tmp_path: pathlib.Path) -> None:
94+
"""Test that toolchain patchelf is used when available."""
95+
toolchain_root = tmp_path / "toolchain"
96+
toolchain_root.mkdir()
97+
98+
# Create the fake toolchain binary
99+
bin_dir = toolchain_root / "bin"
100+
bin_dir.mkdir(parents=True)
101+
toolchain_patchelf = bin_dir / "patchelf"
102+
toolchain_patchelf.touch()
103+
104+
with patch("relenv.relocate.sys.platform", "linux"):
105+
# Ensure relenv.common is imported so we can patch it
106+
import relenv.common # noqa: F401
107+
108+
with patch("relenv.common.get_toolchain", return_value=toolchain_root):
109+
patchelf = relocate._get_patchelf_binary()
110+
111+
assert patchelf == str(toolchain_patchelf)
112+
assert relocate._PATCHELF_BINARY == str(toolchain_patchelf)
113+
114+
115+
def test_get_patchelf_binary_toolchain_missing(tmp_path: pathlib.Path) -> None:
116+
"""Test that system patchelf is used when toolchain binary is missing."""
117+
toolchain_root = tmp_path / "toolchain"
118+
toolchain_root.mkdir()
119+
120+
# Do NOT create the binary
121+
122+
with patch("relenv.relocate.sys.platform", "linux"):
123+
# Ensure relenv.common is imported so we can patch it
124+
import relenv.common # noqa: F401
125+
126+
with patch("relenv.common.get_toolchain", return_value=toolchain_root):
127+
patchelf = relocate._get_patchelf_binary()
128+
129+
assert patchelf == "patchelf"
130+
assert relocate._PATCHELF_BINARY == "patchelf"
131+
132+
133+
def test_get_patchelf_binary_no_toolchain() -> None:
134+
"""Test that system patchelf is used when get_toolchain returns None."""
135+
with patch("relenv.relocate.sys.platform", "linux"):
136+
# Ensure relenv.common is imported so we can patch it
137+
import relenv.common # noqa: F401
138+
139+
with patch("relenv.common.get_toolchain", return_value=None):
140+
patchelf = relocate._get_patchelf_binary()
141+
142+
assert patchelf == "patchelf"
143+
assert relocate._PATCHELF_BINARY == "patchelf"
144+
145+
146+
def test_get_patchelf_binary_not_linux() -> None:
147+
"""Test that system patchelf is used on non-Linux platforms."""
148+
with patch("relenv.relocate.sys.platform", "darwin"):
149+
patchelf = relocate._get_patchelf_binary()
150+
151+
assert patchelf == "patchelf"
152+
assert relocate._PATCHELF_BINARY == "patchelf"

0 commit comments

Comments
 (0)