add PS5 kernel build pipeline with multi-distro support and arm64 cross-compile

This commit is contained in:
Dan Kluser
2026-05-08 22:15:43 +02:00
parent 1bf99c8369
commit ad45441d40
7 changed files with 178 additions and 86 deletions

18
.github/workflows/dispatch.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
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: resulknad/ps5-vps-setup
event-type: image-builder-updated
client-payload: '{"sha": "${{ github.sha }}"}'

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
cache/
ccache/
work/
output/
linux-bin/
build.log
*.swp
patches.diff

View File

@@ -47,6 +47,8 @@ sudo dd if=output/ps5-ubuntu2604.img of=/dev/sdX bs=4M status=progress
| `--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 |
| `--kernel-only` | Build and package the kernel only, then exit | off |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.0` |
## Caching
@@ -105,10 +107,27 @@ All verbose output goes to `build.log`. The terminal shows a spinner with live p
The boot partition contains kexec scripts to switch between distros at runtime. Ubuntu 26.04 is the default boot target.
## Building the Kernel Standalone
Use `--kernel-only` to compile the PS5 kernel and produce installable packages without building a full disk image.
```bash
./build_image.sh --kernel-only # .deb (default)
./build_image.sh --kernel-only --distro all # .deb + .pkg.tar.zst
./build_image.sh --kernel-only --patches-ref main # fetch from specific branch/tag
./build_image.sh --kernel-only --clean # wipe and rebuild from scratch
```
Output packages are written to `linux-bin/`. Install on a running PS5 Linux system:
```bash
sudo dpkg -i linux-bin/linux-ps5_*.deb
```
## Directory Layout
```
build_image.sh # Main build script
build_image.sh # Image builder (also supports --kernel-only)
docker/
kernel-builder/ # Kernel compilation container
kernel-builder-arch/ # Repackages .deb kernel as .pkg.tar.zst

View File

@@ -8,6 +8,8 @@ DISTRO="ubuntu2604"
KERNEL_SRC=""
CLEAN=false
IMG_SIZE=12000
KERNEL_ONLY=false
PATCHES_REF="v1.0"
MULTI_DISTROS="ubuntu2604 arch alpine cachyos"
@@ -19,15 +21,19 @@ usage() {
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 " --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
}
while [[ $# -gt 0 ]]; do
case $1 in
--distro) DISTRO="$2"; shift 2 ;;
--kernel) KERNEL_SRC="$2"; shift 2 ;;
--img-size) IMG_SIZE="$2"; shift 2 ;;
--clean) CLEAN=true; shift ;;
--distro) DISTRO="$2"; shift 2 ;;
--kernel) KERNEL_SRC="$2"; shift 2 ;;
--img-size) IMG_SIZE="$2"; shift 2 ;;
--clean) CLEAN=true; shift ;;
--kernel-only) KERNEL_ONLY=true; shift ;;
--patches-ref) [ -n "$2" ] && PATCHES_REF="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "Unknown option: $1"; usage ;;
esac
@@ -36,10 +42,8 @@ done
LINUX_REPO="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git"
LINUX_DEFAULT_DIR="$SCRIPT_DIR/work/linux"
PATCHES_REPO="https://github.com/ps5-linux/ps5-linux-patches.git"
PATCHES_BRANCH="v1.0"
PATCHES_REPO="https://github.com/resulknad/ps5-linux-patches.git"
PATCHES_DIR="$SCRIPT_DIR/work/ps5-linux-patches"
PATCHES_CONFIG=".config"
if [ -z "$KERNEL_SRC" ]; then
KERNEL_SRC="$LINUX_DEFAULT_DIR"
@@ -49,14 +53,25 @@ KERNEL_OUT="$SCRIPT_DIR/linux-bin"
OUTPUT_DIR="$SCRIPT_DIR/output"
CHROOT_DIR="$SCRIPT_DIR/work/chroot"
CACHE_DIR="$SCRIPT_DIR/work/cache"
CCACHE_DIR="$SCRIPT_DIR/cache/ccache"
CCACHE_DIR="${CCACHE_DIR:-$SCRIPT_DIR/ccache}"
LOG_FILE="$SCRIPT_DIR/build.log"
DOCKER_NAME="ps5-build-$$"
if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then
IMG_SIZE=32000
fi
if [ -z "$FORMAT" ]; then
case "$DISTRO" in arch) FORMAT="arch" ;; all) FORMAT="all" ;; *) FORMAT="deb" ;; esac
fi
KERNEL_BUILDER_PLATFORM="linux/amd64"
case "$(uname -m)" in
aarch64|arm64) KERNEL_BUILDER_PLATFORM="linux/arm64" ;;
esac
# --- Signal trap: clean up docker containers and background jobs on exit ---
BUILD_PID=""
@@ -73,14 +88,11 @@ trap cleanup INT TERM
# --- Clean ---
if [ "$CLEAN" = true ]; then
echo "Cleaning all build artifacts..."
# Build artifacts contain root-owned files from Docker — use a container to remove them
for dir in "$SCRIPT_DIR/work" "$KERNEL_OUT" "$SCRIPT_DIR/cache"; do
if [ -d "$dir" ]; then
docker run --rm --privileged -v "$dir":/clean alpine sh -c 'rm -rf /clean/*'
rmdir "$dir" 2>/dev/null || true
fi
for dir in "$SCRIPT_DIR/work" "$KERNEL_OUT" "$SCRIPT_DIR/cache" "$OUTPUT_DIR"; do
[ -d "$dir" ] && docker run --rm \
-v "$(dirname "$dir")":/parent \
alpine rm -rf "/parent/$(basename "$dir")"
done
rm -rf "$OUTPUT_DIR"
echo "Done."
echo ""
fi
@@ -89,17 +101,13 @@ fi
SKIP_KERNEL=false
SKIP_CHROOT=false
# Kernel packages already built?
if [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true
elif [ "$DISTRO" = "all" ]; then
ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true
else
ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && SKIP_KERNEL=true
fi
case "$FORMAT" in
arch) ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
all) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && \
ls "$KERNEL_OUT"/*.pkg.tar.zst 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
*) ls "$KERNEL_OUT"/*.deb 1>/dev/null 2>&1 && SKIP_KERNEL=true ;;
esac
# Chroot already populated?
if [ "$DISTRO" = "all" ]; then
SKIP_CHROOT=true
for d in $MULTI_DISTROS; do
@@ -109,21 +117,18 @@ else
[ -d "$CHROOT_DIR/bin" ] && SKIP_CHROOT=true
fi
if [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
PKG_EXT="pkg.tar.zst"
else
PKG_EXT="deb"
fi
# --- Build plan summary ---
echo ""
echo "PS5 Linux Image Builder"
echo "======================="
echo " Distro: $DISTRO"
if [ "$DISTRO" = "all" ]; then
echo " ($MULTI_DISTROS)"
if [ "$KERNEL_ONLY" = true ]; then
echo " Mode: kernel only"
echo " Format: $FORMAT"
else
echo " Distro: $DISTRO"
[ "$DISTRO" = "all" ] && echo " ($MULTI_DISTROS)"
echo " Image size: ${IMG_SIZE}MB"
fi
echo " Image size: ${IMG_SIZE}MB"
if [ -f "$PATCHES_DIR/.config" ]; then
LINUX_BRANCH="v$(grep -m1 "^# Linux/" "$PATCHES_DIR/.config" | grep -oP '\d+\.\d+(\.\d+)?')"
echo " Kernel: $LINUX_BRANCH"
@@ -135,19 +140,19 @@ echo ""
echo "Stages:"
if [ "$SKIP_KERNEL" = true ]; then
echo " 1. Kernel cached"
elif [ -d "$KERNEL_SRC/.git" ]; then
echo " 1. Kernel build (source cached)"
else
if [ -d "$KERNEL_SRC/.git" ]; then
echo " 1. Kernel build (source cached)"
echo " 1. Kernel clone + build"
fi
if [ "$KERNEL_ONLY" = false ]; then
if [ "$SKIP_CHROOT" = true ]; then
echo " 2. Root filesystem cached"
else
echo " 1. Kernel clone + build"
echo " 2. Root filesystem build"
fi
echo " 3. Disk image build"
fi
if [ "$SKIP_CHROOT" = true ]; then
echo " 2. Root filesystem cached"
else
echo " 2. Root filesystem build"
fi
echo " 3. Disk image build"
echo ""
echo "Logs: $LOG_FILE"
echo ""
@@ -160,6 +165,19 @@ SPIN_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
run_stage() {
local name="$1"
shift
if [ "${CI:-}" = "true" ]; then
echo "::group::$name"
local rc=0
"$@" || rc=$?
echo "::endgroup::"
if [ $rc -ne 0 ]; then
echo "::error::Build failed at: $name"
exit $rc
fi
return
fi
local status_msg="$name"
local spin_i=0
@@ -213,27 +231,31 @@ if [ "$SKIP_KERNEL" = true ]; then
printf " ✓ %-60s\n" "Kernel packages (cached)"
else
if [ ! -d "$KERNEL_SRC/.git" ]; then
LINUX_TMP_DIR="${LINUX_DEFAULT_DIR}.tmp"
rm -rf "$LINUX_TMP_DIR"
REPO_URL="$PATCHES_REPO"
[ -n "$PATCHES_TOKEN" ] && REPO_URL="${PATCHES_REPO/https:\/\//https:\/\/${PATCHES_TOKEN}@}"
mkdir -p "$PATCHES_DIR"
if [ ! -d "$PATCHES_DIR/.git" ]; then
run_stage "Clone ps5-linux-patches" \
git clone --branch "$PATCHES_BRANCH" --depth 1 "$PATCHES_REPO" "$PATCHES_DIR"
else
run_stage "Update ps5-linux-patches" bash -c '
git -C "'"$PATCHES_DIR"'" fetch --depth 1 origin tag "'"$PATCHES_BRANCH"'"
git -C "'"$PATCHES_DIR"'" checkout "'"$PATCHES_BRANCH"'"
git -C "'"$PATCHES_DIR"'" reset --hard "'"$PATCHES_BRANCH"'"'
fi
run_stage "Fetch ps5-linux-patches ($PATCHES_REF)" bash -c '
cd "'"$PATCHES_DIR"'"
[ ! -d .git ] && git init && git remote add origin "'"$REPO_URL"'"
git fetch --depth 1 origin "'"$PATCHES_REF"'" || git fetch origin "'"$PATCHES_REF"'"
git reset --hard FETCH_HEAD
'
LINUX_TMP_DIR="${LINUX_DEFAULT_DIR}.tmp"
for dir in "$LINUX_TMP_DIR" "$LINUX_DEFAULT_DIR"; do
[ -d "$dir" ] && docker run --rm \
-v "$(dirname "$dir")":/parent \
alpine rm -rf "/parent/$(basename "$dir")"
done
LINUX_BRANCH="v$(grep -m1 "^# Linux/" "$PATCHES_DIR/.config" | grep -oP '\d+\.\d+(\.\d+)?')"
run_stage "Clone kernel $LINUX_BRANCH" \
git clone --branch "$LINUX_BRANCH" --depth 1 "$LINUX_REPO" "$LINUX_TMP_DIR"
run_stage "Apply patches" bash -c '
set -e
shopt -s nullglob
set -e; shopt -s nullglob
patches=("'"$PATCHES_DIR"'"/*.patch)
[ ${#patches[@]} -eq 0 ] && { echo "No .patch files found in '"$PATCHES_DIR"'"; exit 1; }
for p in "${patches[@]}"; do
@@ -242,63 +264,57 @@ else
done'
run_stage "Copy kernel config" \
cp "$PATCHES_DIR/$PATCHES_CONFIG" "$LINUX_TMP_DIR/.config"
cp "$PATCHES_DIR/.config" "$LINUX_TMP_DIR/.config"
mv "$LINUX_TMP_DIR" "$LINUX_DEFAULT_DIR"
KERNEL_SRC="$LINUX_DEFAULT_DIR"
else
printf " ✓ %-60s\n" "Kernel source (cached)"
fi
KERNEL_SRC="$(cd "$KERNEL_SRC" && pwd)"
rm -f "$KERNEL_OUT"/*.$PKG_EXT
rm -f "$KERNEL_OUT"/*.deb "$KERNEL_OUT"/*.pkg.tar.zst
run_stage "Build kernel builder image" \
docker build -t ps5-kernel-builder -f "$SCRIPT_DIR/docker/kernel-builder/Dockerfile" "$SCRIPT_DIR"
docker build --platform "$KERNEL_BUILDER_PLATFORM" -t ps5-kernel-builder \
-f "$SCRIPT_DIR/docker/kernel-builder/Dockerfile" "$SCRIPT_DIR"
run_stage "Compile kernel" \
docker run --rm --name "$DOCKER_NAME" \
docker run --rm --platform "$KERNEL_BUILDER_PLATFORM" --name "$DOCKER_NAME" \
-v "$KERNEL_SRC":/src \
-v "$KERNEL_OUT":/out \
-v "$CCACHE_DIR":/ccache \
ps5-kernel-builder
if [ "$DISTRO" = "all" ]; then
ls "$KERNEL_OUT/staging/lib/modules/" | head -1 > "$KERNEL_OUT/VERSION"
case "$FORMAT" in deb|all)
run_stage "Package kernel (.deb)" \
docker run --rm --name "$DOCKER_NAME" \
docker run --rm --platform "$KERNEL_BUILDER_PLATFORM" --name "$DOCKER_NAME" \
-v "$KERNEL_SRC":/src \
-v "$KERNEL_OUT":/out \
-v "$CCACHE_DIR":/ccache \
ps5-kernel-builder \
bash -c 'make -j$(nproc) bindeb-pkg && cp /*.deb /out/'
/package-deb.sh
esac
case "$FORMAT" in arch|all)
run_stage "Build arch packager image" \
docker build -t ps5-kernel-packager-arch \
-f "$SCRIPT_DIR/docker/kernel-builder-arch/Dockerfile" "$SCRIPT_DIR"
run_stage "Package kernel (.pkg.tar.zst)" \
docker run --rm --name "$DOCKER_NAME" \
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-arch
esac
fi
elif [ "$DISTRO" = "arch" ] || [ "$DISTRO" = "cachyos" ]; then
run_stage "Build arch packager image" \
docker build -t ps5-kernel-packager-arch \
-f "$SCRIPT_DIR/docker/kernel-builder-arch/Dockerfile" "$SCRIPT_DIR"
run_stage "Package kernel (.pkg.tar.zst)" \
docker run --rm --name "$DOCKER_NAME" \
-v "$KERNEL_OUT":/out \
ps5-kernel-packager-arch
else
run_stage "Package kernel (.deb)" \
docker run --rm --name "$DOCKER_NAME" \
-v "$KERNEL_SRC":/src \
-v "$KERNEL_OUT":/out \
-v "$CCACHE_DIR":/ccache \
ps5-kernel-builder \
bash -c 'make -j$(nproc) bindeb-pkg && cp /*.deb /out/'
fi
if [ "$KERNEL_ONLY" = true ]; then
KVER=$(cat "$KERNEL_OUT/VERSION" 2>/dev/null || echo "unknown")
echo ""
echo "Done! Kernel $KVER packages in $KERNEL_OUT/"
exit 0
fi
# --- Step 2: Build distribution image ---

View File

@@ -1,4 +1,7 @@
FROM archlinux:latest
FROM --platform=linux/amd64 archlinux:latest
# pacman 7+ sandbox doesn't work in Docker containers
RUN echo 'DisableSandbox' >> /etc/pacman.conf
# Only packaging tools needed — kernel is already compiled
RUN pacman -Syu --noconfirm && pacman -S --noconfirm \

View File

@@ -7,8 +7,11 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libdw-dev libelf-dev libssl-dev \
python3 rsync dpkg-dev debhelper zstd \
perl git xz-utils ccache \
gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu libc6-dev-amd64-cross \
&& rm -rf /var/lib/apt/lists/*
ENV ARCH=x86_64
ENV CROSS_COMPILE=x86_64-linux-gnu-
ENV PATH="/usr/lib/ccache:${PATH}"
ENV CCACHE_DIR=/ccache
@@ -18,6 +21,7 @@ WORKDIR /src
# Build artifacts are staged to /out/staging
COPY docker/kernel-builder/build.sh /build.sh
RUN chmod +x /build.sh
COPY docker/kernel-builder/package-deb.sh /package-deb.sh
RUN chmod +x /build.sh /package-deb.sh
CMD ["/build.sh"]

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Builds a combined linux-ps5 .deb from the kernel source at /src.
# Runs inside the kernel-builder container; output goes to /out.
set -e
make -j$(nproc) DPKG_FLAGS=-d bindeb-pkg
VER=$(dpkg-deb -f "$(ls /linux-image-[0-9]*.deb | grep -v dbg | head -1)" Version)
ARCH=$(dpkg-deb -f "$(ls /linux-image-[0-9]*.deb | grep -v dbg | head -1)" Architecture)
KVER=$(ls /src/debian/linux-image-[0-9]*/lib/modules/ 2>/dev/null | head -1)
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/DEBIAN"
for deb in $(ls /linux-image-[0-9]*.deb /linux-headers-*.deb /linux-libc-dev_*.deb 2>/dev/null | grep -v -- '-dbg_'); do
dpkg-deb -x "$deb" "$TMPDIR"
done
cat > "$TMPDIR/DEBIAN/control" << CTRL
Package: linux-ps5
Version: $VER
Architecture: $ARCH
Maintainer: PS5 Linux
Provides: linux-image, linux-image-$KVER, linux-headers-$KVER, linux-libc-dev
Conflicts: linux-image-$KVER, linux-headers-$KVER, linux-libc-dev
Replaces: linux-image-$KVER, linux-headers-$KVER, linux-libc-dev
Description: PS5 Linux kernel $KVER (image + headers + libc-dev)
CTRL
dpkg-deb --build --root-owner-group "$TMPDIR" "/out/linux-ps5_${VER}_${ARCH}.deb"