diff options
Diffstat (limited to 'pkgs/build-support/setup-hooks/auto-patchelf.sh')
-rw-r--r-- | pkgs/build-support/setup-hooks/auto-patchelf.sh | 331 |
1 files changed, 17 insertions, 314 deletions
diff --git a/pkgs/build-support/setup-hooks/auto-patchelf.sh b/pkgs/build-support/setup-hooks/auto-patchelf.sh index 4b3a1c5c390..9822674196a 100644 --- a/pkgs/build-support/setup-hooks/auto-patchelf.sh +++ b/pkgs/build-support/setup-hooks/auto-patchelf.sh @@ -1,289 +1,21 @@ #!/usr/bin/env bash declare -a autoPatchelfLibs -declare -Ag autoPatchelfFailedDeps +declare -a extraAutoPatchelfLibs gatherLibraries() { autoPatchelfLibs+=("$1/lib") } -# wrapper around patchelf to raise proper error messages -# containing the tried file name and command -runPatchelf() { - patchelf "$@" || (echo "Command failed: patchelf $*" && exit 1) -} - # shellcheck disable=SC2154 # (targetOffset is referenced but not assigned.) addEnvHooks "$targetOffset" gatherLibraries -isExecutable() { - # For dynamically linked ELF files it would be enough to check just for the - # INTERP section. However, we won't catch statically linked executables as - # they only have an ELF type of EXEC but no INTERP. - # - # So what we do here is just check whether *either* the ELF type is EXEC - # *or* there is an INTERP section. This also catches position-independent - # executables, as they typically have an INTERP section but their ELF type - # is DYN. - isExeResult="$(LANG=C $READELF -h -l "$1" 2> /dev/null \ - | grep '^ *Type: *EXEC\>\|^ *INTERP\>')" - # not using grep -q, because it can cause Broken pipe - # https://unix.stackexchange.com/questions/305547/broken-pipe-when-grepping-output-but-only-with-i-flag - [ -n "$isExeResult" ] -} - -# We cache dependencies so that we don't need to search through all of them on -# every consecutive call to findDependency. -declare -Ag autoPatchelfCachedDepsAssoc -declare -ag autoPatchelfCachedDeps - -addToDepCache() { - if [[ ${autoPatchelfCachedDepsAssoc[$1]+f} ]]; then return; fi - - # store deps in an assoc. array for efficient lookups - # otherwise findDependency would have quadratic complexity - autoPatchelfCachedDepsAssoc["$1"]="" - - # also store deps in normal array to maintain their order - autoPatchelfCachedDeps+=("$1") -} - -declare -gi depCacheInitialised=0 -declare -gi doneRecursiveSearch=0 -declare -g foundDependency - -getDepsFromElfBinary() { - # NOTE: This does not use runPatchelf because it may encounter non-ELF - # files. Caller is expected to check the return code if needed. - patchelf --print-needed "$1" 2> /dev/null -} - -getRpathFromElfBinary() { - # NOTE: This does not use runPatchelf because it may encounter non-ELF - # files. Caller is expected to check the return code if needed. - local rpath - IFS=':' read -ra rpath < <(patchelf --print-rpath "$1" 2> /dev/null) || return $? - - printf "%s\n" "${rpath[@]}" -} - -populateCacheForDep() { - local so="$1" - local rpath found - rpath="$(getRpathFromElfBinary "$so")" || return 1 - - for found in $(getDepsFromElfBinary "$so"); do - local rpathElem - for rpathElem in $rpath; do - # Ignore empty element or $ORIGIN magic variable which should be - # deterministically resolved by adding this package's library - # files early anyway. - # - # shellcheck disable=SC2016 - # (Expressions don't expand in single quotes, use double quotes for - # that.) - if [[ -z "$rpathElem" || "$rpathElem" == *'$ORIGIN'* ]]; then - continue - fi - - local soname="${found%.so*}" - local foundso= - for foundso in "$rpathElem/$soname".so*; do - addToDepCache "$foundso" - done - - # Found in this element of the rpath, no need to check others. - if [ -n "$foundso" ]; then - break - fi - done - done - - # Not found in any rpath element. - return 1 -} - -populateCacheWithRecursiveDeps() { - # Dependencies may add more to the end of this array, so we use a counter - # with while instead of a regular for loop here. - local -i i=0 - while [ $i -lt ${#autoPatchelfCachedDeps[@]} ]; do - populateCacheForDep "${autoPatchelfCachedDeps[$i]}" - i=$i+1 - done -} - -getBinArch() { - $OBJDUMP -f "$1" 2> /dev/null | sed -ne 's/^architecture: *\([^,]\+\).*/\1/p' -} - -# Returns the specific OS ABI for an ELF file in the format produced by -# readelf(1), like "UNIX - System V" or "UNIX - GNU". -getBinOsabi() { - $READELF -h "$1" 2> /dev/null | sed -ne 's/^[ \t]*OS\/ABI:[ \t]*\(.*\)/\1/p' -} - -# Tests whether two OS ABIs are compatible, taking into account the generally -# accepted compatibility of SVR4 ABI with other ABIs. -areBinOsabisCompatible() { - local wanted="$1" - local got="$2" - - if [[ -z "$wanted" || -z "$got" ]]; then - # One of the types couldn't be detected, so as a fallback we'll assume - # they're compatible. - return 0 - fi - - # Generally speaking, the base ABI (0x00), which is represented by - # readelf(1) as "UNIX - System V", indicates broad compatibility with other - # ABIs. - # - # TODO: This isn't always true. For example, some OSes embed ABI - # compatibility into SHT_NOTE sections like .note.tag and .note.ABI-tag. - # It would be prudent to add these to the detection logic to produce better - # ABI information. - if [[ "$wanted" == "UNIX - System V" ]]; then - return 0 - fi - - # Similarly here, we should be able to link against a superset of features, - # so even if the target has another ABI, this should be fine. - if [[ "$got" == "UNIX - System V" ]]; then - return 0 - fi - - # Otherwise, we simply return whether the ABIs are identical. - if [[ "$wanted" == "$got" ]]; then - return 0 - fi - - return 1 -} - -# NOTE: If you want to use this function outside of the autoPatchelf function, -# keep in mind that the dependency cache is only valid inside the subshell -# spawned by the autoPatchelf function, so invoking this directly will possibly -# rebuild the dependency cache. See the autoPatchelf function below for more -# information. -findDependency() { - local filename="$1" - local arch="$2" - local osabi="$3" - local lib dep - - if [ $depCacheInitialised -eq 0 ]; then - for lib in "${autoPatchelfLibs[@]}"; do - for so in "$lib/"*.so*; do addToDepCache "$so"; done - done - depCacheInitialised=1 - fi - - for dep in "${autoPatchelfCachedDeps[@]}"; do - if [ "$filename" = "${dep##*/}" ]; then - if [ "$(getBinArch "$dep")" = "$arch" ] && areBinOsabisCompatible "$osabi" "$(getBinOsabi "$dep")"; then - foundDependency="$dep" - return 0 - fi - fi - done - - # Populate the dependency cache with recursive dependencies *only* if we - # didn't find the right dependency so far and afterwards run findDependency - # again, but this time with $doneRecursiveSearch set to 1 so that it won't - # recurse again (and thus infinitely). - if [ $doneRecursiveSearch -eq 0 ]; then - populateCacheWithRecursiveDeps - doneRecursiveSearch=1 - findDependency "$filename" "$arch" || return 1 - return 0 - fi - return 1 -} - -autoPatchelfFile() { - local dep rpath="" toPatch="$1" - - local interpreter - interpreter="$(< "$NIX_BINTOOLS/nix-support/dynamic-linker")" - - local interpreterArch interpreterOsabi toPatchArch toPatchOsabi - interpreterArch="$(getBinArch "$interpreter")" - interpreterOsabi="$(getBinOsabi "$interpreter")" - toPatchArch="$(getBinArch "$toPatch")" - toPatchOsabi="$(getBinOsabi "$toPatch")" - - if [ "$interpreterArch" != "$toPatchArch" ]; then - # Our target architecture is different than this file's architecture, - # so skip it. - echo "skipping $toPatch because its architecture ($toPatchArch) differs from target ($interpreterArch)" >&2 - return 0 - elif ! areBinOsabisCompatible "$interpreterOsabi" "$toPatchOsabi"; then - echo "skipping $toPatch because its OS ABI ($toPatchOsabi) is not compatible with target ($interpreterOsabi)" >&2 - return 0 - fi - - if isExecutable "$toPatch"; then - runPatchelf --set-interpreter "$interpreter" "$toPatch" - # shellcheck disable=SC2154 - # (runtimeDependencies is referenced but not assigned.) - if [ -n "$runtimeDependencies" ]; then - for dep in $runtimeDependencies; do - rpath="$rpath${rpath:+:}$dep/lib" - done - fi - fi - - local libcLib - libcLib="$(< "$NIX_BINTOOLS/nix-support/orig-libc")/lib" - - echo "searching for dependencies of $toPatch" >&2 - - local missing - missing="$(getDepsFromElfBinary "$toPatch")" || return 0 - - # This ensures that we get the output of all missing dependencies instead - # of failing at the first one, because it's more useful when working on a - # new package where you don't yet know its dependencies. - - for dep in $missing; do - if [[ "$dep" == /* ]]; then - # This is an absolute path. If it exists, just use it. Otherwise, - # we probably want this to produce an error when checked (because - # just updating the rpath won't satisfy it). - if [ -f "$dep" ]; then - continue - fi - elif [ -f "$libcLib/$dep" ]; then - # This library exists in libc, and will be correctly resolved by - # the linker. - continue - fi - - echo -n " $dep -> " >&2 - if findDependency "$dep" "$toPatchArch" "$toPatchOsabi"; then - rpath="$rpath${rpath:+:}${foundDependency%/*}" - echo "found: $foundDependency" >&2 - else - echo "not found!" >&2 - autoPatchelfFailedDeps["$dep"]="$toPatch" - fi - done - - if [ -n "$rpath" ]; then - echo "setting RPATH to: $rpath" >&2 - runPatchelf --set-rpath "$rpath" "$toPatch" - fi -} - # Can be used to manually add additional directories with shared object files # to be included for the next autoPatchelf invocation. addAutoPatchelfSearchPath() { local -a findOpts=() - # XXX: Somewhat similar to the one in the autoPatchelf function, maybe make - # it DRY someday... while [ $# -gt 0 ]; do case "$1" in --) shift; break;; @@ -296,15 +28,19 @@ addAutoPatchelfSearchPath() { esac done - while IFS= read -r -d '' file; do - addToDepCache "$file" + local dir= + while IFS= read -r -d '' dir; do + extraAutoPatchelfLibs+=("$dir") done < <(find "$@" "${findOpts[@]}" \! -type d \ - \( -name '*.so' -o -name '*.so.*' \) -print0) + \( -name '*.so' -o -name '*.so.*' \) -print0 \ + | sed -z 's#/[^/]*$##' \ + | uniq -z + ) } + autoPatchelf() { local norecurse= - while [ $# -gt 0 ]; do case "$1" in --) shift; break;; @@ -317,47 +53,14 @@ autoPatchelf() { esac done - if [ $# -eq 0 ]; then - echo "autoPatchelf: No paths to patch specified." >&2 - return 1 - fi - - echo "automatically fixing dependencies for ELF files" >&2 - - # Add all shared objects of the current output path to the start of - # autoPatchelfCachedDeps so that it's chosen first in findDependency. - addAutoPatchelfSearchPath ${norecurse:+--no-recurse} -- "$@" - - while IFS= read -r -d $'\0' file; do - isELF "$file" || continue - segmentHeaders="$(LANG=C $READELF -l "$file")" - # Skip if the ELF file doesn't have segment headers (eg. object files). - # not using grep -q, because it can cause Broken pipe - grep -q '^Program Headers:' <<<"$segmentHeaders" || continue - if isExecutable "$file"; then - # Skip if the executable is statically linked. - grep -q "^ *INTERP\\>" <<<"$segmentHeaders" || continue - fi - # Jump file if patchelf is unable to parse it - # Some programs contain binary blobs for testing, - # which are identified as ELF but fail to be parsed by patchelf - patchelf "$file" || continue - autoPatchelfFile "$file" - done < <(find "$@" ${norecurse:+-maxdepth 1} -type f -print0) - - # fail if any dependencies were not found and - # autoPatchelfIgnoreMissingDeps is not set - local depsMissing=0 - for failedDep in "${!autoPatchelfFailedDeps[@]}"; do - echo "autoPatchelfHook could not satisfy dependency $failedDep wanted by ${autoPatchelfFailedDeps[$failedDep]}" - depsMissing=1 - done - # shellcheck disable=SC2154 - # (autoPatchelfIgnoreMissingDeps is referenced but not assigned.) - if [[ $depsMissing == 1 && -z "$autoPatchelfIgnoreMissingDeps" ]]; then - echo "Add the missing dependencies to the build inputs or set autoPatchelfIgnoreMissingDeps=true" - exit 1 - fi + local runtimeDependenciesArray=($runtimeDependencies) + @pythonInterpreter@ @autoPatchelfScript@ \ + ${norecurse:+--no-recurse} \ + ${autoPatchelfIgnoreMissingDeps:+--ignore-missing} \ + --paths "$@" \ + --libs "${autoPatchelfLibs[@]}" \ + "${extraAutoPatchelfLibs[@]}" \ + --runtime-dependencies "${runtimeDependenciesArray[@]/%//lib}" } # XXX: This should ultimately use fixupOutputHooks but we currently don't have |