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