nixpkgs/pkgs/build-support/setup-hooks/auto-patchelf.sh
Noah Fontes a7f5e8321e
autoPatchelfHook: fix packages that use stdenvNoCC
autoPatchelfHook actually doesn't depend on stdenv and only needs
bintools (with its wrapper). This change uses $NIX_BINTOOLS instead of
$NIX_CC and makes the dependency on bintools explicit.
2021-09-18 21:58:02 -07:00

381 lines
13 KiB
Bash

#!/usr/bin/env bash
declare -a autoPatchelfLibs
declare -Ag autoPatchelfFailedDeps
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;;
--no-recurse) shift; findOpts+=("-maxdepth" 1);;
--*)
echo "addAutoPatchelfSearchPath: ERROR: Invalid command line" \
"argument: $1" >&2
return 1;;
*) break;;
esac
done
while IFS= read -r -d '' file; do
addToDepCache "$file"
done < <(find "$@" "${findOpts[@]}" \! -type d \
\( -name '*.so' -o -name '*.so.*' \) -print0)
}
autoPatchelf() {
local norecurse=
while [ $# -gt 0 ]; do
case "$1" in
--) shift; break;;
--no-recurse) shift; norecurse=1;;
--*)
echo "autoPatchelf: ERROR: Invalid command line" \
"argument: $1" >&2
return 1;;
*) break;;
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
}
# XXX: This should ultimately use fixupOutputHooks but we currently don't have
# a way to enforce the order. If we have $runtimeDependencies set, the setup
# hook of patchelf is going to ruin everything and strip out those additional
# RPATHs.
#
# So what we do here is basically run in postFixup and emulate the same
# behaviour as fixupOutputHooks because the setup hook for patchelf is run in
# fixupOutput and the postFixup hook runs later.
#
# shellcheck disable=SC2016
# (Expressions don't expand in single quotes, use double quotes for that.)
postFixupHooks+=('
if [ -z "${dontAutoPatchelf-}" ]; then
autoPatchelf -- $(for output in $outputs; do
[ -e "${!output}" ] || continue
echo "${!output}"
done)
fi
')