Clone of https://github.com/NixOS/nixpkgs.git (to stress-test knotserver)
at 23.11-beta 425 lines 14 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# --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}