#!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" DISTRO="ubuntu2604" KERNEL_SRC="" CLEAN=false IMG_SIZE=12000 MULTI_DISTROS="ubuntu2604 ubuntu2404 arch alpine" usage() { echo "Usage: $0 [--distro ] [--kernel ] [--img-size ] [--clean]" echo "" echo "Options:" echo " --distro Distribution to build: ubuntu2604, ubuntu2404, arch, alpine, 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" 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 ;; -h|--help) usage ;; *) echo "Unknown option: $1"; usage ;; esac 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_DIR="$SCRIPT_DIR/work/ps5-linux-patches" PATCHES_CONFIG=".config" if [ -z "$KERNEL_SRC" ]; then KERNEL_SRC="$LINUX_DEFAULT_DIR" fi 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" LOG_FILE="$SCRIPT_DIR/build.log" DOCKER_NAME="ps5-build-$$" if [ "$DISTRO" = "all" ] && [ "$IMG_SIZE" = "12000" ]; then IMG_SIZE=32000 fi # --- Signal trap: clean up docker containers and background jobs on exit --- BUILD_PID="" cleanup() { echo "" echo "Interrupted. 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 # --- 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 done rm -rf "$OUTPUT_DIR" echo "Done." echo "" fi # --- Auto-detect what can be skipped --- SKIP_KERNEL=false SKIP_CHROOT=false # Kernel packages already built? if [ "$DISTRO" = "arch" ]; 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 # Chroot already populated? if [ "$DISTRO" = "all" ]; then SKIP_CHROOT=true for d in $MULTI_DISTROS; do [ -d "$SCRIPT_DIR/work/chroot-$d/bin" ] || SKIP_CHROOT=false done else [ -d "$CHROOT_DIR/bin" ] && SKIP_CHROOT=true fi if [ "$DISTRO" = "arch" ]; 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)" 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" else echo " Kernel: (will fetch)" fi echo " Kernel src: $KERNEL_SRC" echo "" echo "Stages:" if [ "$SKIP_KERNEL" = true ]; then echo " 1. Kernel cached" else if [ -d "$KERNEL_SRC/.git" ]; then echo " 1. Kernel build (source cached)" else echo " 1. Kernel clone + 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 "Logs: $LOG_FILE" echo "" # --- Logging + stage runner --- : > "$LOG_FILE" SPIN_CHARS='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' run_stage() { local name="$1" shift local status_msg="$name" local spin_i=0 # Record log position so we only scan new lines for status updates local log_start log_start=$(wc -l < "$LOG_FILE") # Run command in background "$@" >> "$LOG_FILE" 2>&1 & BUILD_PID=$! # Spinner loop — pick up === status lines from the log while kill -0 "$BUILD_PID" 2>/dev/null; do if (( spin_i % 10 == 0 )); then local new new=$(tail -n +$((log_start + 1)) "$LOG_FILE" 2>/dev/null \ | grep -oP '(?<=^=== ).*(?= ===$)' | tail -1) [ -n "$new" ] && status_msg="$new" fi printf "\r %s %-60s" "${SPIN_CHARS:spin_i%${#SPIN_CHARS}:1}" "$status_msg" spin_i=$((spin_i + 1)) sleep 0.1 done local rc=0 wait "$BUILD_PID" || rc=$? BUILD_PID="" if [ $rc -eq 0 ]; then printf "\r ✓ %-60s\n" "$name" else printf "\r ✗ %-60s\n" "$status_msg" echo "" echo "Build failed at: $status_msg" echo "Logs: $LOG_FILE" echo "Try running with --clean to start fresh." exit 1 fi } # --- Setup directories --- mkdir -p "$KERNEL_OUT" "$OUTPUT_DIR" "$CHROOT_DIR" "$CACHE_DIR" "$CCACHE_DIR" if [ "$DISTRO" = "all" ]; then for d in $MULTI_DISTROS; do mkdir -p "$SCRIPT_DIR/work/chroot-$d" done fi # --- Step 1: Kernel --- 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" mkdir -p "$PATCHES_DIR" if [ ! -d "$PATCHES_DIR/.git" ]; then run_stage "Clone ps5-linux-patches" \ git clone --depth 1 "$PATCHES_REPO" "$PATCHES_DIR" else run_stage "Update ps5-linux-patches" bash -c ' git -C "'"$PATCHES_DIR"'" fetch --depth 1 origin git -C "'"$PATCHES_DIR"'" reset --hard FETCH_HEAD' fi 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 patches=("'"$PATCHES_DIR"'"/*.patch) [ ${#patches[@]} -eq 0 ] && { echo "No .patch files found in '"$PATCHES_DIR"'"; exit 1; } for p in "${patches[@]}"; do echo "Applying $p" git -C "'"$LINUX_TMP_DIR"'" apply --exclude=Makefile "$p" done' run_stage "Copy kernel config" \ cp "$PATCHES_DIR/$PATCHES_CONFIG" "$LINUX_TMP_DIR/.config" mv "$LINUX_TMP_DIR" "$LINUX_DEFAULT_DIR" else printf " ✓ %-60s\n" "Kernel source (cached)" fi KERNEL_SRC="$(cd "$KERNEL_SRC" && pwd)" rm -f "$KERNEL_OUT"/*.$PKG_EXT run_stage "Build kernel builder image" \ docker build -t ps5-kernel-builder -f "$SCRIPT_DIR/docker/kernel-builder/Dockerfile" "$SCRIPT_DIR" run_stage "Compile kernel" \ docker run --rm --name "$DOCKER_NAME" \ -v "$KERNEL_SRC":/src \ -v "$KERNEL_OUT":/out \ -v "$CCACHE_DIR":/ccache \ ps5-kernel-builder if [ "$DISTRO" = "all" ]; then 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/' 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 elif [ "$DISTRO" = "arch" ]; 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 fi # --- Step 2: Build distribution image --- run_stage "Build image builder image" \ docker build -t ps5-image-builder -f "$SCRIPT_DIR/docker/image-builder/Dockerfile" "$SCRIPT_DIR" if [ "$DISTRO" = "all" ]; then DOCKER_ARGS=( docker run --rm --privileged --name "$DOCKER_NAME" --entrypoint /entrypoint-multi.sh -v "$SCRIPT_DIR":/repo:ro -v "$KERNEL_OUT":/kernel-debs:ro -v "$OUTPUT_DIR":/output -v "$CACHE_DIR":/build/cache -e IMG_SIZE="$IMG_SIZE" -e SKIP_CHROOT="$SKIP_CHROOT" -e "DISTROS=$MULTI_DISTROS" ) for d in $MULTI_DISTROS; do DOCKER_ARGS+=(-v "$SCRIPT_DIR/work/chroot-$d:/build/chroot-$d") done DOCKER_ARGS+=(ps5-image-builder) run_stage "Build multi-distro image (${IMG_SIZE}MB)" "${DOCKER_ARGS[@]}" IMG_PATH="$OUTPUT_DIR/ps5-multi.img" else run_stage "Build $DISTRO image (${IMG_SIZE}MB)" \ docker run --rm --privileged --name "$DOCKER_NAME" \ -v "$SCRIPT_DIR":/repo:ro \ -v "$KERNEL_OUT":/kernel-debs:ro \ -v "$OUTPUT_DIR":/output \ -v "$CHROOT_DIR":/build/chroot \ -v "$CACHE_DIR":/build/cache \ -e DISTRO="$DISTRO" \ -e IMG_SIZE="$IMG_SIZE" \ -e SKIP_CHROOT="$SKIP_CHROOT" \ ps5-image-builder IMG_PATH="$OUTPUT_DIR/ps5-${DISTRO}.img" fi echo "" echo "Done! Image: $IMG_PATH" echo "Flash: sudo dd if=$IMG_PATH of=/dev/sdX bs=4M status=progress"