at master 480 lines 16 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-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}