Add SteamOS distro

Adds `--distro steamos` as a first-class build target. Pattern is the
same as bazzite-deck/batocera: distros/steamos/build-rootfs.sh fetches
the upstream rootfs (Valve's Steam Deck recovery .img.bz2 from
steamdeck-images.steamos.cloud), extracts the rootfs-A + var-A btrfs
slots into $CHROOT, swaps in our linux-ps5 kernel pkg.tar.zst, and
rebuilds the initramfs with the modules amdgpu needs for the PS5
Oberon GPU.

PS5-specific fixes applied to the SteamOS rootfs:

- Replace /usr/bin/steamos-session-select with a shim that writes
  /etc/sddm.conf.d/zzz-session-override.conf directly + restarts sddm.
  The vendored helper goes through steamosctl / steamos-manager whose
  Deck-hardware code paths silently no-op on PS5, leaving the UI stuck
  at "Switching to Desktop..." with no actual session swap.

- Install a small Python dbus monitor (steam-session-switch-listener)
  that catches org.freedesktop.DisplayManager.Seat.SwitchToUser from
  Big Picture's "Switch to Desktop" button and writes the same
  override file. No -steamos3 / -steampal needed (those trigger Steam's
  A/B update reboot loop on a non-Deck box).

- Drop a Return-to-Gaming-Mode.desktop into /home/deck/Desktop pointing
  at steamos-session-select gamescope.

- chown /home/deck to deck:deck (the recovery image leaves it root:root,
  which breaks ~/.config writability on first Plasma launch).

- Enable sshd + set deck:deck so the box is reachable for further
  iteration without dragging a USB keyboard around.

