feat: add CachyOS image (Gamescope + Steam)

- Add distros/cachyos image.yaml and helper files under files/
- Wire kernel packaging and --distro cachyos in build_image.sh
- Stage CachyOS artifacts in image-builder entrypoints; include cachyos in multi image
- Add boot/kexec-cachyos.sh and document in README

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Chihab Djaidja
2026-05-08 20:40:44 +02:00
parent 260d78baec
commit 292c9c02fd
16 changed files with 743 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
# PS5 Linux Image Builder # PS5 Linux Image Builder
Builds bootable Linux USB images for PlayStation 5 using Docker containers. Supports Ubuntu 26.04, Ubuntu 24.04, Arch, 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, Ubuntu 24.04, Arch, CachyOS (Gamescope + Steam), and Alpine, individually or as a multi-distro image with kexec switching.
## Prerequisites ## Prerequisites
@@ -27,7 +27,12 @@ OR
OR OR
# Build a multi-distro image (ubuntu2604 + ubuntu2404 + arch + alpine) # Build CachyOS (Arch-based, Gamescope + Steam Big Picture)
./build_image.sh --distro cachyos
OR
# Build a multi-distro image (ubuntu2604 + ubuntu2404 + arch + alpine + cachyos)
./build_image.sh --distro all ./build_image.sh --distro all
``` ```
@@ -43,7 +48,7 @@ sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
| Flag | Description | Default | | Flag | Description | Default |
|------|-------------|---------| |------|-------------|---------|
| `--distro` | `ubuntu2604`, `ubuntu2404`, `arch`, `alpine`, or `all` | `ubuntu2604` | | `--distro` | `ubuntu2604`, `ubuntu2404`, `arch`, `cachyos`, `alpine`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` | | `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` |
| `--img-size` | Disk image size in MB | `12000` (`32000` for `all`) | | `--img-size` | Disk image size in MB | `12000` (`32000` for `all`) |
| `--clean` | Remove all cached build artifacts and start fresh | off | | `--clean` | Remove all cached build artifacts and start fresh | off |
@@ -64,7 +69,7 @@ Use `--clean` to wipe everything and rebuild from scratch. The build will also s
PS5 Linux Image Builder PS5 Linux Image Builder
======================= =======================
Distro: all Distro: all
(ubuntu2604 ubuntu2404 arch alpine) (ubuntu2604 ubuntu2404 arch alpine cachyos)
Image size: 32000MB Image size: 32000MB
Kernel src: /path/to/work/linux Kernel src: /path/to/work/linux
@@ -89,11 +94,12 @@ All verbose output goes to `build.log`. The terminal shows a spinner with live p
| Ubuntu 24.04 (Noble) | GNOME | `.deb` | systemd | | Ubuntu 24.04 (Noble) | GNOME | `.deb` | systemd |
| Ubuntu 26.04 (Resolute) | GNOME | `.deb` | systemd | | Ubuntu 26.04 (Resolute) | GNOME | `.deb` | systemd |
| Arch | Sway | `.pkg.tar.zst` | 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 | | Alpine (3.21) | GNOME | extracted from `.deb` | OpenRC |
## Multi-distro Image ## Multi-distro Image
`--distro all` builds a 32GB image with 5 partitions: `--distro all` builds a 32GB image with 6 partitions (one EFI boot partition plus five root filesystems):
| Partition | Type | Label | Content | | Partition | Type | Label | Content |
|-----------|------|-------|---------| |-----------|------|-------|---------|
@@ -102,6 +108,7 @@ All verbose output goes to `build.log`. The terminal shows a spinner with live p
| p3 | ext4 | ubuntu2404 | Ubuntu 24.04 rootfs | | p3 | ext4 | ubuntu2404 | Ubuntu 24.04 rootfs |
| p4 | ext4 | arch | Arch rootfs | | p4 | ext4 | arch | Arch rootfs |
| p5 | ext4 | alpine | Alpine rootfs | | p5 | ext4 | alpine | Alpine rootfs |
| p6 | ext4 | cachyos | CachyOS rootfs |
The boot partition contains kexec scripts to switch between distros at runtime. Ubuntu 26.04 is the default boot target. The boot partition contains kexec scripts to switch between distros at runtime. Ubuntu 26.04 is the default boot target.
@@ -111,7 +118,7 @@ The boot partition contains kexec scripts to switch between distros at runtime.
build_image.sh # Main build script build_image.sh # Main build script
docker/ docker/
kernel-builder/ # Kernel compilation container 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/ image-builder/
Dockerfile # Image building container (distrobuilder) Dockerfile # Image building container (distrobuilder)
entrypoint.sh # Single-distro build logic entrypoint.sh # Single-distro build logic
@@ -120,12 +127,13 @@ distros/
ubuntu2404/ # Ubuntu 24.04 (Noble) ubuntu2404/ # Ubuntu 24.04 (Noble)
ubuntu2604/ # Ubuntu 26.04 (Resolute) ubuntu2604/ # Ubuntu 26.04 (Resolute)
arch/ # Arch Linux arch/ # Arch Linux
cachyos/ # CachyOS repos + Gamescope/Steam
alpine/ # Alpine 3.21 alpine/ # Alpine 3.21
shared/ # Kernel postinst hooks (single + multi) shared/ # Kernel postinst hooks (single + multi)
boot/ boot/
cmdline.txt # Kernel cmdline template (__DISTRO__ placeholder) cmdline.txt # Kernel cmdline template (__DISTRO__ placeholder)
vram.txt # VRAM allocation vram.txt # VRAM allocation
kexec-{ubuntu2604,ubuntu2404,arch,alpine}.sh kexec-{ubuntu2604,ubuntu2404,arch,alpine,cachyos}.sh
work/ # Build artifacts (auto-created) work/ # Build artifacts (auto-created)
linux-bin/ # Compiled kernel packages linux-bin/ # Compiled kernel packages
output/ # Final .img files output/ # Final .img files

6
boot/kexec-cachyos.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
# Switch to CachyOS (Gamescope + Steam) via kexec
set -e
BOOT=/boot/efi
kexec -l "$BOOT/bzImage" --initrd="$BOOT/initrd-cachyos.img" --command-line="$(cat $BOOT/cmdline-cachyos.txt)"
kexec -e

View File

@@ -9,13 +9,13 @@ KERNEL_SRC=""
CLEAN=false CLEAN=false
IMG_SIZE=12000 IMG_SIZE=12000
MULTI_DISTROS="ubuntu2604 ubuntu2404 arch alpine" MULTI_DISTROS="ubuntu2604 ubuntu2404 arch alpine cachyos"
usage() { 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, ubuntu2404, arch, alpine, all (default: ubuntu2604)" echo " --distro Distribution to build: ubuntu2604, ubuntu2404, arch, cachyos, alpine, 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"
@@ -90,7 +90,7 @@ SKIP_KERNEL=false
SKIP_CHROOT=false SKIP_CHROOT=false
# Kernel packages already built? # Kernel packages already built?
if [ "$DISTRO" = "arch" ]; then if [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true
elif [ "$DISTRO" = "all" ]; then elif [ "$DISTRO" = "all" ]; then
ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \ ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \
@@ -109,7 +109,7 @@ else
[ -d "$CHROOT_DIR/bin" ] && SKIP_CHROOT=true [ -d "$CHROOT_DIR/bin" ] && SKIP_CHROOT=true
fi fi
if [ "$DISTRO" = "arch" ]; then if [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
PKG_EXT="pkg.tar.zst" PKG_EXT="pkg.tar.zst"
else else
PKG_EXT="deb" PKG_EXT="deb"
@@ -281,7 +281,7 @@ else
-v "$KERNEL_OUT":/out \ -v "$KERNEL_OUT":/out \
ps5-kernel-packager-arch ps5-kernel-packager-arch
elif [ "$DISTRO" = "arch" ]; then elif [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
run_stage "Build arch packager image" \ run_stage "Build arch packager image" \
docker build -t ps5-kernel-packager-arch \ docker build -t ps5-kernel-packager-arch \
-f "$SCRIPT_DIR/docker/kernel-builder-arch/Dockerfile" "$SCRIPT_DIR" -f "$SCRIPT_DIR/docker/kernel-builder-arch/Dockerfile" "$SCRIPT_DIR"

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# First-boot interactive setup for CachyOS PS5 image.
# Sets the password for the 'steam' user and optionally the hostname,
# then creates a sentinel so this script never runs again.
set -e
# Keep interactive prompts readable on tty1 by suppressing kernel console noise
# during setup. Restore the previous setting before exiting.
ORIG_PRINTK="$(cat /proc/sys/kernel/printk 2>/dev/null || true)"
if [ -n "$ORIG_PRINTK" ]; then
echo "1 4 1 7" > /proc/sys/kernel/printk || true
fi
cleanup_printk() {
if [ -n "$ORIG_PRINTK" ]; then
echo "$ORIG_PRINTK" > /proc/sys/kernel/printk 2>/dev/null || true
fi
}
trap cleanup_printk EXIT
clear
echo "=================================================="
echo " Welcome to CachyOS on PS5!"
echo "=================================================="
echo
echo "User: steam (fixed — used for both Gaming Mode and Desktop)"
echo
# --- Password ---
while true; do
read -rsp "Set a password for 'steam': " PASSWORD; echo
read -rsp "Confirm password: " PASSWORD2; echo
if [[ -z "$PASSWORD" ]]; then
echo "Password cannot be empty."
elif [[ "$PASSWORD" != "$PASSWORD2" ]]; then
echo "Passwords do not match. Try again."
else
break
fi
done
# --- Hostname ---
read -rp "Hostname [ps5]: " HOSTNAME
HOSTNAME="${HOSTNAME:-ps5}"
echo
echo "Applying settings..."
echo "steam:$PASSWORD" | chpasswd
hostnamectl set-hostname "$HOSTNAME" 2>/dev/null || echo "$HOSTNAME" > /etc/hostname
# Update /etc/hosts if a 127.0.1.1 line exists, otherwise append one.
if grep -q '^127\.0\.1\.1' /etc/hosts; then
sed -i "s/^127\.0\.1\.1.*/127.0.1.1 $HOSTNAME/" /etc/hosts
else
echo "127.0.1.1 $HOSTNAME" >> /etc/hosts
fi
# Sentinel — prevents this service from running on subsequent boots.
touch /etc/ps5-first-boot-done
echo
echo "=================================================="
echo " Setup complete!"
echo "=================================================="
echo
echo "The system will log in on tty1 and start Steam Gaming Mode (Gamescope)."
echo "(No display manager — same pattern as the Arch Sway image in this repo.)"
echo
echo "Tips:"
echo " In Steam: Power > Switch to Desktop → KDE Plasma (X11)"
echo " In KDE: Double-click 'Return to Gaming Mode' on the desktop"
echo
sleep 3

View File

@@ -0,0 +1,23 @@
[Unit]
Description=First Boot Interactive Setup
# Run before tty1 autologin so the steam user never races the installer on tty1.
Before=getty@tty1.service
After=systemd-user-sessions.service
# Skip entirely on every boot after the first.
ConditionPathExists=!/etc/ps5-first-boot-done
DefaultDependencies=no
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/first-boot-setup
StandardInput=tty-force
StandardOutput=tty
StandardError=tty
TTYPath=/dev/tty1
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,28 @@
#!/bin/bash
# PS5 gamescope session launcher — invoked by tty1 (ps5-tty-session.sh).
#
# All flags are PS5-specific workarounds:
# LIBSEAT_BACKEND=logind : avoids seatd/logind conflict; matches the user
# session so gamescope can take DRM/input devices
# via logind TakeDevice()
# WLR_LIBINPUT_NO_DEVICES=1: suppresses libinput "no input devices" abort;
# PS5 has no directly-attached HID devices on boot
# --backend drm : take the DRM device directly (no nested compositor)
# -e : embedded / Steam Big Picture mode
# --prefer-output DP-1 : target the PS5's internal DP connector
# --sdr-gamut-wideness 0 : keep SDR colour space at sRGB; the PS5's internal
# DP->HDMI bridge cannot translate BT2020 signalling
#
# The ps5-display.lua script in ~/.config/gamescope/scripts/ enforces sRGB
# colorimetry at the DRM atomic commit level (belt-and-suspenders with the flag).
export LIBSEAT_BACKEND=logind
export WLR_LIBINPUT_NO_DEVICES=1
exec /usr/bin/gamescope \
--backend drm \
-e \
--prefer-output DP-1 \
--sdr-gamut-wideness 0 \
-- \
/usr/bin/steam -gamepadui -steamos3

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# 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="/dev/$(lsblk -ndo PKNAME "$ROOT_DEV")"
PART_NUM=$(cat /sys/class/block/$(basename "$ROOT_DEV")/partition)
growpart "$DISK" "$PART_NUM"
resize2fs "$ROOT_DEV"
systemctl disable grow-rootfs.service

View File

@@ -0,0 +1,13 @@
[Unit]
Description=Grow root filesystem to fill disk
DefaultDependencies=no
Before=local-fs.target
After=local-fs-pre.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/grow-rootfs
RemainAfterExit=yes
[Install]
WantedBy=local-fs.target

View File

@@ -0,0 +1,9 @@
#!/bin/sh
# Sourced very early by startplasma-wayland (before KWin). PS5 routes the internal
# DP through an HDMI bridge that mishandles HDR / wide-gamut / 10bpc signalling:
# assumed-HDR EDID paths and AR30/AB30 primaries can yield a valid mode but a black
# HDMI image. Force HDR off and prefer 8bpc/24bpp framebuffers.
# - KWIN_FORCE_ASSUME_HDR_SUPPORT=0 (https://invent.kde.org/plasma/kwin/-/merge_requests/7337)
# - KWIN_DRM_PREFER_COLOR_DEPTH=24
export KWIN_FORCE_ASSUME_HDR_SUPPORT=0
export KWIN_DRM_PREFER_COLOR_DEPTH=24

View File

@@ -0,0 +1,25 @@
-- Force sRGB colorimetry so gamescope does not set BT2020_RGB on the DP
-- connector. The PS5's internal DP->HDMI bridge cannot correctly translate
-- BT2020 signalling to HDMI, causing a black screen.
gamescope.config.known_displays.ps5_srgb_override = {
pretty_name = "PS5 sRGB Override",
hdr = {
supported = false,
force_enabled = false,
eotf = gamescope.eotf.gamma22,
max_content_light_level = 400,
max_frame_average_luminance = 400,
min_content_light_level = 0.5
},
colorimetry = {
r = { x = 0.640, y = 0.330 },
g = { x = 0.300, y = 0.600 },
b = { x = 0.150, y = 0.060 },
w = { x = 0.3127, y = 0.3290 }
},
matches = function(display)
debug("[ps5_srgb_override] Forcing sRGB for PS5 external display")
return 100
end
}
debug("Registered PS5 sRGB display override")

View File

@@ -0,0 +1,73 @@
# /etc/profile.d/ps5-tty-session.sh — tty1 autologin: Gamescope or Plasma (no SDDM).
# Sourced from /etc/profile (login shells) and from ~/.bashrc (agetty autologin
# usually starts a non-login shell). Idempotent guard avoids double-exec.
case ${PS5_TTY_SESSION_INIT-} in
1) return ;;
esac
PS5_TTY_SESSION_INIT=1
export PS5_TTY_SESSION_INIT
tty_path=$(tty 2>/dev/null) || tty_path=""
[ "$tty_path" = "/dev/tty1" ] || return 0
[ -z "${WAYLAND_DISPLAY-}" ] || return 0
[ -z "${DISPLAY-}" ] || return 0
SESSION_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/ps5-next-session"
mkdir -p "$(dirname "$SESSION_FILE")"
[ -f "$SESSION_FILE" ] || echo gamescope >"$SESSION_FILE"
read -r SESSION <"$SESSION_FILE" 2>/dev/null || SESSION=gamescope
SESSION=$(printf '%s' "$SESSION" | tr -d '\r\n\t ')
export LIBSEAT_BACKEND=logind
case "$SESSION" in
desktop|plasma|plasma-wayland|plasmax11)
# Do not `exec` Plasma: if startplasma-{wayland,x11} exits immediately the login
# shell ends, getty respawns in a tight loop, and start-limit-hit fires. Default
# is Plasma Wayland from VT (no SDDM greeter Xorg). The plasmax11 token is an
# explicit X11 escape hatch via startx + startplasma-x11.
LOG=/tmp/ps5-tty-session.log
# KDE helper: wraps Plasma in a session bus only when one is not already provided
# by systemd --user. Plain dbus-run-session would start an isolated bus and break
# org.freedesktop.systemd1 activation, PipeWire sockets, and xdg-desktop-portal.
PLASMA_DBUS_WRAP=""
[ -x /usr/lib/plasma-dbus-run-session-if-needed ] && PLASMA_DBUS_WRAP=/usr/lib/plasma-dbus-run-session-if-needed
_run_plasma_dbus() {
if [ -n "${PLASMA_DBUS_WRAP}" ]; then
"$PLASMA_DBUS_WRAP" "$@"
elif [ -n "${DBUS_SESSION_BUS_ADDRESS-}" ]; then
"$@"
else
dbus-run-session -- "$@"
fi
}
rv=0
case "$SESSION" in
plasmax11)
if [ -x /usr/bin/startx ] && [ -x /usr/bin/startplasma-x11 ]; then
_run_plasma_dbus startx /usr/bin/startplasma-x11 -- >>"$LOG" 2>&1 || rv=$?
else
echo "$(date) startx or startplasma-x11 missing — falling back to gamescope" >>"$LOG"
fi
;;
*)
if [ -x /usr/bin/startplasma-wayland ]; then
_run_plasma_dbus /usr/bin/startplasma-wayland >>"$LOG" 2>&1 || rv=$?
else
echo "$(date) startplasma-wayland missing — falling back to gamescope" >>"$LOG"
fi
;;
esac
[ "$rv" = 0 ] || echo "$(date) plasma session exited rv=$rv" >>"$LOG"
echo gamescope >"$SESSION_FILE"
unset PS5_TTY_SESSION_INIT
export WLR_LIBINPUT_NO_DEVICES=1
exec /usr/bin/gamescope-session-ps5
;;
gamescope|*)
echo gamescope >"$SESSION_FILE"
export WLR_LIBINPUT_NO_DEVICES=1
exec /usr/bin/gamescope-session-ps5
;;
esac

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Return to Gaming Mode
Comment=Switch back to Steam Big Picture (Gamescope) session
Exec=steamos-session-select gamescope
Icon=steam
Terminal=false
Categories=Game;
Keywords=steam;gamescope;gaming;

View File

@@ -0,0 +1,78 @@
#!/bin/bash
# steamos-session-select — switch between Gamescope and KDE Plasma (no SDDM).
#
# Called by Steam's "Power > Switch to Desktop" (passes "desktop") and by the
# "Return to Gaming Mode" desktop shortcut (passes "gamescope").
#
# Mechanism
# Root : write ~/.config/ps5-next-session to "plasma" or "gamescope"
# Non-root: sudo to write the token, then end / restart the session.
#
# Direction notes
# Plasma -> Gamescope: terminate the Plasma session; agetty autologin re-runs the
# profile.d script, reads the token and execs gamescope-session-ps5.
# Gamescope -> Plasma: a soft tty handoff leaves the AMD GPU's DP->HDMI bridge in
# a state that produces a black HDMI image even with KWin running and a valid
# 2560x1440 mode set. PCI function-level reset hangs the GPU; amdgpu module
# reload fails with EBUSY because fbcon / snd-hda hold refcounts. Only a real
# reboot — which power-cycles PCI and re-runs atombios — clears the bridge,
# so this direction calls `systemctl reboot`. The next-session token has
# already been written, so tty1 boots straight into Plasma after the reboot.
# On PS5 the boot chain requires re-running the jailbreak after a reboot;
# this is an accepted limitation.
#
# Optional: write `plasmax11` to ~/.config/ps5-next-session for an X11 escape
# hatch via startx + startplasma-x11 (ps5-tty-session.sh).
set -euo pipefail
MARKER="/etc/ps5-use-tty-sessions"
SESSION_FILE="/home/steam/.config/ps5-next-session"
session="${1:-gamescope}"
# ---------- Root path: write next tty session token ----------
if [[ $EUID -eq 0 ]]; then
case "$session" in
gamescope)
next="gamescope"
;;
desktop|plasma|plasma-wayland|plasmax11)
next="plasma"
;;
*)
echo >&2 "steamos-session-select: unrecognised session '$session'"
exit 1
;;
esac
if [[ ! -f "$MARKER" ]]; then
echo >&2 "steamos-session-select: $MARKER missing — image not using tty sessions"
exit 1
fi
mkdir -p "$(dirname "$SESSION_FILE")"
printf '%s\n' "$next" >"$SESSION_FILE"
sync
chown steam:steam "$SESSION_FILE"
chmod 644 "$SESSION_FILE"
echo "steamos-session-select: next tty1 session set to $next"
exit 0
fi
# ---------- Non-root path: escalate, then end / restart the session ----------
sudo "$(realpath "$0")" "$session"
case "$session" in
gamescope)
if [[ -n "${XDG_SESSION_ID:-}" ]]; then
loginctl terminate-session "$XDG_SESSION_ID"
else
pkill -u steam kwin_wayland || true
pkill -u steam kwin_x11 || true
fi
;;
desktop|plasma|plasma-wayland|plasmax11)
exec sudo systemctl reboot
;;
esac

335
distros/cachyos/image.yaml Normal file
View File

@@ -0,0 +1,335 @@
image:
name: ps5-cachyos
distribution: archlinux
description: Arch + CachyOS extras — tty1 autologin, Gamescope + Steam, KDE Plasma (no SDDM)
architecture: x86_64
source:
downloader: archlinux-http
url: https://geo.mirror.pkgbuild.com/iso
skip_verification: true
packages:
manager: pacman
# Do not "pacman -Su" the whole bootstrap: CachyOS *-v3 replaces glibc/pacman, breaks Docker/QEMU hooks and multilib.
update: false
cleanup: true
repositories:
- name: mirrorlist
url: |-
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
sets:
- packages:
# Gamescope + Steam (Big Picture / gamepad UI)
- gamescope
- multilib/steam
- mangohud
- gamemode
- multilib/lib32-mesa
- multilib/lib32-vulkan-radeon
- multilib/lib32-mangohud
- xorg-xwayland
- xorg-xinit
# KDE Plasma (no SDDM — tty + dbus-run-session + startplasma-wayland). plasma-meta
# pulls plasma-workspace, which ships /usr/bin/startplasma-{wayland,x11}; xorg-xinit
# below provides startx for the optional `plasmax11` next-session token (X11 fallback).
- plasma-meta
- sddm
- qt6-wayland
- egl-wayland
- konsole
- dolphin
# Audio / media
- pipewire
- wireplumber
- pipewire-pulse
# Display / GPU (AMD) — qualify extra/ so pacman does not prompt vs [cachyos] mesa
- extra/mesa
- extra/vulkan-radeon
- extra/libva-mesa-driver
- extra/xf86-video-amdgpu
- libinput
- xkeyboard-config
# Networking
- networkmanager
- linux-firmware
# System
- base
- openssh
- sudo
- nano
- bash
- mkinitcpio
- kmod
- e2fsprogs
- cloud-utils
- parted
- polkit
- dbus
- kexec-tools
- curl
# Fonts (Steam UI + KDE) — CJK + emoji avoid "tofu" boxes in Steam Big Picture language list
- ttf-dejavu
- ttf-liberation
- noto-fonts
- noto-fonts-cjk
- noto-fonts-emoji
- 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/files/grow-rootfs
mode: "0755"
- path: /etc/systemd/system/grow-rootfs.service
generator: copy
source: /tmp/build-staging/files/grow-rootfs.service
- path: /opt/pkgs/
generator: copy
source: /tmp/build-staging/pkgs
- path: /usr/local/sbin/first-boot-setup
generator: copy
source: /tmp/build-staging/files/first-boot-setup
mode: "0755"
- path: /etc/systemd/system/first-boot.service
generator: copy
source: /tmp/build-staging/files/first-boot.service
- path: /etc/profile.d/ps5-tty-session.sh
generator: copy
source: /tmp/build-staging/files/ps5-tty-session.sh
mode: "0644"
- path: /usr/bin/gamescope-session-ps5
generator: copy
source: /tmp/build-staging/files/gamescope-session-ps5
mode: "0755"
- path: /usr/bin/steamos-session-select
generator: copy
source: /tmp/build-staging/files/steamos-session-select
mode: "0755"
- path: /usr/share/applications/return-to-gaming-mode.desktop
generator: copy
source: /tmp/build-staging/files/return-to-gaming-mode.desktop
- path: /usr/local/share/ps5-display.lua
generator: copy
source: /tmp/build-staging/files/ps5-display.lua
- path: /usr/local/share/plasma-workspace-env-ps5.sh
generator: copy
source: /tmp/build-staging/files/plasma-workspace-env-ps5.sh
mode: "0755"
actions:
- trigger: post-unpack
action: |-
#!/bin/bash
set -eux
# Pacman 7 Landlock/seccomp sandbox breaks under distrobuilder/Docker (unsupported kernel).
if grep -q '^#DisableSandbox' /etc/pacman.conf; then
sed -i 's/^#DisableSandbox/DisableSandbox/' /etc/pacman.conf
elif ! grep -q '^DisableSandbox' /etc/pacman.conf; then
sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf
fi
CACHYOS_PKG="https://mirror.cachyos.org/repo/x86_64/cachyos"
pacman-key --init
pacman-key --populate archlinux
pacman-key --recv-keys F3B607488DB35A47 --keyserver keyserver.ubuntu.com
pacman-key --lsign-key F3B607488DB35A47
# Keyring + main mirrorlist only (no *-v3 repos — those force a full distro migration in the chroot).
pacman -U --noconfirm \
"${CACHYOS_PKG}/cachyos-keyring-20240331-1-any.pkg.tar.zst" \
"${CACHYOS_PKG}/cachyos-mirrorlist-27-1-any.pkg.tar.zst"
# Optional CachyOS packages: repo listed *after* Arch so core/extra/multilib win on conflicts (mesa, steam deps).
if ! grep -q '^\[cachyos\]' /etc/pacman.conf; then
printf '\n[cachyos]\nInclude = /etc/pacman.d/cachyos-mirrorlist\n' >> /etc/pacman.conf
fi
# Enable #[multilib] / #Include block (\\\\ in YAML would break the sed address regex)
sed -i '/^#\[multilib\]/,/^#Include/s/^#//' /etc/pacman.conf
curl -fsSL "https://archlinux.org/mirrorlist/?country=all&protocol=https&use_mirror_status=on" \
| sed 's/^#Server/Server/' > /etc/pacman.d/mirrorlist
pacman -Syy --noconfirm
- trigger: post-packages
action: |-
#!/bin/bash
set -eux
# 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
local base
base=$(basename "$src")
mkdir -p "/etc/systemd/system/${target}.wants"
ln -sf "$src" "/etc/systemd/system/${target}.wants/${base}"
}
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication yes/' /etc/ssh/sshd_config
symlink_wants /usr/lib/systemd/system/sshd.service multi-user.target
symlink_wants /usr/lib/systemd/system/NetworkManager.service multi-user.target
# PipeWire/WirePlumber: user session after tty autologin; default.target matches Arch Sway image.
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
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
# Stub out steamos-update so Steam's -steamos3 OOBE skips the SteamOS OTA
# step cleanly. Exit 7 = "no update available" per Valve's convention.
printf '#!/bin/bash\n# Stub: no SteamOS OTA on this image\nexit 7\n' \
> /usr/bin/steamos-update
chmod +x /usr/bin/steamos-update
useradd -m -G wheel,seat,video,audio,input -s /bin/bash steam
echo "steam:steam" | chpasswd
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" > /etc/sudoers.d/wheel
# --- No SDDM: tty1 autologin + /etc/profile.d/ps5-tty-session.sh (+ ~/.bashrc source) ---
# SDDM is only a display manager (login UI, VT handoff, relaunch). plasma-meta may
# still pull sddm as a dependency; keep it masked. Remove leftover SDDM config from older images.
rm -f /etc/sddm.conf.d/steamos.conf /etc/sddm.conf.d/zz-steamos-autologin.conf 2>/dev/null || true
rm -f /etc/systemd/system/sddm.service.d/ps5-seat.conf 2>/dev/null || true
rmdir /etc/systemd/system/sddm.service.d 2>/dev/null || true
rm -f /usr/local/share/steam-bash_profile.ps5 2>/dev/null || true
touch /etc/ps5-use-tty-sessions
ln -sf /usr/lib/systemd/system/multi-user.target /etc/systemd/system/default.target
ln -sf /dev/null /etc/systemd/system/sddm.service
mkdir -p /etc/systemd/system/getty@tty1.service.d
printf '%s\n' '[Service]' 'ExecStart=' 'ExecStart=-/sbin/agetty --autologin steam --noclear %I $TERM' \
> /etc/systemd/system/getty@tty1.service.d/autologin.conf
printf '%s\n' '[Unit]' 'After=first-boot.service' \
> /etc/systemd/system/getty@tty1.service.d/after-first-boot.conf
mkdir -p /etc/systemd/system/multi-user.target.wants
ln -sf /etc/systemd/system/first-boot.service \
/etc/systemd/system/multi-user.target.wants/first-boot.service
# agetty --autologin often starts a non-login shell — ~/.bashrc must source profile.d.
# Write via printf (not heredoc) to avoid column-0 lines that would terminate this
# YAML block scalar.
printf '%s\n' \
'# ps5-linux-image: tty1 autostart when /etc/profile was skipped (agetty autologin).' \
'if [ -f /etc/profile.d/ps5-tty-session.sh ]; then' \
' . /etc/profile.d/ps5-tty-session.sh' \
'fi' \
> /home/steam/.bashrc
chown steam:steam /home/steam/.bashrc
chmod 644 /home/steam/.bashrc
printf '%s\n' \
'# Login shells: /etc/profile sources profile.d first; keep .bashrc for interactive tools.' \
'[ -f ~/.bashrc ] && . ~/.bashrc' \
> /home/steam/.bash_profile
chown steam:steam /home/steam/.bash_profile
chmod 644 /home/steam/.bash_profile
mkdir -p /home/steam/.config
echo gamescope > /home/steam/.config/ps5-next-session
# User PipeWire (systemd --user): post-packages symlinks system default.target, which does
# not pull user units. Enable sockets/services under the steam user's default.target so
# Plasma (real session bus) can connect to PipeWire.
mkdir -p /home/steam/.config/systemd/user/default.target.wants
for u in /usr/lib/systemd/user/pipewire.socket /usr/lib/systemd/user/pipewire-pulse.socket \
/usr/lib/systemd/user/wireplumber.service; do
[ -e "$u" ] || continue
ln -sf "$u" "/home/steam/.config/systemd/user/default.target.wants/$(basename "$u")"
done
chown -R steam:steam /home/steam/.config/systemd
# Gamescope display profile: force sRGB colorimetry so gamescope does not set
# BT2020_RGB on the DP connector. The PS5's internal DP->HDMI bridge cannot
# correctly translate BT2020 signalling to HDMI, causing a black screen.
# Staged via files: to /usr/local/share/ps5-display.lua to avoid heredoc
# indentation conflicts with the YAML literal block scalar.
mkdir -p /home/steam/.config/gamescope/scripts
cp /usr/local/share/ps5-display.lua /home/steam/.config/gamescope/scripts/ps5-display.lua
# Plasma (X11 from tty): avoid HDR negotiation that black-screens the PS5 DP→HDMI path.
mkdir -p /home/steam/.config/plasma-workspace/env
cp /usr/local/share/plasma-workspace-env-ps5.sh \
/home/steam/.config/plasma-workspace/env/ps5-drm-bridge.sh
chmod +x /home/steam/.config/plasma-workspace/env/ps5-drm-bridge.sh
# KDE Desktop shortcut: "Return to Gaming Mode"
# chmod +x lets KDE trust and execute it without showing an "untrusted" dialog.
mkdir -p /home/steam/Desktop
cp /usr/share/applications/return-to-gaming-mode.desktop /home/steam/Desktop/
chmod +x /home/steam/Desktop/return-to-gaming-mode.desktop
chown -R steam:steam /home/steam
[ -f /etc/vconsole.conf ] || echo "KEYMAP=us" > /etc/vconsole.conf
# Busybox-based initrd: polls for the root device (works after kexec, unlike
# the systemd initrd which waits for udev events that never re-fire).
# `kms` loads the amdgpu module + firmware early so the display comes up
# before the rootfs is mounted. `keyboard` allows emergency-mode input.
sed -i \
-e 's/^HOOKS=.*/HOOKS=(base udev modconf kms keyboard block filesystems fsck)/' \
-e 's/^MODULES=.*/MODULES=(amdgpu)/' \
/etc/mkinitcpio.conf
if ls /opt/pkgs/*.pkg.tar.zst 1>/dev/null 2>&1; then
pacman -U --noconfirm /opt/pkgs/*.pkg.tar.zst
rm -rf /opt/pkgs
KVER=$(ls -1t /lib/modules | head -1)
mkinitcpio -k "$KVER" -g "/boot/initrd.img-$KVER" || true
fi
mkdir -p /boot/efi
/etc/kernel/postinst.d/zz-update-boot
mappings:
architecture_map: archlinux

View File

@@ -5,7 +5,7 @@ set -ex
IMG_SIZE="${IMG_SIZE:-32000}" IMG_SIZE="${IMG_SIZE:-32000}"
SKIP_CHROOT="${SKIP_CHROOT:-false}" SKIP_CHROOT="${SKIP_CHROOT:-false}"
DISTROS="${DISTROS:-ubuntu2404 ubuntu2604 arch alpine}" DISTROS="${DISTROS:-ubuntu2404 ubuntu2604 arch alpine cachyos}"
STAGING="/tmp/build-staging" STAGING="/tmp/build-staging"
EFI_LABEL="boot" EFI_LABEL="boot"
IMG="/output/ps5-multi.img" IMG="/output/ps5-multi.img"
@@ -34,22 +34,38 @@ for DISTRO in $DISTROS; do
printf 'LABEL=%-14s / ext4 rw,relatime 0 1\nLABEL=%-14s /boot/efi vfat rw,relatime 0 2\n' \ printf 'LABEL=%-14s / ext4 rw,relatime 0 1\nLABEL=%-14s /boot/efi vfat rw,relatime 0 2\n' \
"$ROOT_LABEL" "$EFI_LABEL" > "$STAGING/fstab" "$ROOT_LABEL" "$EFI_LABEL" > "$STAGING/fstab"
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/${DISTRO}/nm-dns.conf "$STAGING/" 2>/dev/null || true cp /repo/distros/${DISTRO}/nm-dns.conf "$STAGING/" 2>/dev/null || true
case "$DISTRO" in case "$DISTRO" in
ubuntu*) ubuntu*)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/" cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
cp /kernel-debs/*.deb "$STAGING/debs/" cp /kernel-debs/*.deb "$STAGING/debs/"
;; ;;
alpine) alpine)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/" cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/"
;; ;;
arch) arch)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/arch/grow-rootfs.service "$STAGING/" cp /repo/distros/arch/grow-rootfs.service "$STAGING/"
cp /repo/distros/arch/first-boot-setup "$STAGING/" cp /repo/distros/arch/first-boot-setup "$STAGING/"
cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/" cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/"
;; ;;
cachyos)
mkdir -p "$STAGING/files"
cp /repo/distros/cachyos/files/grow-rootfs "$STAGING/files/"
cp /repo/distros/cachyos/files/grow-rootfs.service "$STAGING/files/"
cp /repo/distros/cachyos/files/first-boot-setup "$STAGING/files/"
cp /repo/distros/cachyos/files/first-boot.service "$STAGING/files/"
cp /repo/distros/cachyos/files/gamescope-session-ps5 "$STAGING/files/"
cp /repo/distros/cachyos/files/steamos-session-select "$STAGING/files/"
cp /repo/distros/cachyos/files/return-to-gaming-mode.desktop "$STAGING/files/"
cp /repo/distros/cachyos/files/ps5-display.lua "$STAGING/files/"
cp /repo/distros/cachyos/files/plasma-workspace-env-ps5.sh "$STAGING/files/"
cp /repo/distros/cachyos/files/ps5-tty-session.sh "$STAGING/files/"
cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/"
;;
esac esac
# --- Build rootfs --- # --- Build rootfs ---

View File

@@ -26,23 +26,40 @@ else
LABEL=$ROOT_LABEL / ext4 defaults 0 1 LABEL=$ROOT_LABEL / ext4 defaults 0 1
LABEL=$EFI_LABEL /boot/efi vfat defaults 0 1 LABEL=$EFI_LABEL /boot/efi vfat defaults 0 1
EOF EOF
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/${DISTRO}/nm-dns.conf "$STAGING/" 2>/dev/null || true cp /repo/distros/${DISTRO}/nm-dns.conf "$STAGING/" 2>/dev/null || true
case "$DISTRO" in case "$DISTRO" in
ubuntu*) ubuntu*)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/" cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
cp /kernel-debs/*.deb "$STAGING/debs/" cp /kernel-debs/*.deb "$STAGING/debs/"
;; ;;
alpine) alpine)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/" cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/"
;; ;;
arch) arch)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/arch/grow-rootfs.service "$STAGING/" cp /repo/distros/arch/grow-rootfs.service "$STAGING/"
cp /repo/distros/arch/first-boot-setup "$STAGING/" cp /repo/distros/arch/first-boot-setup "$STAGING/"
mkdir -p "$STAGING/pkgs" mkdir -p "$STAGING/pkgs"
cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/" cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/"
;; ;;
cachyos)
mkdir -p "$STAGING/files"
cp /repo/distros/cachyos/files/grow-rootfs "$STAGING/files/"
cp /repo/distros/cachyos/files/grow-rootfs.service "$STAGING/files/"
cp /repo/distros/cachyos/files/first-boot-setup "$STAGING/files/"
cp /repo/distros/cachyos/files/first-boot.service "$STAGING/files/"
cp /repo/distros/cachyos/files/gamescope-session-ps5 "$STAGING/files/"
cp /repo/distros/cachyos/files/steamos-session-select "$STAGING/files/"
cp /repo/distros/cachyos/files/return-to-gaming-mode.desktop "$STAGING/files/"
cp /repo/distros/cachyos/files/ps5-display.lua "$STAGING/files/"
cp /repo/distros/cachyos/files/plasma-workspace-env-ps5.sh "$STAGING/files/"
cp /repo/distros/cachyos/files/ps5-tty-session.sh "$STAGING/files/"
mkdir -p "$STAGING/pkgs"
cp /kernel-debs/*.pkg.tar.zst "$STAGING/pkgs/"
;;
esac esac
# --- Build rootfs --- # --- Build rootfs ---