Skip to content
Snippets Groups Projects
pressure-vessel-wrap 26.73 KiB
#!/bin/bash

# pressure-vessel — use containers for Steam
#
# Copyright © 2017-2018 Collabora Ltd.
#
# SPDX-License-Identifier: MIT
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

set -e
set -o pipefail
set -u
shopt -s nullglob

me="$0"
me="$(readlink -f "$0")"
here="${me%/*}"
me="${me##*/}"

default_path="/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"

# Undo any weird environment before we start running external
# executables. We put it back before running the actual app/game.

if { [ -n "${LD_LIBRARY_PATH-}" ] && \
        [ "${LD_LIBRARY_PATH-}" != "${SYSTEM_LD_LIBRARY_PATH-}" ]; } \
    || [ -n "${LD_PRELOAD-}" ]; then
    caller_ld_preload="${LD_PRELOAD:-}"
    unset LD_PRELOAD

    caller_ld_library_path="${LD_LIBRARY_PATH:-}"

    if [ -n "${SYSTEM_LD_LIBRARY_PATH-}" ]; then
        export LD_LIBRARY_PATH="$SYSTEM_LD_LIBRARY_PATH"
    else
        unset LD_LIBRARY_PATH
    fi

    caller_path="${PATH:-"${default_path}"}"

    if [ -n "${SYSTEM_PATH-}" ]; then
        export PATH="$here:$SYSTEM_PATH"
    else
        export PATH="$here:$default_path"
    fi

    if [ -n "$caller_ld_preload" ]; then
        set -- --ld-preload="$caller_ld_preload" "$@"
    fi

    if [ -n "$caller_ld_library_path" ]; then
        set -- --ld-library-path="$caller_ld_library_path" "$@"
    fi

    if [ -n "$caller_path" ]; then
        set -- --path="$caller_path" "$@"
    fi

    exec "$0" "$@"
fi

# If bwrap isn't installed as a standalone binary on the PATH, see
# whether we have a copy of Flatpak configured with the bundled
# bubblewrap (--without-system-bubblewrap). If we do, use that one.
if [ -z "${BWRAP:-}" ]; then
    if ! BWRAP="$(command -v bwrap)"; then
        for dir in /usr/local/libexec /usr/libexec /usr/lib/flatpak; do
            if [ -x "$dir/flatpak-bwrap" ]; then
                BWRAP="$dir/flatpak-bwrap"
                break
            fi
        done
    fi
fi

declare -a bwrap_options
declare -a multiarch_tuples
declare -a wrapped_command
declare -a original_argv
original_argv=("$@")
fake_home=
host_fallback=
interactive=
ld_library_path=
ld_preload=
path=
runtime=
tmpdir=
verbose=
xterm=

multiarch_tuples=(x86_64-linux-gnu i386-linux-gnu)

# Pop the pressure-vessel-wrap options from $@, leaving the command
# and arguments.

usage () {
    code="$1"
    shift

    if [ "$code" -ne 0 ]; then
        exec >&2
    fi

    echo
    echo "Usage: $me [OPTIONS] [--] COMMAND [ARGS...]"
    echo
    echo "Run COMMAND [ARGS...] in a container that protects \$HOME."
    echo
    echo "The current working directory will be writeable for COMMAND,"
    echo "unless it is the real home directory."
    echo
    echo "Options:"
    echo "--steam-app-id=123            Use ~/.var/app/com.steampowered.App123"
    echo "                              as home directory."
    echo "                              [Default: \$SteamAppId]"
    echo "--freedesktop-app-id=ID       Use ~/.var/app/ID as home directory."
    echo "                              ID is com.example.MyApp or similar."
    echo "                              This interoperates with Flatpak."
    echo "--home=HOME                   Use HOME as home directory."
    echo
    echo "--host-fallback               Run COMMAND on the host system if we"
    echo "                              cannot run it in a container."
    echo "--interactive                 Run an interactive shell instead of"
    echo "                              COMMAND. Executing \"\$@\" in that"
    echo "                              shell will run COMMAND [ARGS]."
    echo "--ld-library-path=DIR[:DIR]   Set LD_LIBRARY_PATH to DIR[:DIR]"
    echo "                              when executing COMMAND."
    echo "--ld-preload=MODULE[:MODULE]  Set LD_PRELOAD to MODULE[:MODULE]"
    echo "                              when executing COMMAND."
    echo "--path=DIR[:DIR]              Set PATH to DIR[:DIR] when executing"
    echo "                              COMMAND."
    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 "--xterm                       Same as --interactive, but run an"
    echo "                              xterm in the container."
    echo

    exit "$code"
}