- Patch /etc/fstab: comment out the by-partsets atomic mount entries
  (those paths don't exist on a flat-rootfs flash), append a LABEL-based
  /efi mount so the FAT boot partition comes up cleanly. Required for
  ps5-iw620-firmware.service to find the WLAN blob.

- Append amdgpu.dpm=0 + amdgpu.gpu_recovery=0 to the kernel cmdline.
  modprobe.d alone isn't enough — amdgpu loads before the rootfs conf
  is read, so the cmdline is the only reliable path. Without these
  Switch-to-Desktop produces a black screen on the PS5 HDMI output.

- Polkit-deny the deck user reboot/poweroff to break Steam's update
  reboot loop on first boot.

- Patch steam-jupiter to skip its OOBE rm -rf path. The OOBE assumes
  Deck factory layout and wipes /home/deck/.local if it runs.

Verified end-to-end on real PS5 (HW-rev EA-1102): autologin lands in
gamescope, Switch-to-Desktop and Return-to-Gaming both work, sshd is
reachable, ps5-iw620 brings up internal WiFi after firmware staging.

Files:
- distros/steamos/build-rootfs.sh  (646 lines)
- distros/steamos/README.md
- distros/steamos/return-to-gaming-mode.desktop
- distros/steamos/steam-session-switch-listener.{py,service}
- build_image.sh: steamos → FORMAT=arch (uses linux-ps5 pkg.tar.zst)
- .github/workflows/build-image.yml: steamos in matrix options + distros list
This commit is contained in:
mia
2026-06-27 17:10:07 -04:00
committed by mia26MAjFm
parent 096675b60a
commit 9fa102e098
7 changed files with 797 additions and 3 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ on:
description: 'Distribution'
default: 'ubuntu2604'
type: choice
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck]
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
workflow_call:
inputs:
distro:
@@ -34,7 +34,7 @@ jobs:
run: |
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
if [ "$INPUT" = "all" ]; then
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck"]' >> "$GITHUB_OUTPUT"
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
else
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
fi
+1 -1
View File
@@ -85,7 +85,7 @@ esac
if [ -z "$FORMAT" ]; then
case "$DISTRO" in
arch|cachyos) FORMAT="arch" ;;
arch|cachyos|steamos) FORMAT="arch" ;;
fedora|bazzite*) FORMAT="rpm" ;;
all) FORMAT="all" ;;
*) FORMAT="deb" ;;
+42
View File
@@ -0,0 +1,42 @@
# steamos
SteamOS 3 image variant for PS5, built by extracting the rootfs from
Valve's official Steam Deck recovery image and swapping in our
`linux-ps5` kernel.
## Source
- Upstream: <https://steamdeck-images.steamos.cloud/recovery/steamdeck-repair-latest.img.bz2>
(Steam Deck recovery image; SteamOS Holo Arch-based.)
- ~3.4 GB compressed bz2, ~10 GB decompressed.
- Valve CDN — no per-IP throttle observed; first-time cache pull on a
cold runner is ~3-5 min, subsequent runs are instant via
`/data/cache/ps5/downloads/`.
## What the build does
1. Resolve `steamdeck-repair-latest.img.bz2` → its dated filename so
the cache key is stable.
2. Decompress → losetup -P.
3. Mount the first **erofs** partition (rootfs-A; A and B are
identical) read-only, `cp -a` to `$CHROOT`.
4. Mount the first **ext4 < 2 GB** partition that contains `/lib`
(var-A), `cp -a` to `$CHROOT/var` — SteamOS splits `/var` off the
rootfs and first-boot fails without it.
5. Extract our `linux-ps5-*.pkg.tar.zst` and merge `usr/`, `boot/`,
`etc/` into `$CHROOT`.
6. Delete SteamOS's `linux-neptune` kernel + modules so grub boots
ours.
7. `depmod -a $KVER`.
The standard image-builder flow then packs `$CHROOT` into an ext4
image with our EFI partition.
## Caveats
- The Steam UI session expects the Deck's `jupiter` hardware quirks
(gamepad, ALS, fan curve daemons) — most of those won't apply on
PS5. Falling back to plasma desktop on the first session is normal.
- SteamOS is read-only by design (steamos-readonly enable). The image
we build here is regular ext4; if you want the read-only A/B atomic
experience, that needs a separate variant.
+646
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=Return to Gaming Mode
Comment=Switch the autologin session back to gamescope / Steam Deck UI
Exec=bash -c 'steamos-session-select gamescope'
Icon=steamdeck-gaming-return
Terminal=false
Categories=System;
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Listens on the system bus for Steam Big Picture's Switch-to-Desktop
dbus call (org.freedesktop.DisplayManager.Seat.SwitchToUser) and performs
the session swap directly — runs as root so no pkexec / polkit needed.
Flow:
1. Steam UI's Power-Menu Switch-to-Desktop fires
dbus-send --system --dest=org.freedesktop.DisplayManager \\
/org/freedesktop/DisplayManager/Seat0 \\
org.freedesktop.DisplayManager.Seat.SwitchToUser \\
string:doorstop string:plasma
2. This listener catches the call (BecomeMonitor mode on system bus)
3. Writes /etc/sddm.conf.d/zzt-steamos-temp-login.conf for plasma
4. Runs 'steam -shutdown' as the deck user — gamescope-session ends
cleanly because Steam (gamescope's child) exits, DRM master is
released the gentle way, sddm bounces and autologs into plasma.
5. The zzt- conf is one-shot — sddm's ExecStartPre wipes it on the
next sddm restart, so closing plasma drops back to gamescope.
"""
import dbus, dbus.mainloop.glib, subprocess, sys, traceback
from gi.repository import GLib
LOG = "[steam-session-switch-listener]"
# zzz- sorts AFTER zz-steamos-autologin.conf (so it overrides), AND it
# doesn't match the ExecStartPre rm pattern that only deletes 'zzt-...'
# — so the conf survives `systemctl restart sddm` cycles.
ZZT = "/etc/sddm.conf.d/zzz-session-override.conf"
SESSION_MAP = {
"plasma": "plasma.desktop",
"plasma-wayland": "plasma.desktop",
"plasma-wayland-persistent": "plasma.desktop",
"desktop": "plasma.desktop",
"plasma-x11": "plasmax11.desktop",
"plasma-x11-persistent": "plasmax11.desktop",
"gamescope": "gamescope-wayland.desktop",
}
def handle_switch(session):
target = SESSION_MAP.get(session, "gamescope-wayland.desktop")
print(f"{LOG} writing zzt- conf -> {target}", flush=True)
with open(ZZT, "w") as f:
f.write(f"[Autologin]\nUser=deck\nSession={target}\nRelogin=true\n")
# Restart sddm directly. Since we used zzz-session-override.conf,
# the ExecStartPre that only kills zzt-... doesn't wipe it. sddm
# restart reads the conf (zzz- sorts after zz-steamos-autologin.conf,
# so it wins) and autologs into the requested session.
print(f"{LOG} systemctl restart sddm", flush=True)
subprocess.Popen(["systemctl", "restart", "sddm"], stdin=subprocess.DEVNULL)
def on_message(_bus, message):
try:
if message.get_interface() != "org.freedesktop.DisplayManager.Seat":
return
if message.get_member() != "SwitchToUser":
return
args = list(message.get_args_list())
if len(args) < 2:
return
user, session = str(args[0]), str(args[1])
print(f"{LOG} caught SwitchToUser({user!r}, {session!r})", flush=True)
handle_switch(session)
except Exception:
# NEVER let an exception kill the monitor — log and continue
traceback.print_exc()
def main():
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
bus_obj = bus.get_object("org.freedesktop.DBus", "/org/freedesktop/DBus")
monitoring = dbus.Interface(bus_obj, "org.freedesktop.DBus.Monitoring")
monitoring.BecomeMonitor(
["type='method_call',"
"interface='org.freedesktop.DisplayManager.Seat',"
"member='SwitchToUser'"],
dbus.UInt32(0),
)
bus.add_message_filter(on_message)
print(f"{LOG} listening on system bus for SwitchToUser", flush=True)
GLib.MainLoop().run()
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,14 @@
[Unit]
Description=Translate Steam Big Picture Switch-to-Desktop dbus call into steamos-session-select
Documentation=https://gist.github.com/Rishikant181/e26fb23d4c57db74bddaa0a57b26cd26
After=dbus.service sddm.service
PartOf=sddm.service
[Service]
Type=simple
ExecStart=/usr/local/sbin/steam-session-switch-listener
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target