Skip to content
Merged
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
88 changes: 85 additions & 3 deletions relenv/relocate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
import pathlib
import shutil as _shutil
import subprocess as _subprocess
import sys as _sys
from typing import Optional

log = logging.getLogger(__name__)

os = _os
shutil = _shutil
subprocess = _subprocess
sys = _sys

__all__ = [
"is_macho",
Expand Down Expand Up @@ -70,6 +72,83 @@
LC_LOAD_DYLIB = "LC_LOAD_DYLIB"
LC_RPATH = "LC_RPATH"

# Cache for readelf binary path
_READELF_BINARY: Optional[str] = None

# Cache for patchelf binary path
_PATCHELF_BINARY: Optional[str] = None


def _get_readelf_binary() -> str:
"""
Get the path to readelf binary, preferring toolchain version.

Returns the cached value if already computed. On Linux, prefers the
toolchain's readelf over the system version. Falls back to "readelf"
from PATH if toolchain is unavailable.

:return: Path to readelf binary
:rtype: str
"""
global _READELF_BINARY
if _READELF_BINARY is not None:
return _READELF_BINARY

# Only Linux has the toolchain with readelf
if sys.platform == "linux":
try:
from relenv.common import get_toolchain, get_triplet

toolchain = get_toolchain()
if toolchain:
triplet = get_triplet()
toolchain_readelf = toolchain / "bin" / f"{triplet}-readelf"
if toolchain_readelf.exists():
_READELF_BINARY = str(toolchain_readelf)
return _READELF_BINARY
except Exception:
# Fall through to system readelf
pass

# Fall back to system readelf
_READELF_BINARY = "readelf"
return _READELF_BINARY


def _get_patchelf_binary() -> str:
"""
Get the path to patchelf binary, preferring toolchain version.

Returns the cached value if already computed. On Linux, prefers the
toolchain's patchelf over the system version. Falls back to "patchelf"
from PATH if toolchain is unavailable.

:return: Path to patchelf binary
:rtype: str
"""
global _PATCHELF_BINARY
if _PATCHELF_BINARY is not None:
return _PATCHELF_BINARY

# Only Linux has the toolchain with patchelf
if sys.platform == "linux":
try:
from relenv.common import get_toolchain

toolchain = get_toolchain()
if toolchain:
toolchain_patchelf = toolchain / "bin" / "patchelf"
if toolchain_patchelf.exists():
_PATCHELF_BINARY = str(toolchain_patchelf)
return _PATCHELF_BINARY
except Exception:
# Fall through to system patchelf
pass

# Fall back to system patchelf
_PATCHELF_BINARY = "patchelf"
return _PATCHELF_BINARY


def is_macho(path: str | os.PathLike[str]) -> bool:
"""
Expand Down Expand Up @@ -192,8 +271,9 @@ def parse_rpath(path: str | os.PathLike[str]) -> list[str]:
:return: The RPATH's found.
:rtype: list
"""
readelf = _get_readelf_binary()
proc = subprocess.run(
["readelf", "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
[readelf, "-d", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
return parse_readelf_d(proc.stdout.decode())

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

log.info("Remove RPATH from %s (was: %s)", path, old_rpath)
patchelf = _get_patchelf_binary()
proc = subprocess.run(
["patchelf", "--remove-rpath", path],
[patchelf, "--remove-rpath", path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
Expand Down Expand Up @@ -319,8 +400,9 @@ def patch_rpath(
if new_rpath not in old_rpath:
patched_rpath = ":".join([new_rpath] + old_rpath)
log.info("Set RPATH=%s %s", patched_rpath, path)
patchelf = _get_patchelf_binary()
proc = subprocess.run(
["patchelf", "--force-rpath", "--set-rpath", patched_rpath, path],
[patchelf, "--force-rpath", "--set-rpath", patched_rpath, path],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
Expand Down
47 changes: 28 additions & 19 deletions relenv/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,25 +384,30 @@ def wrapper(
direct_url,
requested,
)
plat = pathlib.Path(scheme.platlib)
rootdir = relenv_root()
with open(plat / info_dir / "RECORD") as fp:
for line in fp.readlines():
file = plat / line.split(",", 1)[0]
if not file.exists():
debug(f"Relenv - File not found {file}")
continue
if relocate().is_elf(file):
debug(f"Relenv - Found elf {file}")
relocate().handle_elf(plat / file, rootdir / "lib", True, rootdir)
elif relocate().is_macho(file):
otool_bin = shutil.which("otool")
if otool_bin:
relocate().handle_macho(str(plat / file), str(rootdir), True)
else:
debug(
"The otool command is not available, please run `xcode-select --install`"
if "RELENV_BUILDENV" in os.environ:
plat = pathlib.Path(scheme.platlib)
rootdir = relenv_root()
with open(plat / info_dir / "RECORD") as fp:
for line in fp.readlines():
file = plat / line.split(",", 1)[0]
if not file.exists():
debug(f"Relenv - File not found {file}")
continue
if relocate().is_elf(file):
debug(f"Relenv - Found elf {file}")
relocate().handle_elf(
plat / file, rootdir / "lib", True, rootdir
)
elif relocate().is_macho(file):
otool_bin = shutil.which("otool")
if otool_bin:
relocate().handle_macho(
str(plat / file), str(rootdir), True
)
else:
debug(
"The otool command is not available, please run `xcode-select --install`"
)

return wrapper

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

toolchain = common().get_toolchain()
try:
toolchain = common().get_toolchain()
except PermissionError:
pass

if not toolchain:
debug("Unable to set CARGO_HOME ppbt package not installed")
return
Expand Down
152 changes: 152 additions & 0 deletions tests/test_relocate_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Copyright 2022-2026 Broadcom.
# SPDX-License-Identifier: Apache-2.0

import pathlib
from typing import Iterator
from unittest.mock import patch

import pytest

from relenv import relocate


@pytest.fixture(autouse=True) # type: ignore[misc]
def reset_globals() -> Iterator[None]:
"""Reset global caches in relocate module before and after each test."""
relocate._READELF_BINARY = None
relocate._PATCHELF_BINARY = None
yield
relocate._READELF_BINARY = None
relocate._PATCHELF_BINARY = None


def test_get_readelf_binary_toolchain_exists(tmp_path: pathlib.Path) -> None:
"""Test that toolchain readelf is used when available."""
toolchain_root = tmp_path / "toolchain"
toolchain_root.mkdir()
triplet = "x86_64-linux-gnu"

# Create the fake toolchain binary
bin_dir = toolchain_root / "bin"
bin_dir.mkdir(parents=True)
toolchain_readelf = bin_dir / f"{triplet}-readelf"
toolchain_readelf.touch()

with patch("relenv.relocate.sys.platform", "linux"):
# We need to mock relenv.common.get_toolchain and get_triplet
# Since they are imported inside the function, we can patch the module if it's already imported
# or use patch.dict(sys.modules)

# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=toolchain_root):
with patch("relenv.common.get_triplet", return_value=triplet):
readelf = relocate._get_readelf_binary()

assert readelf == str(toolchain_readelf)
assert relocate._READELF_BINARY == str(toolchain_readelf)


def test_get_readelf_binary_toolchain_missing(tmp_path: pathlib.Path) -> None:
"""Test that system readelf is used when toolchain binary is missing."""
toolchain_root = tmp_path / "toolchain"
toolchain_root.mkdir()
triplet = "x86_64-linux-gnu"

# Do NOT create the binary

with patch("relenv.relocate.sys.platform", "linux"):
# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=toolchain_root):
with patch("relenv.common.get_triplet", return_value=triplet):
readelf = relocate._get_readelf_binary()

assert readelf == "readelf"
assert relocate._READELF_BINARY == "readelf"


def test_get_readelf_binary_no_toolchain() -> None:
"""Test that system readelf is used when get_toolchain returns None."""
with patch("relenv.relocate.sys.platform", "linux"):
# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=None):
readelf = relocate._get_readelf_binary()

assert readelf == "readelf"
assert relocate._READELF_BINARY == "readelf"


def test_get_readelf_binary_not_linux() -> None:
"""Test that system readelf is used on non-Linux platforms."""
with patch("relenv.relocate.sys.platform", "darwin"):
readelf = relocate._get_readelf_binary()

assert readelf == "readelf"
assert relocate._READELF_BINARY == "readelf"


def test_get_patchelf_binary_toolchain_exists(tmp_path: pathlib.Path) -> None:
"""Test that toolchain patchelf is used when available."""
toolchain_root = tmp_path / "toolchain"
toolchain_root.mkdir()

# Create the fake toolchain binary
bin_dir = toolchain_root / "bin"
bin_dir.mkdir(parents=True)
toolchain_patchelf = bin_dir / "patchelf"
toolchain_patchelf.touch()

with patch("relenv.relocate.sys.platform", "linux"):
# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=toolchain_root):
patchelf = relocate._get_patchelf_binary()

assert patchelf == str(toolchain_patchelf)
assert relocate._PATCHELF_BINARY == str(toolchain_patchelf)


def test_get_patchelf_binary_toolchain_missing(tmp_path: pathlib.Path) -> None:
"""Test that system patchelf is used when toolchain binary is missing."""
toolchain_root = tmp_path / "toolchain"
toolchain_root.mkdir()

# Do NOT create the binary

with patch("relenv.relocate.sys.platform", "linux"):
# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=toolchain_root):
patchelf = relocate._get_patchelf_binary()

assert patchelf == "patchelf"
assert relocate._PATCHELF_BINARY == "patchelf"


def test_get_patchelf_binary_no_toolchain() -> None:
"""Test that system patchelf is used when get_toolchain returns None."""
with patch("relenv.relocate.sys.platform", "linux"):
# Ensure relenv.common is imported so we can patch it
import relenv.common # noqa: F401

with patch("relenv.common.get_toolchain", return_value=None):
patchelf = relocate._get_patchelf_binary()

assert patchelf == "patchelf"
assert relocate._PATCHELF_BINARY == "patchelf"


def test_get_patchelf_binary_not_linux() -> None:
"""Test that system patchelf is used on non-Linux platforms."""
with patch("relenv.relocate.sys.platform", "darwin"):
patchelf = relocate._get_patchelf_binary()

assert patchelf == "patchelf"
assert relocate._PATCHELF_BINARY == "patchelf"
Loading
Loading