a7f5e8321e
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.
382 lines
13 KiB
Bash
382 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
|
|
')
|