15 Commits

Author SHA1 Message Date
Dan Kluser
03a8dc0f6d deprecate alpine 2026-05-18 08:16:31 +02:00
Dan
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
Dan
492e124991 Merge pull request #4 from ps5-linux/feature/ci-integration
Feature/ci integration
2026-05-09 18:14:38 +02:00
16 changed files with 529 additions and 449 deletions

View File

@@ -1,7 +1,6 @@
linux_deb/
linux-bin/
work/
work-alpine/
output/
.git/
ps5-linux-patches/

196
.github/workflows/build-image.yml vendored Normal file
View File

@@ -0,0 +1,196 @@
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]
workflow_call:
inputs:
distro:
type: string
default: 'ubuntu2604'
secrets:
REMOTE_SSH_KEY:
required: true
REMOTE_SSH_CONFIG:
required: true
REMOTE_SSH_KNOWN_HOSTS:
required: true
R2_ACCESS_KEY_ID:
required: true
R2_SECRET_ACCESS_KEY:
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"]' >> "$GITHUB_OUTPUT"
else
echo "distros=[\"$INPUT\"]" >> "$GITHUB_OUTPUT"
fi
build:
needs: matrix
runs-on: self-hosted
timeout-minutes: 240
permissions:
contents: write
strategy:
fail-fast: false
max-parallel: 1
matrix:
distro: ${{ fromJson(needs.matrix.outputs.distros) }}
env:
CACHE_DIR: /home/opc/ccache
steps:
- name: Checkout image builder
uses: actions/checkout@v6
with:
repository: ps5-linux/ps5-linux-image
path: image
ref: main
- 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 -9 -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
- name: Upload to remote
if: github.event_name != 'pull_request' && github.event_name != 'pull_request_target'
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
DEST="image-${{ matrix.distro }}-${{ steps.version.outputs.ts }}"
$SSH remote mkdir -p "./$DEST"
$SCP image/output/*.img.xz "remote:./$DEST/"
$SSH remote 'ls -dt image-${{ matrix.distro }}-*/ | tail -n +5 | xargs rm -rf' || true
rm -rf "$DIR"
release:
needs: build
runs-on: self-hosted
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
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: 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

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 }}"}'

172
.github/workflows/test-uploads.yml vendored Normal file
View File

@@ -0,0 +1,172 @@
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: Upload to remote
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
SCP="scp -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote mkdir -p ./test-upload
$SCP image/output/ps5-${{ matrix.distro }}.img.xz "remote:./test-upload/"
rm -rf "$DIR"
- name: Cleanup
if: always()
run: rm -rf image/output meta
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
- name: Clean up remote test files
env:
SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
run: |
DIR=$(mktemp -d)
echo "$SSH_KEY" > "$DIR/key"
chmod 600 "$DIR/key"
echo "$SSH_CONFIG" > "$DIR/config"
sed -i "s|~/.ssh/remote_key|$DIR/key|" "$DIR/config"
echo "$SSH_KNOWN_HOSTS" > "$DIR/known_hosts"
SSH="ssh -F $DIR/config -o UserKnownHostsFile=$DIR/known_hosts"
$SSH remote rm -rf ./test-upload
rm -rf "$DIR"

