Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate tiny compiled binary for wrapping executables #124556

Merged
merged 49 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5edd17c
Add bergkvist to maintainer list
bergkvist May 26, 2021
de1f53b
Add make-c-wrapper.sh for creating binary executable wrappers
bergkvist May 26, 2021
eef4fa8
make-c-wrapper: Fix typo in generated code documentation
bergkvist May 26, 2021
131ed20
make-c-wrapper: Remove trailing whitespace (editorconfig)
bergkvist May 26, 2021
e8cedf3
make-c-wrapper: Fix typo in generated documentation
bergkvist May 27, 2021
8d2964a
Rename make-c-wrapper.sh to make-binary-wrapper.sh. Refactor to match…
bergkvist May 31, 2021
1d64281
Remove line at the bottom of make-binary-wrapper that executes makeBi…
bergkvist May 31, 2021
dcba417
Add support for --add-flags, --prefix and --suffix
bergkvist Aug 23, 2021
b58c857
Switch to using strlen in concat3Fn. Make sure uses-variables are loc…
bergkvist Aug 26, 2021
b62216a
Fix shellcheck warnings. Use single quotes for printf format strings.…
bergkvist Oct 1, 2021
ac99a6f
Add makeBinaryWrapper to pkgs/top-level/all-packages.nix
bergkvist Oct 1, 2021
d930fec
Return an #error macro if the wrong number of arguments are supplied
bergkvist Oct 1, 2021
3df841b
Make error messages more consistent
bergkvist Oct 1, 2021
adef70c
Specify uses_prefix, uses_suffix and uses_concat3 as local vars. Make…
bergkvist Oct 4, 2021
b7d36b8
Add golden tests for make-binary-wrapper.
bergkvist Oct 4, 2021
4b833cc
EditorConfig: Switch from tabs to spaces
bergkvist Oct 4, 2021
a45c5db
makeBinaryWrapper: Assert $1 is executable
doronbehar Oct 1, 2021
c310cb0
makeBinaryWrapper: add wrapProgramBinary (like wrapProgram)
doronbehar Oct 1, 2021
ba86a19
makeBinaryWrapper: Document
doronbehar Oct 1, 2021
1218b82
Move assertExecutable from makeCWrapper to makeBinaryWrapper to ensur…
bergkvist Oct 19, 2021
6517e5b
Improve explenations for wrap*Program
doronbehar Oct 19, 2021
7cca19a
Set strictDeps = true in makeGoldenTest
bergkvist Oct 19, 2021
a95a7a2
Switch from buildInputs to nativeBuildInpuits in makeGoldenTest
bergkvist Oct 19, 2021
eb048d8
Rephrase makeWrapper setup-hook
doronbehar Oct 20, 2021
3a014be
Assert that malloc does not return a NULL pointer for better error me…
bergkvist Nov 9, 2021
4e55d34
Add assertValidEnvName and check that variable name is valid during c…
bergkvist Dec 1, 2021
97d62a9
Switch from exit(1) to abort() in assert_success
bergkvist Dec 1, 2021
3997e9d
Switch from malloc to calloc in addFlags
bergkvist Dec 1, 2021
a1e6226
Replace concat3 with asprintf in set_env_prefix and set_env_suffix.
bergkvist Dec 1, 2021
e3c94f3
Use cc instead of gcc in makeBinaryWrapper
bergkvist Dec 2, 2021
a47286f
Add argument --inherit-argv0 to replace use case `--argv0 '$0'`. Fix …
bergkvist Dec 2, 2021
64da827
Add new argument: --chdir DIR (alternative to --run "cd DIR" in makeW…
bergkvist Dec 2, 2021
d8375fb
Add tests for `--inherit-argv0` and `--chdir DIR`
bergkvist Dec 2, 2021
2b103ab
Remove TODO in documentation
doronbehar Dec 2, 2021
2b5a2d4
Switch to embedding input arguments instead of generated C code in bi…
bergkvist Dec 7, 2021
7cf1aa1
Separate out indentation responsibility to indent4 in makeCWrapper us…
bergkvist Dec 7, 2021
f3b16a6
Fix typo in make-binary-wrapper
tfc Dec 8, 2021
32d566e
wrapProgramBinary -> binaryWrapProgram
doronbehar Dec 8, 2021
2bc7345
Add golden effects test
tfc Dec 7, 2021
e7c70ce
Inject gcc path into makewrapper script
Dec 9, 2021
177f0a6
make makeWrapper and makeBinaryWrapper drop-in-replaceable
tfc Dec 9, 2021
df13841
Merge branch 'bergkvist/make-c-wrapper' into make-c-wrapper
bergkvist Dec 9, 2021
c42e674
Rephrase documentation for both makeWrapper implementations
doronbehar Dec 9, 2021
b7e00ed
make-binary-wrapper: Add -Wall -Werror -Wpedantic
tfc Dec 9, 2021
87fcb7b
make-binary-wrapper: Add -euo pipefail to bash script
tfc Dec 9, 2021
d5e028a
make-binary-wrapper: Make CC substitution safer
tfc Dec 9, 2021
bdaa0e2
make-binary-wrapper: Add sanitizer default option
tfc Dec 9, 2021
ceffea6
Small rephrase of wrapProgram documentation
doronbehar Dec 9, 2021
39b0aa4
Change default cc from gcc to stdenv.cc.cc to reduce closure size on …
bergkvist Dec 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions doc/stdenv/stdenv.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,7 @@ The standard environment provides a number of useful functions.

### `makeWrapper` \<executable\> \<wrapperfile\> \<args\> {#fun-makeWrapper}

Constructs a wrapper for a program with various possible arguments. For example:
Constructs a wrapper for a program with various possible arguments. It is defined as part of a setup-hook by the same name, so to use it you have to add `makeWrapper` to your `nativeBuildInputs`. Here's a usage example:

```bash
# adds `FOOBAR=baz` to `$out/bin/foo`’s environment
Expand All @@ -790,6 +790,14 @@ There’s many more kinds of arguments, they are documented in `nixpkgs/pkgs/bui

`wrapProgram` is a convenience function you probably want to use most of the time.

### `makeBinaryWrapper` \<executable\> \<wrapperfile\> \<args\> {#fun-makeBinaryWrapper}

A setup-hook very similar to `makeWrapper`, only it creates a tiny _compiled_ wrapper executable, that can be used as a shebang interpreter. This is needed mostly on Darwin, where shebangs cannot point to scripts, [due to a limitation with the `execve`-syscall](https://stackoverflow.com/questions/67100831/macos-shebang-with-absolute-path-not-working). The arguments it accepts are similar to those of `makeWrapper` and they are documented in `nixpkgs/pkgs/build-support/setup-hooks/make-binary-wrapper.sh`.

Compiled wrappers generated by `makeBinaryWrapper` can be inspected with `less <path-to-wrapper>` - by scrolling past the binary data you should be able to see the C code that generated the executable and there see the environment variables that were injected into the wrapper.

Similarly to `wrapProgram`, the `makeBinaryWrapper` setup-hook provides a `wrapProgramBinary` with similar command line arguments.

### `substitute` \<infile\> \<outfile\> \<subs\> {#fun-substitute}

Performs string substitution on the contents of \<infile\>, writing the result to \<outfile\>. The substitutions in \<subs\> are of the following form:
Expand Down Expand Up @@ -863,9 +871,15 @@ someVar=$(stripHash $name)

### `wrapProgram` \<executable\> \<makeWrapperArgs\> {#fun-wrapProgram}

Convenience function for `makeWrapper` that automatically creates a sane wrapper file. It takes all the same arguments as `makeWrapper`, except for `--argv0`.
Convenience function for `makeWrapper` that replaces `<\executable\>` with a wrapper that executes the original program. It takes all the same arguments as `makeWrapper`, except for `--argv0`.

If you will apply it multiple times, it will overwrite the wrapper file and you will end up with double wrapping, which should be avoided.

### `wrapProgramBinary` \<executable\> \<makeBinaryWrapperArgs\> {#fun-wrapProgramBinary}
doronbehar marked this conversation as resolved.
Show resolved Hide resolved

Convenience function for `makeBinaryWrapper` that replaces `<\executable\>` with a wrapper that executes the original program. It takes all the same arguments as `makeBinaryWrapper`, except for `--argv0`.

It cannot be applied multiple times, since it will overwrite the wrapper file.
If you will apply it multiple times, it will overwrite the wrapper file and you will end up with double wrapping, which should be avoided.

## Package setup hooks {#ssec-setup-hooks}

Expand Down
6 changes: 6 additions & 0 deletions maintainers/maintainer-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,12 @@
githubId = 251106;
name = "Daniel Bergey";
};
bergkvist = {
email = "tobias@bergkv.ist";
github = "bergkvist";
githubId = 410028;
name = "Tobias Bergkvist";
};
betaboon = {
email = "betaboon@0x80.ninja";
github = "betaboon";
Expand Down
297 changes: 297 additions & 0 deletions pkgs/build-support/setup-hooks/make-binary-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# Assert that FILE exists and is executable
#
# assertExecutable FILE
assertExecutable() {
local file="$1"
[[ -f "$file" && -x "$file" ]] || \
doronbehar marked this conversation as resolved.
Show resolved Hide resolved
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 <PROGRAM> <MAKE-WRAPPER FLAGS...>
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")
bergkvist marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another "just an idea": you're invoking the compiler without any warnings turned on, just emitting all of those parts all the time would simplify the logic here and result in no larger of a binary (assuming that the code isn't embedded into the executable anymore, of course)—all the main concatenation could go away as well

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 <unistd.h>"
printf '%s\n' "#include <stdlib.h>"
[ -z "$uses_assert" ] || printf '%s\n' "#include <assert.h>"
[ -z "$uses_stdio" ] || printf '%s\n' "#include <stdio.h>"
[ -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() {
bergkvist marked this conversation as resolved.
Show resolved Hide resolved
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));
}
}
"
}
2 changes: 2 additions & 0 deletions pkgs/test/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ with pkgs;

