# Assert that FILE exists and is executable # # assertExecutable FILE assertExecutable() { local file="$1" [[ -f "$file" && -x "$file" ]] || \ die "Cannot wrap '$file' because it is not an executable file" } # Generate a binary executable wrapper for wrapping an executable. # The binary is compiled from generated C-code using gcc. # makeBinaryWrapper EXECUTABLE OUT_PATH ARGS # ARGS: # --argv0 NAME : set name of executed process to NAME # (otherwise it’s called …-wrapped) # --inherit-argv0 : the executable inherits argv0 from the wrapper. # (use instead of --argv0 '$0') # --set VAR VAL : add VAR with value VAL to the executable’s # environment # --set-default VAR VAL : like --set, but only adds VAR if not already set in # the environment # --unset VAR : remove VAR from the environment # --chdir DIR : change working directory (use instead of --run "cd DIR") # --add-flags FLAGS : add FLAGS to invocation of executable # --prefix ENV SEP VAL : suffix/prefix ENV with VAL, separated by SEP # --suffix # To troubleshoot a binary wrapper after you compiled it, # use the `strings` command or open the binary file in a text editor. makeBinaryWrapper() { assertExecutable "$1" makeDocumentedCWrapper "$1" "${@:3}" | cc -Os -x c -o "$2" - } # Syntax: wrapProgramBinary wrapProgramBinary() { local prog="$1" local hidden assertExecutable "$prog" hidden="$(dirname "$prog")/.$(basename "$prog")"-wrapped while [ -e "$hidden" ]; do hidden="${hidden}_" done mv "$prog" "$hidden" # Silence warning about unexpanded $0: # shellcheck disable=SC2016 makeBinaryWrapper "$hidden" "$prog" --inherit-argv0 "${@:2}" } # Generate source code for the wrapper in such a way that the wrapper source code # will still be readable even after compilation # makeDocumentedCWrapper EXECUTABLE ARGS # ARGS: same as makeBinaryWrapper makeDocumentedCWrapper() { local src docs src=$(makeCWrapper "$@") docs=$(documentationString "$src") printf '%s\n\n' "$src" printf '%s\n' "$docs" } # makeCWrapper EXECUTABLE ARGS # ARGS: same as makeBinaryWrapper makeCWrapper() { local argv0 inherit_argv0 n params cmd main flagsBefore flags executable params length local uses_prefix uses_suffix uses_assert uses_assert_success uses_stdio uses_asprintf executable=$(escapeStringLiteral "$1") params=("$@") length=${#params[*]} for ((n = 1; n < length; n += 1)); do p="${params[n]}" case $p in --set) cmd=$(setEnv "${params[n + 1]}" "${params[n + 2]}") main="$main $cmd"$'\n' n=$((n + 2)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 2 arguments"$'\n' ;; --set-default) cmd=$(setDefaultEnv "${params[n + 1]}" "${params[n + 2]}") main="$main $cmd"$'\n' uses_stdio=1 uses_assert_success=1 n=$((n + 2)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 2 arguments"$'\n' ;; --unset) cmd=$(unsetEnv "${params[n + 1]}") main="$main $cmd"$'\n' uses_stdio=1 uses_assert_success=1 n=$((n + 1)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 1 argument"$'\n' ;; --prefix) cmd=$(setEnvPrefix "${params[n + 1]}" "${params[n + 2]}" "${params[n + 3]}") main="$main $cmd"$'\n' uses_prefix=1 uses_asprintf=1 uses_stdio=1 uses_assert_success=1 uses_assert=1 n=$((n + 3)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 3 arguments"$'\n' ;; --suffix) cmd=$(setEnvSuffix "${params[n + 1]}" "${params[n + 2]}" "${params[n + 3]}") main="$main $cmd"$'\n' uses_suffix=1 uses_asprintf=1 uses_stdio=1 uses_assert_success=1 uses_assert=1 n=$((n + 3)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 3 arguments"$'\n' ;; --chdir) cmd=$(changeDir "${params[n + 1]}") main="$main $cmd"$'\n' uses_stdio=1 uses_assert_success=1 n=$((n + 1)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 1 argument"$'\n' ;; --add-flags) flags="${params[n + 1]}" flagsBefore="$flagsBefore $flags" uses_assert=1 n=$((n + 1)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 1 argument"$'\n' ;; --argv0) argv0=$(escapeStringLiteral "${params[n + 1]}") inherit_argv0= n=$((n + 1)) [ $n -ge "$length" ] && main="$main #error makeCWrapper: $p takes 1 argument"$'\n' ;; --inherit-argv0) # Whichever comes last of --argv0 and --inherit-argv0 wins inherit_argv0=1 ;; *) # Using an error macro, we will make sure the compiler gives an understandable error message main="$main #error makeCWrapper: Unknown argument ${p}"$'\n' ;; esac done # shellcheck disable=SC2086 [ -z "$flagsBefore" ] || main="$main"${main:+$'\n'}$(addFlags $flagsBefore)$'\n'$'\n' [ -z "$inherit_argv0" ] && main="$main argv[0] = \"${argv0:-${executable}}\";"$'\n' main="$main return execv(\"${executable}\", argv);"$'\n' [ -z "$uses_asprintf" ] || printf '%s\n' "#define _GNU_SOURCE /* See feature_test_macros(7) */" printf '%s\n' "#include " printf '%s\n' "#include " [ -z "$uses_assert" ] || printf '%s\n' "#include " [ -z "$uses_stdio" ] || printf '%s\n' "#include " [ -z "$uses_assert_success" ] || printf '\n%s\n' "#define assert_success(e) do { if ((e) < 0) { perror(#e); abort(); } } while (0)" [ -z "$uses_prefix" ] || printf '\n%s\n' "$(setEnvPrefixFn)" [ -z "$uses_suffix" ] || printf '\n%s\n' "$(setEnvSuffixFn)" printf '\n%s' "int main(int argc, char **argv) {" printf '\n%s' "$main" printf '%s\n' "}" } addFlags() { local result n flag flags var var="argv_tmp" flags=("$@") for ((n = 0; n < ${#flags[*]}; n += 1)); do flag=$(escapeStringLiteral "${flags[$n]}") result="$result ${var}[$((n+1))] = \"$flag\";"$'\n' done printf ' %s\n' "char **$var = calloc($((n+1)) + argc, sizeof(*$var));" printf ' %s\n' "assert($var != NULL);" printf ' %s\n' "${var}[0] = argv[0];" printf '%s' "$result" printf ' %s\n' "for (int i = 1; i < argc; ++i) {" printf ' %s\n' " ${var}[$n + i] = argv[i];" printf ' %s\n' "}" printf ' %s\n' "${var}[$n + argc] = NULL;" printf ' %s\n' "argv = $var;" } # chdir DIR changeDir() { local dir dir=$(escapeStringLiteral "$1") printf '%s' "assert_success(chdir(\"$dir\"));" } # prefix ENV SEP VAL setEnvPrefix() { local env sep val env=$(escapeStringLiteral "$1") sep=$(escapeStringLiteral "$2") val=$(escapeStringLiteral "$3") printf '%s' "set_env_prefix(\"$env\", \"$sep\", \"$val\");" assertValidEnvName "$1" } # suffix ENV SEP VAL setEnvSuffix() { local env sep val env=$(escapeStringLiteral "$1") sep=$(escapeStringLiteral "$2") val=$(escapeStringLiteral "$3") printf '%s' "set_env_suffix(\"$env\", \"$sep\", \"$val\");" assertValidEnvName "$1" } # setEnv KEY VALUE setEnv() { local key value key=$(escapeStringLiteral "$1") value=$(escapeStringLiteral "$2") printf '%s' "putenv(\"$key=$value\");" assertValidEnvName "$1" } # setDefaultEnv KEY VALUE setDefaultEnv() { local key value key=$(escapeStringLiteral "$1") value=$(escapeStringLiteral "$2") printf '%s' "assert_success(setenv(\"$key\", \"$value\", 0));" assertValidEnvName "$1" } # unsetEnv KEY unsetEnv() { local key key=$(escapeStringLiteral "$1") printf '%s' "assert_success(unsetenv(\"$key\"));" assertValidEnvName "$1" } # Put the entire source code into const char* SOURCE_CODE to make it readable after compilation. # documentationString SOURCE_CODE documentationString() { local docs docs=$(escapeStringLiteral $'\n----------\n// This binary wrapper was compiled from the following generated C-code:\n'"$1"$'\n----------\n') printf '%s' "const char * SOURCE_CODE = \"$docs\";" } # Makes it safe to insert STRING within quotes in a C String Literal. # escapeStringLiteral STRING escapeStringLiteral() { local result result=${1//$'\\'/$'\\\\'} result=${result//\"/'\"'} result=${result//$'\n'/"\n"} result=${result//$'\r'/"\r"} printf '%s' "$result" } assertValidEnvName() { case "$1" in *=*) printf '\n%s\n' " #error Illegal environment variable name \`$1\` (cannot contain \`=\`)";; "") printf '\n%s\n' " #error Environment variable name can't be empty.";; esac } setEnvPrefixFn() { printf '%s' "\ void set_env_prefix(char *env, char *sep, char *prefix) { char *existing = getenv(env); if (existing) { char *val; assert_success(asprintf(&val, \"%s%s%s\", prefix, sep, existing)); assert_success(setenv(env, val, 1)); free(val); } else { assert_success(setenv(env, prefix, 1)); } } " } setEnvSuffixFn() { printf '%s' "\ void set_env_suffix(char *env, char *sep, char *suffix) { char *existing = getenv(env); if (existing) { char *val; assert_success(asprintf(&val, \"%s%s%s\", existing, sep, suffix)); assert_success(setenv(env, val, 1)); free(val); } else { assert_success(setenv(env, suffix, 1)); } } " }