{ lib , stdenv , callPackage , runCommandLocal , writeShellScript , glibc , pkgsi686Linux , runCommandCC , coreutils , bubblewrap }: { runScript ? "bash" , nativeBuildInputs ? [] , extraInstallCommands ? "" , meta ? {} , passthru ? {} , extraPreBwrapCmds ? "" , extraBwrapArgs ? [] , unshareUser ? false , unshareIpc ? false , unsharePid ? false , unshareNet ? false , unshareUts ? false , unshareCgroup ? false , privateTmp ? false , dieWithParent ? true , ... } @ args: assert (!args ? pname || !args ? version) -> (args ? name); # You must provide name if pname or version (preferred) is missing. let inherit (lib) concatLines concatStringsSep escapeShellArgs filter optionalString splitString ; inherit (lib.attrsets) removeAttrs; name = args.name or "${args.pname}-${args.version}"; executableName = args.pname or args.name; # we don't know which have been supplied, and want to avoid defaulting missing attrs to null. Passed into runCommandLocal nameAttrs = lib.filterAttrs (key: value: builtins.elem key [ "name" "pname" "version" ]) args; buildFHSEnv = callPackage ./buildFHSEnv.nix { }; fhsenv = buildFHSEnv (removeAttrs args [ "runScript" "extraInstallCommands" "meta" "passthru" "extraPreBwrapCmds" "extraBwrapArgs" "dieWithParent" "unshareUser" "unshareCgroup" "unshareUts" "unshareNet" "unsharePid" "unshareIpc" "privateTmp" ]); etcBindEntries = let files = [ # NixOS Compatibility "static" "nix" # mainly for nixUnstable users, but also for access to nix/netrc # Shells "shells" "bashrc" "zshenv" "zshrc" "zinputrc" "zprofile" # Users, Groups, NSS "passwd" "group" "shadow" "hosts" "resolv.conf" "nsswitch.conf" # User profiles "profiles" # Sudo & Su "login.defs" "sudoers" "sudoers.d" # Time "localtime" "zoneinfo" # Other Core Stuff "machine-id" "os-release" # PAM "pam.d" # Fonts "fonts" # ALSA "alsa" "asound.conf" # SSL "ssl/certs" "ca-certificates" "pki" ]; in map (path: "/etc/${path}") files; # Here's the problem case: # - we need to run bash to run the init script # - LD_PRELOAD may be set to another dynamic library, requiring us to discover its dependencies # - oops! ldconfig is part of the init script, and it hasn't run yet # - everything explodes # # In particular, this happens with fhsenvs in fhsenvs, e.g. when running # a wrapped game from Steam. # # So, instead of doing that, we build a tiny static (important!) shim # that executes ldconfig in a completely clean environment to generate # the initial cache, and then execs into the "real" init, which is the # first time we see anything dynamically linked at all. # # Also, the real init is placed strategically at /init, so we don't # have to recompile this every time. containerInit = runCommandCC "container-init" { buildInputs = [ stdenv.cc.libc.static or null ]; } '' $CXX -static -s -o $out ${./container-init.cc} ''; realInit = run: writeShellScript "${name}-init" '' source /etc/profile exec ${run} "$@" ''; indentLines = str: concatLines (map (s: " " + s) (filter (s: s != "") (splitString "\n" str))); bwrapCmd = { initArgs ? "" }: '' ignored=(/nix /dev /proc /etc ${optionalString privateTmp "/tmp"}) ro_mounts=() symlinks=() etc_ignored=() ${extraPreBwrapCmds} # loop through all entries of root in the fhs environment, except its /etc. for i in ${fhsenv}/*; do path="/''${i##*/}" if [[ $path == '/etc' ]]; then : elif [[ -L $i ]]; then symlinks+=(--symlink "$(${coreutils}/bin/readlink "$i")" "$path") ignored+=("$path") else ro_mounts+=(--ro-bind "$i" "$path") ignored+=("$path") fi done # loop through the entries of /etc in the fhs environment. if [[ -d ${fhsenv}/etc ]]; then for i in ${fhsenv}/etc/*; do path="/''${i##*/}" # NOTE: we're binding /etc/fonts and /etc/ssl/certs from the host so we # don't want to override it with a path from the FHS environment. if [[ $path == '/fonts' || $path == '/ssl' ]]; then continue fi if [[ -L $i ]]; then symlinks+=(--symlink "$i" "/etc$path") else ro_mounts+=(--ro-bind "$i" "/etc$path") fi etc_ignored+=("/etc$path") done fi # propagate /etc from the actual host if nested if [[ -e /.host-etc ]]; then ro_mounts+=(--ro-bind /.host-etc /.host-etc) else ro_mounts+=(--ro-bind /etc /.host-etc) fi # link selected etc entries from the actual root for i in ${escapeShellArgs etcBindEntries}; do if [[ "''${etc_ignored[@]}" =~ "$i" ]]; then continue fi if [[ -e $i ]]; then symlinks+=(--symlink "/.host-etc/''${i#/etc/}" "$i") fi done declare -a auto_mounts # loop through all directories in the root for dir in /*; do # if it is a directory and it is not ignored if [[ -d "$dir" ]] && [[ ! "''${ignored[@]}" =~ "$dir" ]]; then # add it to the mount list auto_mounts+=(--bind "$dir" "$dir") fi done declare -a x11_args # Always mount a tmpfs on /tmp/.X11-unix # Rationale: https://github.com/flatpak/flatpak/blob/be2de97e862e5ca223da40a895e54e7bf24dbfb9/common/flatpak-run.c#L277 x11_args+=(--tmpfs /tmp/.X11-unix) # Try to guess X socket path. This doesn't cover _everything_, but it covers some things. if [[ "$DISPLAY" == *:* ]]; then # recover display number from $DISPLAY formatted [host]:num[.screen] display_nr=''${DISPLAY/#*:} # strip host display_nr=''${display_nr/%.*} # strip screen local_socket=/tmp/.X11-unix/X$display_nr x11_args+=(--ro-bind-try "$local_socket" "$local_socket") fi ${optionalString privateTmp '' # sddm places XAUTHORITY in /tmp if [[ "$XAUTHORITY" == /tmp/* ]]; then x11_args+=(--ro-bind-try "$XAUTHORITY" "$XAUTHORITY") fi # dbus-run-session puts the socket in /tmp IFS=";" read -ra addrs <<<"$DBUS_SESSION_BUS_ADDRESS" for addr in "''${addrs[@]}"; do [[ "$addr" == unix:* ]] || continue IFS="," read -ra parts <<<"''${addr#unix:}" for part in "''${parts[@]}"; do printf -v part '%s' "''${part//\\/\\\\}" printf -v part '%b' "''${part//%/\\x}" [[ "$part" == path=/tmp/* ]] || continue x11_args+=(--ro-bind-try "''${part#path=}" "''${part#path=}") done done ''} cmd=( ${bubblewrap}/bin/bwrap --dev-bind /dev /dev --proc /proc --chdir "$(pwd)" ${optionalString unshareUser "--unshare-user"} ${optionalString unshareIpc "--unshare-ipc"} ${optionalString unsharePid "--unshare-pid"} ${optionalString unshareNet "--unshare-net"} ${optionalString unshareUts "--unshare-uts"} ${optionalString unshareCgroup "--unshare-cgroup"} ${optionalString dieWithParent "--die-with-parent"} --ro-bind /nix /nix ${optionalString privateTmp "--tmpfs /tmp"} # Our glibc will look for the cache in its own path in `/nix/store`. # As such, we need a cache to exist there, because pressure-vessel # depends on the existence of an ld cache. However, adding one # globally proved to be a bad idea (see #100655), the solution we # settled on being mounting one via bwrap. # Also, the cache needs to go to both 32 and 64 bit glibcs, for games # of both architectures to work. --tmpfs ${glibc}/etc \ --tmpfs /etc \ --symlink /etc/ld.so.conf ${glibc}/etc/ld.so.conf \ --symlink /etc/ld.so.cache ${glibc}/etc/ld.so.cache \ --ro-bind ${glibc}/etc/rpc ${glibc}/etc/rpc \ --remount-ro ${glibc}/etc \ --symlink ${realInit runScript} /init \ '' + optionalString fhsenv.isMultiBuild (indentLines '' --tmpfs ${pkgsi686Linux.glibc}/etc \ --symlink /etc/ld.so.conf ${pkgsi686Linux.glibc}/etc/ld.so.conf \ --symlink /etc/ld.so.cache ${pkgsi686Linux.glibc}/etc/ld.so.cache \ --ro-bind ${pkgsi686Linux.glibc}/etc/rpc ${pkgsi686Linux.glibc}/etc/rpc \ --remount-ro ${pkgsi686Linux.glibc}/etc \ '') + '' "''${ro_mounts[@]}" "''${symlinks[@]}" "''${auto_mounts[@]}" "''${x11_args[@]}" ${concatStringsSep "\n " extraBwrapArgs} ${containerInit} ${initArgs} ) exec "''${cmd[@]}" ''; bin = writeShellScript "${name}-bwrap" (bwrapCmd { initArgs = ''"$@"''; }); in runCommandLocal name (nameAttrs // { inherit nativeBuildInputs meta; passthru = passthru // { env = runCommandLocal "${name}-shell-env" { shellHook = bwrapCmd {}; } '' echo >&2 "" echo >&2 "*** User chroot 'env' attributes are intended for interactive nix-shell sessions, not for building! ***" echo >&2 "" exit 1 ''; inherit args fhsenv; }; }) '' mkdir -p $out/bin ln -s ${bin} $out/bin/${executableName} ${extraInstallCommands} ''