macOSSierraShared = callPackage ./macos-sierra-shared {};

make-binary-wrapper = callPackage ./make-binary-wrapper { inherit makeBinaryWrapper; };

cross = callPackage ./cross {};

rustCustomSysroot = callPackage ./rust-sysroot {};
Expand Down
21 changes: 21 additions & 0 deletions pkgs/test/make-binary-wrapper/add-flags.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>

int main(int argc, char **argv) {
char **argv_tmp = calloc(5 + argc, sizeof(*argv_tmp));
assert(argv_tmp != NULL);
argv_tmp[0] = argv[0];
argv_tmp[1] = "-x";
argv_tmp[2] = "-y";
argv_tmp[3] = "-z";
argv_tmp[4] = "-abc";
for (int i = 1; i < argc; ++i) {
argv_tmp[4 + i] = argv[i];
}
argv_tmp[4 + argc] = NULL;
argv = argv_tmp;

argv[0] = "/send/me/flags";
return execv("/send/me/flags", argv);
}
2 changes: 2 additions & 0 deletions pkgs/test/make-binary-wrapper/add-flags.cmdline
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--add-flags "-x -y -z" \
--add-flags -abc
6 changes: 6 additions & 0 deletions pkgs/test/make-binary-wrapper/add-flags.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CWD=SUBST_CWD
SUBST_ARGV0
-x
-y
-z
-abc
Loading