getopt_temp="help"
getopt_temp="${getopt_temp},freedesktop-app-id:"
getopt_temp="${getopt_temp},home:"
getopt_temp="${getopt_temp},host-fallback"
getopt_temp="${getopt_temp},interactive"
getopt_temp="${getopt_temp},ld-library-path:"
getopt_temp="${getopt_temp},ld-preload:"
getopt_temp="${getopt_temp},path:"
getopt_temp="${getopt_temp},runtime:"
getopt_temp="${getopt_temp},steam-app-id:"
getopt_temp="${getopt_temp},verbose"
getopt_temp="${getopt_temp},xterm"

getopt_temp="$(getopt -o '' --long "$getopt_temp" -n "$me" -- "$@")"
eval set -- "$getopt_temp"
unset getopt_temp

while [ "$#" -gt 0 ]; do
    case "$1" in
        (--freedesktop-app-id)
            fake_home="$HOME/.var/app/$2"
            shift 2
            ;;

        (--steam-app-id)
            fake_home="$HOME/.var/app/com.steampowered.App$2"
            shift 2
            ;;

        (--home)
            fake_home="$2"
            shift 2
            ;;

        (--host-fallback)
            host_fallback=yes
            shift
            ;;

        (--interactive)
            interactive=yes
            shift
            ;;

        (--ld-library-path)
            ld_library_path="$2"
            shift 2
            ;;

        (--ld-preload)
            ld_preload="$2"
            shift 2
            ;;

        (--path)
            path="$2"
            shift 2
            ;;

        (--runtime)
            runtime="$2"
            shift 2
            ;;

        (--verbose)
            verbose=yes
            shift
            ;;

        (--xterm)
            xterm=yes
            shift
            ;;

        (--help)
            usage 0
            # not reached
            ;;

        (--)
            shift
            break
            ;;

        (--*)
            echo "$me: unknown option: $1" >&2
            usage 2
            # not reached
            ;;

        (*)
            break
            ;;
    esac
done

if [ "$#" -eq 0 ]; then
    echo "$me: an executable to run is required" >&2
    exit 2
fi

if [ -z "$fake_home" ]; then
    if [ -n "${SteamAppId:-}" ]; then
        fake_home="$HOME/.var/app/com.steampowered.App$SteamAppId"
    else
        echo "$me: either --home, --freedesktop-app-id, --steam-app-id" \
            "or \$SteamAppId is required" >&2
        exit 2
    fi
fi

if [ -n "$verbose" ]; then
    echo "Original argv:" >&2
    printf '\t%s\n' "\$0: $0" >&2
    for arg in "${original_argv[@]}"; do
        printf '\t%s\n' "\$@: $arg" >&2
    done
    echo "Current working directory:" >&2
    printf '\t%s\n' "Logical: $(pwd -L)" >&2
    printf '\t%s\n' "Physical: $(pwd -P)" >&2
    echo "Environment:" >&2
    printf '\t%s\n' "PATH: $path" >&2
    printf '\t%s\n' "LD_LIBRARY_PATH: $ld_library_path" >&2
    printf '\t%s\n' "LD_PRELOAD: $ld_preload" >&2
    echo "Other environment variables:" >&2
    env | sed -e 's/^/\t/' >&2
fi

if [ -n "$xterm" ]; then
    wrapped_command=(xterm -e sh -c 'echo; echo "$1: Starting interactive shell (original command is in \"\$@\")"; echo; shift; exec "$@"' sh "$me" bash -i -s "$@")
elif [ -n "$interactive" ]; then
    exec </dev/tty
    exec >/dev/tty
    exec 2>/dev/tty
    echo "$me: Starting interactive shell (original command is in \"\$@\")">&2
    wrapped_command=(bash -i -s "$@")
else
    wrapped_command=("$@")
