Uses bubblewrap under the hood. Targeting NixOS, but can be modified to be run on other distributions
chmod +x run-wrap.fish
./run-wrap.fish <exe>
# check mounted paths
./run-wrap.fish findmntUses bubblewrap under the hood. Targeting NixOS, but can be modified to be run on other distributions
chmod +x run-wrap.fish
./run-wrap.fish <exe>
# check mounted paths
./run-wrap.fish findmnt| #!/usr/bin/env -S nix shell nixpkgs#fish nixpkgs#bubblewrap --command fish | |
| # Run executable in isolated environment | |
| # - share network | |
| # - inherit /nix/store paths from $PATH | |
| # - inherit some env variables | |
| # - ro bind /nix, /bin, /usr/bin folders, ssl certs | |
| # - rw bind current directory | |
| # - ro bind .git and .jj | |
| # - hide git-ignored paths in current directory (bind as empty dirs/files). | |
| # can be overriden by profiles | |
| # - when running inside git-ignored dir, bind it as-is | |
| # - bind known dirs/files for some profiles. currently for: | |
| # - go | |
| # - nodejs | |
| # - nodejs-npm | |
| # - nodejs-yarn | |
| # - rust | |
| # utils | |
| function echo-lines; for a in $argv; echo $a; end; end | |
| function ro-bind -a name; echo-lines --ro-bind (realpath $name) $name; end | |
| function ro-bind-exists -a name | |
| if test -e $name; ro-bind $name; end | |
| end | |
| function passenv -a name; echo-lines --setenv "$name" (eval "echo \$$name"); end | |
| function with_color -a color | |
| set_color $color | |
| echo -n $argv[2..] | |
| set_color normal | |
| end | |
| function log; echo $argv >&2; end | |
| function warn; log (with_color yellow $argv); end | |
| # help | |
| if test "$argv[1]" = '-h' | |
| set -l name (status basename) | |
| log "Run executable in isolated environment | |
| Usage: | |
| $name <exe> | |
| $name <exe> [exe args..] | |
| $name <exe> -- [bwrap args..] | |
| $name <exe> [exe args..] -- [bwrap args..]" | |
| exit | |
| end | |
| # parse args | |
| set -l exe $argv[1] | |
| if test -z $exe | |
| log Executable is required: $(status basename) '<exe>' | |
| exit 1 | |
| end | |
| set -l argv $argv[2..] | |
| set -l exe_args | |
| set -l bwrap_args | |
| set -l arg_sep_found false | |
| for arg in $argv | |
| if test $arg = '--' | |
| set arg_sep_found true | |
| continue | |
| end | |
| if $arg_sep_found | |
| set -a bwrap_args $arg | |
| else | |
| set -a exe_args $arg | |
| end | |
| end | |
| # basic variables | |
| set -l dir (pwd) | |
| set -l user (whoami) | |
| set -l uid (id -u) | |
| set -l home $HOME | |
| # -g so variable is visible in functions | |
| set -g tmpdir /tmp/tmp.run-wrap | |
| mkdir -p $tmpdir | |
| getent passwd $uid 65534 > $tmpdir/passwd | |
| getent group (id -g) 65534 > $tmpdir/group | |
| touch $tmpdir/empty | |
| mkdir -p $tmpdir/emptydir | |
| echo 'experimental-features = nix-command flakes' > $tmpdir/nix.conf | |
| function _hide -a empty name; echo-lines --ro-bind $empty $name; end | |
| function hide-dir -a name; _hide $tmpdir/emptydir $name; end | |
| function hide-file -a name; _hide $tmpdir/empty $name; end | |
| # extra bwrap args | |
| set -l args | |
| # list of git-ignored paths that will be handled by profile | |
| set -l allow_ignored | |
| set -l profile unset | |
| # detect profile | |
| if test -f Cargo.toml; set profile rust; end | |
| if test -f go.mod; set profile golang; end | |
| if test -f package.json; | |
| if test -f package-lock.json; set profile nodejs-npm; end | |
| if test -f yarn.lock; set profile nodejs-yarn; end | |
| end | |
| # profile-specific binds | |
| if test $profile = golang | |
| set -a args (ro-bind $home/go) | |
| end | |
| if test $profile = nodejs-npm | |
| set -a args (ro-bind $home/.npm) | |
| end | |
| if test $profile = nodejs-yarn | |
| set -a args (ro-bind $home/.config/yarn) | |
| set -a args (ro-bind $home/.yarn) | |
| end | |
| if test $profile = rust | |
| set -a allow_ignored target/ | |
| set -a args (ro-bind $home/.cargo) | |
| set -a args (hide-file $home/.cargo/credentials.toml) | |
| set -a args (ro-bind $home/.rustup) | |
| end | |
| # hide .git and git-ignored dirs/files in current dir. profiles can override it | |
| set -l gitroot | |
| set -l gitignored | |
| set -l ingit false | |
| set -l ingitignored false | |
| if git rev-parse &>/dev/null | |
| set gitroot (git rev-parse --show-toplevel) | |
| set ingit true | |
| function ignored | |
| git status --ignored --porcelain=v1 | string match -r --groups-only '^!! (.+)' | |
| end | |
| if git check-ignore --quiet $dir | |
| # current directory is ignored | |
| set ingitignored true | |
| else if test $gitroot = $dir | |
| set gitignored (ignored) | |
| else | |
| # path relative to git root | |
| set -l subdir (realpath --relative-to=$gitroot $dir) | |
| # ignored paths relative to the current directory | |
| for path in (ignored) | |
| set -l prefix "^$subdir/" | |
| if string match --quiet -r $prefix $path | |
| set -a gitignored (string replace -r $prefix '' $path) | |
| end | |
| end | |
| end | |
| end | |
| if test -e .git; set -a args (ro-bind $dir/.git); end | |
| for path in $gitignored | |
| if contains $path $allow_ignored | |
| continue | |
| else if contains $path .jj/ | |
| set -a args (ro-bind $dir/$path) | |
| continue | |
| end | |
| set -l path $dir/$path | |
| if test -d $path | |
| set -a args (hide-dir $path) | |
| else if test -f $path | |
| # todo: also mount symlink. currently not possible, see | |
| # https://github.com/containers/bubblewrap/issues/390 | |
| set -a args (hide-file $path) | |
| end | |
| end | |
| # inherit paths from PATH pointing directly to /nix/store, they're from nix shell | |
| set -l --path path /bin /usr/bin | |
| # skip binaries from this script | |
| set -l skip_nix fish bubblewrap | |
| set -l semver_regex '\d+\.\d+\.\d+' | |
| for p in $PATH | |
| set -l skip false | |
| for name in $skip_nix | |
| if string match --quiet -r "^/nix/store.+$name-$semver_regex/bin\$" $p | |
| set skip true | |
| break | |
| end | |
| end | |
| if $skip; continue; end | |
| if string match -r '^/nix/store' $p --quiet | |
| set -a path $p | |
| end | |
| end | |
| log Running (with_color cyan $exe) in isolation | |
| if ! $ingit | |
| warn Running outside of git repo | |
| else if $ingitignored | |
| warn Current directory is git-ignored, mounting as-is | |
| end | |
| exec bwrap \ | |
| --die-with-parent \ | |
| --dev /dev \ | |
| --proc /proc \ | |
| --tmpfs /tmp \ | |
| (ro-bind /nix) \ | |
| --ro-bind /run/current-system/sw/bin /bin \ | |
| --ro-bind /etc/profiles/per-user/$user/bin /usr/bin \ | |
| --unshare-all \ | |
| --share-net \ | |
| # network settings | |
| (ro-bind /etc/resolv.conf) \ | |
| # ssl certs | |
| (ro-bind /etc/ssl/certs/ca-bundle.crt) \ | |
| (ro-bind /etc/ssl/certs/ca-certificates.crt) \ | |
| # basic user info | |
| --ro-bind $tmpdir/passwd /etc/passwd \ | |
| --ro-bind $tmpdir/group /etc/group \ | |
| # nix config | |
| --ro-bind $tmpdir/nix.conf $home/.config/nix/nix.conf \ | |
| # shell configs | |
| # fish | |
| (ro-bind-exists $home/.config/fish) \ | |
| (ro-bind-exists /etc/config.fish) \ | |
| (ro-bind-exists /etc/generated_completions) \ | |
| # other | |
| (ro-bind-exists $home/.config/starship.toml) \ | |
| --clearenv \ | |
| # $path should be quoted so its values are separated with ":" | |
| --setenv PATH "$path" \ | |
| (passenv HOME) \ | |
| (passenv NIX_PATH) \ | |
| (passenv EDITOR) \ | |
| (passenv LS_COLORS) \ | |
| (passenv TERM) \ | |
| --setenv SHLVL (math max 0, (math $SHLVL - 1)) \ | |
| --bind $dir $dir \ | |
| $bwrap_args $args $exe $exe_args |