Bazzite's /usr/bin/dnf is a 1KB bash wrapper that intercepts install/remove
args and prints a docs URL pointing at rpm-ostree unless it detects a
container or a dev-mode ostree deployment. Our kexec-booted install has
neither: the image build wipes /sysroot/ostree/deploy/ + masks
rpm-ostreed, so dnf install <anything> prints the URL and exits 1.
Since ostree is gone the guard has no purpose here; point dnf at dnf5
directly so 'dnf install linux-ps5' works Fedora-style.
The deb and arch packagers already copy /out/staging/{etc,usr/local}
into their packages, but the rpm packager was missing that step, so
Fedora/Bazzite installs never got the firmware-bridge service or
the moal modprobe.d config. Symptom: moal loads on boot, requests
nxp/pcieuartiw620_combo_v1.bin, kernel returns -ENOENT because the
firmware sits at /efi/lib/nxp/ instead of /lib/firmware/nxp/, and
wifi is dead until fix-wifi.sh runs by hand.
Build #28301886085 failed with 'curl: command not found' on the URL
resolution step. Upstream Dockerfile only installs wget + python3,
not curl. Use urllib.request.urlopen().geturl() to follow Valve's
redirect and pull the versioned IMG name (e.g. steamdeck-recovery-
4-20250603.5-3.7.10.img.bz2) for the cache key.
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
Cachyos build on minipc-gh-2 failed with "Directory not empty"
because a previous run left a loop device backed by a (deleted) file
in image/work/, and rm -rf can't cross the live mountpoint. Lazy-
umount everything under the workspace + detach matching loops before
the rm so stale mounts don't fail subsequent runs.
5h cold-cache download from o2switch (only mirror, rate-limited to
~250 KB/s), and no persistent cache plumbing yet means every run pays
the full cost. Ship bazzite/bazzite-deck for emulation use cases.
distros/batocera/build-rootfs.sh:211 calls python3 inline to patch
libretroControllers.py with the PS5 HOTKEY-on-gamepad fix. Without it
the batocera build exits 127 at the very end of an otherwise-clean
run.
The mirror rotates older builds out — the pinned 43-20260430 went 404
on its own within ~4 weeks. Scrape the `last/` index for whatever
batocera-x86_64-*.img.gz is there now (currently 43.1-20260529). VER
and BUILD are then derived from the discovered filename so the cache
key + log lines remain informative.
BATOCERA_URL still wins if set (override path unchanged).
CACHEBUST only invalidates the umoci layer; the broken cached layer on
the Oracle runner is upstream (apt-install), so step 10 still tried to
exec /bin/sh from a corrupt parent FS. --no-cache rebuilds every layer
fresh — slower (~5 min apt re-run per build) but reliable on every
runner regardless of buildkit storage state.
One org runner has a broken cached layer for the umoci install (runc:
"/bin/sh: no such file"). Pass a unique build-arg each invocation so
that layer + downstream get rebuilt fresh, while the heavy apt install
above stays cached.
One of the runners has a corrupted buildkit cache for the umoci layer
("/bin/sh: no such file or directory" mid-RUN). --pull refreshes the
ubuntu:24.04 base, which invalidates all downstream cached layers and
forces the umoci step to actually re-execute.
The kernel rpm built by kernel-builder/build.sh now ships mlan.ko / moal.ko
plus modprobe options for the PS5 IW620, so the image side no longer needs
its own out-of-tree build (which was pinned to an old ps5-linux-mwifiex
ref and only applied 1 of 3 patches, breaking against kernel 7.1.x).
Suppress the rpm's modules-load.d autoload so firmware-copy still runs
first via ps5-iw620.service.
git clone https://github.com/... fails with 'server certificate verification
failed. CAfile: none' because ubuntu:24.04 with just 'git' doesn't pull in
ca-certificates. Hit in 'Building NXP IW620 mwifiex driver' stage.
#20 added the distro directories but missed updating the dispatch choice
enum and the 'all' matrix expansion, so the three new distros couldn't
be triggered through CI.
linux-ps5 currently ships no wifi driver — the PS5 onboard wifi is NXP
IW620 (not the upstream mwifiex), which lives at ps5-linux/ps5-linux-
mwifiex as an out-of-tree patch against nxp-imx/mwifiex. Users have to
clone that repo + run install.sh on the target, which needs gcc + kernel
headers and is a manual step every kernel bump.
Build it in the kernel-builder container right after the kernel: clone
both repos, apply ps5-iw620.patch, make against the kernel src tree,
install mlan.ko+moal.ko into /out/staging/lib/modules/$KVER/extra/
ps5-iw620/ with the NXP modprobe options + autoload conf. depmod -b
refreshes the index. All package formats (.deb/.rpm/.pkg.tar.zst) then
include the driver automatically.
MWIFIEX_REPO / MWIFIEX_REF overridable via env in case downstream wants
to test a fork or specific commit.
Steam in -steamos3 mode polls for SteamOS atomic OS updates via
steamos-update, steamos-update-rauc, steamos-atomupd-client. CachyOS
ships none of these (pacman handles rolling updates), so every poll
fires "YieldingApplyUpdateOS: OS update result: 2" and the GamepadUI
shows "Updater apply error: 2: null".
That alone is just a banner, but the first-run OOBE blocks on the
"Software updates" step waiting for a successful update result, so
users can't get past initial setup. Field-reported.
Dropping -steamos3 keeps -gamepadui (controller-friendly UI) without
the SteamOS-mode update polling. OS updates: sudo pacman -Syu.
Polls ps5-linux/ps5-linux-patches for new kernel-X.Y.Z-<sha> tags at
04:00 UTC. If newer than build_image.sh's PATCHES_REF, bumps + pushes
to main; the push fires trigger-builds.yml which rebuilds all images
against the new kernel. Safe-skips if PATCHES_REPO has been forked.
The dispatch in #20 ran build-rootfs.sh without exporting $CHROOT, so
inside the script it was empty — meaning '/' expansions like
`mv "$UNPACK/rootfs"/* "$CHROOT/"` resolved to '/' and the bazzite
rootfs went into the build container's root. Container-contained, but
the build failed with a confusing 'Device or resource busy' on
/etc/resolv.conf.
Two fixes:
- entrypoint exports DISTRO/CHROOT/KVER/ROOT_LABEL/EFI_LABEL when
calling the per-distro script, and asserts $CHROOT non-empty.
- bazzite + batocera scripts have a top-of-file guard that bails if
$CHROOT is unset, missing, or '/'.
Co-authored-by: mia26MAjFm <mia26MAjFm@users.noreply.github.com>
Each distro ships with a self-contained build-rootfs.sh that the entrypoint
calls in place of distrobuilder. This keeps upstream's distrobuilder flow
untouched for the existing distros (ubuntu/arch/cachyos/fedora/etc.) and
isolates the new distros' build logic in their own dirs.
distros/bazzite/
build-rootfs.sh — skopeo+umoci on ghcr.io/ublue-os/bazzite:stable,
promote ostree /usr/etc -> /etc, install linux-ps5 RPM,
mask rpm-ostree services, set up grow-rootfs + DTM-TA
amdgpu reprobe.
image.yaml, grow-rootfs, grow-rootfs.service
distros/bazzite-deck/
Symlinks into distros/bazzite/ — bazzite-deck shares all build logic;
the case branch in build-rootfs.sh dispatches via $DISTRO.
distros/batocera/
build-rootfs.sh — download upstream batocera-x86_64-*.img.gz, mount,
extract /boot/batocera squashfs, swap in linux-ps5
kernel, patch libretroControllers.py, install first-boot
SHARE-partition creator (ps5-share-init).
docker/image-builder/entrypoint.sh
- detect_kver() helper from /kernel-debs/ package filenames
- dispatch to distros/$DISTRO/build-rootfs.sh when present (skips
distrobuilder for that distro)
- EFI assembly detects /boot/efi/ vs /boot/bzImage layouts so batocera
(which mounts FAT at /boot, not /boot/efi, for batocera-part SHARE
detection) works alongside the standard layout
build_image.sh
- IMG_SIZE defaults: bazzite* -> 24 GB, batocera* -> 16 GB
- FORMAT: bazzite* -> rpm (linux-ps5 RPM); batocera* stays deb
CI workflows are NOT modified — the upstream build-image.yml /
trigger-builds.yml continue to apply and will skip these distros unless
explicitly dispatched (they're not in MULTI_DISTROS). Local builds:
./build_image.sh --distro bazzite
./build_image.sh --distro bazzite-deck
./build_image.sh --distro batocera
Co-authored-by: mia26MAjFm <mia26MAjFm@users.noreply.github.com>