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