diff --git a/pressure-vessel-wrap b/pressure-vessel-wrap index 140bb2683669933d0c44b7dcdda6a3d74d12ccf6..04ed1f50aadc1c3221bd817944ba7800a3f0d105 100755 --- a/pressure-vessel-wrap +++ b/pressure-vessel-wrap @@ -28,8 +28,11 @@ set -e set -o pipefail set -u +shopt -s nullglob me="$0" +me="$(readlink -f "$0")" +here="${me%/*}" me="${me##*/}" # Undo any weird environment before we start running external @@ -88,11 +91,16 @@ if [ -z "$BWRAP" ] || ! "$BWRAP" --bind / / true; then fi declare -a bwrap_options +declare -a multiarch_tuples declare -a wrapped_command fake_home= interactive= +runtime= +tmpdir= verbose= +multiarch_tuples=(x86_64-linux-gnu i386-linux-gnu) + # Pop the pressure-vessel-wrap options from $@, leaving the command # and arguments. @@ -124,6 +132,10 @@ usage () { echo "--interactive Run an interactive shell instead of" echo " COMMAND. Executing \"\$@\" in that" echo " shell will run COMMAND [ARGS]." + echo "--runtime=RUNTIME Mount the given sysroot or" + echo " merged /usr in the container," + echo " and augment it with the host" + echo " system's graphics stack." echo "--verbose Be more verbose." echo @@ -131,7 +143,7 @@ usage () { } getopt_temp="$(getopt -o '' --long \ - 'freedesktop-app-id:,help,home:,interactive,steam-app-id:,verbose' \ + 'freedesktop-app-id:,help,home:,interactive,runtime:,steam-app-id:,verbose' \ -n "$me" -- "$@")" eval set -- "$getopt_temp" @@ -159,6 +171,11 @@ while [ "$#" -gt 0 ]; do shift ;; + (--runtime) + runtime="$2" + shift 2 + ;; + (--verbose) verbose=yes shift @@ -233,6 +250,18 @@ if [ -z "$fake_home" ]; then fi fi +cleanup () { + if [ -n "$tmpdir" ]; then + rm -fr "$tmpdir" + fi +} + +trap cleanup EXIT + +tmpdir="$(mktemp -d)" +scratch="${tmpdir}/scratch" +mkdir "$scratch" + bwrap_options=() # Protect the controlling terminal from the app/game, unless we are @@ -251,15 +280,275 @@ fi if [ -n "$caller_path" ]; then bwrap_options+=(--setenv PATH "$caller_path") fi -if [ -n "$caller_ld_library_path" ]; then - bwrap_options+=(--setenv LD_LIBRARY_PATH "$caller_ld_library_path") + +overrides= + +for t in "${multiarch_tuples[@]}"; do + overrides="${overrides:+"$overrides:"}/overrides/lib/$t" +done + +if [ -n "$runtime" ]; then + # The LD_LIBRARY_PATH from the host system is not very relevant + # inside the container, so skip it + bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides") +elif [ -n "$caller_ld_library_path" ]; then + bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides:$caller_ld_library_path") else - bwrap_options+=(--unsetenv LD_LIBRARY_PATH) + bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides") fi -# Everything comes from the root filesystem for now, except where -# modified below -bwrap_options+=(--bind / /) +declare -a bind_usr_result +bind_usr () { + local tree="$1" + local dest="$2" + local dir + bind_usr_result=() + + if [ -d "$tree/usr" ]; then + bind_usr_result+=(--ro-bind "$tree/usr" "$dest/usr") + else + bind_usr_result+=(--ro-bind "$tree" "$dest/usr") + fi + + for dir in "$tree"/lib* "$tree"/bin "$tree"/sbin; do + if [ -d "$dir" ]; then + local basename + basename="$(basename "$dir")" + + if [ -d "$tree/usr" ]; then + bind_usr_result+=(--ro-bind "$dir" "$dest/$basename") + else + bind_usr_result+=(--symlink "usr/$basename" "$dest/$basename") + fi + fi + done + + for etc in "/etc/ld.so.cache" "/etc/alternatives"; do + if [ -e "$tree$etc" ]; then + bind_usr_result+=(--ro-bind "$tree$etc" "$dest$etc") + fi + done +} + +if [ -n "$runtime" ]; then + uid="$(id -u)" + + bind_usr "$runtime" / + bwrap_options+=("${bind_usr_result[@]}") + bwrap_options+=(--setenv XDG_RUNTIME_DIR "/run/user/$uid") + bwrap_options+=(--proc /proc) + bwrap_options+=(--ro-bind /sys /sys) + bwrap_options+=(--tmpfs /run) + bwrap_options+=(--tmpfs /tmp) + bwrap_options+=(--tmpfs /var) + bwrap_options+=(--symlink ../run /var/run) + + for dir in /etc /var/cache /var/lib; do + if [ -d "$runtime$dir" ]; then + for member in "$runtime$dir"/*; do + member="$(basename "$member")" + + # These paths from the container aren't made available + case "$dir/$member" in + (/etc/group|/etc/passwd) + break + ;; + (/etc/host.conf|/etc/hosts|/etc/localtime) + break + ;; + (/etc/machine-id|/etc/resolv.conf) + break + ;; + (/var/lib/dbus|/var/lib/dhcp|/var/lib/sudo|/var/lib/urandom) + break + ;; + esac + + if target="$(readlink "$runtime$dir/$member")"; then + bwrap_options+=(--symlink "$target" "$dir/$member") + else + bwrap_options+=(--ro-bind "$runtime$dir/$member" \ + "$dir/$member") + fi + done + fi + done + + # Take the machine ID from the host + if [ -e /etc/machine-id ]; then + bwrap_options+=(--ro-bind /etc/machine-id /etc/machine-id) + bwrap_options+=(--symlink /etc/machine-id /var/lib/dbus/machine-id) + elif [ -e /var/lib/dbus/machine-id ]; then + bwrap_options+=(--ro-bind /var/lib/dbus/machine-id /etc/machine-id) + bwrap_options+=(--symlink /etc/machine-id /var/lib/dbus/machine-id) + fi + + # Take the timezone from the host + if target="$(readlink /etc/localtime)" && \ + [ "${target#/usr/}" != "$target" ]; then + bwrap_options+=(--symlink "$target" /etc/localtime) + elif [ -e /etc/localtime ]; then + bwrap_options+=(--ro-bind /etc/localtime /etc/localtime) + fi + + for file in resolv.conf host.conf hosts passwd group; do + if [ -e "/etc/$file" ]; then + bwrap_options+=(--ro-bind "/etc/$file" "/etc/$file") + fi + done + + # Simplified version of flatpak run --socket=X11 + if [ -n "${DISPLAY-}" ]; then + bwrap_options+=(--bind /tmp/.X11-unix /tmp/.X11-unix) + bwrap_options+=(--setenv DISPLAY "$DISPLAY") + fi + for xauth in "${XAUTHORITY-}" "$HOME/.Xauthority"; do + if [ -n "$xauth" ] && [ -e "$xauth" ]; then + bwrap_options+=(--ro-bind "$xauth" "/run/user/$uid/Xauthority") + bwrap_options+=(--setenv XAUTHORITY "/run/user/$uid/Xauthority") + break + fi + done + + # Simplified version of flatpak run --socket=wayland + if [ -n "${XDG_RUNTIME_DIR-}" ] && \ + [ -S "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY:="wayland-0"}" ]; then + bwrap_options+=(--bind "$XDG_RUNTIME_DIR/${WAYLAND_DISPLAY}" \ + "/run/user/$uid/$WAYLAND_DISPLAY") + fi + + # Simplified version of flatpak run --socket=pulseaudio, modified to + # support the SteamOS system-wide PulseAudio instance + if false; then # FIXME + if [ -n "${XDG_RUNTIME_DIR-}" ] && \ + [ -S "$XDG_RUNTIME_DIR/pulse/native" ]; then + bwrap_options+=(--bind "$XDG_RUNTIME_DIR/pulse/native" \ + "/run/user/$uid/pulse/native") + bwrap_options+=(--setenv PULSE_SERVER "unix:/run/user/$uid/pulse/native") + elif [ -S /var/run/pulse/native ]; then + bwrap_options+=(--bind /var/run/pulse/native \ + "/run/user/$uid/pulse/native") + bwrap_options+=(--setenv PULSE_SERVER "unix:/run/user/$uid/pulse/native") + fi + fi + + # Simplified version of flatpak run --socket=system-bus + if [ -n "${DBUS_SYSTEM_BUS_ADDRESS-}" ]; then + path="$(perl -e '$_ = shift; print $1 if m/^unix:path=([^,;]*)/' \ + "$DBUS_SYSTEM_BUS_ADDRESS")" + if [ -n "$path" ] && [ -S "$path" ]; then + bwrap_options+=(--bind "$path" /var/run/dbus/system_bus_socket) + bwrap_options+=(--unsetenv DBUS_SYSTEM_BUS_ADDRESS) + else + echo "$me: warning: Unable to parse DBUS_SYSTEM_BUS_ADDRESS" \ + "\"$DBUS_SYSTEM_BUS_ADDRESS\"" >&2 + fi + elif [ -S /var/run/dbus/system_bus_socket ]; then + bwrap_options+=(--bind /var/run/dbus/system_bus_socket \ + /run/dbus/system_bus_socket) + fi + + # Simplified version of flatpak run --socket=system-bus + if [ -n "${DBUS_SESSION_BUS_ADDRESS-}" ]; then + path="$(perl -e '$_ = shift; print $1 if m/^unix:path=([^,;]*)/' \ + "$DBUS_SESSION_BUS_ADDRESS")" + if [ "${DBUS_SESSION_BUS_ADDRESS#*abstract=}" != "$DBUS_SESSION_BUS_ADDRESS" ]; then + : # Do nothing: abstract Unix sockets can pass through + elif [ -n "$path" ] && [ -S "$path" ]; then + bwrap_options+=(--bind "$path" "/run/user/$uid/bus") + bwrap_options+=(--setenv DBUS_SESSION_BUS_ADDRESS \ + "unix:path=/run/user/$uid/bus") + else + echo "$me: warning: Unable to parse DBUS_SESSION_BUS_ADDRESS" \ + "\"$DBUS_SESSION_BUS_ADDRESS\"" >&2 + fi + elif [ -n "${XDG_RUNTIME_DIR-}" ] && \ + [ -S "$XDG_RUNTIME_DIR/bus" ]; then + bwrap_options+=(--bind "$XDG_RUNTIME_DIR/bus" \ + "/run/user/$uid/bus") + bwrap_options+=(--setenv DBUS_SESSION_BUS_ADDRESS \ + "unix:path=/run/user/$uid/bus") + fi + + dri_path= + + for t in "${multiarch_tuples[@]}"; do + # This has the side-effect of testing whether we can run + # binaries for this architecture + if ldso="$($here/${t}-capsule-capture-libs --print-ld.so)"; then + dri_path="${dri_path:+"$dri_path:"}/overrides/lib/$t/dri" + + mkdir -p "$tmpdir/overrides/lib/$t" + + bind_usr "$runtime" "$scratch" + + "$BWRAP" \ + --ro-bind / / \ + --bind "$tmpdir/overrides" "$tmpdir/overrides" \ + --tmpfs "$scratch" \ + "${bind_usr_result[@]}" \ + "$here/${t}-capsule-capture-libs" \ + --container="$scratch" \ + --link-target="/run/host" \ + --dest="$tmpdir/overrides/lib/$t" \ + --provider="/" \ + gl: + + # If we are going to use the host system's libc6 (likely) + # then we have to use its ld.so too. + if [ -L "$tmpdir/overrides/lib/$t/libc.so.6" ]; then + real_path_in_host="$(readlink -f "$ldso")" + real_path_in_runtime="$("$BWRAP" \ + --ro-bind / / \ + --bind "$tmpdir/overrides" "$tmpdir/overrides" \ + --tmpfs "$scratch" \ + "${bind_usr_result[@]}" \ + "$here/${t}-capsule-capture-libs" \ + --resolve-ld.so="$scratch")" + + if [ -d "$runtime/usr" ] || \ + [ "$real_path_in_runtime#/usr/" != "$real_path_in_runtime" ]; then + bwrap_options+=(--ro-bind "$real_path_in_host" \ + "$real_path_in_runtime") + else + # /lib, /lib64 are just going to be symlinks anyway + bwrap_options+=(--ro-bind "$real_path_in_host" \ + "/usr$real_path_in_runtime") + fi + fi + + # /lib, /lib32 or /lib64 + libQUAL="$(dirname "$ldso")" + + for dir in "/lib/$t" "/usr/lib/$t" "$libQUAL" "/usr$libQUAL"; do + if [ -d "$dir/dri" ]; then + ln -s "/run/host$dir/dri" "$tmpdir/overrides/lib/$t/dri" + + "$BWRAP" \ + --ro-bind / / \ + --bind "$tmpdir/overrides" "$tmpdir/overrides" \ + --tmpfs "$scratch" \ + "${bind_usr_result[@]}" \ + "$here/${t}-capsule-capture-libs" \ + --container="$scratch" \ + --link-target="/run/host" \ + --dest="$tmpdir/overrides/lib/$t" \ + --provider="/" \ + "only-dependencies:path-match:$dir/dri/*" + fi + done + fi + done + + bwrap_options+=(--setenv LIBGL_DRIVERS_PATH "$dri_path") + bwrap_options+=(--ro-bind "$tmpdir/overrides" /overrides) + bind_usr / /run/host + bwrap_options+=("${bind_usr_result[@]}") +else + # Everything comes from the root filesystem, except where + # modified below + bwrap_options+=(--bind / /) +fi # Make /dev available bwrap_options+=(--dev-bind /dev /dev) @@ -359,6 +648,11 @@ fi # Replace this process with bwrap, which replaces itself with the # desired command (unless exec fails) if [ -n "$verbose" ]; then + echo "$BWRAP options:">&2 + for x in "${bwrap_options[@]}"; do + printf '\t%s\n' "$x" + done + echo "$me: '$BWRAP ${bwrap_options[*]} $bwrap_end_of_options ${wrapped_command[*]}'" >&2 fi