fi

if [ -z "$BWRAP" ] || ! "$BWRAP" --bind / / true; then
    echo "$me: Cannot find bwrap or it doesn't work" >&2

    if [ -z "$host_fallback" ]; then
        exit 1
    fi

    echo "$me: Falling back to executing '${wrapped_command[*]}' directly" >&2

    if [ -n "$ld_preload" ]; then
        export LD_PRELOAD="$ld_preload"
    fi
    if [ -n "$path" ]; then
        export PATH="$path"
    else
        export PATH="$default_path"
    fi
    if [ -n "$ld_library_path" ]; then
        export LD_LIBRARY_PATH="$ld_library_path"
    else
        unset LD_LIBRARY_PATH
    fi

    exec "${wrapped_command[@]}"
fi

if "$BWRAP" --help | grep -F '[--]' 2>/dev/null; then
    bwrap_end_of_options="--"
else
    # bwrap doesn't support the -- syntax
    bwrap_end_of_options=""
    case "$1" in
        (-*)
            case "$1" in
                (*=*)
                    # env would interpret this as an environment variable
                    echo "$me: Cannot execute command '$1' with bubblewrap < 0.2.2">&2
                    exit 1
                    ;;
                (*)
                    # Emulate it via env(1)
                    bwrap_end_of_options="env"
                    ;;
            esac
            ;;
    esac
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
# running an interactive shell in which case that would break its
# job control.
if [ -z "$interactive" ]; then
    bwrap_options+=(--new-session)
fi

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 "$ld_library_path" ]; then
    bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides:$ld_library_path")
else
    bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides")
fi

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

                # We should also pick up its locale archives.
                if [ -e /usr/lib/locale ]; then
                    bwrap_options+=(--ro-bind /usr/lib/locale /usr/lib/locale)
                fi

                if [ -e /usr/share/i18n ]; then
                    bwrap_options+=(--ro-bind /usr/share/i18n /usr/share/i18n)
                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

                # S3TC or S2TC support for Mesa
                if [ -e "$dir/libtxc_dxtn.so" ]; then
                    "$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="/" \
                        "path-match:$dir/libtxc_dxtn.so"
                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)
if [ -d /dev/pts ]; then
    bwrap_options+=(--dev-bind /dev/pts /dev/pts)
fi
if [ -d /dev/shm ]; then
    bwrap_options+=(--dev-bind /dev/shm /dev/shm)
fi

# Protect other users' homes (but guard against the unlikely situation
# that they don't exist)
if [ -d /home ]; then
    bwrap_options+=(--tmpfs /home)
fi

# The shellcheck tool warns that -m is only applied to the deepest
# directory and not any parents created via -p, but that's fine here:
# we use a separate mkdir -p for each directory whose permissions we
# care about.
# shellcheck disable=SC2174
{
    # Set up the home directory
    mkdir -m700 -p "$fake_home"
    bwrap_options+=(--bind "$fake_home" "$HOME")
    bwrap_options+=(--bind "$fake_home" "$fake_home")

    # Set up /var/tmp, XDG_*_HOME like Flatpak does
    mkdir -m700 -p "$fake_home/cache"
    mkdir -m700 -p "$fake_home/cache/tmp"
    bwrap_options+=(--bind "$fake_home/cache/tmp" /var/tmp)
    bwrap_options+=(--setenv XDG_CACHE_HOME "$fake_home/cache")
    mkdir -m700 -p "$fake_home/config"
    bwrap_options+=(--setenv XDG_CONFIG_HOME "$fake_home/config")
    mkdir -m700 -p "$fake_home/data"
    bwrap_options+=(--setenv XDG_DATA_HOME "$fake_home/data")
}

# These might be API entry points, according to .steam/steam/steam.sh.
# On the host system they're symlinks to a real location, but in a
# container it's easier to bind-mount them (this follows symlinks).
# TODO: We probably want to hide part or all of root, steam, steambeta?
for api in bin bin32 bin64 root sdk32 sdk64 steam steambeta; do
    dir="$HOME/.steam/$api"
    if [ -d "$dir" ]; then
        bwrap_options+=(--ro-bind "$dir" "$dir")
    fi
done

