1
2set -euo pipefail
3
4# Assert that FILE exists and is executable
5#
6# assertExecutable FILE
7assertExecutable() {
8 local file="$1"
9 [[ -f "$file" && -x "$file" ]] || \
10 die "Cannot wrap '$file' because it is not an executable file"
11}
12
13# Generate a binary executable wrapper for wrapping an executable.
14# The binary is compiled from generated C-code using gcc.
15# makeWrapper EXECUTABLE OUT_PATH ARGS
16
17# ARGS:
18# --argv0 NAME : set the name of the executed process to NAME
19# (if unset or empty, defaults to EXECUTABLE)
20# --inherit-argv0 : the executable inherits argv0 from the wrapper.
21# (use instead of --argv0 '$0')
22# --set VAR VAL : add VAR with value VAL to the executable's environment
23# --set-default VAR VAL : like --set, but only adds VAR if not already set in
24# the environment
25# --unset VAR : remove VAR from the environment
26# --chdir DIR : change working directory (use instead of --run "cd DIR")
27# --add-flags ARGS : prepend ARGS to the invocation of the executable
28# (that is, *before* any arguments passed on the command line)
29# --append-flags ARGS : append ARGS to the invocation of the executable
30# (that is, *after* any arguments passed on the command line)
31
32# --prefix ENV SEP VAL : suffix/prefix ENV with VAL, separated by SEP
33# --suffix
34
35# To troubleshoot a binary wrapper after you compiled it,
36# use the `strings` command or open the binary file in a text editor.
37makeWrapper() { makeBinaryWrapper "$@"; }
38makeBinaryWrapper() {
39 local NIX_CFLAGS_COMPILE= NIX_CFLAGS_LINK=
40 local original="$1"
41 local wrapper="$2"
42 shift 2
43
44 assertExecutable "$original"
45
46 mkdir -p "$(dirname "$wrapper")"
47
48 makeDocumentedCWrapper "$original" "$@" | \
49 @cc@ \
50 -Wall -Werror -Wpedantic \
51 -Wno-overlength-strings \
52 -Os \
53 -x c \
54 -o "$wrapper" -
55}
56
57# Syntax: wrapProgram <PROGRAM> <MAKE-WRAPPER FLAGS...>
58wrapProgram() { wrapProgramBinary "$@"; }
59wrapProgramBinary() {
60 local prog="$1"
61 local hidden
62
63 assertExecutable "$prog"
64
65 hidden="$(dirname "$prog")/.$(basename "$prog")"-wrapped
66 while [ -e "$hidden" ]; do
67 hidden="${hidden}_"
68 done
69 mv "$prog" "$hidden"
70 makeBinaryWrapper "$hidden" "$prog" --inherit-argv0 "${@:2}"
71}
72
73# Generate source code for the wrapper in such a way that the wrapper inputs
74# will still be readable even after compilation
75# makeDocumentedCWrapper EXECUTABLE ARGS
76# ARGS: same as makeWrapper
77makeDocumentedCWrapper() {
78 local src docs
79 src=$(makeCWrapper "$@")
80 docs=$(docstring "$@")
81 printf '%s\n\n' "$src"
82 printf '%s\n' "$docs"
83}
84
85# makeCWrapper EXECUTABLE ARGS
86# ARGS: same as makeWrapper
87makeCWrapper() {
88 local argv0 inherit_argv0 n params cmd main flagsBefore flagsAfter flags executable length
89 local uses_prefix uses_suffix uses_assert uses_assert_success uses_stdio uses_asprintf
90 executable=$(escapeStringLiteral "$1")
91 params=("$@")
92 length=${#params[*]}
93 for ((n = 1; n < length; n += 1)); do
94 p="${params[n]}"
95 case $p in
96 --set)
97 cmd=$(setEnv "${params[n + 1]}" "${params[n + 2]}")
98 main="$main$cmd"$'\n'
99 n=$((n + 2))
100 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 2 arguments"$'\n'
101 ;;
102 --set-default)
103 cmd=$(setDefaultEnv "${params[n + 1]}" "${params[n + 2]}")
104 main="$main$cmd"$'\n'
105 uses_stdio=1
106 uses_assert_success=1
107 n=$((n + 2))
108 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 2 arguments"$'\n'
109 ;;
110 --unset)
111 cmd=$(unsetEnv "${params[n + 1]}")
112 main="$main$cmd"$'\n'
113 uses_stdio=1
114 uses_assert_success=1
115 n=$((n + 1))
116 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 1 argument"$'\n'
117 ;;
118 --prefix)
119 cmd=$(setEnvPrefix "${params[n + 1]}" "${params[n + 2]}" "${params[n + 3]}")
120 main="$main$cmd"$'\n'
121 uses_prefix=1
122 uses_asprintf=1
123 uses_stdio=1
124 uses_assert_success=1
125 uses_assert=1
126 n=$((n + 3))
127 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 3 arguments"$'\n'
128 ;;
129 --suffix)
130 cmd=$(setEnvSuffix "${params[n + 1]}" "${params[n + 2]}" "${params[n + 3]}")
131 main="$main$cmd"$'\n'
132 uses_suffix=1
133 uses_asprintf=1
134 uses_stdio=1
135 uses_assert_success=1
136 uses_assert=1
137 n=$((n + 3))
138 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 3 arguments"$'\n'
139 ;;
140 --chdir)
141 cmd=$(changeDir "${params[n + 1]}")
142 main="$main$cmd"$'\n'
143 uses_stdio=1
144 uses_assert_success=1
145 n=$((n + 1))
146 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 1 argument"$'\n'
147 ;;
148 --add-flags)
149 flags="${params[n + 1]}"
150 flagsBefore="$flagsBefore $flags"
151 uses_assert=1
152 n=$((n + 1))
153 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 1 argument"$'\n'
154 ;;
155 --append-flags)
156 flags="${params[n + 1]}"
157 flagsAfter="$flagsAfter $flags"
158 uses_assert=1
159 n=$((n + 1))
160 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 1 argument"$'\n'
161 ;;
162 --argv0)
163 argv0=$(escapeStringLiteral "${params[n + 1]}")
164 inherit_argv0=
165 n=$((n + 1))
166 [ $n -ge "$length" ] && main="$main#error makeCWrapper: $p takes 1 argument"$'\n'
167 ;;
168 --inherit-argv0)
169 # Whichever comes last of --argv0 and --inherit-argv0 wins
170 inherit_argv0=1
171 ;;
172 *) # Using an error macro, we will make sure the compiler gives an understandable error message
173 main="$main#error makeCWrapper: Unknown argument ${p}"$'\n'
174 ;;
175 esac
176 done
177 [[ -z "$flagsBefore" && -z "$flagsAfter" ]] || main="$main"${main:+$'\n'}$(addFlags "$flagsBefore" "$flagsAfter")$'\n'$'\n'
178 [ -z "$inherit_argv0" ] && main="${main}argv[0] = \"${argv0:-${executable}}\";"$'\n'
179 main="${main}return execv(\"${executable}\", argv);"$'\n'
180
181 [ -z "$uses_asprintf" ] || printf '%s\n' "#define _GNU_SOURCE /* See feature_test_macros(7) */"
182 printf '%s\n' "#include <unistd.h>"
183 printf '%s\n' "#include <stdlib.h>"
184 [ -z "$uses_assert" ] || printf '%s\n' "#include <assert.h>"
185 [ -z "$uses_stdio" ] || printf '%s\n' "#include <stdio.h>"
186 [ -z "$uses_assert_success" ] || printf '\n%s\n' "#define assert_success(e) do { if ((e) < 0) { perror(#e); abort(); } } while (0)"
187 [ -z "$uses_prefix" ] || printf '\n%s\n' "$(setEnvPrefixFn)"
188 [ -z "$uses_suffix" ] || printf '\n%s\n' "$(setEnvSuffixFn)"
189 printf '\n%s' "int main(int argc, char **argv) {"
190 printf '\n%s' "$(indent4 "$main")"
191 printf '\n%s\n' "}"
192}
193
194addFlags() {
195 local n flag before after var
196
197 # Disable file globbing, since bash will otherwise try to find
198 # filenames matching the the value to be prefixed/suffixed if
199 # it contains characters considered wildcards, such as `?` and
200 # `*`. We want the value as is, except we also want to split
201 # it on on the separator; hence we can't quote it.
202 local reenableGlob=0
203 if [[ ! -o noglob ]]; then
204 reenableGlob=1
205 fi
206 set -o noglob
207 # shellcheck disable=SC2086
208 before=($1) after=($2)
209 if (( reenableGlob )); then
210 set +o noglob
211 fi
212
213 var="argv_tmp"
214 printf '%s\n' "char **$var = calloc(${#before[@]} + argc + ${#after[@]} + 1, sizeof(*$var));"
215 printf '%s\n' "assert($var != NULL);"
216 printf '%s\n' "${var}[0] = argv[0];"
217 for ((n = 0; n < ${#before[@]}; n += 1)); do
218 flag=$(escapeStringLiteral "${before[n]}")
219 printf '%s\n' "${var}[$((n + 1))] = \"$flag\";"
220 done
221 printf '%s\n' "for (int i = 1; i < argc; ++i) {"
222 printf '%s\n' " ${var}[${#before[@]} + i] = argv[i];"
223 printf '%s\n' "}"
224 for ((n = 0; n < ${#after[@]}; n += 1)); do
225 flag=$(escapeStringLiteral "${after[n]}")
226 printf '%s\n' "${var}[${#before[@]} + argc + $n] = \"$flag\";"
227 done
228 printf '%s\n' "${var}[${#before[@]} + argc + ${#after[@]}] = NULL;"
229 printf '%s\n' "argv = $var;"
230}
231
232# chdir DIR
233changeDir() {
234 local dir
235 dir=$(escapeStringLiteral "$1")
236 printf '%s' "assert_success(chdir(\"$dir\"));"
237}
238
239# prefix ENV SEP VAL
240setEnvPrefix() {
241 local env sep val
242 env=$(escapeStringLiteral "$1")
243 sep=$(escapeStringLiteral "$2")
244 val=$(escapeStringLiteral "$3")
245 printf '%s' "set_env_prefix(\"$env\", \"$sep\", \"$val\");"
246 assertValidEnvName "$1"
247}
248
249# suffix ENV SEP VAL
250setEnvSuffix() {
251 local env sep val
252 env=$(escapeStringLiteral "$1")
253 sep=$(escapeStringLiteral "$2")
254 val=$(escapeStringLiteral "$3")
255 printf '%s' "set_env_suffix(\"$env\", \"$sep\", \"$val\");"
256 assertValidEnvName "$1"
257}
258
259# setEnv KEY VALUE
260setEnv() {
261 local key value
262 key=$(escapeStringLiteral "$1")
263 value=$(escapeStringLiteral "$2")
264 printf '%s' "putenv(\"$key=$value\");"
265 assertValidEnvName "$1"
266}
267
268# setDefaultEnv KEY VALUE
269setDefaultEnv() {
270 local key value
271 key=$(escapeStringLiteral "$1")
272 value=$(escapeStringLiteral "$2")
273 printf '%s' "assert_success(setenv(\"$key\", \"$value\", 0));"
274 assertValidEnvName "$1"
275}
276
277# unsetEnv KEY
278unsetEnv() {
279 local key
280 key=$(escapeStringLiteral "$1")
281 printf '%s' "assert_success(unsetenv(\"$key\"));"
282 assertValidEnvName "$1"
283}
284
285# Makes it safe to insert STRING within quotes in a C String Literal.
286# escapeStringLiteral STRING
287escapeStringLiteral() {
288 local result
289 result=${1//$'\\'/$'\\\\'}
290 result=${result//\"/'\"'}
291 result=${result//$'\n'/"\n"}
292 result=${result//$'\r'/"\r"}
293 printf '%s' "$result"
294}
295
296# Indents every non-empty line by 4 spaces. To avoid trailing whitespace, we don't indent empty lines
297# indent4 TEXT_BLOCK
298indent4() {
299 printf '%s' "$1" | awk '{ if ($0 != "") { print " "$0 } else { print $0 }}'
300}
301
302assertValidEnvName() {
303 case "$1" in
304 *=*) printf '\n%s\n' "#error Illegal environment variable name \`$1\` (cannot contain \`=\`)";;
305 "") printf '\n%s\n' "#error Environment variable name can't be empty.";;
306 esac
307}
308
309setEnvPrefixFn() {
310 printf '%s' "\
311void set_env_prefix(char *env, char *sep, char *prefix) {
312 char *existing = getenv(env);
313 if (existing) {
314 char *val;
315 assert_success(asprintf(&val, \"%s%s%s\", prefix, sep, existing));
316 assert_success(setenv(env, val, 1));
317 free(val);
318 } else {
319 assert_success(setenv(env, prefix, 1));
320 }
321}
322"
323}
324
325setEnvSuffixFn() {
326 printf '%s' "\
327void set_env_suffix(char *env, char *sep, char *suffix) {
328 char *existing = getenv(env);
329 if (existing) {
330 char *val;
331 assert_success(asprintf(&val, \"%s%s%s\", existing, sep, suffix));
332 assert_success(setenv(env, val, 1));
333 free(val);
334 } else {
335 assert_success(setenv(env, suffix, 1));
336 }
337}
338"
339}
340
341# Embed a C string which shows up as readable text in the compiled binary wrapper,
342# giving instructions for recreating the wrapper.
343# Keep in sync with makeBinaryWrapper.extractCmd
344docstring() {
345 printf '%s' "const char * DOCSTRING = \"$(escapeStringLiteral "
346
347
348# ------------------------------------------------------------------------------------
349# The C-code for this binary wrapper has been generated using the following command:
350
351
352makeCWrapper $(formatArgs "$@")
353
354
355# (Use \`nix-shell -p makeBinaryWrapper\` to get access to makeCWrapper in your shell)
356# ------------------------------------------------------------------------------------
357
358
359")\";"
360}
361
362# formatArgs EXECUTABLE ARGS
363formatArgs() {
364 printf '%s' "${1@Q}"
365 shift
366 while [ $# -gt 0 ]; do
367 case "$1" in
368 --set)
369 formatArgsLine 2 "$@"
370 shift 2
371 ;;
372 --set-default)
373 formatArgsLine 2 "$@"
374 shift 2
375 ;;
376 --unset)
377 formatArgsLine 1 "$@"
378 shift 1
379 ;;
380 --prefix)
381 formatArgsLine 3 "$@"
382 shift 3
383 ;;
384 --suffix)
385 formatArgsLine 3 "$@"
386 shift 3
387 ;;
388 --chdir)
389 formatArgsLine 1 "$@"
390 shift 1
391 ;;
392 --add-flags)
393 formatArgsLine 1 "$@"
394 shift 1
395 ;;
396 --append-flags)
397 formatArgsLine 1 "$@"
398 shift 1
399 ;;
400 --argv0)
401 formatArgsLine 1 "$@"
402 shift 1
403 ;;
404 --inherit-argv0)
405 formatArgsLine 0 "$@"
406 ;;
407 esac
408 shift
409 done
410 printf '%s\n' ""
411}
412
413# formatArgsLine ARG_COUNT ARGS
414formatArgsLine() {
415 local ARG_COUNT LENGTH
416 ARG_COUNT=$1
417 LENGTH=$#
418 shift
419 printf '%s' $' \\\n '"$1"
420 shift
421 while [ "$ARG_COUNT" -gt $((LENGTH - $# - 2)) ]; do
422 printf ' %s' "${1@Q}"
423 shift
424 done
425}