14 , nativeBuildInputs ? []
15 , extraInstallCommands ? ""
18 , extraPreBwrapCmds ? ""
25 , unshareCgroup ? false
27 , dieWithParent ? true
31 assert (!args ? pname || !args ? version) -> (args ? name); # You must provide name if pname or version (preferred) is missing.
43 inherit (lib.attrsets) removeAttrs;
45 name = args.name or "${args.pname}-${args.version}";
46 executableName = args.pname or args.name;
47 # we don't know which have been supplied, and want to avoid defaulting missing attrs to null. Passed into runCommandLocal
48 nameAttrs = lib.filterAttrs (key: value: builtins.elem key [ "name" "pname" "version" ]) args;
50 buildFHSEnv = callPackage ./buildFHSEnv.nix { };
52 fhsenv = buildFHSEnv (removeAttrs args [
53 "runScript" "extraInstallCommands" "meta" "passthru" "extraPreBwrapCmds" "extraBwrapArgs" "dieWithParent"
54 "unshareUser" "unshareCgroup" "unshareUts" "unshareNet" "unsharePid" "unshareIpc" "privateTmp"
61 "nix" # mainly for nixUnstable users, but also for access to nix/netrc
100 in map (path: "/etc/${path}") files;
102 # Here's the problem case:
103 # - we need to run bash to run the init script
104 # - LD_PRELOAD may be set to another dynamic library, requiring us to discover its dependencies
105 # - oops! ldconfig is part of the init script, and it hasn't run yet
106 # - everything explodes
108 # In particular, this happens with fhsenvs in fhsenvs, e.g. when running
109 # a wrapped game from Steam.
111 # So, instead of doing that, we build a tiny static (important!) shim
112 # that executes ldconfig in a completely clean environment to generate
113 # the initial cache, and then execs into the "real" init, which is the
114 # first time we see anything dynamically linked at all.
116 # Also, the real init is placed strategically at /init, so we don't
117 # have to recompile this every time.
118 containerInit = runCommandCC "container-init" {
119 buildInputs = [ stdenv.cc.libc.static or null ];
121 $CXX -static -s -o $out ${./container-init.cc}
124 realInit = run: writeShellScript "${name}-init" ''
129 indentLines = str: concatLines (map (s: " " + s) (filter (s: s != "") (splitString "\n" str)));
130 bwrapCmd = { initArgs ? "" }: ''
131 ignored=(/nix /dev /proc /etc ${optionalString privateTmp "/tmp"})
138 # loop through all entries of root in the fhs environment, except its /etc.
139 for i in ${fhsenv}/*; do
141 if [[ $path == '/etc' ]]; then
143 elif [[ -L $i ]]; then
144 symlinks+=(--symlink "$(${coreutils}/bin/readlink "$i")" "$path")
147 ro_mounts+=(--ro-bind "$i" "$path")
152 # loop through the entries of /etc in the fhs environment.
153 if [[ -d ${fhsenv}/etc ]]; then
154 for i in ${fhsenv}/etc/*; do
156 # NOTE: we're binding /etc/fonts and /etc/ssl/certs from the host so we
157 # don't want to override it with a path from the FHS environment.
158 if [[ $path == '/fonts' || $path == '/ssl' ]]; then
162 symlinks+=(--symlink "$i" "/etc$path")
164 ro_mounts+=(--ro-bind "$i" "/etc$path")
166 etc_ignored+=("/etc$path")
170 # propagate /etc from the actual host if nested
171 if [[ -e /.host-etc ]]; then
172 ro_mounts+=(--ro-bind /.host-etc /.host-etc)
174 ro_mounts+=(--ro-bind /etc /.host-etc)
177 # link selected etc entries from the actual root
178 for i in ${escapeShellArgs etcBindEntries}; do
179 if [[ "''${etc_ignored[@]}" =~ "$i" ]]; then
183 symlinks+=(--symlink "/.host-etc/''${i#/etc/}" "$i")
187 declare -a auto_mounts
188 # loop through all directories in the root
190 # if it is a directory and it is not ignored
191 if [[ -d "$dir" ]] && [[ ! "''${ignored[@]}" =~ "$dir" ]]; then
192 # add it to the mount list
193 auto_mounts+=(--bind "$dir" "$dir")
198 # Always mount a tmpfs on /tmp/.X11-unix
199 # Rationale: https://github.com/flatpak/flatpak/blob/be2de97e862e5ca223da40a895e54e7bf24dbfb9/common/flatpak-run.c#L277
200 x11_args+=(--tmpfs /tmp/.X11-unix)
202 # Try to guess X socket path. This doesn't cover _everything_, but it covers some things.
203 if [[ "$DISPLAY" == *:* ]]; then
204 # recover display number from $DISPLAY formatted [host]:num[.screen]
205 display_nr=''${DISPLAY/#*:} # strip host
206 display_nr=''${display_nr/%.*} # strip screen
207 local_socket=/tmp/.X11-unix/X$display_nr
208 x11_args+=(--ro-bind-try "$local_socket" "$local_socket")
211 ${optionalString privateTmp ''
212 # sddm places XAUTHORITY in /tmp
213 if [[ "$XAUTHORITY" == /tmp/* ]]; then
214 x11_args+=(--ro-bind-try "$XAUTHORITY" "$XAUTHORITY")
217 # dbus-run-session puts the socket in /tmp
218 IFS=";" read -ra addrs <<<"$DBUS_SESSION_BUS_ADDRESS"
219 for addr in "''${addrs[@]}"; do
220 [[ "$addr" == unix:* ]] || continue
221 IFS="," read -ra parts <<<"''${addr#unix:}"
222 for part in "''${parts[@]}"; do
223 printf -v part '%s' "''${part//\\/\\\\}"
224 printf -v part '%b' "''${part//%/\\x}"
225 [[ "$part" == path=/tmp/* ]] || continue
226 x11_args+=(--ro-bind-try "''${part#path=}" "''${part#path=}")
232 ${bubblewrap}/bin/bwrap
236 ${optionalString unshareUser "--unshare-user"}
237 ${optionalString unshareIpc "--unshare-ipc"}
238 ${optionalString unsharePid "--unshare-pid"}
239 ${optionalString unshareNet "--unshare-net"}
240 ${optionalString unshareUts "--unshare-uts"}
241 ${optionalString unshareCgroup "--unshare-cgroup"}
242 ${optionalString dieWithParent "--die-with-parent"}
244 ${optionalString privateTmp "--tmpfs /tmp"}
245 # Our glibc will look for the cache in its own path in `/nix/store`.
246 # As such, we need a cache to exist there, because pressure-vessel
247 # depends on the existence of an ld cache. However, adding one
248 # globally proved to be a bad idea (see #100655), the solution we
249 # settled on being mounting one via bwrap.
250 # Also, the cache needs to go to both 32 and 64 bit glibcs, for games
251 # of both architectures to work.
252 --tmpfs ${glibc}/etc \
254 --symlink /etc/ld.so.conf ${glibc}/etc/ld.so.conf \
255 --symlink /etc/ld.so.cache ${glibc}/etc/ld.so.cache \
256 --ro-bind ${glibc}/etc/rpc ${glibc}/etc/rpc \
257 --remount-ro ${glibc}/etc \
258 --symlink ${realInit runScript} /init \
259 '' + optionalString fhsenv.isMultiBuild (indentLines ''
260 --tmpfs ${pkgsi686Linux.glibc}/etc \
261 --symlink /etc/ld.so.conf ${pkgsi686Linux.glibc}/etc/ld.so.conf \
262 --symlink /etc/ld.so.cache ${pkgsi686Linux.glibc}/etc/ld.so.cache \
263 --ro-bind ${pkgsi686Linux.glibc}/etc/rpc ${pkgsi686Linux.glibc}/etc/rpc \
264 --remount-ro ${pkgsi686Linux.glibc}/etc \
268 "''${auto_mounts[@]}"
270 ${concatStringsSep "\n " extraBwrapArgs}
271 ${containerInit} ${initArgs}
276 bin = writeShellScript "${name}-bwrap" (bwrapCmd { initArgs = ''"$@"''; });
277 in runCommandLocal name (nameAttrs // {
278 inherit nativeBuildInputs meta;
280 passthru = passthru // {
281 env = runCommandLocal "${name}-shell-env" {
282 shellHook = bwrapCmd {};
285 echo >&2 "*** User chroot 'env' attributes are intended for interactive nix-shell sessions, not for building! ***"
293 ln -s ${bin} $out/bin/${executableName}
295 ${extraInstallCommands}