auto-patchelf: add --preserve-origin feature, add test (#471666)
This commit is contained in:
@@ -204,6 +204,36 @@ def find_dependency(soname: str, soarch: str, soabi: str) -> Optional[Path]:
|
||||
return None
|
||||
|
||||
|
||||
def find_first_matching_rpath_with_origin(binary: Path, lib_dir: Path, rpaths: list[str]) -> Optional[Path]:
|
||||
"""
|
||||
If lib_dir equals one of the existing $ORIGIN-based RPATH entries
|
||||
(after resolving $ORIGIN relative to this binary), return that $ORIGIN
|
||||
entry to preserve it; otherwise return None.
|
||||
|
||||
Note: we return a Path constructed from the RPATH string (which may
|
||||
contain $ORIGIN). This is intentional so that the caller can treat it
|
||||
uniformly with other Path entries when building the final RPATH string
|
||||
(via Path.as_posix). We do not expect this Path to exist on the
|
||||
filesystem; it functions as a display value.
|
||||
"""
|
||||
try:
|
||||
# resolve(strict=False) does not require the path to exist, but may still
|
||||
# raise RuntimeError (cycles) or OSError in odd environments. Guard those.
|
||||
lib_dir_realpath = lib_dir.resolve()
|
||||
except (OSError, RuntimeError):
|
||||
return None
|
||||
for rpath in rpaths:
|
||||
if "$ORIGIN" not in rpath:
|
||||
continue
|
||||
try:
|
||||
candidate_rpath_realpath = (binary.parent / rpath.replace("$ORIGIN", ".")).resolve()
|
||||
except (OSError, RuntimeError):
|
||||
continue
|
||||
if candidate_rpath_realpath == lib_dir_realpath:
|
||||
return Path(rpath)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Dependency:
|
||||
file: Path # The file that contains the dependency
|
||||
@@ -211,7 +241,7 @@ class Dependency:
|
||||
found: bool = False # Whether it was found somewhere
|
||||
|
||||
|
||||
def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list[Path] = [], keep_libc: bool = False, extra_args: list[str] = []) -> list[Dependency]:
|
||||
def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list[Path] = [], keep_libc: bool = False, preserve_origin: bool = False, extra_args: list[str] = []) -> list[Dependency]:
|
||||
try:
|
||||
with open_elf(path) as elf:
|
||||
|
||||
@@ -242,6 +272,7 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
|
||||
file_is_dynamic_executable = is_dynamic_executable(elf)
|
||||
|
||||
file_dependencies = get_dependencies(elf) + get_dlopen_dependencies(elf)
|
||||
existing_rpaths = get_rpath(elf)
|
||||
|
||||
except ELFError:
|
||||
return []
|
||||
@@ -279,7 +310,10 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
|
||||
# and resolved automatically by the dynamic linker, unless
|
||||
# keep_libc is enabled.
|
||||
# 3. If a candidate is found in our library dependencies, that
|
||||
# dependency should be added to rpath.
|
||||
# dependency should be added to rpath. If --preserve-origin
|
||||
# is set and this file's existing RUNPATH/RPATH contains a $ORIGIN
|
||||
# entry that resolves to the same directory, we add that $ORIGIN
|
||||
# entry instead of an absolute path.
|
||||
# 4. If all of the above fail, libc dependencies should still be
|
||||
# considered found. This is in contrast to step 2, because
|
||||
# enabling keep_libc should allow libc to be found in step 3
|
||||
@@ -299,7 +333,8 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
|
||||
was_found = True
|
||||
break
|
||||
elif found_dependency := find_dependency(candidate.name, file_arch, file_osabi):
|
||||
rpath.append(found_dependency)
|
||||
origin_rpath_entry = find_first_matching_rpath_with_origin(path, found_dependency, existing_rpaths) if preserve_origin else None
|
||||
rpath.append(origin_rpath_entry or found_dependency)
|
||||
dependencies.append(Dependency(path, candidate, found=True))
|
||||
print(f" {candidate} -> found: {found_dependency}")
|
||||
was_found = True
|
||||
@@ -315,6 +350,12 @@ def auto_patchelf_file(path: Path, runtime_deps: list[Path], append_rpaths: list
|
||||
|
||||
rpath.extend(append_rpaths)
|
||||
|
||||
# Keep any existing $ORIGIN entries that didn't match a discovered dependency (i.e. dlopen-only lookups)
|
||||
if preserve_origin:
|
||||
for existing_rpath in existing_rpaths:
|
||||
if "$ORIGIN" in existing_rpath:
|
||||
rpath.append(Path(existing_rpath))
|
||||
|
||||
# Dedup the rpath
|
||||
rpath_str = ":".join(dict.fromkeys(map(Path.as_posix, rpath)))
|
||||
|
||||
@@ -335,6 +376,7 @@ def auto_patchelf(
|
||||
ignore_missing: list[str] = [],
|
||||
append_rpaths: list[Path] = [],
|
||||
keep_libc: bool = False,
|
||||
preserve_origin: bool = False,
|
||||
add_existing: bool = True,
|
||||
extra_args: list[str] = []) -> None:
|
||||
|
||||
@@ -351,7 +393,7 @@ def auto_patchelf(
|
||||
dependencies = []
|
||||
for path in chain.from_iterable(glob(p, '*', recursive) for p in paths_to_patch):
|
||||
if not path.is_symlink() and path.is_file():
|
||||
dependencies += auto_patchelf_file(path, runtime_deps, append_rpaths, keep_libc, extra_args)
|
||||
dependencies += auto_patchelf_file(path, runtime_deps, append_rpaths, keep_libc, preserve_origin, extra_args)
|
||||
|
||||
missing = [dep for dep in dependencies if not dep.found]
|
||||
|
||||
@@ -431,6 +473,12 @@ def main() -> None:
|
||||
action="store_true",
|
||||
help="Attempt to search for and relink libc dependencies.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--preserve-origin",
|
||||
dest="preserve_origin",
|
||||
action="store_true",
|
||||
help="When possible, replace absolute RPATH entries with original $ORIGIN entries that resolve to the same directory.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ignore-existing",
|
||||
dest="add_existing",
|
||||
@@ -460,6 +508,7 @@ def main() -> None:
|
||||
args.ignore_missing,
|
||||
append_rpaths=args.append_rpaths,
|
||||
keep_libc=args.keep_libc,
|
||||
preserve_origin=args.preserve_origin,
|
||||
add_existing=args.add_existing,
|
||||
extra_args=args.extra_args)
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
tests,
|
||||
autoPatchelfHook,
|
||||
patchelf,
|
||||
}:
|
||||
|
||||
let
|
||||
foo = tests.stdenv-inputs.foo;
|
||||
bar = tests.stdenv-inputs.bar;
|
||||
|
||||
# similar to lib-check in stdenv-inputs, but we ship
|
||||
# binary and libraries in the same output, and only
|
||||
# $ORIGIN/../lib in RUNPATH.
|
||||
lib-check = stdenv.mkDerivation {
|
||||
name = "lib-check-bundle";
|
||||
|
||||
buildInputs = [
|
||||
foo
|
||||
bar
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
patchelf
|
||||
];
|
||||
|
||||
buildCommand = ''
|
||||
$CC -lfoo -lbar -Wl,-rpath,'$ORIGIN/../lib' -o lib-check ${./lib-main.c}
|
||||
|
||||
# Shrink RUNPATH to only keep the $ORIGIN/../lib one,
|
||||
# dropping references to foo and bar store path.
|
||||
patchelf --shrink-rpath --allowed-rpath-prefixes '$ORIGIN/../lib' lib-check
|
||||
|
||||
|
||||
mkdir -p $out/lib $out/bin
|
||||
cp ${lib.getDev foo}/lib/* ${lib.getDev bar}/lib/* $out/lib/
|
||||
cp lib-check $out/bin/
|
||||
|
||||
# Make sure the binary still works
|
||||
$out/bin/lib-check
|
||||
'';
|
||||
|
||||
disallowedReferences = [
|
||||
(lib.getDev foo)
|
||||
(lib.getDev bar)
|
||||
];
|
||||
};
|
||||
# We treat `lib-check` as binaries and libraries coming from somewhere,
|
||||
# and run `autoPatchelfHook` on them, but setting `--preserve-origin`.
|
||||
# If we wouldn't,`autoPatchelfHook` would replace `RUNPATH` with a
|
||||
# (self-)reference.
|
||||
lib-check-autopatchelfed = stdenv.mkDerivation {
|
||||
name = "lib-check-autopatchelfed";
|
||||
|
||||
nativeBuildInputs = [
|
||||
autoPatchelfHook
|
||||
];
|
||||
|
||||
autoPatchelfFlags = [ "--preserve-origin" ];
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
# we don't set buildCommand because we want to ensure fixupPhase
|
||||
# (containing autoPatchelfHook) is run.
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -R ${lib-check}/* $out
|
||||
'';
|
||||
|
||||
# Should not refer to our source nor to itself.
|
||||
disallowedReferences = [
|
||||
lib-check
|
||||
"out"
|
||||
];
|
||||
};
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
name = "auto-patchelf-hook-preserve-origin";
|
||||
|
||||
buildCommand = ''
|
||||
# Ensure the binary still works
|
||||
${lib-check-autopatchelfed}/bin/lib-check
|
||||
touch $out
|
||||
'';
|
||||
|
||||
meta.platforms = lib.platforms.all;
|
||||
passthru = {
|
||||
inherit lib-check lib-check-autopatchelfed;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#include <stdio.h>
|
||||
|
||||
extern unsigned int foo(void);
|
||||
extern unsigned int bar(void);
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (foo() != 42)
|
||||
return 1;
|
||||
if (bar() != 42)
|
||||
return 1;
|
||||
fprintf(stderr, "ok\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -225,6 +225,8 @@ in
|
||||
|
||||
auto-patchelf-hook = callPackage ./auto-patchelf-hook { };
|
||||
|
||||
auto-patchelf-hook-preserve-origin = callPackage ./auto-patchelf-hook-preserve-origin { };
|
||||
|
||||
# Accumulate all passthru.tests from arrayUtilities into a single attribute set.
|
||||
arrayUtilities = recurseIntoAttrs (
|
||||
concatMapAttrs (
|
||||
|
||||
@@ -70,4 +70,7 @@ stdenv.mkDerivation {
|
||||
'';
|
||||
|
||||
meta.platforms = lib.platforms.all;
|
||||
passthru = {
|
||||
inherit foo bar;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user