summary refs log tree commit diff
path: root/pkgs/build-support/setup-hooks/auto-patchelf.sh
blob: 4b3a1c5c39092e297afea9c4e6ce53dd4d3e38d1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
#!/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
')