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:
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.DS_Store
|
||||||
|
.claude/
|
||||||
|
.codex
|
||||||
|
.mise.local.toml
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
node_modules/
|
||||||
|
.venv/
|
||||||
|
vendor/bundle/
|
||||||
|
target/
|
||||||
|
.gradle/
|
||||||
119
README.md
Normal file
119
README.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Steam Shared Library
|
||||||
|
|
||||||
|
Share one Steam game library across multiple Linux users with fully isolated Proton prefixes.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Steam stores Proton/Wine prefixes (`compatdata`) inside the game library folder. When multiple users share a library, they all write to the same prefix directory, causing:
|
||||||
|
|
||||||
|
- `wineserver: pfx is not owned by you`
|
||||||
|
- `PermissionError: os.chmod`
|
||||||
|
- `pressure-vessel: Permission denied`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Uses [bubblewrap](https://github.com/containers/bubblewrap) to create a per-user kernel overlay on `/opt/steam/steamapps/compatdata/`:
|
||||||
|
|
||||||
|
- **Game files** (`common/`, `shadercache/`) → shared directly, readable and writable by all group members
|
||||||
|
- **Proton prefixes** (`compatdata/`) → overlaid per-user via bwrap's mount namespace
|
||||||
|
|
||||||
|
Each user gets their own overlay. Writes go to `~/.local/share/steam-shared/upper/`. Reads fall through to the shared directory. Multiple users can be logged in simultaneously (GDM user switching) — each session has its own isolated mount namespace.
|
||||||
|
|
||||||
|
**No compatibility tool selection. No per-game configuration. Works with any Proton variant.**
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Linux with a modern kernel (overlay support)
|
||||||
|
- `bubblewrap` (`pacman -S bubblewrap` / `apt install bubblewrap`)
|
||||||
|
- `acl` package for `setfacl` (usually pre-installed)
|
||||||
|
- Steam (native package or Flatpak)
|
||||||
|
- Nix (for installation via flake)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install (requires sudo)
|
||||||
|
nix run github:felixfoertsch/steam-shared-library
|
||||||
|
# or from a local checkout:
|
||||||
|
sudo ./scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates:
|
||||||
|
- `steamshare` group
|
||||||
|
- `/opt/steam/` with correct permissions and ACLs
|
||||||
|
- `/usr/local/bin/steam-shared` launcher
|
||||||
|
- `.desktop` override so Steam launches through the shared launcher
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# add users to the shared library group
|
||||||
|
sudo usermod -aG steamshare <username>
|
||||||
|
# user must log out and back in for group membership
|
||||||
|
|
||||||
|
# each user: open Steam → Settings → Storage → add /opt/steam
|
||||||
|
# install games to /opt/steam — they're shared for all users
|
||||||
|
# use any Proton version — prefix isolation is automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steam Detection
|
||||||
|
|
||||||
|
The launcher prefers native Steam over Flatpak:
|
||||||
|
|
||||||
|
1. If `/usr/bin/steam` exists → native Steam
|
||||||
|
2. Else if Flatpak Steam is installed → Flatpak with filesystem access to `/opt/steam`
|
||||||
|
3. Else → error
|
||||||
|
|
||||||
|
**If both are installed, native wins.** Having both native and Flatpak Steam is discouraged — it leads to two separate configs, library confusion, and wasted disk space. Pick one.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix run github:felixfoertsch/steam-shared-library#uninstall
|
||||||
|
# or from a local checkout:
|
||||||
|
sudo ./scripts/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This removes the launcher, .desktop override, and steamshare group. Game data at `/opt/steam/` and per-user prefixes at `~/.local/share/steam-shared/` are preserved (remove manually if desired).
|
||||||
|
|
||||||
|
## How It Works (Technical)
|
||||||
|
|
||||||
|
The Steam launcher (`steam-shared`) does:
|
||||||
|
|
||||||
|
1. Verify prerequisites (shared dir exists, user in steamshare group, bwrap installed)
|
||||||
|
2. Detect Steam installation (native or Flatpak)
|
||||||
|
3. Create per-user overlay directories (`~/.local/share/steam-shared/{upper,work}`)
|
||||||
|
4. Launch Steam inside a bwrap mount namespace with an overlay on `compatdata/`
|
||||||
|
|
||||||
|
```
|
||||||
|
bwrap --dev-bind / / \
|
||||||
|
--overlay-src /opt/steam/steamapps/compatdata \
|
||||||
|
--overlay ~/.local/share/steam-shared/upper \
|
||||||
|
~/.local/share/steam-shared/work \
|
||||||
|
/opt/steam/steamapps/compatdata \
|
||||||
|
-- steam
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the namespace, any write to `/opt/steam/steamapps/compatdata/` goes to the per-user upper layer. The shared lower layer is read-only from the overlay's perspective. Game files outside `compatdata/` are not overlaid — they're shared normally.
|
||||||
|
|
||||||
|
## Caveats
|
||||||
|
|
||||||
|
### Do not nest `steam-shared` inside another bwrap wrapper
|
||||||
|
|
||||||
|
`bwrap` without the setuid bit (the default on modern distros) creates an **unprivileged user namespace**. User namespaces cannot preserve supplementary groups — every group except the primary one becomes `nobody` inside the namespace.
|
||||||
|
|
||||||
|
That has two consequences if you wrap `steam-shared` inside an outer bwrap (for example, a Steam lancache wrapper that bind-mounts a custom `resolv.conf`):
|
||||||
|
|
||||||
|
1. The `steamshare` group check in `steam-shared` fails — `id -nG` no longer lists `steamshare` inside the outer namespace.
|
||||||
|
2. Even if you bypass the check, Steam itself cannot write to `/opt/steam/steamapps/common/`. The group bit that grants access is gone inside the namespace, so new game installs fail with `EACCES`.
|
||||||
|
|
||||||
|
If you need a lancache or similar DNS redirect, put it at the **system level** (a `systemd-resolved` drop-in, or a local `dnsmasq`), not inside a second bwrap around `steam-shared`.
|
||||||
|
|
||||||
|
## Prior Art
|
||||||
|
|
||||||
|
This project originally used a Proton compatibility tool wrapper to redirect `STEAM_COMPAT_DATA_PATH`. That approach was brittle:
|
||||||
|
- Required selecting the wrapper as the compatibility tool for every game
|
||||||
|
- Per-game Proton overrides bypassed the wrapper entirely
|
||||||
|
- Steam sometimes wrote to the original path before the wrapper ran
|
||||||
|
|
||||||
|
The bwrap overlay approach solves all of these by operating at the filesystem level, below Steam and Proton.
|
||||||
46
desktop/steam.desktop
Normal file
46
desktop/steam.desktop
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=Steam
|
||||||
|
Comment=Application for managing and playing games on Steam
|
||||||
|
Exec=steam-shared %U
|
||||||
|
Icon=steam
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Network;FileTransfer;Game;
|
||||||
|
MimeType=x-scheme-handler/steam;x-scheme-handler/steamlink;
|
||||||
|
Actions=Store;Community;Library;Servers;Screenshots;News;Settings;BigPicture;Friends;
|
||||||
|
|
||||||
|
[Desktop Action Store]
|
||||||
|
Name=Store
|
||||||
|
Exec=steam-shared steam://store
|
||||||
|
|
||||||
|
[Desktop Action Community]
|
||||||
|
Name=Community
|
||||||
|
Exec=steam-shared steam://url/SteamIDControlPage
|
||||||
|
|
||||||
|
[Desktop Action Library]
|
||||||
|
Name=Library
|
||||||
|
Exec=steam-shared steam://open/games
|
||||||
|
|
||||||
|
[Desktop Action Servers]
|
||||||
|
Name=Servers
|
||||||
|
Exec=steam-shared steam://open/servers
|
||||||
|
|
||||||
|
[Desktop Action Screenshots]
|
||||||
|
Name=Screenshots
|
||||||
|
Exec=steam-shared steam://open/screenshots
|
||||||
|
|
||||||
|
[Desktop Action News]
|
||||||
|
Name=News
|
||||||
|
Exec=steam-shared steam://open/news
|
||||||
|
|
||||||
|
[Desktop Action Settings]
|
||||||
|
Name=Settings
|
||||||
|
Exec=steam-shared steam://open/settings
|
||||||
|
|
||||||
|
[Desktop Action BigPicture]
|
||||||
|
Name=Big Picture
|
||||||
|
Exec=steam-shared steam://open/bigpicture
|
||||||
|
|
||||||
|
[Desktop Action Friends]
|
||||||
|
Name=Friends
|
||||||
|
Exec=steam-shared steam://open/friends
|
||||||
768
docs/plans/2026-04-04-bwrap-overlay-implementation.md
Normal file
768
docs/plans/2026-04-04-bwrap-overlay-implementation.md
Normal file
@@ -0,0 +1,768 @@
|
|||||||
|
# Steam Shared Library (bwrap overlay) Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the brittle Proton wrapper approach with a bubblewrap overlay on `/opt/steam/steamapps/compatdata/` that transparently gives each user their own Proton prefix directory.
|
||||||
|
|
||||||
|
**Architecture:** A Nix flake in `/path/to/steam-shared-library/` outputs two packages: `activate` (system setup with sudo) and `uninstall` (reversal). The activate script creates the steamshare group, shared directory with ACLs, a Steam launcher script that uses bwrap overlay, and a .desktop override. The old wrapper system is cleaned up via a legacy-cleanup script.
|
||||||
|
|
||||||
|
**Tech Stack:** Nix flakes, bash, bubblewrap, overlayfs, systemd (for .desktop), Ansible (legacy cleanup only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### steam-shared-library repo (clean slate — all old files removed)
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|------|---------------|
|
||||||
|
| `flake.nix` | Nix flake entry point: outputs activate + uninstall packages |
|
||||||
|
| `scripts/activate.sh` | System setup: group, dirs, ACLs, install launcher + .desktop |
|
||||||
|
| `scripts/uninstall.sh` | Reversal: remove launcher, .desktop, group |
|
||||||
|
| `scripts/steam-shared.sh` | Per-user Steam launcher with bwrap overlay |
|
||||||
|
| `desktop/steam-shared.desktop` | .desktop override that launches steam-shared |
|
||||||
|
| `README.md` | User-facing documentation |
|
||||||
|
| `docs/specs/2026-04-04-bwrap-overlay-design.md` | Design spec (already written) |
|
||||||
|
| `.project.toml` | Project metadata |
|
||||||
|
| `.gitignore` | Git ignore rules |
|
||||||
|
|
||||||
|
### Dotfiles repo (modifications)
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `roles/legacy-cleanup/tasks/main.yml` | Add tasks to remove old wrapper system |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Clean the repo
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Remove: all old files (`steam-setup.sh`, `steam-cleanup.sh`, `steam-fix-perms.sh`, `steam-ensure-wrapper.sh`, `steam-uninstall.sh`, `proton-shared-lib/`, `docs/plans/2026-03-10-*`)
|
||||||
|
- Keep: `docs/specs/2026-04-04-bwrap-overlay-design.md`, `.project.toml`, `.gitignore`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Remove all old files**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/steam-shared-library
|
||||||
|
git rm steam-setup.sh steam-cleanup.sh steam-fix-perms.sh steam-ensure-wrapper.sh README.md
|
||||||
|
git rm -r proton-shared-lib/
|
||||||
|
git rm -r docs/plans/
|
||||||
|
rm -f steam-uninstall.sh .DS_Store
|
||||||
|
echo ".DS_Store" >> .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create directory structure**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p scripts desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update .project.toml**
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "Steam Shared Library"
|
||||||
|
description = "Multi-user shared Steam library for Linux using bubblewrap overlay — per-user Proton prefixes with zero configuration."
|
||||||
|
status = "aktiv"
|
||||||
|
priority = "hoch"
|
||||||
|
location = "/path/to/steam-shared-library"
|
||||||
|
|
||||||
|
[dates]
|
||||||
|
created = "2026-03-01"
|
||||||
|
last_activity = "2026-04-04"
|
||||||
|
|
||||||
|
[notes]
|
||||||
|
next_steps = "Implement bwrap overlay approach, test with managed users"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "remove old wrapper approach, prepare for bwrap overlay"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Steam launcher script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/steam-shared.sh`
|
||||||
|
|
||||||
|
The core of the solution. Detects native/Flatpak Steam, sets up bwrap overlay on compatdata, launches Steam.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the launcher script**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/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")
|
||||||
|
if [[ -d "$OVERLAY_WORK/work" ]]; then
|
||||||
|
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[@]}" "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 755 scripts/steam-shared.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Test the launcher manually**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Quick smoke test — verify bwrap overlay is set up (don't actually launch Steam GUI)
|
||||||
|
bash scripts/steam-shared.sh --help 2>&1 | head -5
|
||||||
|
# Should show Steam's help output, meaning it launched successfully inside bwrap
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/steam-shared.sh
|
||||||
|
git commit -m "add steam launcher with bwrap overlay on compatdata"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Desktop file override
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `desktop/steam-shared.desktop`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the .desktop file**
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=Steam
|
||||||
|
Comment=Application for managing and playing games on Steam
|
||||||
|
Exec=steam-shared %U
|
||||||
|
Icon=steam
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Network;FileTransfer;Game;
|
||||||
|
MimeType=x-scheme-handler/steam;x-scheme-handler/steamlink;
|
||||||
|
Actions=Store;Community;Library;Servers;Screenshots;News;Settings;BigPicture;Friends;
|
||||||
|
|
||||||
|
[Desktop Action Store]
|
||||||
|
Name=Store
|
||||||
|
Exec=steam-shared steam://store
|
||||||
|
|
||||||
|
[Desktop Action Community]
|
||||||
|
Name=Community
|
||||||
|
Exec=steam-shared steam://url/SteamIDControlPage
|
||||||
|
|
||||||
|
[Desktop Action Library]
|
||||||
|
Name=Library
|
||||||
|
Exec=steam-shared steam://open/games
|
||||||
|
|
||||||
|
[Desktop Action Servers]
|
||||||
|
Name=Servers
|
||||||
|
Exec=steam-shared steam://open/servers
|
||||||
|
|
||||||
|
[Desktop Action Screenshots]
|
||||||
|
Name=Screenshots
|
||||||
|
Exec=steam-shared steam://open/screenshots
|
||||||
|
|
||||||
|
[Desktop Action News]
|
||||||
|
Name=News
|
||||||
|
Exec=steam-shared steam://open/news
|
||||||
|
|
||||||
|
[Desktop Action Settings]
|
||||||
|
Name=Settings
|
||||||
|
Exec=steam-shared steam://open/settings
|
||||||
|
|
||||||
|
[Desktop Action BigPicture]
|
||||||
|
Name=Big Picture
|
||||||
|
Exec=steam-shared steam://open/bigpicture
|
||||||
|
|
||||||
|
[Desktop Action Friends]
|
||||||
|
Name=Friends
|
||||||
|
Exec=steam-shared steam://open/friends
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add desktop/steam-shared.desktop
|
||||||
|
git commit -m "add .desktop override for steam-shared launcher"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Activation script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/activate.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the activation script**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/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/5] 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/5] 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/5] Clean stale compatdata prefixes"
|
||||||
|
# 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"
|
||||||
|
|
||||||
|
echo "[4/5] 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/5] Install .desktop override"
|
||||||
|
mkdir -p /usr/local/share/applications
|
||||||
|
install -m 644 "$PROJECT_DIR/desktop/steam-shared.desktop" /usr/local/share/applications/steam-shared.desktop
|
||||||
|
echo " .desktop override installed"
|
||||||
|
|
||||||
|
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-shared.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 will use the shared library launcher automatically)"
|
||||||
|
echo " 2. Steam → Settings → Storage → add $STEAM_DIR"
|
||||||
|
echo " 3. Use any Proton version — isolation is automatic"
|
||||||
|
echo "========================================"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 755 scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/activate.sh
|
||||||
|
git commit -m "add activation script for system-level setup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Uninstall script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `scripts/uninstall.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the uninstall script**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/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/4] Remove launcher"
|
||||||
|
rm -f /usr/local/bin/steam-shared
|
||||||
|
echo " done"
|
||||||
|
|
||||||
|
echo "[2/4] Remove .desktop override"
|
||||||
|
rm -f /usr/local/share/applications/steam-shared.desktop
|
||||||
|
echo " done"
|
||||||
|
|
||||||
|
echo "[3/4] 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 "[4/4] 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 "========================================"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Make executable and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 755 scripts/uninstall.sh
|
||||||
|
git add scripts/uninstall.sh
|
||||||
|
git commit -m "add uninstall script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Nix flake
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `flake.nix`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the flake**
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
description = "Multi-user shared Steam library for Linux using bubblewrap overlay";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
activate = pkgs.writeShellApplication {
|
||||||
|
name = "steam-shared-activate";
|
||||||
|
runtimeInputs = with pkgs; [ acl coreutils findutils ];
|
||||||
|
text = ''
|
||||||
|
exec "${self}/scripts/activate.sh" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
uninstall = pkgs.writeShellApplication {
|
||||||
|
name = "steam-shared-uninstall";
|
||||||
|
runtimeInputs = with pkgs; [ coreutils ];
|
||||||
|
text = ''
|
||||||
|
exec "${self}/scripts/uninstall.sh" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit activate uninstall;
|
||||||
|
default = activate;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify flake evaluates**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/steam-shared-library
|
||||||
|
nix flake check --no-build 2>&1
|
||||||
|
# Expected: warning about dirty tree, then "all checks passed!" or similar
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add flake.nix
|
||||||
|
git commit -m "add nix flake with activate and uninstall packages"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: README
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the README**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Steam Shared Library
|
||||||
|
|
||||||
|
Share one Steam game library across multiple Linux users with fully isolated Proton prefixes.
|
||||||
|
|
||||||
|
## The Problem
|
||||||
|
|
||||||
|
Steam stores Proton/Wine prefixes (`compatdata`) inside the game library folder. When multiple users share a library, they all write to the same prefix directory, causing:
|
||||||
|
|
||||||
|
- `wineserver: pfx is not owned by you`
|
||||||
|
- `PermissionError: os.chmod`
|
||||||
|
- `pressure-vessel: Permission denied`
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
Uses [bubblewrap](https://github.com/containers/bubblewrap) to create a per-user kernel overlay on `/opt/steam/steamapps/compatdata/`:
|
||||||
|
|
||||||
|
- **Game files** (`common/`, `shadercache/`) → shared directly, readable and writable by all group members
|
||||||
|
- **Proton prefixes** (`compatdata/`) → overlaid per-user via bwrap's mount namespace
|
||||||
|
|
||||||
|
Each user gets their own overlay. Writes go to `~/.local/share/steam-shared/upper/`. Reads fall through to the shared directory. Multiple users can be logged in simultaneously (GDM user switching) — each session has its own isolated mount namespace.
|
||||||
|
|
||||||
|
**No compatibility tool selection. No per-game configuration. Works with any Proton variant.**
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Linux with a modern kernel (overlay support)
|
||||||
|
- `bubblewrap` (`pacman -S bubblewrap` / `apt install bubblewrap`)
|
||||||
|
- `acl` package for `setfacl` (usually pre-installed)
|
||||||
|
- Steam (native package or Flatpak)
|
||||||
|
- Nix (for installation via flake)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# install (requires sudo)
|
||||||
|
nix run github:felixfoertsch/steam-shared-library -- activate
|
||||||
|
# or from a local checkout:
|
||||||
|
sudo ./scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates:
|
||||||
|
- `steamshare` group
|
||||||
|
- `/opt/steam/` with correct permissions and ACLs
|
||||||
|
- `/usr/local/bin/steam-shared` launcher
|
||||||
|
- `.desktop` override so Steam launches through the shared launcher
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# add users to the shared library group
|
||||||
|
sudo usermod -aG steamshare <username>
|
||||||
|
# user must log out and back in for group membership
|
||||||
|
|
||||||
|
# each user: open Steam → Settings → Storage → add /opt/steam
|
||||||
|
# install games to /opt/steam — they're shared for all users
|
||||||
|
# use any Proton version — prefix isolation is automatic
|
||||||
|
```
|
||||||
|
|
||||||
|
## Steam Detection
|
||||||
|
|
||||||
|
The launcher prefers native Steam over Flatpak:
|
||||||
|
|
||||||
|
1. If `/usr/bin/steam` exists → native Steam
|
||||||
|
2. Else if Flatpak Steam is installed → Flatpak with filesystem access to `/opt/steam`
|
||||||
|
3. Else → error
|
||||||
|
|
||||||
|
**If both are installed, native wins.** Having both native and Flatpak Steam is discouraged — it leads to two separate configs, library confusion, and wasted disk space. Pick one.
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix run github:felixfoertsch/steam-shared-library#uninstall
|
||||||
|
# or from a local checkout:
|
||||||
|
sudo ./scripts/uninstall.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This removes the launcher, .desktop override, and steamshare group. Game data at `/opt/steam/` and per-user prefixes at `~/.local/share/steam-shared/` are preserved (remove manually if desired).
|
||||||
|
|
||||||
|
## How It Works (Technical)
|
||||||
|
|
||||||
|
The Steam launcher (`steam-shared`) does:
|
||||||
|
|
||||||
|
1. Verify prerequisites (shared dir exists, user in steamshare group, bwrap installed)
|
||||||
|
2. Detect Steam installation (native or Flatpak)
|
||||||
|
3. Create per-user overlay directories (`~/.local/share/steam-shared/{upper,work}`)
|
||||||
|
4. Launch Steam inside a bwrap mount namespace with an overlay on `compatdata/`
|
||||||
|
|
||||||
|
```
|
||||||
|
bwrap --dev-bind / / \
|
||||||
|
--overlay-src /opt/steam/steamapps/compatdata \
|
||||||
|
--overlay ~/.local/share/steam-shared/upper \
|
||||||
|
~/.local/share/steam-shared/work \
|
||||||
|
/opt/steam/steamapps/compatdata \
|
||||||
|
-- steam
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the namespace, any write to `/opt/steam/steamapps/compatdata/` goes to the per-user upper layer. The shared lower layer is read-only from the overlay's perspective. Game files outside `compatdata/` are not overlaid — they're shared normally.
|
||||||
|
|
||||||
|
## Prior Art
|
||||||
|
|
||||||
|
This project originally used a Proton compatibility tool wrapper to redirect `STEAM_COMPAT_DATA_PATH`. That approach was brittle:
|
||||||
|
- Required selecting the wrapper as the compatibility tool for every game
|
||||||
|
- Per-game Proton overrides bypassed the wrapper entirely
|
||||||
|
- Steam sometimes wrote to the original path before the wrapper ran
|
||||||
|
|
||||||
|
The bwrap overlay approach solves all of these by operating at the filesystem level, below Steam and Proton.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md
|
||||||
|
git commit -m "add README documenting bwrap overlay approach"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 8: Legacy cleanup in dotfiles
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.syncthing/dotfiles/roles/legacy-cleanup/tasks/main.yml`
|
||||||
|
|
||||||
|
Add tasks to remove the old wrapper system (compat tool, login hook, systemd watcher, wrapper source files).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add cleanup tasks to legacy-cleanup role**
|
||||||
|
|
||||||
|
Append to the end of `roles/legacy-cleanup/tasks/main.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
|
||||||
|
# --- steam-shared-library: old proton wrapper approach ---
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: stop steam-fix-perms watcher"
|
||||||
|
become: true
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: steam-fix-perms.path
|
||||||
|
state: stopped
|
||||||
|
enabled: false
|
||||||
|
failed_when: false
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: remove steam-fix-perms systemd units"
|
||||||
|
become: true
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- /etc/systemd/system/steam-fix-perms.path
|
||||||
|
- /etc/systemd/system/steam-fix-perms.service
|
||||||
|
- /usr/local/bin/steam-fix-perms
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: remove steam shared-lib login hook"
|
||||||
|
become: true
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /etc/profile.d/steam-shared-lib.sh
|
||||||
|
state: absent
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: remove steam wrapper source"
|
||||||
|
become: true
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: /usr/local/share/proton-shared-lib
|
||||||
|
state: absent
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: remove per-user steam wrappers"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ user_home }}/.steam/steam/compatibilitytools.d/proton-shared-lib"
|
||||||
|
state: absent
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
|
||||||
|
- name: "legacy-cleanup: remove per-user steam wrapper (flatpak path)"
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ user_home }}/.local/share/Steam/compatibilitytools.d/proton-shared-lib"
|
||||||
|
state: absent
|
||||||
|
when: ansible_facts['os_family'] != "Darwin"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit in dotfiles repo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.syncthing/dotfiles
|
||||||
|
git add roles/legacy-cleanup/tasks/main.yml
|
||||||
|
git commit -m "add legacy cleanup for old steam proton wrapper system"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 9: End-to-end test
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run activation on FFCACHY**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/steam-shared-library
|
||||||
|
sudo ./scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
- steamshare group exists
|
||||||
|
- `/opt/steam/` has correct permissions (2775, steamshare, ACLs)
|
||||||
|
- `/usr/local/bin/steam-shared` exists and is executable
|
||||||
|
- `/usr/local/share/applications/steam-shared.desktop` exists
|
||||||
|
- Stale compatdata prefixes are cleaned
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test launcher as main user**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# verify detection
|
||||||
|
steam-shared --help
|
||||||
|
# should show Steam's help output
|
||||||
|
|
||||||
|
# verify overlay is working — launch Steam, install/run a small Proton game
|
||||||
|
# check that prefix lands in ~/.local/share/steam-shared/upper/<appid>/
|
||||||
|
ls ~/.local/share/steam-shared/upper/
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run legacy cleanup via dotfiles**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.syncthing/dotfiles
|
||||||
|
sudo ./scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
- `/etc/systemd/system/steam-fix-perms.path` removed
|
||||||
|
- `/etc/profile.d/steam-shared-lib.sh` removed
|
||||||
|
- `/usr/local/share/proton-shared-lib/` removed
|
||||||
|
- `~/.steam/steam/compatibilitytools.d/proton-shared-lib/` removed
|
||||||
|
|
||||||
|
- [ ] **Step 4: Test with a second user (GG-5, GG-6)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# create a test user
|
||||||
|
sudo useradd -m testuser
|
||||||
|
sudo usermod -aG steamshare testuser
|
||||||
|
|
||||||
|
# log in as testuser via GDM, launch Steam
|
||||||
|
# verify: shared games visible, prefix in testuser's ~/.local/share/steam-shared/upper/
|
||||||
|
# verify: main user's session still works (concurrent GDM sessions)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit any fixes discovered during testing**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/steam-shared-library
|
||||||
|
git add -A
|
||||||
|
git commit -m "fixes from end-to-end testing"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 10: Nix flake lock and final commit
|
||||||
|
|
||||||
|
- [ ] **Step 1: Generate flake.lock**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/steam-shared-library
|
||||||
|
nix flake update
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Test nix run activation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nix run .#activate
|
||||||
|
# should produce the same result as sudo ./scripts/activate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "pin flake dependencies"
|
||||||
|
```
|
||||||
199
docs/specs/2026-04-04-bwrap-overlay-design.md
Normal file
199
docs/specs/2026-04-04-bwrap-overlay-design.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# Steam Shared Library: Bubblewrap Overlay Approach
|
||||||
|
|
||||||
|
**Date:** 2026-04-04
|
||||||
|
**Status:** Approved
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Multiple users on a shared Linux machine need to play games from a single Steam library (`/opt/steam/`) without Proton prefix ownership conflicts. Steam mixes shared state (game files in `common/`) with per-user state (Wine prefixes in `compatdata/`) in the same directory. Every Proton variant writes to `compatdata/` — and when two users share the same prefix, Proton/Wine fails with ownership errors (`wineserver: pfx is not owned by you`).
|
||||||
|
|
||||||
|
The previous approach (a Proton compatibility tool wrapper that redirected `STEAM_COMPAT_DATA_PATH`) was brittle: it required manual per-game selection, any per-game Proton override bypassed it, and Steam sometimes wrote to the original path before the wrapper ran.
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
|
||||||
|
Use `bubblewrap` (bwrap) to mount a kernel overlay on `/opt/steam/steamapps/compatdata/` per user. Each user launches Steam through a wrapper script that creates a private mount namespace via bwrap. Writes to compatdata go to a per-user upper layer directory. Reads fall through to the shared lower layer. Game files (`common/`, `shadercache/`, etc.) are not overlaid — they remain shared and writable by all `steamshare` group members.
|
||||||
|
|
||||||
|
This is transparent to Steam and every Proton variant. No compatibility tool selection, no environment variable hacks, no per-game configuration.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/steam/steamapps/
|
||||||
|
├── common/ ← shared game files (real filesystem, group writable)
|
||||||
|
├── shadercache/ ← shared shader cache (real filesystem)
|
||||||
|
├── compatdata/ ← OVERLAID per-user via bwrap
|
||||||
|
│ ├── <appid>/ ← lower layer: shared (mostly empty dirs Steam creates)
|
||||||
|
│ └── ... ← upper layer: ~/.local/share/steam-shared/upper/
|
||||||
|
└── ...
|
||||||
|
|
||||||
|
Per user (inside bwrap mount namespace):
|
||||||
|
/opt/steam/steamapps/compatdata/ ← overlay mount
|
||||||
|
lower: /opt/steam/steamapps/compatdata/ (shared, read-through)
|
||||||
|
upper: ~/.local/share/steam-shared/upper/ (per-user writes)
|
||||||
|
work: ~/.local/share/steam-shared/work/ (overlayfs workdir)
|
||||||
|
```
|
||||||
|
|
||||||
|
When a user launches a Proton game:
|
||||||
|
1. Steam sets `STEAM_COMPAT_DATA_PATH=/opt/steam/steamapps/compatdata/<appid>`
|
||||||
|
2. Proton creates/opens the prefix at that path
|
||||||
|
3. The kernel overlay redirects the write to `~/.local/share/steam-shared/upper/<appid>/`
|
||||||
|
4. Other users are unaffected — they have their own mount namespace with their own upper layer
|
||||||
|
|
||||||
|
## Target Platforms
|
||||||
|
|
||||||
|
- Linux (Arch/CachyOS) with native Steam — primary target
|
||||||
|
- Linux with Flatpak Steam — supported via `--filesystem=/opt/steam` override
|
||||||
|
- NixOS — future, via NixOS module output from the same flake
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- `bubblewrap` (already installed as Flatpak dependency on most systems)
|
||||||
|
- `acl` package for `setfacl`
|
||||||
|
- Kernel overlay support (standard on all modern kernels)
|
||||||
|
- Native Steam (`pacman -S steam`) or Flatpak Steam
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
steam-shared-library/
|
||||||
|
├── flake.nix ← Nix flake: outputs activate + uninstall packages
|
||||||
|
├── flake.lock
|
||||||
|
├── scripts/
|
||||||
|
│ ├── activate.sh ← system setup (requires sudo)
|
||||||
|
│ ├── uninstall.sh ← reversal (requires sudo)
|
||||||
|
│ └── steam-shared.sh ← per-user Steam launcher
|
||||||
|
├── desktop/
|
||||||
|
│ └── steam-shared.desktop ← .desktop override for Steam
|
||||||
|
├── README.md
|
||||||
|
└── docs/
|
||||||
|
└── specs/
|
||||||
|
└── 2026-04-04-bwrap-overlay-design.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Activation Script (`nix run .#activate`)
|
||||||
|
|
||||||
|
Requires sudo. Idempotent — safe to re-run.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
- Create `steamshare` group if it doesn't exist
|
||||||
|
- Create `/opt/steam/steamapps/compatdata/` with setgid `2775` and group ACLs for `steamshare`
|
||||||
|
- Wipe all contents of `/opt/steam/steamapps/compatdata/*/` (clean slate — stale shared prefixes)
|
||||||
|
- Install `steam-shared` launcher script to `/usr/local/bin/steam-shared`
|
||||||
|
- Install `steam-shared.desktop` to `/usr/local/share/applications/` (shadows system `steam.desktop`)
|
||||||
|
- Verify `bubblewrap` is installed (error if not)
|
||||||
|
|
||||||
|
**Does NOT:**
|
||||||
|
- Add users to the steamshare group (manual: `sudo usermod -aG steamshare <user>`)
|
||||||
|
- Install Steam itself
|
||||||
|
- Install a permission watcher (ACLs + setgid should suffice)
|
||||||
|
|
||||||
|
### 2. Steam Launcher Script (`/usr/local/bin/steam-shared`)
|
||||||
|
|
||||||
|
Runs as the logged-in user. No root required.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OVERLAY_DIR="$HOME/.local/share/steam-shared"
|
||||||
|
SHARED_COMPATDATA="/opt/steam/steamapps/compatdata"
|
||||||
|
|
||||||
|
mkdir -p "$OVERLAY_DIR/upper" "$OVERLAY_DIR/work"
|
||||||
|
|
||||||
|
# Detect Steam installation: prefer native over Flatpak
|
||||||
|
STEAM_CMD=()
|
||||||
|
if [ -x /usr/bin/steam ]; then
|
||||||
|
STEAM_CMD=(/usr/bin/steam)
|
||||||
|
elif flatpak info com.valvesoftware.Steam &>/dev/null; then
|
||||||
|
flatpak override --user --filesystem=/opt/steam com.valvesoftware.Steam 2>/dev/null
|
||||||
|
STEAM_CMD=(flatpak run com.valvesoftware.Steam)
|
||||||
|
else
|
||||||
|
echo "steam-shared: no Steam installation found (checked /usr/bin/steam and Flatpak)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bwrap \
|
||||||
|
--dev-bind / / \
|
||||||
|
--overlay-src "$SHARED_COMPATDATA" \
|
||||||
|
--overlay "$OVERLAY_DIR/upper" "$OVERLAY_DIR/work" "$SHARED_COMPATDATA" \
|
||||||
|
-- "${STEAM_CMD[@]}" "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Desktop File Override (`/usr/local/share/applications/steam-shared.desktop`)
|
||||||
|
|
||||||
|
Shadows the system-installed `steam.desktop` by using the same `Name` and higher-priority location. Uses the same icon and categories so it looks identical in the app menu.
|
||||||
|
|
||||||
|
Key fields:
|
||||||
|
- `Exec=steam-shared %U`
|
||||||
|
- `Name=Steam`
|
||||||
|
- `Icon=steam` (uses system Steam icon)
|
||||||
|
|
||||||
|
### 4. Uninstall Script (`nix run .#uninstall`)
|
||||||
|
|
||||||
|
Requires sudo. Reverses everything the activation script does:
|
||||||
|
- Remove `/usr/local/bin/steam-shared`
|
||||||
|
- Remove `/usr/local/share/applications/steam-shared.desktop`
|
||||||
|
- Remove `steamshare` group (and membership)
|
||||||
|
- Print instructions for optional manual cleanup (`/opt/steam/`, per-user `~/.local/share/steam-shared/`)
|
||||||
|
|
||||||
|
Does NOT remove Steam itself or per-user save data.
|
||||||
|
|
||||||
|
### 5. Nix Flake
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- `packages.<system>.activate` — activation script (wraps `scripts/activate.sh`)
|
||||||
|
- `packages.<system>.uninstall` — uninstall script (wraps `scripts/uninstall.sh`)
|
||||||
|
- Future: `nixosModules.default` — native NixOS module for declarative system config
|
||||||
|
|
||||||
|
## Steam Detection Logic
|
||||||
|
|
||||||
|
The launcher prefers native Steam over Flatpak Steam:
|
||||||
|
1. If `/usr/bin/steam` exists → use native
|
||||||
|
2. Else if `flatpak info com.valvesoftware.Steam` succeeds → use Flatpak with `--filesystem=/opt/steam`
|
||||||
|
3. Else → error
|
||||||
|
|
||||||
|
If both are installed, native wins. This is documented in the README — having both installed is discouraged because it leads to two separate Steam configs, library confusion, and double disk usage.
|
||||||
|
|
||||||
|
## Flatpak Steam Specifics
|
||||||
|
|
||||||
|
Flatpak Steam runs inside its own sandbox. For the bwrap overlay to work:
|
||||||
|
- The Flatpak app needs filesystem access to `/opt/steam/` (granted via `flatpak override --user --filesystem=/opt/steam`)
|
||||||
|
- The bwrap overlay is set up OUTSIDE Flatpak's sandbox — bwrap creates the mount namespace first, then `flatpak run` inherits it
|
||||||
|
- Confirmed working: writes inside Flatpak's sandbox go through bwrap's overlay to the per-user upper layer
|
||||||
|
|
||||||
|
## Integration with Dotfiles
|
||||||
|
|
||||||
|
The dotfiles project (`~/.syncthing/dotfiles/`) integrates this via:
|
||||||
|
- `nix run github:felixfoertsch/steam-shared-library#activate` runs on Linux machines
|
||||||
|
- Old wrapper system cleanup is handled by `roles/legacy-cleanup/` in the Ansible playbook
|
||||||
|
- Users are added to `steamshare` group via the `managed-users` role's create flow
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
- **bubblewrap not installed:** activation script checks and fails with clear error message
|
||||||
|
- **Steam not installed:** launcher script checks both native and Flatpak, fails with clear message
|
||||||
|
- **User not in steamshare group:** Steam launches but can't access `/opt/steam/` — standard permission denied from filesystem
|
||||||
|
- **Stale work dir:** overlayfs work directories can become stale (causes "Device or resource busy"). The launcher creates fresh dirs if the work dir contains a stale `work/` subdirectory.
|
||||||
|
- **/opt/steam/ doesn't exist:** activation script creates it; launcher checks and fails with clear message
|
||||||
|
|
||||||
|
## User Workflow
|
||||||
|
|
||||||
|
1. Admin runs `nix run .#activate`
|
||||||
|
2. Admin adds users to steamshare group: `sudo usermod -aG steamshare <user>`
|
||||||
|
3. User logs out and back in (for group membership)
|
||||||
|
4. User launches Steam normally via app menu or command line
|
||||||
|
5. Games install to `/opt/steam/` — visible to all users
|
||||||
|
6. Proton prefixes go to `~/.local/share/steam-shared/upper/<appid>/` — per-user, isolated
|
||||||
|
7. Multiple users can be logged in simultaneously (GDM user switching) — each has their own mount namespace
|
||||||
|
|
||||||
|
## Guided Gates
|
||||||
|
|
||||||
|
- **GG-1:** Run `nix run .#activate`, verify group/dir/ACLs/desktop file/launcher script created correctly
|
||||||
|
- **GG-2:** Launch Steam via the desktop entry, verify overlay is active (write to compatdata goes to upper layer)
|
||||||
|
- **GG-3:** Install a game from Steam, verify it lands in `/opt/steam/steamapps/common/` (shared, not overlaid)
|
||||||
|
- **GG-4:** Launch a Proton game with any Proton variant, verify prefix lands in `~/.local/share/steam-shared/upper/<appid>/` and NOT in `/opt/steam/steamapps/compatdata/<appid>/`
|
||||||
|
- **GG-5:** Create a second user, add to steamshare, launch Steam, verify shared games visible and prefix is isolated
|
||||||
|
- **GG-6:** Both users logged in via GDM simultaneously — both can access Steam without conflicts
|
||||||
|
- **GG-7:** Run `nix run .#uninstall`, verify clean removal of all artifacts
|
||||||
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1775126147,
|
||||||
|
"narHash": "sha256-J0dZU4atgcfo4QvM9D92uQ0Oe1eLTxBVXjJzdEMQpD0=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "8d8c1fa5b412c223ffa47410867813290cdedfef",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
48
flake.nix
Normal file
48
flake.nix
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
description = "Multi-user shared Steam library for Linux using bubblewrap overlay";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
activate = pkgs.writeShellApplication {
|
||||||
|
name = "steam-shared-activate";
|
||||||
|
runtimeInputs = with pkgs; [ acl coreutils findutils ];
|
||||||
|
text = ''
|
||||||
|
exec "${self}/scripts/activate.sh" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
uninstall = pkgs.writeShellApplication {
|
||||||
|
name = "steam-shared-uninstall";
|
||||||
|
runtimeInputs = with pkgs; [ coreutils ];
|
||||||
|
text = ''
|
||||||
|
exec "${self}/scripts/uninstall.sh" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
add-user = pkgs.writeShellApplication {
|
||||||
|
name = "steam-shared-add-user";
|
||||||
|
runtimeInputs = with pkgs; [ coreutils ];
|
||||||
|
text = ''
|
||||||
|
exec "${self}/scripts/add-user.sh" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit activate uninstall add-user;
|
||||||
|
default = activate;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
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