26
.github/workflows/trigger-builds.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
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:
REMOTE_SSH_KEY: ${{ secrets.REMOTE_SSH_KEY }}
REMOTE_SSH_CONFIG: ${{ secrets.REMOTE_SSH_CONFIG }}
REMOTE_SSH_KNOWN_HOSTS: ${{ secrets.REMOTE_SSH_KNOWN_HOSTS }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
R2_BUCKET: ${{ secrets.R2_BUCKET }}

View File

@@ -1,6 +1,6 @@
# 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, and CachyOS (Gamescope + Steam), individually or as a multi-distro image with kexec switching.
## Prerequisites
@@ -27,7 +27,7 @@ OR
OR
# Build a multi-distro image (ubuntu2604 + arch + alpine + cachyos)
# Build a multi-distro image (ubuntu2604 + arch + cachyos)
./build_image.sh --distro all
```
@@ -43,7 +43,7 @@ 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` |
| `--distro` | `ubuntu2604`, `arch`, `cachyos`, or `all` | `ubuntu2604` |
| `--kernel` | Path to kernel source directory | auto-clone `v6.19.10` |
| `--img-size` | Disk image size in MB | `12000` (`32000` for `all`) |
| `--clean` | Remove all cached build artifacts and start fresh | off |
@@ -66,7 +66,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 +91,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.
@@ -139,12 +137,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

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

View File

@@ -9,18 +9,19 @@ KERNEL_SRC=""
CLEAN=false
IMG_SIZE=12000
KERNEL_ONLY=false
PATCHES_REF="v1.0"
PATCHES_REF="v1.1"
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, 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)"
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 ;;
@@ -64,7 +66,7 @@ if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
fi
if [ -z "$FORMAT" ]; then
case "$DISTRO" in arch) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
case "$DISTRO" in arch|cachyos) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
fi
KERNEL_BUILDER_PLATFORM="linux/amd64"
@@ -76,14 +78,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 +96,7 @@ if [ "$CLEAN" = true ]; then
alpine rm -rf "/parent/$(basename "$dir")"
done
echo "Done."
[ "$CLEAN_EXIT" = true ] && exit 0
echo ""
fi

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

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 $?
}

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

View File

@@ -118,6 +118,9 @@ 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
@@ -132,21 +135,33 @@ 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
# Create default user (ps5/ps5)
useradd -m -G wheel,seat -s /bin/bash ps5

View File

