initial implementation: multi-user Steam shared library via bwrap overlay

Share one Steam game library across multiple Linux users with fully
isolated Proton prefixes. Uses bubblewrap to create a per-user kernel
overlay on /opt/steam/steamapps/compatdata/ so game files stay shared
while Proton prefixes are isolated per user, with no compatibility
tool selection or per-game configuration required.

Includes:
- steam-shared launcher that sets up the per-user overlay and execs
  Steam inside a bwrap mount namespace
- activate/uninstall scripts plus an add-user helper for steamshare
  group membership
- permission watcher (steam-fix-perms.path/.service) to keep ACLs
  correct under pressure-vessel's restrictive mode bits
- .desktop override that routes the system Steam launcher through
  steam-shared
- Nix flake exposing activate, uninstall, and add-user packages
- design doc and implementation plan covering the approach
This commit is contained in:
2026-04-15 09:53:09 +02:00
commit 1ece944a45
12 changed files with 1524 additions and 0 deletions

116
scripts/activate.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env bash
# activate.sh — set up the shared Steam library system
# Requires sudo. Idempotent — safe to re-run.
set -euo pipefail
STEAM_GROUP="steamshare"
STEAM_DIR="/opt/steam"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# --- preflight ---
if [[ $EUID -ne 0 ]]; then
echo "error: run this script with sudo" >&2
exit 1
fi
if ! command -v bwrap &>/dev/null; then
echo "error: bubblewrap (bwrap) is not installed" >&2
echo " arch/cachyos: pacman -S bubblewrap" >&2
echo " debian/ubuntu: apt install bubblewrap" >&2
exit 1
fi
echo "[1/7] Group"
if ! getent group "$STEAM_GROUP" >/dev/null; then
groupadd "$STEAM_GROUP"
echo " created group $STEAM_GROUP"
else
echo " group $STEAM_GROUP exists"
fi
echo "[2/7] Shared library directory"
mkdir -p "$STEAM_DIR/steamapps/compatdata"
find "$STEAM_DIR" -type d -exec chmod 2775 {} +
chown -R root:"$STEAM_GROUP" "$STEAM_DIR"
setfacl -R -m g:"$STEAM_GROUP":rwx "$STEAM_DIR"
setfacl -dR -m g:"$STEAM_GROUP":rwx "$STEAM_DIR"
echo " $STEAM_DIR ready (setgid + ACLs)"
echo "[3/7] Clean stale compatdata prefixes"
# Only wipe compatdata contents on first activation. Re-running activate.sh
# on a live system would otherwise delete every user's Proton prefixes
# (they live inside compatdata/<appid>/pfx, not in the per-user overlay
# until the game is launched at least once under the shared-library setup).
# A launcher at /usr/local/bin/steam-shared is proof the system has been
# activated before — skip the wipe in that case.
if [[ -e /usr/local/bin/steam-shared ]]; then
echo " skipped (steam-shared already installed — refusing to wipe live prefixes)"
else
# remove all contents inside compatdata app dirs (not the dirs themselves —
# Steam recreates empty ones and they're harmless as lower-layer placeholders)
find "$STEAM_DIR/steamapps/compatdata" -mindepth 2 -delete 2>/dev/null || true
echo " compatdata cleaned"
fi
echo "[4/7] Install launcher"
install -m 755 "$PROJECT_DIR/scripts/steam-shared.sh" /usr/local/bin/steam-shared
echo " /usr/local/bin/steam-shared installed"
echo "[5/7] Install .desktop override"
mkdir -p /usr/local/share/applications
install -m 644 "$PROJECT_DIR/desktop/steam.desktop" /usr/local/share/applications/steam.desktop
echo " .desktop override installed"
echo "[6/7] Install permission watcher"
install -m 755 "$PROJECT_DIR/scripts/fix-perms.sh" /usr/local/bin/steam-fix-perms
cat > /etc/systemd/system/steam-fix-perms.service << 'UNIT'
[Unit]
Description=Fix shared Steam library permissions after pressure-vessel changes
[Service]
Type=oneshot
ExecStart=/usr/local/bin/steam-fix-perms
UNIT
cat > /etc/systemd/system/steam-fix-perms.path << UNIT
[Unit]
Description=Watch shared Steam library for permission changes
[Path]
PathChanged=$STEAM_DIR/steamapps/common
PathChanged=$STEAM_DIR/steamapps/shadercache
TriggerLimitIntervalSec=10
TriggerLimitBurst=1
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable --now steam-fix-perms.path
echo " permission watcher enabled"
echo "[7/7] Fix current permissions"
bash "$PROJECT_DIR/scripts/fix-perms.sh"
echo " permissions fixed"
echo
echo "========================================"
echo " Activation complete."
echo
echo " Shared library: $STEAM_DIR"
echo " Launcher: /usr/local/bin/steam-shared"
echo " Desktop file: /usr/local/share/applications/steam.desktop"
echo
echo " To add a user:"
echo " sudo usermod -aG $STEAM_GROUP <username>"
echo " (user must log out and back in)"
echo
echo " Each user must:"
echo " 1. Launch Steam (it uses the shared library launcher automatically)"
echo " 2. Steam → Settings → Storage → add $STEAM_DIR"
echo " 3. Use any Proton version — isolation is automatic"
echo "========================================"

