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:
116
scripts/activate.sh
Executable file
116
scripts/activate.sh
Executable 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
35
scripts/add-user.sh
Executable 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
26
scripts/fix-perms.sh
Executable 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
68
scripts/steam-shared.sh
Executable 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
60
scripts/uninstall.sh
Executable 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 "========================================"
|
||||
Reference in New Issue
Block a user