about summary refs log tree commit diff
path: root/nixpkgs/pkgs/build-support/setup-hooks/patch-shebangs.sh
blob: 80a29d727c8581aec93fe595d2ec3d9d1536bd15 (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
# This setup hook causes the fixup phase to rewrite all script
# interpreter file names (`#!  /path') to paths found in $PATH.  E.g.,
# /bin/sh will be rewritten to /nix/store/<hash>-some-bash/bin/sh.
# /usr/bin/env gets special treatment so that ".../bin/env python" is
# rewritten to /nix/store/<hash>/bin/python.  Interpreters that are
# already in the store are left untouched.
# A script file must be marked as executable, otherwise it will not be
# considered.

fixupOutputHooks+=(patchShebangsAuto)

# Run patch shebangs on a directory or file.
# Can take multiple paths as arguments.
# patchShebangs [--build | --host | --update] [--] PATH...

# Flags:
# --build : Lookup commands available at build-time
# --host  : Lookup commands available at runtime
# --update : Update shebang paths that are in Nix store

# Example use cases,
# $ patchShebangs --host /nix/store/...-hello-1.0/bin
# $ patchShebangs --build configure

patchShebangs() {
    local pathName
    local update

    while [[ $# -gt 0 ]]; do
        case "$1" in
        --host)
            pathName=HOST_PATH
            shift
            ;;
        --build)
            pathName=PATH
            shift
            ;;
        --update)
            update=true
            shift
            ;;
        --)
            shift
            break
            ;;
        -*|--*)
            echo "Unknown option $1 supplied to patchShebangs" >&2
            return 1
            ;;
        *)
            break
            ;;
        esac
    done

    echo "patching script interpreter paths in $@"
    local f
    local oldPath
    local newPath
    local arg0
    local args
    local oldInterpreterLine
    local newInterpreterLine

    if [[ $# -eq 0 ]]; then
        echo "No arguments supplied to patchShebangs" >&2
        return 0
    fi

    local f
    while IFS= read -r -d $'\0' f; do
        isScript "$f" || continue

        # read exits unclean if the shebang does not end with a newline, but still assigns the variable.
        # So if read returns errno != 0, we check if the assigned variable is non-empty and continue.
        read -r oldInterpreterLine < "$f" || [ "$oldInterpreterLine" ]

        read -r oldPath arg0 args <<< "${oldInterpreterLine:2}"

        if [[ -z "${pathName:-}" ]]; then
            if [[ -n $strictDeps && $f == "$NIX_STORE"* ]]; then
                pathName=HOST_PATH
            else
                pathName=PATH
            fi
        fi

        if [[ "$oldPath" == *"/bin/env" ]]; then
            if [[ $arg0 == "-S" ]]; then
                arg0=${args%% *}
                args=${args#* }
                newPath="$(PATH="${!pathName}" command -v "env" || true)"
                args="-S $(PATH="${!pathName}" command -v "$arg0" || true) $args"

            # Check for unsupported 'env' functionality:
            # - options: something starting with a '-' besides '-S'
            # - environment variables: foo=bar
            elif [[ $arg0 == "-"* || $arg0 == *"="* ]]; then
                echo "$f: unsupported interpreter directive \"$oldInterpreterLine\" (set dontPatchShebangs=1 and handle shebang patching yourself)" >&2
                exit 1
            else
                newPath="$(PATH="${!pathName}" command -v "$arg0" || true)"
            fi
        else
            if [[ -z $oldPath ]]; then
                # If no interpreter is specified linux will use /bin/sh. Set
                # oldpath="/bin/sh" so that we get /nix/store/.../sh.
                oldPath="/bin/sh"
            fi

            newPath="$(PATH="${!pathName}" command -v "$(basename "$oldPath")" || true)"

            args="$arg0 $args"
        fi

        # Strip trailing whitespace introduced when no arguments are present
        newInterpreterLine="$newPath $args"
        newInterpreterLine=${newInterpreterLine%${newInterpreterLine##*[![:space:]]}}

        if [[ -n "$oldPath" && ( "$update" == true || "${oldPath:0:${#NIX_STORE}}" != "$NIX_STORE" ) ]]; then
            if [[ -n "$newPath" && "$newPath" != "$oldPath" ]]; then
                echo "$f: interpreter directive changed from \"$oldInterpreterLine\" to \"$newInterpreterLine\""
                # escape the escape chars so that sed doesn't interpret them
                escapedInterpreterLine=${newInterpreterLine//\\/\\\\}

                # Preserve times, see: https://github.com/NixOS/nixpkgs/pull/33281
                timestamp=$(stat --printf "%y" "$f")
                sed -i -e "1 s|.*|#\!$escapedInterpreterLine|" "$f"
                touch --date "$timestamp" "$f"
            fi
        fi
    done < <(find "$@" -type f -perm -0100 -print0)
}

patchShebangsAuto () {
    if [[ -z "${dontPatchShebangs-}" && -e "$prefix" ]]; then

        # Dev output will end up being run on the build platform. An
        # example case of this is sdl2-config. Otherwise, we can just
        # use the runtime path (--host).
        if [[ "$output" != out && "$output" = "$outputDev" ]]; then
            patchShebangs --build "$prefix"
        else
            patchShebangs --host "$prefix"
        fi
    fi
}