mirror of
https://github.com/ps5-linux/ps5-linux-image.git
synced 2026-07-16 01:50:40 +00:00
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>
This commit is contained in:
@@ -17,7 +17,7 @@ usage() {
|
|||||||
echo "Usage: $0 [--distro <distro>] [--kernel <path>] [--img-size <MB>] [--clean]"
|
echo "Usage: $0 [--distro <distro>] [--kernel <path>] [--img-size <MB>] [--clean]"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --distro Distribution to build: ubuntu2604, arch, cachyos, fedora, proxmox, debian, 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 " --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 " --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 Remove all cached build artifacts and start from scratch"
|
||||||
@@ -65,8 +65,31 @@ if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
|
|||||||
IMG_SIZE=32000
|
IMG_SIZE=32000
|
||||||
fi
|
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
|
if [ -z "$FORMAT" ]; then
|
||||||
case "$DISTRO" in arch|cachyos) FORMAT="arch" ;; fedora) FORMAT="rpm" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
|
case "$DISTRO" in
|
||||||
|
arch|cachyos) FORMAT="arch" ;;
|
||||||
|
fedora|bazzite*) FORMAT="rpm" ;;
|
||||||
|
all) FORMAT="all" ;;
|
||||||
|
*) FORMAT="deb" ;;
|
||||||
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
KERNEL_BUILDER_PLATFORM="linux/amd64"
|
KERNEL_BUILDER_PLATFORM="linux/amd64"
|
||||||
|
|||||||
28
distros/batocera/README.md
Normal file
28
distros/batocera/README.md
Normal 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).
|
||||||
346
distros/batocera/build-rootfs.sh
Executable file
346
distros/batocera/build-rootfs.sh
Executable file
@@ -0,0 +1,346 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
BATOCERA_VER="${BATOCERA_VER:-43}"
|
||||||
|
BATOCERA_BUILD="${BATOCERA_BUILD:-20260430}"
|
||||||
|
BATOCERA_URL="${BATOCERA_URL:-https://mirrors.o2switch.fr/batocera/x86_64/stable/last/batocera-x86_64-${BATOCERA_VER}-${BATOCERA_BUILD}.img.gz}"
|
||||||
|
|
||||||
|
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
distros/bazzite-deck/build-rootfs.sh
Symbolic link
1
distros/bazzite-deck/build-rootfs.sh
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../bazzite/build-rootfs.sh
|
||||||
1
distros/bazzite-deck/grow-rootfs
Symbolic link
1
distros/bazzite-deck/grow-rootfs
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../bazzite/grow-rootfs
|
||||||
1
distros/bazzite-deck/grow-rootfs.service
Symbolic link
1
distros/bazzite-deck/grow-rootfs.service
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../bazzite/grow-rootfs.service
|
||||||
1
distros/bazzite-deck/image.yaml
Symbolic link
1
distros/bazzite-deck/image.yaml
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../bazzite/image.yaml
|
||||||
30
distros/bazzite/README.md
Normal file
30
distros/bazzite/README.md
Normal 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.
|
||||||
377
distros/bazzite/build-rootfs.sh
Executable file
377
distros/bazzite/build-rootfs.sh
Executable file
@@ -0,0 +1,377 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
# 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
|
||||||
|
# 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
distros/bazzite/grow-rootfs
Executable file
18
distros/bazzite/grow-rootfs
Executable 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
distros/bazzite/grow-rootfs.service
Normal file
14
distros/bazzite/grow-rootfs.service
Normal 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
distros/bazzite/image.yaml
Normal file
13
distros/bazzite/image.yaml
Normal 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
|
||||||
@@ -10,8 +10,30 @@ EFI_LABEL="boot"
|
|||||||
CHROOT="/build/chroot"
|
CHROOT="/build/chroot"
|
||||||
IMG="/output/ps5-${DISTRO}.img"
|
IMG="/output/ps5-${DISTRO}.img"
|
||||||
|
|
||||||
|
# Detect kernel version from the staged kernel package (used by bazzite +
|
||||||
|
# batocera rootfs builders which install the linux-ps5 kernel manually).
|
||||||
|
detect_kver() {
|
||||||
|
for f in /kernel-debs/linux-ps5-*.rpm /kernel-debs/linux-ps5_*.deb \
|
||||||
|
/kernel-debs/linux-ps5-*.pkg.tar.zst; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
echo "$(basename "$f")" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
}
|
||||||
|
KVER="$(detect_kver)"
|
||||||
|
[ -n "$KVER" ] || { echo "WARN: could not detect KVER from /kernel-debs/"; }
|
||||||
|
export KVER
|
||||||
|
|
||||||
if [ "$SKIP_CHROOT" = "true" ] && [ -d "$CHROOT/bin" ]; then
|
if [ "$SKIP_CHROOT" = "true" ] && [ -d "$CHROOT/bin" ]; then
|
||||||
echo "=== Reusing cached $DISTRO rootfs ==="
|
echo "=== Reusing cached $DISTRO rootfs ==="
|
||||||
|
elif [ -x "/repo/distros/${DISTRO}/build-rootfs.sh" ]; then
|
||||||
|
# Distros with their own build-rootfs.sh (bazzite, bazzite-deck, batocera, ...)
|
||||||
|
# are responsible for populating $CHROOT themselves — distrobuilder is
|
||||||
|
# bypassed entirely. Most often used for OCI atomic images (bazzite via
|
||||||
|
# skopeo+umoci) or pre-built rootfs images (batocera squashfs).
|
||||||
|
echo "=== Building $DISTRO rootfs via distros/${DISTRO}/build-rootfs.sh ==="
|
||||||
|
rm -rf "$CHROOT"/* "$CHROOT"/.[!.]* 2>/dev/null || true
|
||||||
|
bash "/repo/distros/${DISTRO}/build-rootfs.sh"
|
||||||
else
|
else
|
||||||
echo "=== Building $DISTRO rootfs ==="
|
echo "=== Building $DISTRO rootfs ==="
|
||||||
# --- Stage files for distrobuilder's copy generators ---
|
# --- Stage files for distrobuilder's copy generators ---
|
||||||
@@ -139,7 +161,17 @@ cp -a "$CHROOT"/* /tmp/usb_root/
|
|||||||
sync
|
sync
|
||||||
|
|
||||||
echo "=== Assembling boot partition ==="
|
echo "=== Assembling boot partition ==="
|
||||||
mv /tmp/usb_root/boot/efi/* /tmp/usb_efi/ 2>/dev/null || true
|
# Batocera mounts the FAT32 partition at /boot (not /boot/efi) so its
|
||||||
|
# batocera-part SHARE auto-detection works (greps /proc/mounts for /boot).
|
||||||
|
# Detect either layout and copy from the right place.
|
||||||
|
if [ -d /tmp/usb_root/boot/efi ] && [ -n "$(ls -A /tmp/usb_root/boot/efi 2>/dev/null)" ]; then
|
||||||
|
mv /tmp/usb_root/boot/efi/* /tmp/usb_efi/ 2>/dev/null || true
|
||||||
|
elif [ -f /tmp/usb_root/boot/bzImage ]; then
|
||||||
|
# batocera-style: bzImage + cmdline + everything else lives directly
|
||||||
|
# in /boot. Move everything into the EFI partition so the PS5 loader
|
||||||
|
# can find bzImage/initrd.img/cmdline.txt at the FAT root.
|
||||||
|
mv /tmp/usb_root/boot/* /tmp/usb_efi/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
CMDLINE_TEMPLATE="/repo/distros/${DISTRO}/cmdline.txt"
|
CMDLINE_TEMPLATE="/repo/distros/${DISTRO}/cmdline.txt"
|
||||||
[ -f "$CMDLINE_TEMPLATE" ] || CMDLINE_TEMPLATE="/repo/boot/cmdline.txt"
|
[ -f "$CMDLINE_TEMPLATE" ] || CMDLINE_TEMPLATE="/repo/boot/cmdline.txt"
|
||||||
sed "s|__DISTRO__|$ROOT_LABEL|" "$CMDLINE_TEMPLATE" > /tmp/usb_efi/cmdline.txt
|
sed "s|__DISTRO__|$ROOT_LABEL|" "$CMDLINE_TEMPLATE" > /tmp/usb_efi/cmdline.txt
|
||||||
|
|||||||
Reference in New Issue
Block a user