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

12
.gitignore vendored Normal file
View 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
View 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
View 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

View 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"
```

View 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
View 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
View 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
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 "========================================"