35
scripts/add-user.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# add-user.sh — add a user to the shared Steam library
# Requires sudo.
set -euo pipefail
STEAM_GROUP="steamshare"
if [[ $EUID -ne 0 ]]; then
echo "error: run this script with sudo" >&2
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "usage: $0 <username> [username...]" >&2
exit 1
fi
if ! getent group "$STEAM_GROUP" >/dev/null 2>&1; then
echo "error: group '$STEAM_GROUP' does not exist — run activate first" >&2
exit 1
fi
for user in "$@"; do
if ! id "$user" &>/dev/null; then
echo " $user: user does not exist, skipping" >&2
continue
fi
if id -nG "$user" | grep -qw "$STEAM_GROUP"; then
echo " $user: already in $STEAM_GROUP"
else
usermod -aG "$STEAM_GROUP" "$user"
echo " $user: added to $STEAM_GROUP (log out and back in to take effect)"
fi
done

26
scripts/fix-perms.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# fix-perms.sh — fix permissions on the shared Steam library
# Called by systemd path unit when pressure-vessel creates dirs with
# restrictive permissions. Also safe to run manually.
set -uo pipefail
STEAM_DIR="/opt/steam"
STEAM_GROUP="steamshare"
# fix dirs missing group rwx (pressure-vessel tmp-*, var/, etc.)
find "$STEAM_DIR/steamapps" -type d ! -perm -g+rwx -exec chmod 2775 {} +
# fix files missing group rw
find "$STEAM_DIR/steamapps" -type f ! -perm -g+rw -exec chmod g+rw {} +
# restore execute bits on ELF binaries and shebang scripts
find "$STEAM_DIR/steamapps" -type f ! -perm -a+x -exec sh -c '
for f; do
head -c4 "$f" 2>/dev/null | grep -qP "^\x7fELF|^#!" && chmod a+x "$f"
done
' _ {} +
# fix group ownership (skip broken symlinks)
find "$STEAM_DIR/steamapps" -not -type l ! -group "$STEAM_GROUP" -exec chown root:"$STEAM_GROUP" {} +
find "$STEAM_DIR/steamapps" -type l ! -group "$STEAM_GROUP" -exec chown -h root:"$STEAM_GROUP" {} + 2>/dev/null || true