@@ -1,11 +1,8 @@
#!/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
# First-boot: dialog wizard on tty1 (timezone, steam password, hostname).
# Defaults: UTC, ps5, keep image password (steam:steam). Skips on Cancel use defaults.
set -euo pipefail
# 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
@@ -17,57 +14,93 @@ cleanup_printk() {
}
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
export TERM="${TERM:-linux}"
# --- 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."
apply_timezone() {
local tz="${1:-UTC}"
timedatectl set-timezone "$tz" 2>/dev/null || ln -sf "/usr/share/zoneinfo/$tz" /etc/localtime
}
apply_hostname() {
local hn="${1:-ps5}"
hostnamectl set-hostname "$hn" 2>/dev/null || echo "$hn" > /etc/hostname
if grep -q '^127\.0\.1\.1' /etc/hosts; then
sed -i "s/^127\.0\.1\.1.*/127.0.1.1 $hn/" /etc/hosts
else
break
echo "127.0.1.1 $hn" >> /etc/hosts
fi
done
}
# --- Hostname ---
read -rp "Hostname [ps5]: " HOSTNAME
HOSTNAME="${HOSTNAME:-ps5}"
main() {
if ! command -v dialog >/dev/null 2>&1; then
echo "first-boot-setup: dialog not installed" >&2
exit 1
fi
echo
echo "Applying settings..."
clear
echo "steam:$PASSWORD" | chpasswd
local choice
choice="$(dialog --stdout --clear --menu \
"Welcome to CachyOS on PS5\nUser: steam (Gaming Mode + Desktop)" \
17 64 4 \
defaults "Use defaults (UTC, hostname ps5, keep password)" \
custom "Customize timezone, password, hostname" )" \
|| choice="defaults"
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
if [[ "$choice" == "defaults" ]]; then
apply_timezone UTC
apply_hostname ps5
else
local zones
mapfile -t zones < <(timedatectl list-timezones 2>/dev/null | LC_ALL=C sort -u)
local -a menu_args=()
local z
for z in "${zones[@]}"; do
menu_args+=("$z" "$z")
done
# Sentinel — prevents this service from running on subsequent boots.
touch /etc/ps5-first-boot-done
local tz_pick
if ((${#menu_args[@]} == 0)); then
apply_timezone UTC
elif tz_pick="$(dialog --stdout --clear --menu "Select timezone (Cancel = UTC)" 22 70 15 \
"${menu_args[@]}")"; then
apply_timezone "$tz_pick"
else
apply_timezone UTC
fi
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
if dialog --clear --yesno "Change password for user steam?" 8 55; then
local p1 p2
while true; do
p1="$(dialog --stdout --clear --insecure --passwordbox "New password (empty = skip)" 9 55)" \
|| break
p2="$(dialog --stdout --clear --insecure --passwordbox "Confirm password" 9 55)" \
|| break
if [[ -z "$p1" ]]; then
dialog --clear --msgbox "Password unchanged (empty)." 7 44
break
fi
if [[ "$p1" != "$p2" ]]; then
dialog --clear --msgbox "Passwords do not match. Try again." 7 50
continue
fi
echo "steam:$p1" | chpasswd
break
done
fi
local hn
hn="$(dialog --stdout --clear --inputbox "Hostname (empty = ps5)" 9 50 "ps5")" \
|| hn="ps5"
hn="${hn:-ps5}"
apply_hostname "$hn"
fi
touch /etc/ps5-first-boot-done
dialog --clear --msgbox \
"Setup complete.\n\nYou will log in on tty1 (Steam Gaming Mode).\n\nDesktop: Steam > Power > Switch to KDE\nReturn: double-click the desktop shortcut." \
14 62
}
main "$@"

View File

@@ -59,6 +59,11 @@ packages:
# Networking
- networkmanager
- linux-firmware
# GIO module libgiognutls.so (KDE/Konsole, GVfs, etc.): needs gnutls → nettle (libhogweed).
# Explicit install avoids a sparse dependency tree under distrobuilder cleanup: true.
- gnutls
- nettle
- glib-networking
# System
- base
@@ -75,6 +80,7 @@ packages:
- dbus
- kexec-tools
- curl
- dialog
# Fonts (Steam UI + KDE) — CJK + emoji avoid "tofu" boxes in Steam Big Picture language list
- ttf-dejavu
@@ -156,12 +162,8 @@ actions:
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
# Pacman 7 Landlock sandbox is unsupported under QEMU; disable it.
sed -i '/^\[options\]/a DisableSandbox' /etc/pacman.conf
CACHYOS_PKG="https://mirror.cachyos.org/repo/x86_64/cachyos"
@@ -245,6 +247,8 @@ actions:
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
# First-boot wizard handles timezone/password/hostname; avoid duplicate prompts.
ln -sf /dev/null /etc/systemd/system/systemd-firstboot.service
mkdir -p /etc/systemd/system/getty@tty1.service.d
printf '%s\n' '[Service]' 'ExecStart=' 'ExecStart=-/sbin/agetty --autologin steam --noclear %I $TERM' \
@@ -322,7 +326,7 @@ actions:
/etc/mkinitcpio.conf
if ls /opt/pkgs/*.pkg.tar.zst 1>/dev/null 2>&1; then
pacman -U --noconfirm /opt/pkgs/*.pkg.tar.zst
pacman -U --noconfirm --ask 4 /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

View File

@@ -5,7 +5,7 @@ set -ex
IMG_SIZE="${IMG_SIZE:-32000}"
SKIP_CHROOT="${SKIP_CHROOT:-false}"
DISTROS="${DISTROS:-ubuntu2604 arch alpine cachyos}"
DISTROS="${DISTROS:-ubuntu2604 arch cachyos}"
STAGING="/tmp/build-staging"
EFI_LABEL="boot"
IMG="/output/ps5-multi.img"
@@ -42,10 +42,6 @@ for DISTRO in $DISTROS; do
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
cp /kernel-debs/*.deb "$STAGING/debs/"
;;
alpine)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/"
;;
arch)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/arch/grow-rootfs.service "$STAGING/"
@@ -87,75 +83,6 @@ for DISTRO in $DISTROS; do
echo "$DISTRO" > "$CHROOT/etc/ps5-distro"
fi
# --- Alpine kernel gap: no kernel installed via image.yaml ---
# This runs even with --skip-chroot because Alpine's rootfs never includes a kernel;
# we must always extract it from the .deb artifacts and generate an initrd.
if [ "$DISTRO" = "alpine" ]; then
echo "=== Alpine: installing kernel from .deb artifacts ==="
# Extract modules + vmlinuz from the linux-image .deb
ALPINE_STAGING="/tmp/alpine-kernel-staging"
rm -rf "$ALPINE_STAGING"
mkdir -p "$ALPINE_STAGING"
for deb in /kernel-debs/linux-image-*.deb; do
[ -f "$deb" ] || continue
dpkg-deb -x "$deb" "$ALPINE_STAGING"
done
# Identify kernel version from the extracted .deb before copying
KVER=$(ls -1 "$ALPINE_STAGING/lib/modules" 2>/dev/null | head -1)
if [ -n "$KVER" ]; then
# Resolve the real modules path inside the chroot.
# Alpine may use usr-merge (/lib -> usr/lib), so we must follow
# symlinks to find the actual directory on disk.
if [ -L "$CHROOT/lib" ]; then
MODDIR="$CHROOT/usr/lib/modules"
else
MODDIR="$CHROOT/lib/modules"
fi
mkdir -p "$MODDIR"
# Remove any stale modules from a previous build
rm -rf "$MODDIR/$KVER"
cp -a "$ALPINE_STAGING/lib/modules/$KVER" "$MODDIR/"
mkdir -p "$CHROOT/boot"
cp "$ALPINE_STAGING/boot/vmlinuz-$KVER" "$CHROOT/boot/vmlinuz-$KVER"
echo ">> Alpine: modules copied to $MODDIR/$KVER"
ls -la "$MODDIR/"
fi
rm -rf "$ALPINE_STAGING"
if [ -n "$KVER" ]; then
echo "=== Alpine: generating initrd ==="
chroot "$CHROOT" depmod -a "$KVER" 2>/dev/null || true
# Bind-mount essentials and run mkinitfs inside the alpine chroot
mount --bind /dev "$CHROOT/dev"
mount --bind /proc "$CHROOT/proc"
mount --bind /sys "$CHROOT/sys"
chroot "$CHROOT" mkinitfs -k "$KVER" -o "/boot/initrd.img-$KVER" "$KVER" || true
umount "$CHROOT/sys" "$CHROOT/proc" "$CHROOT/dev"
# Populate /boot/efi/ for boot partition assembly
mkdir -p "$CHROOT/boot/efi"
cp "$CHROOT/boot/vmlinuz-$KVER" "$CHROOT/boot/efi/bzImage"
# mkinitfs may output as initramfs-<flavor> — find whatever was generated
if [ -f "$CHROOT/boot/initrd.img-$KVER" ]; then
cp "$CHROOT/boot/initrd.img-$KVER" "$CHROOT/boot/efi/initrd.img"
else
# mkinitfs default output: /boot/initramfs-vanilla or similar
INITRD=$(ls -1t "$CHROOT"/boot/initramfs-* "$CHROOT"/boot/initrd* 2>/dev/null | head -1)
if [ -n "$INITRD" ]; then
cp "$INITRD" "$CHROOT/boot/efi/initrd.img"
else
echo "WARNING: No initrd found for alpine after mkinitfs"
fi
fi
echo ">> Alpine: kernel $KVER staged to boot/efi/"
else
echo "WARNING: No kernel modules found in .deb for alpine, skipping initrd generation"
fi
fi
done
# ======================================================================
@@ -259,9 +186,6 @@ for DISTRO in $DISTROS; do
cp "$EFIDIR/initrd.img" "/tmp/mnt_boot/initrd-${DISTRO}.img"
elif [ -n "$KVER" ] && [ -f "$BOOTDIR/initrd.img-$KVER" ]; then
cp "$BOOTDIR/initrd.img-$KVER" "/tmp/mnt_boot/initrd-${DISTRO}.img"
elif [ -f "$BOOTDIR/initramfs-vanilla" ]; then
# Alpine mkinitfs names its output initramfs-vanilla
cp "$BOOTDIR/initramfs-vanilla" "/tmp/mnt_boot/initrd-${DISTRO}.img"
fi
# Clean up /boot/efi contents from the rootfs (they're on the boot partition now)

View File

@@ -34,10 +34,6 @@ EOF
cp /repo/distros/${DISTRO}/grow-rootfs.service "$STAGING/"
cp /kernel-debs/*.deb "$STAGING/debs/"
;;
alpine)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/alpine/grow-rootfs.openrc "$STAGING/"
;;
arch)
cp /repo/distros/${DISTRO}/grow-rootfs "$STAGING/"
cp /repo/distros/arch/grow-rootfs.service "$STAGING/"
@@ -77,65 +73,6 @@ case "$DISTRO" in
;;
esac
# --- Alpine kernel gap: no kernel installed via image.yaml ---
# Extract kernel from .deb, copy modules + bzImage, then chroot to run mkinitfs
if [ "$DISTRO" = "alpine" ]; then
echo "=== Alpine: installing kernel from .deb artifacts ==="
ALPINE_STAGING="/tmp/alpine-kernel-staging"
rm -rf "$ALPINE_STAGING"
mkdir -p "$ALPINE_STAGING"
for deb in /kernel-debs/linux-image-*.deb; do
[ -f "$deb" ] || continue
dpkg-deb -x "$deb" "$ALPINE_STAGING"
done
KVER=$(ls -1 "$ALPINE_STAGING/lib/modules" 2>/dev/null | head -1)
if [ -n "$KVER" ]; then
# Resolve the real modules path (Alpine may use usr-merge: /lib -> usr/lib)
if [ -L "$CHROOT/lib" ]; then
MODDIR="$CHROOT/usr/lib/modules"
else
MODDIR="$CHROOT/lib/modules"
fi
mkdir -p "$MODDIR"
rm -rf "$MODDIR/$KVER"
cp -a "$ALPINE_STAGING/lib/modules/$KVER" "$MODDIR/"
mkdir -p "$CHROOT/boot"
cp "$ALPINE_STAGING/boot/vmlinuz-$KVER" "$CHROOT/boot/vmlinuz-$KVER"
echo ">> Alpine: modules copied to $MODDIR/$KVER"
fi
rm -rf "$ALPINE_STAGING"
if [ -n "$KVER" ]; then
chroot "$CHROOT" depmod -a "$KVER" 2>/dev/null || true
mount --bind /dev "$CHROOT/dev"
mount --bind /proc "$CHROOT/proc"
mount --bind /sys "$CHROOT/sys"
chroot "$CHROOT" mkinitfs -k "$KVER" -o "/boot/initrd.img-$KVER" "$KVER" || true
umount "$CHROOT/sys" "$CHROOT/proc" "$CHROOT/dev"
# Populate /boot/efi/ for boot partition assembly
mkdir -p "$CHROOT/boot/efi"
cp "$CHROOT/boot/vmlinuz-$KVER" "$CHROOT/boot/efi/bzImage"
if [ -f "$CHROOT/boot/initrd.img-$KVER" ]; then
cp "$CHROOT/boot/initrd.img-$KVER" "$CHROOT/boot/efi/initrd.img"
else
INITRD=$(ls -1t "$CHROOT"/boot/initramfs-* "$CHROOT"/boot/initrd* 2>/dev/null | head -1)
if [ -n "$INITRD" ]; then
cp "$INITRD" "$CHROOT/boot/efi/initrd.img"
else
echo "WARNING: No initrd found for alpine after mkinitfs"
fi
fi
echo ">> Alpine: kernel $KVER staged to boot/efi/"
else
echo "WARNING: No kernel modules found in .deb for alpine"
fi
fi
# --- Create GPT disk image ---
echo "=== Creating ${IMG_SIZE}MB disk image ==="
TMPIMG="/build/ps5-${DISTRO}.img"