Author SHA1 Message Date
Dan Kluser 842c30bd7a change kernel ref 2026-07-31 21:31:34 +02:00
Dan Kluser fdc0f22eb2 update ref for test 2026-07-31 08:11:56 +02:00
Dan Kluser 787ec039ba debian for better cross-compile 2026-07-31 08:05:50 +02:00
Dan Kluser d7355b7b3c Remove platform arg from Dockerfile; controller by build_image.sh 2026-07-29 08:33:21 +02:00
Andy Nguyen ed43393b8b Add platform arg to Dockerfile. 2026-07-21 21:46:06 +02:00
TertiumandGitHub 8de5e02050 grow-rootfs: skip rootfs auto-grow on m2 (#26) 2026-07-11 13:55:37 +02:00
mia26MAjFmandGitHub 38c61af4a0 kernel-builder: add zlib1g-dev (#25)
gendwarfksyms links -lz.
2026-07-08 21:05:31 +02:00
ps5-linux-bot 94284d3c14 kernel: auto-bump kernel-7.1.2-bf16f7e -> kernel-7.1.3-e81e281 2026-07-06 08:37:43 +00:00
ps5-linux-bot 3c3be20587 kernel: auto-bump kernel-7.1.2-a57d783 -> kernel-7.1.2-bf16f7e 2026-07-05 07:33:52 +00:00
mia26MAjFm b67f9a5a47 cachyos: pkill plasmashell + kwin before switching to gamescope 2026-07-04 23:47:09 -04:00
mia26MAjFm 34fa9da3f1 cachyos: soft-handoff on return-to-desktop instead of reboot 2026-07-04 23:07:26 -04:00
Your Name 74f978b1bb bazzite: replace dnf shell wrapper with dnf5 symlink
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.
2026-07-03 13:48:40 -04:00
Your Name d763b8496f kernel-builder-rpm: ship ps5-stage-firmware + ps5-bt-quiet services
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.
2026-07-03 13:43:26 -04:00
mia26MAjFm babdcf0762 build-image: fail-fast=false 2026-07-01 09:40:51 -04:00
mia26MAjFm 3db2fb65b3 watcher: add actions:write for createWorkflowDispatch 2026-07-01 09:00:30 -04:00
ps5-linux-bot 415d6e9ee8 kernel: auto-bump kernel-7.1.1-07f9d7f -> kernel-7.1.2-a57d783 2026-07-01 08:18:34 +00:00
mia d7d3b22c1f kernel-builder: ship ps5-stage-firmware + ps5-bt-quiet services 2026-06-30 21:05:29 -04:00
mia 7b8a097651 grow-rootfs: drop stale partitions from kernel + BLKPG ioctl (mounted-safe) 2026-06-30 20:00:14 -04:00
mia 7f8d0302b7 kernel pkg: bundle ps5-stage-firmware service + modprobe.d (fixes wifi) 2026-06-30 19:58:23 -04:00
mia 44bd2b4cf5 bazzite: NetworkManager backend=wpa_supplicant (iwd breaks mwifiex) 2026-06-30 18:19:50 -04:00
mia d891b0c01e watcher: dispatch build-image after bump 2026-06-30 11:06:54 -04:00
ps5-linux-bot f05b110a46 kernel: auto-bump kernel-7.1.1-6cfe53c -> kernel-7.1.1-07f9d7f 2026-06-30 07:58:31 +00:00
ps5-linux-bot 6a10749945 kernel: auto-bump kernel-7.1.1-76db3a4 -> kernel-7.1.1-6cfe53c 2026-06-28 15:11:10 +00:00
mia 4c27d97ce0 watcher: ubuntu-latest — no need for self-hosted (api only) 2026-06-28 11:10:56 -04:00
mia b17ce46319 watcher: use releases api (chronological), not tags 2026-06-28 11:04:45 -04:00
mia af8c7d472f watcher: use github-script (no gh, no checkout) 2026-06-28 11:03:29 -04:00
mia bfdb43f1a1 watcher: no checkout — read+write build_image.sh via api 2026-06-28 11:00:39 -04:00
mia 33c6bc2660 watcher: skip workspace clean (root-owned build leftovers) 2026-06-28 10:56:19 -04:00
mia 7f4b0009d0 watcher: use gh api tag order, not sort -V on hex sha 2026-06-28 10:51:55 -04:00
mia 9f9fd3fd7f release: add steamos to ALL_DISTROS table 2026-06-28 01:36:49 -04:00
mia eafb7fbd56 steamos: tar --zstd instead of bsdtar 2026-06-27 20:51:47 -04:00
mia 21f86f562e ship bluez + enable bluetooth.service across distros 2026-06-27 19:05:04 -04:00
mia de5d60da56 steamos: drop curl dep — image-builder doesn't ship it
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.
2026-06-27 18:44:59 -04:00
miaandmia26MAjFm 9fa102e098 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
2026-06-27 17:10:07 -04:00
mia26MAjFm 096675b60a release: include bazzite + bazzite-deck in the table 2026-06-24 09:45:33 -04:00
mia26MAjFm 00d89868c8 build-image: clean step now umounts + detaches loops before rm
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.
2026-06-24 06:59:55 -04:00
mia26MAjFm b1aa5e5b50 build-image: drop batocera from matrix
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.
2026-06-24 06:49:50 -04:00
mia26MAjFm b2c77d5cb2 image-builder: add python3 (needed by batocera build-rootfs.sh)
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.
2026-06-24 02:01:13 -04:00
mia26MAjFm f8d0b65b36 batocera: auto-discover the current build instead of hardcoding
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).
2026-06-23 19:27:48 -04:00
mia26MAjFm 875bb35fef image-builder: --no-cache instead of CACHEBUST arg
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.
2026-06-23 18:13:15 -04:00
mia26MAjFm 7e42201d88 image-builder: CACHEBUST arg to force-rebuild umoci layer
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.
2026-06-23 18:03:17 -04:00
mia26MAjFm 0367dcb89c image-builder: pass --pull on docker build
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.
2026-06-23 17:33:55 -04:00
mia26MAjFm 98892eec6b fedora: drop duplicate in-image mwifiex build, use rpm-shipped modules
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.
2026-06-23 16:07:45 -04:00
rmuxandGitHub 9350227d7b apply cmd-timeout-recover and kernel71-compat mwifiex patches (#22) 2026-06-23 19:47:13 +02:00
mia26MAjFm b10c0b2855 kernel-builder: install ca-certificates
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.
2026-06-21 09:37:42 -04:00
mia26MAjFm bf96770651 build-image: max-parallel 1 -> 5 (we have 5 self-hosted runners) 2026-06-21 09:29:35 -04:00
mia26MAjFm 7f5dc8df0c build-image: wire bazzite, bazzite-deck, batocera into matrix + dispatch
#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.
2026-06-21 09:23:04 -04:00
ps5-linux-bot 7fbb7df59d kernel: auto-bump kernel-7.1.1-2ee6846 -> kernel-7.1.1-76db3a4 (upstream watch) 2026-06-21 11:13:29 +00:00
mia26MAjFm 247e4ea01b kernel-builder: build + bundle NXP IW620 mwifiex driver
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.
2026-06-20 17:07:11 -04:00
mia26MAjFm 71bce4c5cb cachyos: drop -steamos3 from gamescope-session-ps5
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.
2026-06-20 16:02:56 -04:00
mia26MAjFm a32505e94d ci: daily upstream patches watch — auto-bump PATCHES_REF
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.
2026-06-20 15:29:30 -04:00
mia26MAjFm fe4e686d62 bump PATCHES_REF v1.3 -> kernel-7.1.1-2ee6846
7.1.1 series. Includes SVM implementation + running
transmitter_control in guest mode.
2026-06-20 15:26:50 -04:00
e1f17f7e38 fix: export CHROOT/DISTRO/KVER to build-rootfs.sh + sanity guard (#21)
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>
2026-06-20 12:43:53 -04:00
Andy Nguyen f090f170a0 Remove kali from github workflows. 2026-06-20 17:53:28 +02:00
9682cada8d Add bazzite, bazzite-deck, batocera distro support (#20)
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>
2026-06-20 11:51:53 -04:00
Andy Nguyen ccc30f19b4 Remove kali since it's too big. 2026-06-20 17:40:50 +02:00
Andy Nguyen 34df7b0b64 Undo changes to zz-update-boot. 2026-06-19 20:14:58 +02:00
Hasanuddin Abu BakarandGitHub 76824ba725 fix(kernel): merge UAPI headers into existing usr/ directory (#17)
Both packagers copied the staged UAPI headers with:

    cp -a /out/staging/headers/usr "$DEST/usr"

Because $DEST/usr already exists (modules live under $STAGING/usr/lib/modules,
and other files may be staged under $PKG/usr), cp placed the source directory
inside the destination, producing a nested usr/usr/include/ path. The headers
were therefore installed at the wrong location and not found by builds.

Merge the contents with cp -a .../usr/. "$DEST/usr/" so the headers land at
the expected /usr/include/linux/... path in both .deb and .pkg.tar.zst
packages.
2026-06-16 20:13:44 +02:00
Bug Bounty Zip 7d5efa0583 Add Debian 12 (XFCE) image support
Plain Debian 12 Bookworm with XFCE desktop, LightDM autologin, Firefox
ESR, and standard desktop utilities. Fills the gap between the heavier
Kali (Debian + security tools) and the headless Proxmox (Debian +
hypervisor) — a clean, lightweight Debian desktop for PS5.

Tested on PS5 hardware: XFCE displays over HDMI, boots to desktop.

[skip ci]
2026-06-14 00:39:13 +08:00
Bug Bounty Zip 76d3ba281e ci: merge release table instead of overwriting on single-distro builds
The release step now fetches the existing release body and merges the
new distro rows into it, instead of rebuilding the table from scratch.
This means a single-distro build (e.g. --distro fedora) only updates
that distro's row and checksum — all other distros stay in the table.

Previously, every single-distro CI run wiped the other distros from
the release page, requiring a manual API patch each time.

[skip ci]
2026-06-14 00:30:19 +08:00
Bug Bounty Zip 177cc5cfbf Add Proxmox VE 8 image support
Debian 12 Bookworm base with Proxmox VE 8.4 (pve-manager, pve-qemu-kvm,
qemu-server). Headless hypervisor managed via web UI at https://<ip>:8006.
Login: root/proxmox.

The PS5 kernel can't satisfy proxmox-ve's hard dependency on a Proxmox
kernel, so the recipe installs an equivs stub (ps5-proxmox-kernel-stub)
that Provides: proxmox-default-kernel before installing proxmox-ve. First
boot auto-bridges the PS5 ethernet into vmbr0 with DHCP. WiFi is not
included (Proxmox VM networking requires wired bridging).

Also wires proxmox into the CI matrix, entrypoint staging, and release
table.

[skip ci]
2026-06-13 23:02:59 +08:00
Bug Bounty Zip ddfda4de88 image-builder: build umoci from source to fix arm64 CI runner crash
Ubuntu's apt umoci (0.4.7) crashes on the self-hosted arm64 runner with
"fatal error: lfstack.push invalid packing" while unpacking Fedora's OCI
base image. The image-builder is an amd64 image, so it runs under qemu on
the arm64 runner, and the emulated process inherits the host's 52-bit-VA
high addresses that the old Go in apt's umoci mis-packs in lfstack. Build
umoci v0.5.0 from source with the Go 1.25 toolchain already installed
(Go >=1.21 handles high virtual addresses correctly). Only Fedora uses
umoci (OCI base); the other distros use debootstrap/pacstrap.

[skip ci]
2026-06-13 07:57:12 +08:00
Bug Bounty ZipandClaude Fable 5 ed54e99482 Add Fedora 44 (GNOME) image support
New fedora distro: distrobuilder fedora-http recipe with full GNOME
desktop (GDM autologin), the PS5 IW620 internal WiFi modules built from
nxp-imx/mwifiex + ps5-linux-mwifiex, and an RPM packaging path for the
shared PS5 kernel (docker/kernel-builder-rpm).

Notable PS5-specific fix: the kernel patches write into the
request_firmware() buffer to skip the firmware signature header
(gfx_v10_0_early_init, amdgpu_sdma_init_microcode). Fedora ships
firmware xz-compressed, and the kernel maps xz-decompressed firmware
read-only (fw_decompress_xz_pages -> fw_map_paged_buf -> vmap
PAGE_KERNEL_RO), so the write oopses amdgpu before /dev/dri exists and
the display never comes up. Distros with zstd or uncompressed firmware
decompress into writable buffers, which is why only Fedora was
affected. The recipe therefore ships the cyan_skillfish GPU firmware
uncompressed, materializing linux-firmware's dedup symlinks first
(unxz refuses symlinks).

Also installs umoci/skopeo in the image builder (Fedora 41+ bases ship
as OCI archives) and wires fedora into build_image.sh (FORMAT=rpm) and
the image-builder entrypoint.

Tested on PS5 hardware: GNOME displays over HDMI, internal WiFi scans
and connects.

[skip ci]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 07:00:06 +08:00
Dan Kluser 50d8ec0b03 ci: reduce compression level 2026-06-03 07:43:58 +02:00
Dan Kluser 5f7cfddd3f ci: update actions version; move release date 2026-06-02 22:00:25 +02:00
Dan Kluser 47b35f48dc ci: move release commit tag 2026-06-02 21:53:31 +02:00
Dan Kluser 2ea14c3e0c ci: remove obsolete upload; using r2 going forward 2026-06-02 21:43:03 +02:00
Andy Nguyen f624aef764 Update to v1.3 patches. 2026-05-30 22:03:16 +02:00
Bug Bounty ZipandGitHub 1c629a881e Merge pull request #13 from BugBountyzip/feature/kali-iw620-default-wifi
Add Kali internal IW620 WiFi support
2026-05-29 16:13:08 +03:00
Bug Bounty Zip 90f06ee77b Add Kali internal IW620 WiFi support 2026-05-29 15:53:36 +03:00
Bug Bounty Zip b68aa667d1 Extend image build timeout 2026-05-27 15:22:44 +03:00
Bug Bounty Zip a1e2bc92e5 Reduce Kali release image scope 2026-05-27 15:19:17 +03:00
Bug Bounty Zip 0adaf1c6d6 Use Kali snapshot for release builds 2026-05-27 10:38:58 +03:00
Bug Bounty Zip d0efec98fe Harden self-hosted image builds 2026-05-27 09:17:16 +03:00
Bug Bounty ZipandGitHub 7f905df12c Merge pull request #11 from ps5-linux/kali-official-support
Include Kali in full image builds
2026-05-27 01:37:57 +03:00
55 changed files with 3405 additions and 536 deletions
+158 -61
View File
@@ -8,19 +8,13 @@ on:
description: 'Distribution'
default: 'ubuntu2604'
type: choice
options: [all, ubuntu2604, arch, cachyos, kali]
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
workflow_call:
inputs:
distro:
type: string
default: 'ubuntu2604'
secrets:
REMOTE_SSH_KEY:
required: true
REMOTE_SSH_CONFIG:
required: true
REMOTE_SSH_KNOWN_HOSTS:
required: true
R2_ACCESS_KEY_ID:
required: true
R2_SECRET_ACCESS_KEY:
@@ -40,7 +34,7 @@ jobs:
run: |
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
if [ "$INPUT" = "all" ]; then
echo 'distros=["ubuntu2604","arch","cachyos","kali"]' >> "$GITHUB_OUTPUT"
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
else
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
fi
@@ -48,20 +42,50 @@ jobs:
build:
needs: matrix
runs-on: self-hosted
timeout-minutes: 240
timeout-minutes: 720
permissions:
contents: write
strategy:
fail-fast: false
max-parallel: 1
max-parallel: 5
matrix:
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
env:
CACHE_DIR: /home/opc/ccache
CCACHE_DIR: /home/opc/ccache
steps:
- name: Clean image workspace
run: |
set -euo pipefail
if [ -z "${GITHUB_WORKSPACE:-}" ] || [ "$GITHUB_WORKSPACE" = "/" ]; then
echo "Invalid GITHUB_WORKSPACE" >&2
exit 1
fi
# A previous run can leave loop mounts (kpartx / losetup -P) and
# bind mounts (distrobuilder chroots) live inside image/work/.
# rm -rf then trips on "Directory not empty" because it can't
# cross the mountpoint. Unmount everything under the workspace
# first, then detach any loop devices still backed by files in
# the workspace, then rm.
WS="$GITHUB_WORKSPACE"
# Lazy-unmount any mount whose target is under the workspace.
# Reverse order so deepest mounts go first.
for m in $(findmnt -rn -o TARGET | grep "^$WS/" | sort -r || true); do
echo "unmounting stale $m"
sudo umount -l "$m" || true
done
# Detach any loop device whose backing file lives under the
# workspace (or shows '(deleted)' against one — that means the
# backing file was removed but the loop wasn't released).
for l in $(sudo losetup -l --noheadings -O NAME,BACK-FILE 2>/dev/null \
| awk -v ws="$WS" '$2 ~ "^"ws || $2 ~ ws"/" || $2 ~ "\\(deleted\\)$" {print $1}'); do
echo "detaching stale loop $l"
sudo losetup -d "$l" || true
done
sudo rm -rf "$WS/image"
- name: Checkout image builder
uses: actions/checkout@v6
with:
@@ -88,7 +112,7 @@ jobs:
- name: Compress image
run: |
xz -T0 -9 -c image/output/ps5-${{ matrix.distro }}.img \
xz -T0 -4 -c image/output/ps5-${{ matrix.distro }}.img \
> image/output/ps5-${{ matrix.distro }}.img.xz
- name: Upload to R2
@@ -120,27 +144,6 @@ jobs:
path: meta/
retention-days: 1
- name: Upload to remote
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
DEST="image-${{ matrix.distro }}-${{ steps.version.outputs.ts }}"
$SSH remote mkdir -p "./$DEST"
$SCP image/output/*.img.xz "remote:./$DEST/"
$SSH remote 'ls -dt image-${{ matrix.distro }}-*/ | tail -n +5 | xargs rm -rf' || true
rm -rf "$DIR"
release:
needs: build
runs-on: self-hosted
@@ -157,35 +160,112 @@ jobs:
- name: Prepare release body
id: body
run: |
KVER=$(cat meta/kver)
PSHA=$(cat meta/psha)
TS=$(cat meta/ts)
SUMS=$(cat meta/*.sha256)
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const path = require('path');
{
echo 'text<<EOF'
echo "PS5 Linux images — built from latest \`main\`."
echo ""
echo "Kernel: \`$KVER\`"
echo "Patches: [\`$PSHA\`](https://github.com/ps5-linux/ps5-linux-patches/commit/$PSHA)"
echo "Built: \`$TS\`"
echo ""
echo "| Image | Download |"
echo "|-------|----------|"
echo "| Ubuntu 26.04 | [\`ps5-ubuntu2604.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-ubuntu2604.img.xz) |"
echo "| Arch | [\`ps5-arch.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-arch.img.xz) |"
echo "| CachyOS | [\`ps5-cachyos.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-cachyos.img.xz) |"
if [ -f meta/kali.sha256 ]; then
echo "| Kali | [\`ps5-kali.img.xz\`](https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev/ps5-kali.img.xz) |"
fi
echo ""
echo "**SHA256 checksums:**"
echo "\`\`\`"
echo "$SUMS"
echo "\`\`\`"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
const kver = fs.readFileSync('meta/kver', 'utf8').trim();
const psha = fs.readFileSync('meta/psha', 'utf8').trim();
const ts = fs.readFileSync('meta/ts', 'utf8').trim();
// Collect new checksums from this run
const newSums = {};
for (const f of fs.readdirSync('meta').filter(f => f.endsWith('.sha256'))) {
const distro = f.replace('.sha256', '');
newSums[distro] = fs.readFileSync(path.join('meta', f), 'utf8').trim();
}
// All known distros and their display names (order = table order)
const R2 = 'https://pub-561df4012f1a46fbbdf618d5cc5941f6.r2.dev';
const ALL_DISTROS = [
{ key: 'ubuntu2604', label: 'Ubuntu 26.04', file: 'ps5-ubuntu2604.img.xz' },
{ key: 'arch', label: 'Arch', file: 'ps5-arch.img.xz' },
{ key: 'cachyos', label: 'CachyOS', file: 'ps5-cachyos.img.xz' },
{ key: 'fedora', label: 'Fedora 44', file: 'ps5-fedora.img.xz' },
{ key: 'proxmox', label: 'Proxmox VE 8', file: 'ps5-proxmox.img.xz' },
{ key: 'debian', label: 'Debian 12', file: 'ps5-debian.img.xz' },
{ key: 'bazzite', label: 'Bazzite (KDE gaming)', file: 'ps5-bazzite.img.xz' },
{ key: 'bazzite-deck', label: 'Bazzite Deck UI', file: 'ps5-bazzite-deck.img.xz' },
{ key: 'steamos', label: 'SteamOS 3 (Holo)', file: 'ps5-steamos.img.xz' },
];
// Fetch existing release body to preserve distros not built this run
const existingSums = {};
try {
const { data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
});
// Extract existing checksum lines (format: "hash filename")
const sumBlock = (rel.body || '').match(/```\n([\s\S]*?)```/);
if (sumBlock) {
for (const line of sumBlock[1].trim().split('\n')) {
const m = line.match(/^(\w+)\s+ps5-(\S+)\.img\.xz$/);
if (m) existingSums[m[2]] = line.trim();
}
}
// Also preserve distros that appear in the table even without checksums
for (const d of ALL_DISTROS) {
if (rel.body && rel.body.includes(d.file) && !existingSums[d.key]) {
existingSums[d.key] = '';
}
}
} catch (e) {
core.info('No existing release found, starting fresh');
}
// Merge: new checksums override existing ones
const mergedSums = { ...existingSums };
for (const [k, v] of Object.entries(newSums)) {
mergedSums[k] = v;
}
// Build the table — only include distros that have been built at least once
const rows = ALL_DISTROS
.filter(d => d.key in mergedSums || d.key in newSums)
.map(d => `| ${d.label} | [\`${d.file}\`](${R2}/${d.file}) |`);
// Combine all checksum lines
const allSumLines = ALL_DISTROS
.filter(d => mergedSums[d.key])
.map(d => mergedSums[d.key])
.join('\n');
const body = [
'PS5 Linux images — built from latest `main`.',
'',
`Kernel: \`${kver}\``,
`Patches: [\`${psha}\`](https://github.com/ps5-linux/ps5-linux-patches/commit/${psha})`,
`Built: \`${ts}\``,
'',
'| Image | Download |',
'|-------|----------|',
...rows,
'',
'**SHA256 checksums:**',
'```',
allSumLines,
'```',
].join('\n');
core.setOutput('text', body);
- name: Move latest tag to current commit
uses: actions/github-script@v8
with:
script: |
const ref = 'tags/latest';
try {
await github.rest.git.updateRef({
...context.repo, ref, sha: context.sha, force: true,
});
} catch (e) {
if (e.status !== 422) throw e;
await github.rest.git.createRef({
...context.repo, ref: 'refs/tags/latest', sha: context.sha,
});
}
- name: Create or update release
uses: softprops/action-gh-release@v3
@@ -195,3 +275,20 @@ jobs:
name: PS5 Linux Image (latest)
body: ${{ steps.body.outputs.text }}
make_latest: true
- name: Refresh release publish date
uses: actions/github-script@v8
with:
script: |
const { data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
});
// Toggling draft off->on->off re-publishes, resetting published_at
// to now while keeping the same release (reactions/comments survive).
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: true,
});
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: false, make_latest: 'true',
});
core.info(`re-published release ${rel.id}`);
+56
View File
@@ -0,0 +1,56 @@
name: Move release tag
on:
workflow_dispatch:
inputs:
commit:
description: 'Commit SHA to point `latest` at (defaults to the selected ref)'
type: string
default: ''
jobs:
move-tag:
runs-on: self-hosted
permissions:
contents: write
steps:
- name: Move latest tag to commit
uses: actions/github-script@v8
with:
script: |
const ref = 'tags/latest';
const sha = '${{ inputs.commit }}'.trim() || context.sha;
try {
await github.rest.git.updateRef({
...context.repo, ref, sha, force: true,
});
} catch (e) {
if (e.status !== 422) throw e;
await github.rest.git.createRef({
...context.repo, ref: 'refs/tags/latest', sha,
});
}
core.info(`latest -> ${sha}`);
- name: Refresh release publish date
uses: actions/github-script@v8
with:
script: |
let rel;
try {
({ data: rel } = await github.rest.repos.getReleaseByTag({
...context.repo, tag: 'latest',
}));
} catch (e) {
if (e.status === 404) { core.info('no latest release yet; skipping'); return; }
throw e;
}
// Toggling draft off->on->off re-publishes, resetting published_at
// to now while keeping the same release (reactions/comments survive).
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: true,
});
await github.rest.repos.updateRelease({
...context.repo, release_id: rel.id, draft: false, make_latest: 'true',
});
core.info(`re-published release ${rel.id}`);
-34
View File
@@ -65,24 +65,6 @@ jobs:
path: meta/
retention-days: 1
- name: Upload to remote
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote mkdir -p ./test-upload
$SCP image/output/ps5-${{ matrix.distro }}.img.xz "remote:./test-upload/"
rm -rf "$DIR"
- name: Cleanup
if: always()
run: rm -rf image/output meta
@@ -154,19 +136,3 @@ jobs:
for d in ubuntu2604 arch cachyos; do
rclone delete "r2:${{ secrets.R2_BUCKET }}/test-${d}.img.xz" -v || true
done
- name: Clean up remote test files
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote rm -rf ./test-upload
rm -rf "$DIR"
-3
View File
@@ -17,9 +17,6 @@ jobs:
with:
distro: all
secrets:
REMOTE_SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
REMOTE_SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
REMOTE_SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
@@ -0,0 +1,72 @@
name: Upstream patches watch
run-name: Watch ps5-linux-patches for new kernel-* tags
on:
schedule:
- cron: '0 4 * * *'
workflow_dispatch:
permissions:
contents: write
actions: write
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
// Read build_image.sh from main via API (no checkout — runner
// workspace has root-owned build leftovers EACCES checkout).
const file = await github.rest.repos.getContent({
owner, repo, path: 'build_image.sh', ref: 'main',
});
const buf = Buffer.from(file.data.content, 'base64');
let body = buf.toString();
const patchesRepo = (body.match(/^PATCHES_REPO="([^"]+)"/m) || [])[1];
if (patchesRepo !== 'https://github.com/ps5-linux/ps5-linux-patches.git') {
core.info(`PATCHES_REPO is a fork (${patchesRepo}) — skipping auto-bump.`);
return;
}
// Releases API returns by published_at (most recent first).
// Tags API order is not chronological — hex SHA suffixes sort
// alphabetically.
const releases = await github.paginate(github.rest.repos.listReleases, {
owner: 'ps5-linux', repo: 'ps5-linux-patches', per_page: 100,
});
const re = /^kernel-\d+\.\d+\.\d+-[a-f0-9]+$/;
const latest = releases.find(r => re.test(r.tag_name))?.tag_name;
if (!latest) { core.setFailed('no kernel-*-* tags found'); return; }
const cur = (body.match(/^PATCHES_REF="([^"]+)"/m) || [])[1];
core.info(`upstream latest: ${latest}`);
core.info(`current ref: ${cur}`);
if (latest === cur) { core.info('already current — no bump.'); return; }
const updated = body.replace(
new RegExp(`^PATCHES_REF="${cur}"`, 'm'),
`PATCHES_REF="${latest}"`,
);
await github.rest.repos.createOrUpdateFileContents({
owner, repo, path: 'build_image.sh', branch: 'main',
message: `kernel: auto-bump ${cur} -> ${latest}`,
content: Buffer.from(updated).toString('base64'),
sha: file.data.sha,
committer: { name: 'ps5-linux-bot', email: 'ps5-linux-bot@users.noreply.github.com' },
author: { name: 'ps5-linux-bot', email: 'ps5-linux-bot@users.noreply.github.com' },
});
core.info(`pushed bump to ${latest}`);
// Pushes authored by GITHUB_TOKEN don't fire workflow_run /
// push events, so trigger-builds.yml won't auto-run. Kick off
// the matrix build directly.
await github.rest.actions.createWorkflowDispatch({
owner, repo, workflow_id: 'build-image.yml', ref: 'main',
inputs: { distro: 'all' },
});
core.info('dispatched build-image.yml distro=all');
+9 -70
View File
@@ -1,13 +1,11 @@
# PS5 Linux Image Builder
Builds bootable Linux USB images for PlayStation 5 using Docker containers. Supports Ubuntu 26.04, Arch, CachyOS (Gamescope + Steam), and full Kali Linux, individually or as a multi-distro image with kexec switching.
Builds bootable Linux USB images for PlayStation 5 using Docker containers. Supports Ubuntu 26.04, Arch, CachyOS (Gamescope + Steam), Fedora (GNOME), individually or as a multi-distro image with kexec switching.
## Prerequisites
- Docker (with permission to run `--privileged` containers) — install as per your distro's instructions
- ~30GB free disk space for Ubuntu, Arch, or CachyOS; a full Kali build needs
substantial working space because it creates a 96GB image and a full rootfs
tree (`~150GB` free recommended for a clean Kali build)
- ~30GB free disk space for Ubuntu, Arch, or CachyOS
Once Docker is installed, add your user to the docker group and apply it without logging out:
@@ -29,8 +27,10 @@ OR
OR
# Build Kali Linux (XFCE + kali-linux-everything)
./build_image.sh --distro kali
OR
# Build Fedora (GNOME desktop)
./build_image.sh --distro fedora
OR
@@ -46,72 +46,13 @@ The script auto-clones the kernel source, applies PS5 patches, compiles, and bui
sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
```
## Kali First Boot Time Sync
The Kali image uses UTC by default and enables `ntpsec`. PS5 hardware may boot
Linux without a correct real-time clock, so the displayed time can be wrong
until a network connection is available. The Kali recipe configures IPv4 NTP
sources that were validated through Android USB tethering, because that
connection may not provide usable IPv6 routing.
The Xfce clock's **Time and Date** window is the legacy `time-admin` utility.
On Kali it can report that NTP support is not installed even though `ntpsec`
is installed and active. Verify or repair synchronization from a terminal:
```bash
systemctl --no-pager status ntpsec
ntpq -pn
timedatectl status
```
If the PS5 clock is still wrong after the internet connection is active, force
one initial correction and restart continuous synchronization:
```bash
sudo systemctl stop ntpsec
sudo ntpd -gq -c /etc/ntpsec/ntp.conf
sudo systemctl start ntpsec
date
```
To use a local timezone after boot, for example Kentucky:
```bash
sudo timedatectl set-timezone America/Kentucky/Louisville
timedatectl status
```
The Kali desktop autologin is enabled for local first boot. SSH is installed
but disabled by default because the initial local account is `kali` with
password `kali`. Before enabling remote access, change that password:
```bash
passwd
sudo systemctl enable --now ssh
```
The image holds its installed kernel packages and protects the boot-copy hook
from deploying a generic Kali kernel. Do not replace or unhold the PS5 kernel
unless you are intentionally testing a new PS5-patched kernel build.
Ghidra is configured to use JDK 21, its documented supported runtime. Full
Kali installations may also contain newer Java versions for other software.
`kali-linux-everything` installs NFS client components, but the PS5-patched
kernel has NFS disabled. A failed `run-rpc_pipefs.mount` unit can therefore be
reported at boot; it only indicates that NFS mounts are unavailable. The Kali
desktop and security tools are unaffected.
The full Kali toolset also enables `chkrootkit.timer`; its daily integrity scan
can use noticeable CPU time while it runs.
## Options
| Flag | Description | Default |
|------|-------------|---------|
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, `kali`, or `all` | `ubuntu2604` |
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, `fedora`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone version selected by PS5 patch set |
| `--img-size` | Disk image size in MB | `12000` (`32000` for `all`, `98304` for `kali`) |
| `--img-size` | Disk image size in MB | `12000` (`32000` for `all`) |
| `--clean` | Remove all cached build artifacts and start fresh | off |
| `--kernel-only` | Build and package the kernel only, then exit | off |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.2` |
@@ -157,7 +98,6 @@ All verbose output goes to `build.log`. The terminal shows a spinner with live p
| Ubuntu 26.04 (Resolute) | GNOME | `.deb` | systemd |
| Arch | Sway | `.pkg.tar.zst` | systemd |
| CachyOS | Gamescope + Steam Big Picture (Arch + `[cachyos]` repo, no v3 migration in image build) | `.pkg.tar.zst` | systemd |
| Kali Linux Rolling | XFCE + `kali-linux-everything` | `.deb` | systemd |
## Multi-distro Image
@@ -195,7 +135,7 @@ sudo dpkg -i linux-bin/linux-ps5_*.deb
build_image.sh # Image builder (also supports --kernel-only)
docker/
kernel-builder/ # Kernel compilation container
kernel-builder-arch/ # Repackages .deb kernel as .pkg.tar.zst
kernel-builder-arch/ # Repackages .deb kernel as .pkg.tar.zst
image-builder/
Dockerfile # Image building container (distrobuilder)
entrypoint.sh # Single-distro build logic
@@ -204,7 +144,6 @@ distros/
ubuntu2604/ # Ubuntu 26.04 (Resolute)
arch/ # Arch Linux
cachyos/ # CachyOS repos + Gamescope/Steam
kali/ # Kali Linux Rolling
shared/ # Kernel postinst hooks (single + multi)
boot/
cmdline.txt # Kernel cmdline template (__DISTRO__ placeholder)
+41 -9
View File
@@ -9,7 +9,7 @@ KERNEL_SRC=""
CLEAN=false
IMG_SIZE=12000
KERNEL_ONLY=false
PATCHES_REF="v1.2"
PATCHES_REF="kernel-7.1.3-e81e281"
MULTI_DISTROS="ubuntu2604 arch cachyos"
@@ -17,9 +17,9 @@ usage() {
echo "Usage: $0 [--distro <distro>] [--kernel <path>] [--img-size <MB>] [--clean]"
echo ""
echo "Options:"
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, kali, all (default: ubuntu2604)"
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, batocera, all (default: ubuntu2604)"
echo " --kernel Path to kernel source directory (default: auto-clone to work/linux/)"
echo " --img-size Disk image size in MB (default: 12000, 32000 for --distro all, 98304 for kali)"
echo " --img-size Disk image size in MB (default: 12000, 32000 for --distro all)"
echo " --clean Remove all cached build artifacts and start from scratch"
echo " --clean-only Remove all cached build artifacts and exit"
echo " --kernel-only Build and package the kernel only, then exit"
@@ -64,12 +64,32 @@ DOCKER_NAME="ps5-build-$$"
if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=32000
fi
if [ "$DISTRO" = "kali" ] && [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=98304
fi
# Bazzite assembles the OCI rootfs + an embedded /sysroot/ostree/repo/objects
# (a deduplicated second copy of the same content) + the linux-ps5 kernel —
# 12 GB is not enough headroom. Bump the default for any bazzite* target.
# Batocera unsquashes to ~6 GB; 12 GB is tight once kernel + initrd +
# /userdata defaults are added. Bump to 16 GB.
case "$DISTRO" in
bazzite*)
if [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=24000
fi
;;
batocera*)
if [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=16000
fi
;;
esac
if [ -z "$FORMAT" ]; then
case "$DISTRO" in arch|cachyos) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
case "$DISTRO" in
arch|cachyos|steamos) FORMAT="arch" ;;
fedora|bazzite*) FORMAT="rpm" ;;
all) FORMAT="all" ;;
*) FORMAT="deb" ;;
esac
fi
KERNEL_BUILDER_PLATFORM="linux/amd64"
@@ -109,6 +129,7 @@ SKIP_CHROOT=false
case "$FORMAT" in
arch) ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
rpm) ls "$KERNEL_OUT"/*.rpm 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
all) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
*) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
@@ -279,7 +300,7 @@ else
KERNEL_SRC="$(cd "$KERNEL_SRC" && pwd)"
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst "$KERNEL_OUT"/*.rpm
run_stage "Build kernel builder image" \
docker build --platform "$KERNEL_BUILDER_PLATFORM" -t ps5-kernel-builder \
@@ -313,6 +334,16 @@ else
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-arch
esac
case "$FORMAT" in rpm)
run_stage "Build rpm packager image" \
docker build -t ps5-kernel-packager-rpm \
-f "$SCRIPT_DIR/docker/kernel-builder-rpm/Dockerfile" "$SCRIPT_DIR"
run_stage "Package kernel (.rpm)" \
docker run --rm --name "$DOCKER_NAME" \
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-rpm
esac
fi
if [ "$KERNEL_ONLY" = true ]; then
@@ -324,7 +355,8 @@ fi
# --- Step 2: Build distribution image ---
run_stage "Build image builder image" \
docker build -t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
docker build --pull --no-cache \
-t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR"
if [ "$DISTRO" = "all" ]; then
DOCKER_ARGS=(
+81 -4
View File
@@ -1,14 +1,91 @@
#!/bin/bash
# Grows the root partition and filesystem to fill the disk.
# Runs once on first boot, then disables itself.
# Grows the root partition + filesystem to fill the disk on first boot.
# Self-disables after success.
#
# Uses sfdisk + partx + resize2fs (all in util-linux + e2fsprogs, both
# present in the minimal Arch/SteamOS base). Earlier versions called
# growpart from cloud-utils — but that package isn't in the SteamOS
# recovery image, so the unit failed with "growpart: command not found"
# and rootfs stayed at the dd'd image size (~14 GB) on any disk larger
# than that.
set -e
ROOT_DEV=$(findmnt -no SOURCE /)
DISK="/dev/$(lsblk -ndo PKNAME "$ROOT_DEV")"
# PS5 M.2 drive appear as /dev/nvme*. Do not modify partition table,
# because it contains m2_init PS5 offset. Only USB drive (/dev/sd*)
# are safe for automatic grow root partition.
if [[ "$(basename "$DISK")" == nvme* ]]; then
echo "grow-rootfs: M.2 detected, skipping"
systemctl disable grow-rootfs.service
exit 0
fi
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)
parted -s "$DISK" resizepart "$PART_NUM" 100%
partprobe "$DISK"
# Our images dd at their built size (~14 GB) onto a much larger USB. The GPT
# backup header is therefore at byte ~built-size, not at end of disk. Move
# the backup header to end-of-disk first so partition table growth works.
sgdisk -e "$DISK"
# If a previous flash left behind partitions BEYOND our rootfs + boot pair
# (e.g. a stale `linux-home` partset at part 3 from a prior SteamOS install
# on the same drive), the partition extend refuses to grow into the space
# already claimed by the leftover partition. Delete every partition with a
# number higher than 2 — we only ever create partitions 1 (rootfs) + 2 (boot).
# Important: sgdisk only rewrites the on-disk GPT; we must ALSO drop those
# stale entries from the kernel's in-memory partition table via partx -d,
# or BLKPG_RESIZE_PARTITION below returns EBUSY because the kernel still
# thinks sda1 would have to grow into space owned by phantom sda3.
for n in $(parted -ms "$DISK" print 2>/dev/null \
| awk -F: 'NR>2 && $1+0>2 {print $1}'); do
sgdisk -d "$n" "$DISK" 2>/dev/null || true
partx -d --nr "$n" "$DISK" 2>/dev/null || true
done
# sfdisk `,+` extends partition $PART_NUM to consume all available free
# space. Works on a mounted/live partition because sfdisk only rewrites
# the GPT entry (kernel re-read of the partition table comes next).
echo ",+" | sfdisk --no-reread -N "$PART_NUM" "$DISK"
# Make the kernel pick up the new partition size without unmounting via
# BLKPG_RESIZE_PARTITION. partx -u also uses BLKPG, but on some images
# (notably SteamOS recovery loop-mounted btrfs) partx returns an error
# updating the mounted partition; the direct ioctl is more reliable.
python3 - "$DISK" "$PART_NUM" <<'PY'
import sys, os, fcntl, ctypes, subprocess
disk, pno = sys.argv[1], int(sys.argv[2])
out = subprocess.run(["sfdisk","--bytes","-q","-l",disk],
capture_output=True, text=True, check=True).stdout
start = sectors = None
for line in out.splitlines():
if line.startswith(f"{disk}{pno} ") or line.startswith(f"{disk}p{pno} "):
f = line.split()
start, sectors = int(f[1]), int(f[3])
break
if start is None:
sys.exit(f"could not parse partition {pno} from sfdisk -l {disk}")
class P(ctypes.Structure):
_fields_=[("start",ctypes.c_longlong),("length",ctypes.c_longlong),
("pno",ctypes.c_int),("devname",ctypes.c_char*64),
("volname",ctypes.c_char*64)]
class A(ctypes.Structure):
_fields_=[("op",ctypes.c_int),("flags",ctypes.c_int),
("datalen",ctypes.c_int),("data",ctypes.c_void_p)]
p = P(start=start*512, length=sectors*512, pno=pno)
a = A(op=3, flags=0, datalen=ctypes.sizeof(p), data=ctypes.addressof(p))
fd = os.open(disk, os.O_RDONLY)
try:
fcntl.ioctl(fd, 0x1269, a) # BLKPG, BLKPG_RESIZE_PARTITION
finally:
os.close(fd)
PY
# Belt-and-suspenders: also try partx in case BLKPG via python failed.
partx -u --nr "$PART_NUM" "$DISK" 2>/dev/null || true
# Online ext4 grow.
resize2fs "$ROOT_DEV"
systemctl disable grow-rootfs.service
+6
View File
@@ -35,6 +35,10 @@ packages:
- wl-clipboard
- xorg-xwayland
# Bluetooth
- bluez
- bluez-utils
# Audio / media
- pipewire
- wireplumber
@@ -166,6 +170,8 @@ actions:
ln -sf /etc/systemd/system/grow-rootfs.service \
/etc/systemd/system/local-fs.target.wants/grow-rootfs.service
systemctl enable bluetooth.service
# Create default user (ps5/ps5)
useradd -m -G wheel,seat -s /bin/bash ps5
echo "ps5:ps5" | chpasswd
+28
View File
@@ -0,0 +1,28 @@
# batocera
Adds support for [Batocera](https://batocera.org/) (Buildroot-based
retro-emulation distro) on PS5 hardware.
Batocera ships as an `.img.gz` with FAT32 boot + ext4 SHARE partitions; the
OS itself lives in a squashfs at `/boot/batocera`.
`distros/batocera/build-rootfs.sh` downloads + unsquashes that image and
swaps in the linux-ps5 kernel:
1. Download `https://mirrors.o2switch.fr/batocera/x86_64/stable/last/`
2. Loop-mount the FAT32, find the embedded squashfs
3. `unsquashfs` to `$CHROOT`
4. Extract the linux-ps5 `.deb`'s `vmlinuz``/boot/bzImage`
5. Patch `libretroControllers.py` (PS5 controller-mapping fix)
6. Set up first-boot SHARE partition creator (`ps5-share-init`)
7. Write fstab with `/boot vfat` (NOT `/boot/efi` — batocera-part's
SHARE auto-detection greps `/proc/mounts` for `/boot`)
## Build locally
```bash
./build_image.sh --distro batocera
```
Image size: 16 GB default (Batocera unsquashes to ~6 GB; headroom for
/userdata). Override the batocera release with `BATOCERA_VER` /
`BATOCERA_BUILD` envs (defaults track the upstream "last" channel).
+369
View File
@@ -0,0 +1,369 @@
#!/bin/bash
# distros/batocera/build-rootfs.sh — download upstream batocera image, extract
# its squashfs to $CHROOT, swap in our PS5 kernel + modules. Called from
# docker/image-builder/entrypoint.sh for DISTRO=batocera*.
#
# Batocera is Buildroot-based, shipping as a single .img.gz with FAT32 boot +
# ext4 SHARE partitions; the OS itself lives in a squashfs file at
# /boot/batocera on the FAT32. We unsquash, swap kernel + modules in, and
# the rest of the standard image-builder flow packs it onto ext4.
#
# Expects in env: DISTRO, CHROOT, KVER, ROOT_LABEL, EFI_LABEL
# Expects on disk: /kernel-debs/*.deb (linux-ps5 .deb to extract bzImage from)
set -ex
: "${CHROOT:?ERROR: \$CHROOT unset/empty}"
[ -d "$CHROOT" ] || { echo "ERROR: \$CHROOT=$CHROOT not a directory"; exit 2; }
case "$CHROOT" in /) echo "ERROR: refuse to operate on /"; exit 2 ;; esac
# Batocera is a Buildroot-based emulation distro. It ships as a
# single .img.gz with FAT32 boot + ext4 SHARE partitions; the OS
# itself lives in a squashfs file (`/boot/batocera`) on the FAT32.
# We unsquash, swap our PS5 kernel + modules in, and let the rest
# of the standard image-builder flow pack everything onto ext4.
# `last/` always points at the current build, but the .img.gz filename
# inside it bakes in the version + date (e.g. batocera-x86_64-43.1-20260529.img.gz),
# and the mirror rotates older builds out. Scrape the index to discover
# whatever's there today rather than hardcoding (the previous hardcoded
# 43-20260430 went 404 within ~4 weeks).
BATOCERA_INDEX_URL="${BATOCERA_INDEX_URL:-https://mirrors.o2switch.fr/batocera/x86_64/stable/last/}"
if [ -z "${BATOCERA_URL:-}" ]; then
IMG_NAME=$(wget -qO- "$BATOCERA_INDEX_URL" \
| grep -oE 'batocera-x86_64-[0-9.]+-[0-9]+\.img\.gz' \
| head -1)
if [ -z "$IMG_NAME" ]; then
echo "ERROR: couldn't find a batocera-x86_64-*.img.gz link at $BATOCERA_INDEX_URL"
exit 1
fi
BATOCERA_URL="${BATOCERA_INDEX_URL}${IMG_NAME}"
else
IMG_NAME="$(basename "$BATOCERA_URL")"
fi
# Pull VER + BUILD out of the discovered (or overridden) filename so the
# cache key + log lines stay informative.
BATOCERA_VER=$(echo "$IMG_NAME" | sed -E 's/^batocera-x86_64-([0-9.]+)-[0-9]+\.img\.gz$/\1/')
BATOCERA_BUILD=$(echo "$IMG_NAME" | sed -E 's/^batocera-x86_64-[0-9.]+-([0-9]+)\.img\.gz$/\1/')
echo "=== Batocera: locate / download $BATOCERA_VER ($BATOCERA_BUILD) ==="
# /build/cache is per-run temp. The workflow symlinks /build/cache/
# persistent -> /data/cache/ps5/downloads, so the .img.gz can be
# pre-staged or survive between runs. The mirror rate-limits per-IP
# to ~250KB/s sustained (4MB/s burst), so re-downloading every run
# is unacceptably slow.
# The workflow hard-links /data/cache/ps5/downloads/* into
# image/work/cache before the build container starts, so the
# image appears as /build/cache/batocera-*.img.gz inside.
CACHED="/build/cache/batocera-${BATOCERA_VER}-${BATOCERA_BUILD}.img.gz"
if [ ! -s "$CACHED" ]; then
echo ">> No cached image, downloading (this will be slow due to mirror rate-limiting)"
wget --tries=3 -O "$CACHED.part" "$BATOCERA_URL"
mv "$CACHED.part" "$CACHED"
fi
echo ">> Using $CACHED ($(du -h "$CACHED" | cut -f1))"
echo "=== Batocera: decompress + loop ==="
BAT_IMG=/build/batocera-src.img
gunzip -c "$CACHED" > "$BAT_IMG"
BATLOOP=$(losetup -Pf --show "$BAT_IMG")
sleep 1
# kpartx fallback in case partition kernel events didn't fire
[ -e "${BATLOOP}p1" ] || kpartx -av "$BATLOOP"
BAT_MNT=$(mktemp -d)
BAT_PART1=""
for p in "${BATLOOP}p1" "/dev/mapper/$(basename "$BATLOOP")p1"; do
[ -e "$p" ] && BAT_PART1="$p" && break
done
mount -o ro "$BAT_PART1" "$BAT_MNT"
BAT_SQUASH=""
for c in /boot/batocera /batocera /boot/batocera.update; do
[ -f "$BAT_MNT$c" ] && BAT_SQUASH="$BAT_MNT$c" && break
done
if [ -z "$BAT_SQUASH" ]; then
echo "ERROR: squashfs not found in batocera image:"
find "$BAT_MNT" -maxdepth 3 -type f | head -30
exit 1
fi
echo "=== Batocera: unsquashfs $BAT_SQUASH -> $CHROOT ==="
unsquashfs -f -d "$CHROOT" "$BAT_SQUASH"
# Batocera ships a SECOND squashfs (boot/rufomaculata) with the
# libretro cores, mame binary, and other emulator assets. At
# runtime it's mounted as a second overlayfs layer on top of the
# main batocera squashfs. We don't do overlay — just unsquash
# rufomaculata on top of $CHROOT so the unified view is realised
# on the ext4 root. Without this, /usr/lib/libretro/ doesn't
# exist and EmulationStation reports "no games start" because
# retroarch fails to load any core.
if [ -f "$BAT_MNT/boot/rufomaculata" ]; then
echo "=== Batocera: unsquashfs boot/rufomaculata (libretro + mame) -> $CHROOT ==="
unsquashfs -f -d "$CHROOT" "$BAT_MNT/boot/rufomaculata"
else
echo "WARN: boot/rufomaculata not found — emulator cores will be missing"
fi
umount "$BAT_MNT"
rmdir "$BAT_MNT"
kpartx -dv "$BATLOOP" 2>/dev/null || true
losetup -d "$BATLOOP"
rm -f "$BAT_IMG"
echo "=== Batocera: install linux-ps5 kernel + modules ==="
KSTAGE=/tmp/bat-kernel-staging
rm -rf "$KSTAGE"; mkdir -p "$KSTAGE"
# The kernel-builder ships a single combined linux-ps5_*.deb
# (Provides: linux-image-X) — there is no linux-image-*.deb on
# disk, so target the actual filename pattern.
shopt -s nullglob
for deb in /kernel-debs/linux-ps5*.deb /kernel-debs/linux-image-*.deb; do
[ -f "$deb" ] && dpkg-deb -x "$deb" "$KSTAGE"
done
shopt -u nullglob
KVER=$(ls -1 "$KSTAGE/lib/modules" 2>/dev/null | head -1)
if [ -z "$KVER" ]; then
echo "ERROR: no kernel modules found after dpkg-deb -x of /kernel-debs/*.deb"
ls -la /kernel-debs/
exit 1
fi
rm -rf "$CHROOT"/lib/modules/*
cp -a "$KSTAGE/lib/modules/$KVER" "$CHROOT/lib/modules/"
mkdir -p "$CHROOT/boot/efi"
cp "$KSTAGE/boot/vmlinuz-$KVER" "$CHROOT/boot/efi/bzImage"
# depmod -b runs from outside the chroot — Batocera's busybox
# depmod may not be present, and host depmod handles -b cleanly.
depmod -a -b "$CHROOT" "$KVER" || true
# Stage WLAN firmware loader + module autoload (same files the
# debian/fedora paths get from /kernel-debs/staging via .deb).
for src in usr/local/sbin etc/modules-load.d etc/systemd/system; do
[ -d "$KSTAGE/$src" ] || continue
mkdir -p "$CHROOT/$src"
cp -an "$KSTAGE/$src/." "$CHROOT/$src/" || true
done
echo "=== Batocera: PS5 modprobe quirks ==="
mkdir -p "$CHROOT/etc/modprobe.d" "$CHROOT/etc/modules-load.d"
cat > "$CHROOT/etc/modprobe.d/ps5-amdgpu.conf" <<MODPROBE
options amdgpu dpm=0 gpu_recovery=0
MODPROBE
# uinput is needed by Batocera's hotkeygen (for virtual keyboard
# events when launching games). It's not autoloaded by default on
# PS5, so hotkeygen crashes with 'UInputError: /dev/uinput does
# not exist'. Force-load on boot.
cat > "$CHROOT/etc/modules-load.d/uinput.conf" <<MODPROBE
uinput
MODPROBE
echo "=== Batocera: build initrd via host mkinitramfs ==="
# Host (image-builder, ubuntu:24.04) has initramfs-tools. Trick
# it into building for our PS5 kernel by symlinking the chroot's
# modules into /lib/modules/$KVER, then unlinking after.
#
# initramfs-tools default behaviour: autodetect kernel modules
# from /sys on the BUILD HOST — which is a docker container with
# no USB, no amdgpu, no real disks. The resulting initrd would
# ship without xhci_pci / usb_storage / ext4 / amdgpu drivers,
# and the PS5 hangs silently when the kernel tries to find the
# USB root partition. Override with an explicit modules list +
# MODULES=most so initramfs-tools includes everything the PS5
# actually needs at boot.
mkdir -p /lib/modules
ln -sfn "$CHROOT/lib/modules/$KVER" "/lib/modules/$KVER"
cat > /etc/initramfs-tools/modules <<'INITMODS'
# USB host controllers (PS5 boot drive is on USB 3 — xhci is the must-have).
xhci_pci
xhci_hcd
ehci_pci
ehci_hcd
ohci_pci
ohci_hcd
# USB storage class + UAS (faster path).
usb_storage
uas
sd_mod
# Filesystems for root + EFI.
ext4
vfat
nls_iso8859-1
nls_cp437
# Common HID so a USB keyboard works at the initramfs shell if we drop there.
usbhid
hid_generic
INITMODS
# Force MODULES=most (curated full driver set, no autodetect).
sed -i 's/^MODULES=.*/MODULES=most/' /etc/initramfs-tools/initramfs.conf
mkinitramfs -k "$KVER" -o "$CHROOT/boot/efi/initrd.img"
rm -f "/lib/modules/$KVER"
rm -rf "$KSTAGE"
echo "=== Batocera: patch configgen to bind HOTKEY combos on gamepad ==="
# Upstream Batocera's libretroControllers.py only sets
# input_enable_hotkey_btn — the hotkey "enable" button — and never
# binds input_exit_emulator_btn / input_menu_toggle_btn /
# input_save_state_btn / input_load_state_btn. The keyboard-side
# bindings (escape = exit, f1 = menu) work fine but on a DualSense
# there's no way out of a game without sshing in and pkill'ing
# retroarch. Patch the function to also bind start/select/L1/R1.
PYFILE="$CHROOT/usr/lib/python3.12/site-packages/configgen/generators/libretro/libretroControllers.py"
if [ -f "$PYFILE" ]; then
python3 - "$PYFILE" <<'PYPATCH'
import sys
p = sys.argv[1]
src = open(p).read()
old = " retroconfig.save('input_enable_hotkey_btn', controllers[0].inputs['hotkey'].id)"
extra = '''
# PS5: map HOTKEY combos to gamepad — upstream sets only the
# enable button, leaving exit-emulator unbound on gamepad. Without
# this, gamepad users can't exit a retroarch game without sshing
# in and pkill'ing retroarch.
for batocera_key, retroarch_key in [
('start', 'input_exit_emulator_btn'),
('select', 'input_menu_toggle_btn'),
('pageup', 'input_load_state_btn'),
('pagedown', 'input_save_state_btn'),
]:
if batocera_key in controllers[0].inputs:
retroconfig.save(retroarch_key, controllers[0].inputs[batocera_key].id)'''
if old in src and extra not in src:
open(p, 'w').write(src.replace(old, old + extra))
print(' patched libretroControllers.py')
else:
print(' skipped (line not found or already patched)')
PYPATCH
else
echo " WARN: $PYFILE missing — configgen patch skipped"
fi
echo "=== Batocera: fstab + users ==="
# NOTE the FAT32 boot partition is mounted at /boot (not
# /boot/efi like the other distros) because batocera-part —
# which S11share uses to autodetect the SHARE partition by
# 'partition next to /boot' — greps /proc/mounts for /boot.
# If we mount at /boot/efi the SHARE auto-detection silently
# fails and S11share falls back to a 256 MB tmpfs at
# /userdata, which won't fit Steam / save data / anything.
# PS5 loader reads bzImage / cmdline.txt from the FAT32
# partition's root regardless of where Linux mounts it.
mkdir -p "$CHROOT/boot"
cat > "$CHROOT/etc/fstab" <<FSTAB
LABEL=$ROOT_LABEL / ext4 defaults 0 1
LABEL=$EFI_LABEL /boot vfat defaults 0 1
LABEL=SHARE /userdata ext4 defaults 0 2
FSTAB
echo "=== Batocera: first-boot SHARE partition creator ==="
# Batocera's design splits the disk into:
# sda1 = rootfs (this image, ~15 GB)
# sda2 = /boot FAT32
# sda3 = /userdata SHARE (everything user-facing — games,
# BIOS, Steam flatpak, screenshots, saves)
# The image only ships sda1+sda2. On first boot, expand the
# GPT backup header to the actual disk end (so parted/sgdisk
# see the full free space) then carve sda3 = SHARE out of
# the remainder. Self-disables after running.
cat > "$CHROOT/usr/local/sbin/ps5-share-init" <<'PS5SHARE'
#!/bin/sh
# First-boot: create the SHARE partition + fs if missing, so /userdata
# is a real disk-backed mount (916 GB on a 1 TB drive) instead of the
# 256 MB tmpfs fallback in /etc/init.d/S11share.
set -e
ROOT_DEV=$(findmnt -no SOURCE /)
DISK=$(/usr/bin/batocera-part prefix "$ROOT_DEV")
SHARE_NUM=$(/usr/bin/batocera-part share_internal_num)
SHARE_DEV="${DISK}${SHARE_NUM}"
[ -b "$DISK" ] || exit 0
# already created on a previous boot?
if [ -b "$SHARE_DEV" ] && blkid -L SHARE >/dev/null 2>&1; then
exit 0
fi
echo "ps5-share-init: extending GPT + creating $SHARE_DEV"
sgdisk -e "$DISK"
partprobe "$DISK"
sleep 1
sgdisk -n "$SHARE_NUM":0:0 -c "$SHARE_NUM":share -t "$SHARE_NUM":8300 "$DISK"
partprobe "$DISK"
sleep 1
mkfs.ext4 -L SHARE -F "$SHARE_DEV"
PS5SHARE
chmod +x "$CHROOT/usr/local/sbin/ps5-share-init"
# Hook into Batocera's init order: run BEFORE S11share so
# S11share's batocera-part share_internal call finds the
# partition we just created.
cat > "$CHROOT/etc/init.d/S07ps5share" <<'INITSHARE'
#!/bin/sh
# First-boot SHARE partition creator — see /usr/local/sbin/ps5-share-init
case "$1" in
start|"") /usr/local/sbin/ps5-share-init >> /tmp/ps5-share-init.log 2>&1 ;;
stop|restart|reload|*) ;;
esac
INITSHARE
chmod +x "$CHROOT/etc/init.d/S07ps5share"
# First-boot defaults for /userdata/system/batocera.conf — set
# display.empty=1 so every system (PSP, PS1, PS2, PS3, PS4,
# Switch, etc) is visible in EmulationStation even before
# ROMs are loaded. S12 runs after S11share has populated
# /userdata. Idempotent: only sets a key if not already
# present, so the user remains free to flip it back.
cat > "$CHROOT/etc/init.d/S12ps5defaults" <<'INITDEF'
#!/bin/sh
case "$1" in
start|"")
CONF=/userdata/system/batocera.conf
[ -f "$CONF" ] || exit 0
grep -qE '^display\.empty=' "$CONF" || echo 'display.empty=1' >> "$CONF"
;;
esac
INITDEF
chmod +x "$CHROOT/etc/init.d/S12ps5defaults"
# Batocera ships root passwordless. Leave root usable (a lot of
# Batocera scripts assume root) but ALSO add a ps5 user so the
# release-page convention works.
if ! grep -q "^ps5:" "$CHROOT/etc/passwd"; then
echo "ps5:x:1000:1000:PS5:/home/ps5:/bin/sh" >> "$CHROOT/etc/passwd"
echo "ps5:!::0:99999:7:::" >> "$CHROOT/etc/shadow"
echo "ps5:x:1000:" >> "$CHROOT/etc/group"
mkdir -p "$CHROOT/home/ps5"
chroot "$CHROOT" /bin/sh -c "chown -R 1000:1000 /home/ps5" 2>/dev/null || true
fi
# Both root and ps5 get pw 'ps5' — Batocera's chpasswd is busybox.
chroot "$CHROOT" /bin/sh -c "printf 'ps5\nps5\n' | passwd ps5 2>/dev/null; printf 'ps5\nps5\n' | passwd root 2>/dev/null" || true
echo "=== Batocera: grow-rootfs first-boot service ==="
mkdir -p "$CHROOT/usr/local/sbin" "$CHROOT/etc/systemd/system"
cat > "$CHROOT/usr/local/sbin/grow-rootfs" <<'GROW'
#!/bin/sh
set -e
ROOT=$(findmnt -no SOURCE / || mount | awk '$3=="/"{print $1; exit}')
DISK=$(lsblk -no PKNAME "$ROOT" 2>/dev/null | head -1)
PARTNUM=$(echo "$ROOT" | grep -oE '[0-9]+$' || true)
[ -z "$DISK" ] || [ -z "$PARTNUM" ] && exit 0
growpart "/dev/$DISK" "$PARTNUM" || true
resize2fs "$ROOT" || true
GROW
chmod +x "$CHROOT/usr/local/sbin/grow-rootfs"
cat > "$CHROOT/etc/systemd/system/grow-rootfs.service" <<SVC
[Unit]
Description=Grow rootfs to fill disk (first boot)
ConditionPathExists=/usr/local/sbin/grow-rootfs
ConditionFirstBoot=yes
After=local-fs.target
Before=basic.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/grow-rootfs
RemainAfterExit=yes
[Install]
WantedBy=sysinit.target
SVC
# Batocera switched to systemd in v33+. Try systemctl enable;
# tolerate buildroot quirks where /etc/systemd/system layout
# differs.
mkdir -p "$CHROOT/etc/systemd/system/sysinit.target.wants"
ln -sf ../grow-rootfs.service \
"$CHROOT/etc/systemd/system/sysinit.target.wants/grow-rootfs.service"
+1
View File
@@ -0,0 +1 @@
../bazzite/build-rootfs.sh
+1
View File
@@ -0,0 +1 @@
../bazzite/grow-rootfs
+1
View File
@@ -0,0 +1 @@
../bazzite/grow-rootfs.service
+1
View File
@@ -0,0 +1 @@
../bazzite/image.yaml
+30
View File
@@ -0,0 +1,30 @@
# bazzite / bazzite-deck
Adds support for [Bazzite](https://bazzite.gg/) (uBlue's gaming-focused
atomic Fedora) and Bazzite-Deck (Steam Deck UI variant) on PS5 hardware.
These are **OCI atomic images** — distrobuilder doesn't apply.
`distros/bazzite/build-rootfs.sh` runs in place of the distrobuilder call:
1. `skopeo copy docker://ghcr.io/ublue-os/bazzite:stable` → OCI archive
2. `umoci unpack` → flat rootfs into `$CHROOT`
3. Promote `/usr/etc` defaults into `/etc`
4. Install the linux-ps5 RPM via `rpm-ostree`/`dnf`, then mask the
rpm-ostree services (we're a flat fs now)
5. Set up grow-rootfs systemd unit + DTM-TA-race amdgpu reprobe udev rule
`bazzite-deck` is built from `ghcr.io/ublue-os/bazzite-deck:stable` via the
same script (the `case "$DISTRO" in bazzite-*)` branch generates the OCI
reference automatically). All `distros/bazzite-deck/*` files are symlinks
into `distros/bazzite/`.
## Build locally
```bash
./build_image.sh --distro bazzite
./build_image.sh --distro bazzite-deck
```
Image size bumped to 24 GB (default). Compressed output is large (~3-5 GB
`.img.xz`) — too big for a 2 GB GitHub release asset, so this image is not
auto-published by the CI workflow.
+401
View File
@@ -0,0 +1,401 @@
#!/bin/bash
# distros/bazzite/build-rootfs.sh — fetch the uBlue OCI image and prep $CHROOT.
# Called from docker/image-builder/entrypoint.sh for DISTRO=bazzite*.
#
# Bazzite is an OCI atomic image; we bypass distrobuilder entirely.
# DISTRO=bazzite -> ghcr.io/ublue-os/bazzite:stable
# DISTRO=bazzite-deck -> ghcr.io/ublue-os/bazzite-deck:stable
#
# Expects in env: DISTRO, CHROOT, KVER
# Expects on disk: /kernel-debs/*.rpm (linux-ps5 RPM), /repo/distros/bazzite/{grow-rootfs,grow-rootfs.service}
set -ex
: "${CHROOT:?ERROR: \$CHROOT unset/empty}"
[ -d "$CHROOT" ] || { echo "ERROR: \$CHROOT=$CHROOT not a directory"; exit 2; }
case "$CHROOT" in /) echo "ERROR: refuse to operate on /"; exit 2 ;; esac
# Bazzite is an OCI atomic image; bypass distrobuilder entirely.
# DISTRO=bazzite -> ghcr.io/ublue-os/bazzite:stable
# DISTRO=bazzite-deck -> ghcr.io/ublue-os/bazzite-deck:stable
# Anything else after `bazzite-` is treated as the same uBlue
# image-name pattern (bazzite-gnome, bazzite-nvidia, ...).
case "$DISTRO" in
bazzite) REF="ghcr.io/ublue-os/bazzite:stable" ;;
bazzite-*) REF="ghcr.io/ublue-os/${DISTRO}:stable" ;;
*) REF="ghcr.io/ublue-os/bazzite:stable" ;;
esac
echo "=== Bazzite: skopeo copy $REF ==="
OCI=$(mktemp -d)
skopeo copy --override-os linux --override-arch amd64 \
"docker://$REF" "oci:$OCI:bazzite"
echo "=== umoci unpack -> $CHROOT ==="
UNPACK=$(mktemp -d)
umoci unpack --keep-dirlinks --image "$OCI:bazzite" "$UNPACK"
# umoci layout: $UNPACK/{config.json, rootfs/}
mv "$UNPACK/rootfs"/* "$CHROOT/" 2>/dev/null || true
mv "$UNPACK/rootfs"/.[!.]* "$CHROOT/" 2>/dev/null || true
rm -rf "$UNPACK" "$OCI"
# ostree convention: /usr/etc holds the defaults; /etc is empty in
# the image. Promote /usr/etc to /etc so the system boots normally.
if [ -d "$CHROOT/usr/etc" ]; then
cp -an "$CHROOT/usr/etc/." "$CHROOT/etc/" || true
rm -rf "$CHROOT/usr/etc"
fi
# Stage PS5 kernel RPMs + grow-rootfs. /opt and /home are ostree
# symlinks in Bazzite, /var is a real dir — drop staging files there.
mkdir -p "$CHROOT/var/cache/ps5-rpms"
cp /kernel-debs/*.rpm "$CHROOT/var/cache/ps5-rpms/"
# /usr/local is a symlink to /var/usrlocal in ostree-based systems;
# mkdir the target before cp to avoid following-symlink-on-missing.
mkdir -p "$CHROOT/var/usrlocal/sbin"
cp /repo/distros/bazzite/grow-rootfs "$CHROOT/var/usrlocal/sbin/grow-rootfs"
chmod +x "$CHROOT/var/usrlocal/sbin/grow-rootfs"
cp /repo/distros/bazzite/grow-rootfs.service "$CHROOT/etc/systemd/system/grow-rootfs.service"
# Force NetworkManager wifi backend off iwd back to wpa_supplicant. Bazzite
# defaults to iwd; iwd is incompatible with the PS5 NXP IW620 mwifiex driver
# (no SSIDs scanned, NM hangs). wpa_supplicant works out of the box.
mkdir -p "$CHROOT/etc/NetworkManager/conf.d"
cat > "$CHROOT/etc/NetworkManager/conf.d/00-no-iwd.conf" <<'NMCONF'
[device]
wifi.backend=wpa_supplicant
NMCONF
# Chroot in: disable ostree stack, install PS5 kernel, user setup.
# Trap to always umount, even if the chroot script exits early.
cleanup_bazzite_mounts() {
for m in dev sys proc; do
mountpoint -q "$CHROOT/$m" && umount "$CHROOT/$m" || true
done
}
trap cleanup_bazzite_mounts RETURN ERR EXIT
mount --bind /proc "$CHROOT/proc"
mount --bind /sys "$CHROOT/sys"
mount --bind /dev "$CHROOT/dev"
# Bazzite has no /etc/resolv.conf inside the chroot (symlink target
# doesn't exist yet) — provide a working one so dnf can reach mirrors.
rm -f "$CHROOT/etc/resolv.conf"
cp /etc/resolv.conf "$CHROOT/etc/resolv.conf"
chroot "$CHROOT" /bin/bash -e <<"BAZIN"
# Disable rpm-ostree services — we're a flat fs now.
systemctl mask rpm-ostreed.service rpm-ostree-countme.service rpm-ostree-bootstatus.service 2>/dev/null || true
# Bazzite ships /usr/bin/dnf as a shell wrapper that refuses `install` /
# `remove` unless it detects a container or a dev-mode ostree deployment
# (points users at rpm-ostree instead). Neither condition holds on our
# flat-fs kexec-booted install, so `dnf install` prints a docs URL and
# exits 1. Replace the wrapper with a direct dnf5 symlink — with ostree
# gone the guard has no purpose here.
if [ -f /usr/bin/dnf ] && head -1 /usr/bin/dnf | grep -q "^#!.*bash"; then
rm -f /usr/bin/dnf
ln -s dnf5 /usr/bin/dnf
fi
# Drop the embedded ostree object store + deploy tree. With
# rpm-ostree masked, the running rootfs is the flat OCI
# extract — /sysroot/ostree/repo/objects/ is a deduplicated
# second copy of the same content (~5GB+), and /ostree/
# deploy/ holds yet another. Wiping them shrinks the disk
# image roughly in half. Leave the dir skeleton in case
# anything probes for it.
rm -rf /sysroot/ostree/repo/objects \
/sysroot/ostree/repo/refs \
/sysroot/ostree/deploy
mkdir -p /sysroot/ostree/repo/objects \
/sysroot/ostree/repo/refs/heads
# Bazzite/rpm-ostree convention: /root is a symlink to
# /var/roothome which doesn't exist in the OCI extract.
# dracut's hostonly enumeration follows the symlink, hits
# ENOENT, fails with `dracut-install: ERROR: installing '/root'`.
# Make /root a real dir so dracut + the kernel postinst's own
# dracut call both work.
mkdir -p /var/roothome
if [ -L /root ]; then
rm -f /root
mkdir -m 0700 /root
fi
# Install PS5 kernel via rpm --replacefiles (handles the file-
# level conflict between our /usr/include/* headers and
# Bazzite's kernel-headers; see fedora image.yaml comment).
# Bazzite ships kernel modules as a dir; our rpm wants a symlink.
rm -rf /lib/modules/*
rpm -Uvh --replacefiles --replacepkgs --nodeps /var/cache/ps5-rpms/*.rpm
rm -rf /var/cache/ps5-rpms
# cyan_skillfish (PS5 Oberon) GPU firmware MUST land in the
# rootfs uncompressed. The linux-ps5 amdgpu patches write into
# the request_firmware() buffer to skip Sony's signature header
# (gfx_v10_0_early_init + amdgpu_sdma_init_microcode). Firmware
# loaded from a .xz file is decompressed into pages the kernel
# maps PAGE_KERNEL_RO (fw_decompress_xz_pages -> fw_map_paged_buf
# -> vmap PAGE_KERNEL_RO), so the write oopses amdgpu at
# gfx_v10_0_early_init+0x415 and /dev/dri never appears.
# Distros that ship .zst (arch) or raw .bin (debian) decompress
# into writable buffers and are unaffected — this fix is for
# the rpm path only. linux-firmware dedupes blobs as symlinks
# (mec2 -> mec) and unxz refuses symlinks, so materialize the
# link targets first while the canonical .xz still exists.
# Upstream did this same fix in
# github.com/ps5-linux/ps5-linux-image@ed54e99 — same kernel
# patches, same firmware, same failure mode.
cd /usr/lib/firmware/amdgpu
for f in cyan_skillfish*.xz; do
if [ -L "$f" ]; then
tgt=$(readlink -f "$f")
rm "$f"
xz -dc "$tgt" > "${f%.xz}"
fi
done
unxz cyan_skillfish*.xz
cd /
# Pre-configure repo.etawen.dev so users can
# `dnf upgrade linux-ps5` after first boot. Per-package
# gpgcheck=0 (alien-converted RPMs aren't per-package
# signed); repodata IS signed by the mia PGP key.
cat > /etc/yum.repos.d/etawen-ps5.repo <<ETAWEN
[etawen-ps5]
name=Etawen PS5 kernel repo
baseurl=https://repo.etawen.dev/rpm/
enabled=1
gpgcheck=0
repo_gpgcheck=1
gpgkey=https://repo.etawen.dev/key.asc
ETAWEN
# amdgpu options — PS5 Oberon GPU needs dpm disabled or HDMI
# stays dark. Must land in initramfs (amdgpu loads early).
mkdir -p /etc/modprobe.d
cat > /etc/modprobe.d/ps5-amdgpu.conf <<AMDGPU
options amdgpu dpm=0 gpu_recovery=0
AMDGPU
# Build the initrd, then deploy bzImage+initrd to /boot/efi/
# for the PS5 kexec loader. (zz-update-boot is the deb-flow
# helper; bazzite never stages it, so we inline the copies.)
KVER=$(ls -1t /lib/modules | head -1)
dracut -f --kver "$KVER" "/boot/initrd.img-$KVER"
mkdir -p /boot/efi
cp "/boot/vmlinuz-$KVER" /boot/efi/bzImage
cp "/boot/initrd.img-$KVER" /boot/efi/initrd.img
# Suppress first-boot wizards. plasma-setup.service runs on every
# boot until /etc/plasma-setup-done exists, and its bootutil
# rewrites SDDM autologin to User=plasma-setup (clobbering our
# User=ps5) and starts the Plasma OOBE wizard, which prompts the
# user to create a fresh account. Our build pre-creates ps5; the
# wizard is unwanted.
touch /etc/plasma-setup-done
systemctl mask plasma-setup.service 2>/dev/null || true
# bazzite-hardware-setup.service runs on every boot until the
# marker files in /etc/bazzite/ match the image-info.json. Seed
# them so the script exits at its early-return; also mask it
# outright since the script calls `rpm-ostree kargs` which fails
# against our masked rpm-ostreed. The script's other work
# (zram, IOMMU karg, hw-specific kargs) isn't applicable on PS5
# anyway — we set our own cmdline in /boot/efi/cmdline.txt.
mkdir -p /etc/bazzite
jq -r '."image-name"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/image_name
jq -r '."image-branch"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/image_branch
jq -r '."fedora-version"' < /usr/share/ublue-os/image-info.json > /etc/bazzite/fedora_version
grep -oP '^HWS_VER=\K[0-9]+' /usr/libexec/bazzite-hardware-setup > /etc/bazzite/hws_version
systemctl mask bazzite-hardware-setup.service 2>/dev/null || true
# User setup. Bazzite exposes video/audio/input/render via
# systemd-userdbd, so `getent group video` returns a row —
# and `groupadd -f` short-circuits as "already exists" and
# does nothing. But useradd reads /etc/group directly (no
# NSS), sees an empty file, and bails with "group X does not
# exist". Materialize each group into /etc/group ourselves,
# preserving the NSS-assigned GID when there is one so
# existing file ownerships in the rootfs stay correct.
passwd -l root
# Ensure /etc/gshadow exists with the right perms; useradd
# refuses to "prepare new entry" silently if it's missing.
[ -e /etc/gshadow ] || { touch /etc/gshadow; chmod 0 /etc/gshadow; }
for g in wheel video audio input render; do
if ! grep -q "^${g}:" /etc/group; then
gid=$(getent group "$g" 2>/dev/null | cut -d: -f3 || true)
if [ -z "$gid" ]; then
# Pick the next free system gid (100-999).
gid=$(awk -F: 'BEGIN{m=100} $3>=100 && $3<1000 && $3>m {m=$3} END{print m+1}' /etc/group)
fi
echo "${g}:x:${gid}:" >> /etc/group
fi
# Always make sure /etc/gshadow has a row.
grep -q "^${g}:" /etc/gshadow || echo "${g}:!::" >> /etc/gshadow
done
if ! id ps5 >/dev/null 2>&1; then
useradd -m -s /bin/bash -G wheel,video,audio,input,render ps5
fi
echo "ps5:ps5" | chpasswd
sed -i 's/^# %wheel ALL=(ALL:ALL) ALL/%wheel ALL=(ALL:ALL) ALL/' /etc/sudoers || true
# Install pieces Bazzite's slim OCI image is missing.
# cloud-utils-growpart + parted: grow-rootfs needs growpart
# and partprobe — without them the rootfs stays sized to
# the build image (~10GB) on whatever USB it lands on.
# plasma-systemmonitor + ksystemstats: standard Plasma
# "System Monitor" app. Bazzite's container drops it.
# kdiff3 / gwenview / ark / okular / spectacle: rest of
# the Plasma utilities most people expect.
# chrony: NTP. PS5's RTC is wrong on boot; without an NTP
# client the system clock is years off and TLS breaks.
dnf install -y --setopt=install_weak_deps=False \
cloud-utils-growpart parted \
plasma-systemmonitor ksystemstats \
kdiff3 gwenview ark okular spectacle \
chrony \
|| echo "WARN: dnf install failed; some pkgs may be missing"
# Services
systemctl enable grow-rootfs.service NetworkManager sshd 2>/dev/null || true
# Time sync. Prefer systemd-timesyncd if present (lighter);
# fall back to chrony (which we just dnf-installed).
systemctl enable systemd-timesyncd 2>/dev/null \
|| systemctl enable chronyd 2>/dev/null || true
# Virtual terminals. Bazzite's preset disables getty@tty2-6;
# explicitly enable them so Ctrl+Alt+F2..F6 give text consoles.
for n in 2 3 4 5 6; do
systemctl enable getty@tty${n}.service 2>/dev/null || true
done
# Default DM (Bazzite ships KDE Plasma + SDDM)
systemctl enable sddm 2>/dev/null || systemctl enable gdm 2>/dev/null || true
# resolv.conf -> systemd-resolved stub
rm -f /etc/resolv.conf
ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
# Steam Deck UI's "Switch to Desktop" button calls SteamOS-
# Manager's SetTemporarySession(s) dbus method, which writes
# Session=<bare-alias> (literally "desktop"/"gamescope") into
# /etc/sddm.conf.d/zzt-steamos-temp-login.conf. That conf
# sorts AFTER zz-steamos-autologin.conf so it wins precedence
# at autologin time — but SDDM has no `desktop.desktop`
# session to resolve the alias to, so the button silently
# no-ops and the user stays on gamescope. (The bash
# steamos-session-select tool works fine because it resolves
# aliases itself before writing — only the dbus path is
# broken.) Fix it with alias symlinks SDDM can follow.
for cand in plasma-steamos-wayland-oneshot.desktop \
gnome-wayland-oneshot.desktop plasma.desktop; do
if [ -e "/usr/share/wayland-sessions/$cand" ]; then
ln -sf "$cand" /usr/share/wayland-sessions/desktop.desktop
break
fi
done
for cand in gamescope-session.desktop gamescope-session-plus.desktop; do
if [ -e "/usr/share/wayland-sessions/$cand" ]; then
ln -sf "$cand" /usr/share/wayland-sessions/gamescope.desktop
break
fi
done
# Autologin straight into Bazzite's gamescope session (Steam
# Big-Picture / Deck UI) — Bazzite is gaming-focused, and a
# field report said it landed on the Plasma desktop instead
# of gamemode. Pick whichever gamescope session file exists,
# fall back to plasma if Bazzite stripped them.
mkdir -p /etc/sddm.conf.d
SESSION=plasma
for s in gamescope-session-plus.desktop gamescope-session.desktop steam-wayland.desktop; do
if [ -e "/usr/share/wayland-sessions/$s" ] || [ -e "/usr/share/xsessions/$s" ]; then
SESSION="${s%.desktop}"
break
fi
done
cat > /etc/sddm.conf.d/autologin.conf <<SDDM
[Autologin]
User=ps5
Session=$SESSION
SDDM
# Gamescope-session fallback. Field report: bazzite-deck boots
# to a black screen on PS5 because gamescope can't grab the
# display (PSP/TA + Salina HDMI bridge weirdness — workaround
# is `steamos-session-select plasma` from a VT). Automate it:
# first-boot oneshot waits 60s for a gamescope process; if
# nothing shows up, flip the session to plasma and bounce
# SDDM. Only arm this when the chosen session is gamescope-
# flavoured. After first boot the user owns session choice
# via the standard steamos-session-select tool + the desktop
# shortcut we drop below.
case "$SESSION" in gamescope*|steam-wayland*)
mkdir -p /usr/local/sbin /etc/systemd/system/graphical.target.wants
cat > /usr/local/sbin/ps5-gamescope-recovery <<'POKE'
#!/bin/bash
# Wait up to 60s for gamescope to actually grab a display. If it doesn't,
# the user is staring at a black screen — fall back to plasma and bounce
# the display manager so they get a usable login session.
for _ in $(seq 1 60); do
sleep 1
pgrep -x gamescope >/dev/null 2>&1 && exit 0
done
logger -t ps5-gamescope-recovery "gamescope didn't start within 60s, switching to plasma"
runuser -u ps5 -- steamos-session-select plasma 2>/dev/null \
|| sed -i 's/^Session=.*/Session=plasma/' /etc/sddm.conf.d/autologin.conf
systemctl restart sddm
POKE
chmod +x /usr/local/sbin/ps5-gamescope-recovery
cat > /etc/systemd/system/ps5-gamescope-recovery.service <<RECOV
[Unit]
Description=Fall back to plasma if gamescope can't grab a display (first boot)
After=graphical.target
ConditionFirstBoot=yes
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ps5-gamescope-recovery
RemainAfterExit=no
[Install]
WantedBy=graphical.target
RECOV
ln -sf ../ps5-gamescope-recovery.service \
/etc/systemd/system/graphical.target.wants/ps5-gamescope-recovery.service
# Desktop shortcut so the user can opt back into gamescope
# after a recovery (or after switching to plasma manually).
mkdir -p /home/ps5/Desktop
cat > /home/ps5/Desktop/Switch-to-Gamescope.desktop <<DESK
[Desktop Entry]
Version=1.0
Type=Application
Name=Switch to Gamescope (Big Picture)
Comment=Switch the autologin session back to gamescope / Steam Deck UI
Exec=bash -c 'steamos-session-select gamescope && systemctl restart sddm'
Icon=steam
Terminal=false
Categories=System;
DESK
chmod +x /home/ps5/Desktop/Switch-to-Gamescope.desktop
chown -R ps5:ps5 /home/ps5/Desktop 2>/dev/null || \
chown -R 1000:1000 /home/ps5/Desktop
;;
esac
# DTM TA race workaround. amdgpu's display-topology TA
# (Trusted Application) loads async via PSP; if DRM probes
# connectors before that finishes, you get
# [drm] Failed to add display topology, DTM TA is not initialized
# and the screen stays dark until the user manually toggles
# VT (ctrl+alt+F7 -> ctrl+alt+F1) which forces a re-probe.
# Mimic that automatically: after amdgpu binds, wait a beat
# then re-trigger DRM connector detection.
mkdir -p /usr/local/sbin /etc/udev/rules.d
cat > /usr/local/sbin/ps5-amdgpu-reprobe <<'POKE'
#!/bin/sh
# Wait for PSP/TA firmware to settle, then re-probe DRM connectors.
# Equivalent of the ctrl+alt+F7 / ctrl+alt+F1 dance.
(
sleep 3
for c in /sys/class/drm/card*-*/status; do
[ -w "$c" ] && echo detect > "$c"
done
) &
POKE
chmod +x /usr/local/sbin/ps5-amdgpu-reprobe
cat > /etc/udev/rules.d/70-ps5-amdgpu-reprobe.rules <<'UDEV'
# Re-trigger DRM hotplug after amdgpu binds, so the DTM TA-not-initialized
# race doesn't leave the user with a dark screen until they manually VT-cycle.
SUBSYSTEM=="drm", ACTION=="add", KERNEL=="card[0-9]*", RUN+="/usr/local/sbin/ps5-amdgpu-reprobe"
UDEV
BAZIN
# explicit cleanup (the trap covers the failure path)
cleanup_bazzite_mounts
trap - RETURN ERR EXIT
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# Grows the root partition and filesystem to fill the disk.
# Runs once on first boot, then disables itself.
ROOT_DEV=$(findmnt -no SOURCE /) || { echo "Cannot find root device"; exit 1; }
DISK=$(lsblk -ndo PKNAME "$ROOT_DEV")
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition 2>/dev/null)
if [ -z "$DISK" ] || [ -z "$PART_NUM" ]; then
echo "Cannot determine disk layout (DISK=$DISK PART_NUM=$PART_NUM)"
exit 1
fi
growpart "/dev/$DISK" "$PART_NUM" || true
partprobe "/dev/$DISK"
resize2fs "$ROOT_DEV"
systemctl disable grow-rootfs.service
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=Grow root filesystem to fill disk
After=systemd-remount-fs.service
# grow-rootfs uses findmnt + growpart on the live /, which needs the
# rootfs mounted RW and userspace tooling available. Drop the
# initramfs-era ordering the previous version used.
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/grow-rootfs
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+13
View File
@@ -0,0 +1,13 @@
# Bazzite is an ostree/OCI atomic image — distrobuilder doesn't handle it.
# This yaml is a placeholder for documentation; the actual build is custom
# in docker/image-builder/entrypoint.sh under the `bazzite*)` case (skopeo
# pull + umoci unpack + dnf-install our PS5 kernel RPM).
image:
name: ps5-bazzite
distribution: bazzite
release: stable
description: Bazzite (uBlue gaming Fedora) with PS5 kernel — ostree-flattened
architecture: x86_64
# Source ref consumed by entrypoint.sh:
# upstream_ref: ghcr.io/ublue-os/bazzite:stable

Some files were not shown because too many files have changed in this diff Show More