68
scripts/steam-shared.sh Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# steam-shared — launch Steam with per-user Proton prefix isolation
#
# Uses bubblewrap to overlay /opt/steam/steamapps/compatdata/ with a
# per-user directory. Writes (Proton prefixes) go to the user's home,
# reads fall through to the shared library. Each user gets their own
# mount namespace — concurrent sessions are fully isolated.
set -euo pipefail
SHARED_LIBRARY="/opt/steam"
SHARED_COMPATDATA="$SHARED_LIBRARY/steamapps/compatdata"
OVERLAY_DIR="$HOME/.local/share/steam-shared"
OVERLAY_UPPER="$OVERLAY_DIR/upper"
OVERLAY_WORK="$OVERLAY_DIR/work"
# --- preflight checks ---
if [[ ! -d "$SHARED_COMPATDATA" ]]; then
echo "steam-shared: shared library not found at $SHARED_COMPATDATA" >&2
echo "steam-shared: run the activate script first (nix run .#activate)" >&2
exit 1
fi
if ! id -nG | grep -qw steamshare; then
echo "steam-shared: current user is not in the 'steamshare' group" >&2
echo "steam-shared: run: sudo usermod -aG steamshare $(whoami)" >&2
exit 1
fi
if ! command -v bwrap &>/dev/null; then
echo "steam-shared: bubblewrap (bwrap) is not installed" >&2
exit 1
fi
# --- detect Steam installation (prefer native over Flatpak) ---
STEAM_CMD=()
if [[ -x /usr/bin/steam ]]; then
STEAM_CMD=(/usr/bin/steam)
elif command -v flatpak &>/dev/null && flatpak info com.valvesoftware.Steam &>/dev/null; then
# grant Flatpak access to the shared library
flatpak override --user --filesystem="$SHARED_LIBRARY" com.valvesoftware.Steam 2>/dev/null
STEAM_CMD=(flatpak run com.valvesoftware.Steam)
else
echo "steam-shared: no Steam installation found" >&2
echo "steam-shared: install native Steam (pacman -S steam) or Flatpak Steam" >&2
exit 1
fi
# --- set up per-user overlay dirs ---
mkdir -p "$OVERLAY_UPPER" "$OVERLAY_WORK"
# clean stale overlayfs work dir (causes "Device or resource busy")
# overlayfs sets work/work to mode 0000 — must chmod before removing
if [[ -d "$OVERLAY_WORK/work" ]]; then
chmod -R u+rwx "$OVERLAY_WORK/work" 2>/dev/null
rm -rf "$OVERLAY_WORK/work"
fi
# --- launch Steam inside bwrap with overlay ---
exec bwrap \
--dev-bind / / \
--overlay-src "$SHARED_COMPATDATA" \
--overlay "$OVERLAY_UPPER" "$OVERLAY_WORK" "$SHARED_COMPATDATA" \
-- "${STEAM_CMD[@]}" "$@"

60
scripts/uninstall.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# uninstall.sh — remove the shared Steam library system
# Requires sudo. Safe to run multiple times.
set -euo pipefail
STEAM_GROUP="steamshare"
STEAM_DIR="/opt/steam"
if [[ $EUID -ne 0 ]]; then
echo "error: run this script with sudo" >&2
exit 1
fi
echo "[1/5] Remove launcher"
rm -f /usr/local/bin/steam-shared
echo " done"
echo "[2/5] Remove .desktop override"
rm -f /usr/local/share/applications/steam.desktop
echo " done"
echo "[3/5] Remove permission watcher"
if systemctl is-active --quiet steam-fix-perms.path 2>/dev/null; then
systemctl disable --now steam-fix-perms.path
fi
rm -f /etc/systemd/system/steam-fix-perms.path
rm -f /etc/systemd/system/steam-fix-perms.service
rm -f /usr/local/bin/steam-fix-perms
systemctl daemon-reload 2>/dev/null
echo " done"
echo "[4/5] Remove steamshare group"
if getent group "$STEAM_GROUP" >/dev/null 2>&1; then
while IFS= read -r u; do
[[ -z "$u" ]] && continue
gpasswd -d "$u" "$STEAM_GROUP" >/dev/null 2>&1 || true
done < <(getent group "$STEAM_GROUP" | cut -d: -f4 | tr ',' '\n')
groupdel "$STEAM_GROUP" 2>/dev/null || true
echo " removed group $STEAM_GROUP"
else
echo " group does not exist"
fi
echo "[5/5] Shared library directory"
if [[ -d "$STEAM_DIR" ]]; then
echo " $STEAM_DIR exists ($(du -sh "$STEAM_DIR" 2>/dev/null | cut -f1) used)"
echo " to remove: sudo rm -rf $STEAM_DIR"
echo " (not removed automatically — contains game data)"
else
echo " does not exist"
fi
echo
echo "========================================"
echo " Uninstall complete."
echo
echo " Each user should:"
echo " 1. Remove $STEAM_DIR from Steam → Settings → Storage"
echo " 2. Optionally remove ~/.local/share/steam-shared/"
echo "========================================"