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/ cache/
ccache/
work/ work/
output/ output/
linux-bin/ linux-bin/
build.log 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` | | `--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 |
| `--kernel-only` | Build and package the kernel only, then exit | off |
| `--patches-ref` | Branch, tag, or commit SHA for patches | `v1.0` |
## Caching ## 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. 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 ## Directory Layout
``` ```
build_image.sh # Main build script build_image.sh # Image builder (also supports --kernel-only)
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

View File

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