# steamclient.so relies on this for communication with Steam
if [ -e "$HOME/.steam/steam.pid" ]; then
    bwrap_options+=(--ro-bind "$HOME/.steam/steam.pid" "$HOME/.steam/steam.pid")
fi

# Make sure Steam IPC is available (TODO: do we need this? do we need more?)
if [ -e "$HOME/.steam/steam.pipe" ]; then
    bwrap_options+=(--bind "$HOME/.steam/steam.pipe" "$HOME/.steam/steam.pipe")
fi

# We need the LD_PRELOADs from Steam visible at the paths that were
# used for them, which might be their physical rather than logical
# locations
old_ifs="$IFS"
IFS=:
adjusted_ld_preload=
for preload in $ld_preload; do
    if [ -e "$preload" ]; then
        case "$preload" in
            (/usr/*|/lib*)
                if [ -n "$runtime" ]; then
                    # We can't write here, so redirect them to the
                    # corresponding locations in /run/host
                    bwrap_options+=(--ro-bind "$preload" "/run/host$preload")
                    adjusted_ld_preload="${adjusted_ld_preload:+"$adjusted_ld_preload:"}/run/host$preload"
                else
                    bwrap_options+=(--ro-bind "$preload" "$preload")
                    adjusted_ld_preload="${adjusted_ld_preload:+"$adjusted_ld_preload:"}$preload"
                fi
                ;;
            (*)
                bwrap_options+=(--ro-bind "$preload" "$preload")
                adjusted_ld_preload="${adjusted_ld_preload:+"$adjusted_ld_preload:"}$preload"
                ;;
        esac
    fi
done
IFS="$old_ifs"

# We need libraries from the Steam Runtime, so make sure that's visible
# (it should never need to be read/write though)
if [ -n "${STEAM_RUNTIME:-}" ]; then
    bwrap_options+=(--ro-bind "$STEAM_RUNTIME" "$STEAM_RUNTIME")
fi

# Make sure the current working directory (the game we are going to
# run) is available. Some games write here.
PWD_L="$(pwd -L)"
if test "$HOME" -ef "$PWD_L"; then
    echo "$me: Not making real $PWD_L available to container because it is" \
         "the home directory" >&2
else
    bwrap_options+=(--bind "$PWD_L" "$PWD_L")
fi
bwrap_options+=(--chdir "$PWD_L")

PWD_P="$(pwd -P)"
if test "$PWD_P" = "$PWD_L"; then
    :   # Nothing special to do
elif test "$HOME" -ef "$PWD_P"; then
    echo "$me: Not making real $PWD_P available to container because it is" \
         "the home directory" >&2
else
    bwrap_options+=(--bind "$PWD_P" "$PWD_P")
fi

# Put the caller's LD_PRELOAD and search paths back.
if [ -n "$adjusted_ld_preload" ]; then
    bwrap_options+=(--setenv LD_PRELOAD "$adjusted_ld_preload")
else
    bwrap_options+=(--unsetenv LD_PRELOAD)
fi
if [ -n "$path" ]; then
    bwrap_options+=(--setenv PATH "$path")
else
    bwrap_options+=(--setenv PATH "$default_path")
fi

if "$BWRAP" --help | grep unshare-uts >/dev/null; then
    # Set a standard hostname for the container to make it easier
    # to see which shell is which
    bwrap_options+=(--hostname pressure-vessel --unshare-uts)
fi

# Potential future expansion: use --unshare-pid for more isolation

# 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" >&2
    done

    echo "$me: '$BWRAP ${bwrap_options[*]} $bwrap_end_of_options ${wrapped_command[*]}'" >&2
fi

e=0
"$BWRAP" "${bwrap_options[@]}" $bwrap_end_of_options "${wrapped_command[@]}" || e=$?
if [ "$e" -eq 0 ]; then
    if [ -n "$verbose" ]; then
        echo "$me: '$BWRAP ${bwrap_options[*]} $bwrap_end_of_options ${wrapped_command[*]}': successful" >&2
    fi
else
    echo "$me: failed to execute '$BWRAP ${bwrap_options[*]} $bwrap_end_of_options ${wrapped_command[*]}': exec status $e" >&2
fi
exit "$e"

# vim:set sw=4 sts=4 et: