Author SHA1 Message Date
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
Bug Bounty Zip daf634a6ed Include Kali in full image builds 2026-05-27 01:36:48 +03:00
Bug Bounty ZipandGitHub 703dc736e9 Merge pull request #10 from ps5-linux/fix/arch-cachyos-mirrors
Fix Arch mirror selection in image builds
2026-05-27 01:15:15 +03:00
Bug Bounty Zip dc365dda15 Fix Arch mirror selection in image builds 2026-05-26 21:18:14 +03:00
Bug Bounty ZipandGitHub 8160bc649b Add Kali Linux image support (#9)
* Add Kali Linux image support

* Remove forced 1080p from Kali cmdline
2026-05-25 21:16:56 +02:00
Andy Nguyen 1ea662d598 Update to v1.2 patches. 2026-05-25 07:44:00 +02:00
Andy Nguyen 04378b78f4 Make backup in zz-update-boot. 2026-05-24 23:01:23 +02:00
DanandGitHub 0e1219f422 deprecate alpine (#7) 2026-05-19 09:11:54 +02:00
Dan Kluser 33189d7e7b ci: pull from branch for ci 2026-05-19 07:43:52 +02:00
DanandGitHub d92e51959b Merge pull request #5 from Shivox/fix-firstboot-cachyos
fix first-boot flow for CachyOS
2026-05-14 18:55:39 +02:00
Dan Kluser 01205507fd CI: no releases for pr runs 2026-05-14 13:15:15 +02:00
Dan Kluser 9d65d7a391 manual trigger for PRs 2026-05-14 13:07:52 +02:00
Dan Kluser d55d87e6bd move actions to image repo 2026-05-14 12:45:22 +02:00
Chihab Djaidja 4a2aef2e03 Fix missing so error 2026-05-12 15:38:08 +02:00
Chihab Djaidja 2d07c87a78 fix first-boot flow 2026-05-12 14:14:11 +02:00
Dan Kluser 4cf616a934 allow manual triggers 2026-05-11 20:49:45 +02:00
Dan Kluser 142d3913c9 remove systemctl enable for qemu compat 2026-05-10 21:17:01 +02:00
Andy Nguyen a76971ba96 Update to v1.1 patches. 2026-05-10 14:20:26 +02:00
Dan Kluser 3db57b8de7 fix cachy with new kernel pkg 2026-05-10 09:15:26 +02:00
Dan Kluser 73bfdd288b fix for arch under qemu 2026-05-10 08:57:43 +02:00
Dan Kluser fda932a3a3 fix arch under qemu build and cachy kernel 2026-05-10 07:15:52 +02:00
Dan Kluser 43d2a65997 add CI trigger workflow 2026-05-09 23:06:04 +02:00
DanandGitHub 492e124991 Merge pull request #4 from ps5-linux/feature/ci-integration
Feature/ci integration
2026-05-09 18:14:38 +02:00
62 changed files with 3922 additions and 481 deletions
-1
View File
@@ -1,7 +1,6 @@
linux_deb/
linux-bin/
work/
work-alpine/
output/
.git/
ps5-linux-patches/
+13
View File
@@ -0,0 +1,13 @@
* text=auto
*.sh text eol=lf
*.bash text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.service text eol=lf
build_image.sh text eol=lf
boot/kexec*.sh text eol=lf
distros/**/grow-rootfs text eol=lf
distros/shared/zz-update-boot* text eol=lf
docker/** text eol=lf
+294
View File
@@ -0,0 +1,294 @@
name: Build Image
run-name: Build Image (${{ inputs.distro || 'all' }})
on:
workflow_dispatch:
inputs:
distro:
description: 'Distribution'
default: 'ubuntu2604'
type: choice
options: [all, ubuntu2604, arch, cachyos, fedora, proxmox, debian, bazzite, bazzite-deck, steamos]
workflow_call:
inputs:
distro:
type: string
default: 'ubuntu2604'
secrets:
R2_ACCESS_KEY_ID:
required: true
R2_SECRET_ACCESS_KEY:
required: true
R2_ENDPOINT:
required: true
R2_BUCKET:
required: true
jobs:
matrix:
runs-on: self-hosted
outputs:
distros: ${{ steps.set.outputs.distros }}
steps:
- id: set
run: |
INPUT="${{ inputs.distro || 'ubuntu2604' }}"
if [ "$INPUT" = "all" ]; then
echo 'distros=["ubuntu2604","arch","cachyos","fedora","proxmox","debian","bazzite","bazzite-deck","steamos"]' >> "$GITHUB_OUTPUT"
else
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
fi
build:
needs: matrix
runs-on: self-hosted
timeout-minutes: 720
permissions:
contents: write
strategy:
fail-fast: false
max-parallel: 5
matrix:
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
env:
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:
path: image
- name: Build ${{ matrix.distro }} image
run: |
bash image/build_image.sh --distro "${{ matrix.distro }}"
- name: Set version info
id: version
run: |
psha=$(git -C image/work/ps5-linux-patches rev-parse --short HEAD 2>/dev/null || echo "unknown")
kver=$(cat image/linux-bin/VERSION 2>/dev/null || echo "unknown")
ts=$(date -u +%Y%m%d-%H%M%S)
echo "psha=$psha" >> "$GITHUB_OUTPUT"
echo "kver=$kver" >> "$GITHUB_OUTPUT"
echo "ts=$ts" >> "$GITHUB_OUTPUT"
- name: Report
run: |
ls -lh image/output/*.img
sha256sum image/output/*.img
- name: Compress image
run: |
xz -T0 -4 -c image/output/ps5-${{ matrix.distro }}.img \
> image/output/ps5-${{ matrix.distro }}.img.xz
- name: Upload to R2
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
env:
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: true
run: |
rclone copyto image/output/ps5-${{ matrix.distro }}.img.xz \
"r2:${{ secrets.R2_BUCKET }}/ps5-${{ matrix.distro }}.img.xz"
- name: Save build metadata
run: |
mkdir -p meta
sha256sum image/output/ps5-${{ matrix.distro }}.img.xz \
| sed 's|image/output/||' > meta/${{ matrix.distro }}.sha256
echo "${{ steps.version.outputs.kver }}" > meta/kver
echo "${{ steps.version.outputs.psha }}" > meta/psha
echo "${{ steps.version.outputs.ts }}" > meta/ts
- name: Upload build metadata
uses: actions/upload-artifact@v6
with:
name: meta-${{ matrix.distro }}
path: meta/
retention-days: 1
release:
needs: build
runs-on: self-hosted
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
permissions:
contents: write
steps:
- name: Download build metadata
uses: actions/download-artifact@v6
with:
pattern: meta-*
path: meta
merge-multiple: true
- name: Prepare release body
id: body
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const path = require('path');
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
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: latest
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}`);
-18
View File
@@ -1,18 +0,0 @@
name: Dispatch to CI
on:
push:
branches:
- main
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Send repository_dispatch
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DISPATCH_TOKEN }}
repository: ps5-linux/ps5-linux-updates
event-type: image-builder-updated
client-payload: '{"sha": "${{ github.sha }}"}'
+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}`);
+138
View File
@@ -0,0 +1,138 @@
name: test uploads
on:
workflow_dispatch:
jobs:
matrix:
runs-on: self-hosted
outputs:
distros: ${{ steps.set.outputs.distros }}
steps:
- id: set
run: echo 'distros=["ubuntu2604","arch","cachyos"]' >> "$GITHUB_OUTPUT"
build:
needs: matrix
runs-on: self-hosted
timeout-minutes: 10
strategy:
fail-fast: false
max-parallel: 1
matrix:
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
steps:
- name: Reset workspace
run: sudo rm -rf ./*
- name: Create fake image
run: |
mkdir -p image/output
dd if=/dev/urandom of=image/output/ps5-${{ matrix.distro }}.img bs=1M count=8
- name: Compress image
run: |
xz -T0 -1 -c image/output/ps5-${{ matrix.distro }}.img \
> image/output/ps5-${{ matrix.distro }}.img.xz
- name: Upload to R2
env:
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: true
run: |
rclone copyto image/output/ps5-${{ matrix.distro }}.img.xz \
"r2:${{ secrets.R2_BUCKET }}/test-${{ matrix.distro }}.img.xz" -v
- name: Save build metadata
run: |
mkdir -p meta
sha256sum image/output/ps5-${{ matrix.distro }}.img.xz \
| sed 's|image/output/||' > meta/${{ matrix.distro }}.sha256
echo "6.x.y-test" > meta/kver
echo "abc1234" > meta/psha
date -u +%Y%m%d-%H%M%S > meta/ts
- name: Upload build metadata
uses: actions/upload-artifact@v6
with:
name: meta-${{ matrix.distro }}
path: meta/
retention-days: 1
- name: Cleanup
if: always()
run: rm -rf image/output meta
release:
needs: build
runs-on: self-hosted
steps:
- name: Download build metadata
uses: actions/download-artifact@v6
with:
pattern: meta-*
path: meta
merge-multiple: true
- name: Prepare release body
id: body
run: |
KVER=$(cat meta/kver)
PSHA=$(cat meta/psha)
TS=$(cat meta/ts)
SUMS=$(cat meta/*.sha256)
{
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) |"
echo ""
echo "**SHA256 checksums:**"
echo "\`\`\`"
echo "$SUMS"
echo "\`\`\`"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Verify release body
run: |
echo "=== Release body ==="
cat <<'EOF'
${{ steps.body.outputs.text }}
EOF
echo ""
echo "=== All checksums ==="
cat meta/*.sha256
cleanup-remote:
needs: release
runs-on: self-hosted
if: always()
steps:
- name: Clean up R2 test files
env:
RCLONE_CONFIG_R2_TYPE: s3
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
RCLONE_CONFIG_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
RCLONE_CONFIG_R2_NO_CHECK_BUCKET: true
run: |
for d in ubuntu2604 arch cachyos; do
rclone delete "r2:${{ secrets.R2_BUCKET }}/test-${d}.img.xz" -v || true
done
+23
View File
@@ -0,0 +1,23 @@
name: deploy
on:
workflow_dispatch:
push:
branches: [main]
pull_request_target:
types: [labeled]
branches: [main]
jobs:
image:
if: github.event.label.name == 'run-ci' || github.event_name != 'pull_request_target'
permissions:
contents: write
uses: ./.github/workflows/build-image.yml
with:
distro: all
secrets:
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}
@@ -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');
+18 -14
View File
@@ -1,11 +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 Alpine, 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
- ~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:
@@ -27,7 +27,14 @@ OR
OR
# Build a multi-distro image (ubuntu2604 + arch + alpine + cachyos)
OR
# Build Fedora (GNOME desktop)
./build_image.sh --distro fedora
OR
# Build a multi-distro image (ubuntu2604 + arch + cachyos)
./build_image.sh --distro all
```
@@ -43,12 +50,12 @@ sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
| Flag | Description | Default |
|------|-------------|---------|
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, `alpine`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` |
| `--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`) |
| `--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.0` |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.2` |
## Caching
@@ -66,7 +73,7 @@ Use `--clean` to wipe everything and rebuild from scratch. The build will also s
PS5 Linux Image Builder
=======================
Distro: all
(ubuntu2604 arch alpine cachyos)
(ubuntu2604 arch cachyos)
Image size: 32000MB
Kernel src: /path/to/work/linux
@@ -91,19 +98,17 @@ 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 |
| Alpine (3.21) | GNOME | extracted from `.deb` | OpenRC |
## Multi-distro Image
`--distro all` builds a 32GB image with 5 partitions (one EFI boot partition plus four root filesystems):
`--distro all` builds a 32GB image with 4 partitions (one EFI boot partition plus three root filesystems):
| Partition | Type | Label | Content |
|-----------|------|-------|---------|
| p1 | FAT32 | boot | Shared kernel, per-distro initrds, kexec scripts |
| p2 | ext4 | ubuntu2604 | Ubuntu 26.04 rootfs |
| p3 | ext4 | arch | Arch rootfs |
| p4 | ext4 | alpine | Alpine rootfs |
| p5 | ext4 | cachyos | CachyOS rootfs |
| p4 | ext4 | cachyos | CachyOS rootfs |
The boot partition contains kexec scripts to switch between distros at runtime. Ubuntu 26.04 is the default boot target.
@@ -130,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
@@ -139,12 +144,11 @@ distros/
ubuntu2604/ # Ubuntu 26.04 (Resolute)
arch/ # Arch Linux
cachyos/ # CachyOS repos + Gamescope/Steam
alpine/ # Alpine 3.21
shared/ # Kernel postinst hooks (single + multi)
boot/
cmdline.txt # Kernel cmdline template (__DISTRO__ placeholder)
vram.txt # VRAM allocation
kexec-{ubuntu2604,arch,alpine,cachyos}.sh
kexec-{ubuntu2604,arch,cachyos}.sh
work/ # Build artifacts (auto-created)
linux-bin/ # Compiled kernel packages
output/ # Final .img files
-6
View File
@@ -1,6 +0,0 @@
#!/bin/sh
# Switch to Alpine Linux via kexec
set -e
BOOT=/boot/efi
kexec -l "$BOOT/bzImage" --initrd="$BOOT/initrd-alpine.img" --command-line="$(cat $BOOT/cmdline-alpine.txt)"
kexec -e
+48 -10
View File
@@ -9,20 +9,21 @@ KERNEL_SRC=""
CLEAN=false
IMG_SIZE=12000
KERNEL_ONLY=false
PATCHES_REF="v1.0"
PATCHES_REF="daa2e496086ae0c9fe22205f703925a2e2596185"
MULTI_DISTROS="ubuntu2604 arch alpine cachyos"
MULTI_DISTROS="ubuntu2604 arch cachyos"
usage() {
echo "Usage: $0 [--distro <distro>] [--kernel <path>] [--img-size <MB>] [--clean]"
echo ""
echo "Options:"
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, alpine, 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)"
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"
echo " --patches-ref Branch, tag, or commit SHA for patches (default: v1.0)"
echo " --patches-ref Branch, tag, or commit SHA for patches (default: v1.2)"
exit 1
}
@@ -32,6 +33,7 @@ while [[ $# -gt 0 ]]; do
--kernel) KERNEL_SRC="$2"; shift 2 ;;
--img-size) IMG_SIZE="$2"; shift 2 ;;
--clean) CLEAN=true; shift ;;
--clean-only) CLEAN=true; CLEAN_EXIT=true; shift ;;
--kernel-only) KERNEL_ONLY=true; shift ;;
--patches-ref) [ -n "$2" ] && PATCHES_REF="$2"; shift 2 ;;
-h|--help) usage ;;
@@ -63,8 +65,31 @@ if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=32000
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) 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"
@@ -76,14 +101,14 @@ esac
BUILD_PID=""
cleanup() {
trap - INT TERM EXIT
echo ""
echo "Interrupted. Cleaning up..."
echo "Cleaning up..."
docker kill "$DOCKER_NAME" 2>/dev/null || true
[ -n "$BUILD_PID" ] && kill "$BUILD_PID" 2>/dev/null || true
wait "$BUILD_PID" 2>/dev/null || true
exit 130
}
trap cleanup INT TERM
trap cleanup INT TERM EXIT
# --- Clean ---
if [ "$CLEAN" = true ]; then
@@ -94,6 +119,7 @@ if [ "$CLEAN" = true ]; then
alpine rm -rf "/parent/$(basename "$dir")"
done
echo "Done."
[ "$CLEAN_EXIT" = true ] && exit 0
echo ""
fi
@@ -103,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 ;;
@@ -273,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 \
@@ -307,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
@@ -318,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=(
-14
View File
@@ -1,14 +0,0 @@
#!/bin/sh
# Grows the root partition and filesystem to fill the disk.
# Runs once on first boot, then disables itself.
set -e
ROOT_DEV=$(findmnt -no SOURCE /)
DISK=$(lsblk -ndo PKNAME "$ROOT_DEV")
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)
growpart "/dev/$DISK" "$PART_NUM" || true
partprobe "/dev/$DISK"
resize2fs "$ROOT_DEV" || true
rc-update del grow-rootfs default
-13
View File
@@ -1,13 +0,0 @@
#!/sbin/openrc-run
description="Grow root filesystem to fill disk"
depend() {
need localmount
}
start() {
ebegin "Growing root filesystem"
/usr/local/sbin/grow-rootfs
eend $?
}
-175
View File
@@ -1,175 +0,0 @@
image:
name: ps5-alpine
distribution: alpine
release: "3.21"
description: Alpine Linux with Weston
architecture: x86_64
source:
downloader: alpinelinux-http
url: https://dl-cdn.alpinelinux.org/alpine
skip_verification: true
packages:
manager: apk
update: true
cleanup: true
repositories:
- name: repositories
url: |-
https://dl-cdn.alpinelinux.org/alpine/v3.21/main
https://dl-cdn.alpinelinux.org/alpine/v3.21/community
sets:
- packages:
# Wayland compositor + basics
- weston
- weston-shell-desktop
- weston-backend-drm
- weston-xwayland
- weston-terminal
- foot
- xwayland
# Wayland support
- dbus
- eudev
- elogind
- polkit-elogind
- seatd
# Audio / media
- pipewire
- wireplumber
- pipewire-pulse
# Display / GPU
- mesa-gbm
- mesa-egl
- mesa-gl
- mesa-dri-gallium
- mesa-va-gallium
- mesa-vulkan-ati
- libinput
- xkeyboard-config
# Networking
- networkmanager
- networkmanager-wifi
- wpa_supplicant
- linux-firmware
- linux-firmware-amdgpu
- xf86-video-amdgpu
# System
- openrc
- openssh
- sudo
- shadow
- nano
- bash
- coreutils
- util-linux
- e2fsprogs
- e2fsprogs-extra
- cloud-utils-growpart
- mkinitfs
- kmod
- kexec-tools
- syslog-ng
# Fonts
- font-ubuntu
- font-liberation
- font-dejavu
- fontconfig
action: install
files:
- path: /etc/hostname
generator: hostname
- path: /etc/hosts
generator: hosts
- path: /etc/kernel/postinst.d/zz-update-boot
generator: copy
source: /tmp/build-staging/zz-update-boot
mode: "0755"
- path: /etc/fstab
generator: copy
source: /tmp/build-staging/fstab
- path: /usr/local/sbin/grow-rootfs
generator: copy
source: /tmp/build-staging/grow-rootfs
mode: "0755"
- path: /etc/init.d/grow-rootfs
generator: copy
source: /tmp/build-staging/grow-rootfs.openrc
mode: "0755"
actions:
- trigger: post-unpack
action: |-
#!/bin/sh
set -eux
# Retry up to 10 times on transient network errors
echo "retries = 10" >> /etc/apk/apk.conf 2>/dev/null || true
# dbus post-install chokes on an empty machine-id
rm -f /etc/machine-id
- trigger: post-packages
action: |-
#!/bin/sh
set -eux
fc-cache -f -v
# SSH
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
rc-update add syslog-ng default
rc-update add udev sysinit
rc-update add udev-trigger sysinit
rc-update add udev-settle sysinit
rc-update add sshd default
rc-update add dbus default
rc-update add elogind default
rc-update add seatd default
rc-update add networkmanager default
- trigger: post-files
action: |-
#!/bin/sh
set -eux
rc-update add grow-rootfs default
# Create default user (ps5/ps5) in groups needed for Wayland/input
addgroup ps5
adduser -D -s /bin/bash -G ps5 ps5
echo "ps5:ps5" | chpasswd
addgroup ps5 wheel
addgroup ps5 video
addgroup ps5 input
addgroup ps5 seat
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/wheel
# Auto-start weston on tty1 login
{
echo 'if [ "$(tty)" = "/dev/tty1" ] && [ -z "$WAYLAND_DISPLAY" ]; then'
echo ' export XDG_RUNTIME_DIR="/tmp/xdg-runtime-$(id -u)"'
echo ' mkdir -p "$XDG_RUNTIME_DIR"'
echo ' chmod 700 "$XDG_RUNTIME_DIR"'
echo ' exec weston --backend=drm-backend.so'
echo 'fi'
} >> /home/ps5/.bash_profile
chown -R ps5:ps5 /home/ps5
mkdir -p /boot/efi
mappings:
architecture_map: alpinelinux
+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
+34 -10
View File
@@ -35,6 +35,10 @@ packages:
- wl-clipboard
- xorg-xwayland
# Bluetooth
- bluez
- bluez-utils
# Audio / media
- pipewire
- wireplumber
@@ -118,13 +122,19 @@ actions:
action: |-
#!/bin/bash
set -eux
# Pacman 7 Landlock sandbox is unsupported under QEMU; disable it.
sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf
# Initialise pacman keyring
pacman-key --init
pacman-key --populate archlinux
# Fetch up-to-date HTTPS mirrors ranked by score, force-refresh package DBs
curl -fsSL "https://archlinux.org/mirrorlist/?country=all&protocol=https&use_mirror_status=on" \
| sed 's/^#Server/Server/' > /etc/pacman.d/mirrorlist
# Keep CI on a small stable mirror set. The full active global list can
# select slow mirrors and fail pacman's low-speed download timeout.
cat > /etc/pacman.d/mirrorlist <<'EOF'
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirrors.kernel.org/archlinux/$repo/os/$arch
EOF
pacman -Syy --noconfirm
@@ -132,21 +142,35 @@ actions:
action: |-
#!/bin/bash
set -eux
fc-cache -f -v
# fc-cache / systemctl can SIGSEGV under QEMU when image plat is linux/amd64 on arm64 hosts.
fc-cache -f 2>/dev/null || true
symlink_wants() {
local src=$1
local target=$2
[ -e "$src" ] || return 0
mkdir -p "/etc/systemd/system/${target}.wants"
ln -sf "$src" "/etc/systemd/system/${target}.wants/$(basename "$src")"
}
# SSH
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
systemctl enable sshd
systemctl enable NetworkManager
systemctl enable seatd
systemctl enable pipewire pipewire-pulse wireplumber || true
symlink_wants /usr/lib/systemd/system/sshd.service multi-user.target
symlink_wants /usr/lib/systemd/system/NetworkManager.service multi-user.target
symlink_wants /usr/lib/systemd/system/seatd.service multi-user.target
for s in pipewire pipewire-pulse wireplumber; do
symlink_wants "/usr/lib/systemd/system/${s}.service" default.target || true
done
- trigger: post-files
action: |-
#!/bin/bash
set -eux
systemctl enable grow-rootfs.service
mkdir -p /etc/systemd/system/local-fs.target.wants
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
+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

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