#!/bin/bash # build-kernel-arm64.sh — Build generic ARM64 kernel (mainline LTS) # # Builds a Linux kernel from kernel.org mainline LTS source, suitable for: # - qemu-system-aarch64 -machine virt # - UEFI ARM64 hosts (Ampere, Graviton, generic ARM64 servers) # - Future ARM64 SBCs with UEFI/u-boot generic-distro support # # This is the GENERIC ARM64 build track. For Raspberry Pi specifically # (raspberrypi/linux fork, RPi firmware boot path, custom DTBs), see # build/scripts/build-kernel-rpi.sh. # # Output is cached in $CACHE_DIR/kernel-arm64-generic/ and reused across builds. # # Requirements: # - gcc-aarch64-linux-gnu (cross-compiler) # - Standard kernel build deps (bc, bison, flex, libelf-dev, libssl-dev) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" CACHE_DIR="${CACHE_DIR:-$PROJECT_ROOT/build/cache}" # shellcheck source=../config/versions.env . "$SCRIPT_DIR/../config/versions.env" KVER="$MAINLINE_KERNEL_VERSION" CUSTOM_KERNEL_DIR="$CACHE_DIR/kernel-arm64-generic" CUSTOM_IMAGE="$CUSTOM_KERNEL_DIR/Image" CUSTOM_MODULES="$CUSTOM_KERNEL_DIR/modules" mkdir -p "$CACHE_DIR" "$CUSTOM_KERNEL_DIR" # --- Skip if already built --- if [ -f "$CUSTOM_IMAGE" ] && [ -d "$CUSTOM_MODULES/lib/modules/$KVER" ]; then echo "==> Generic ARM64 kernel already built (cached)" echo " Image: $CUSTOM_IMAGE ($(du -h "$CUSTOM_IMAGE" | cut -f1))" echo " Kernel: $KVER" exit 0 fi # --- Toolchain selection: native on arm64 hosts, cross-compile elsewhere --- HOST_ARCH="$(uname -m)" if [ "$HOST_ARCH" = "aarch64" ] || [ "$HOST_ARCH" = "arm64" ]; then # Native build — use the host's gcc if ! command -v gcc >/dev/null 2>&1; then echo "ERROR: gcc not found" echo "Install: apt-get install build-essential" exit 1 fi CROSS_COMPILE="" echo "==> Native ARM64 build (host arch: $HOST_ARCH)" else # Cross-build from x86 — use aarch64 cross-compiler if ! command -v aarch64-linux-gnu-gcc >/dev/null 2>&1; then echo "ERROR: aarch64-linux-gnu-gcc not found" echo "Install: apt-get install gcc-aarch64-linux-gnu" exit 1 fi CROSS_COMPILE="aarch64-linux-gnu-" echo "==> Cross-building ARM64 kernel from $HOST_ARCH" fi echo "==> Building generic ARM64 kernel (mainline $KVER)..." echo " Source: $MAINLINE_KERNEL_URL" # --- Download mainline kernel source --- KERNEL_SRC_ARCHIVE="$CACHE_DIR/linux-${KVER}.tar.xz" if [ ! -f "$KERNEL_SRC_ARCHIVE" ]; then echo "==> Downloading mainline kernel source (~140 MB)..." wget -q --show-progress -O "$KERNEL_SRC_ARCHIVE" "$MAINLINE_KERNEL_URL" 2>/dev/null || \ curl -fSL "$MAINLINE_KERNEL_URL" -o "$KERNEL_SRC_ARCHIVE" echo " Downloaded: $(du -h "$KERNEL_SRC_ARCHIVE" | cut -f1)" else echo "==> Kernel source already cached: $(du -h "$KERNEL_SRC_ARCHIVE" | cut -f1)" fi # --- Verify checksum if pinned --- if [ -n "${MAINLINE_KERNEL_SHA256:-}" ]; then actual=$(sha256sum "$KERNEL_SRC_ARCHIVE" | awk '{print $1}') if [ "$actual" != "$MAINLINE_KERNEL_SHA256" ]; then echo "ERROR: Kernel source checksum mismatch" echo " Expected: $MAINLINE_KERNEL_SHA256" echo " Got: $actual" exit 1 fi echo " Checksum OK" fi # --- Extract to case-sensitive fs --- # The kernel source has files differing only by case (xt_mark.h vs xt_MARK.h). # Build in /tmp (ext4 on Linux runners, case-sensitive). KERNEL_BUILD_DIR="/tmp/kernel-build-arm64-generic" rm -rf "$KERNEL_BUILD_DIR" mkdir -p "$KERNEL_BUILD_DIR" echo "==> Extracting kernel source..." tar -xf "$KERNEL_SRC_ARCHIVE" -C "$KERNEL_BUILD_DIR" KERNEL_SRC_DIR=$(find "$KERNEL_BUILD_DIR" -maxdepth 1 -type d -name 'linux-*' | head -1) if [ -z "$KERNEL_SRC_DIR" ]; then echo "ERROR: Could not find extracted source directory" ls -la "$KERNEL_BUILD_DIR"/ exit 1 fi cd "$KERNEL_SRC_DIR" # --- Base config: arm64 defconfig (generic ARMv8) --- echo "==> Applying arm64 defconfig..." make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" defconfig # --- Apply shared container fragment --- CONFIG_FRAGMENT="$PROJECT_ROOT/build/config/kernel-container.fragment" if [ ! -f "$CONFIG_FRAGMENT" ]; then echo "ERROR: Config fragment not found: $CONFIG_FRAGMENT" exit 1 fi apply_fragment() { local fragment="$1" while IFS= read -r line; do case "$line" in "# CONFIG_"*" is not set") key=$(echo "$line" | sed -n 's/^# \(CONFIG_[A-Z0-9_]*\) is not set$/\1/p') [ -n "$key" ] && ./scripts/config --disable "${key#CONFIG_}" continue ;; \#*|"") continue ;; esac key="${line%%=*}" value="${line#*=}" case "$value" in y) ./scripts/config --enable "$key" ;; m) ./scripts/config --module "$key" ;; n) ./scripts/config --disable "${key#CONFIG_}" ;; *) ./scripts/config --set-str "$key" "$value" ;; esac done < "$fragment" } echo "==> Applying kernel-container.fragment (pass 1)..." apply_fragment "$CONFIG_FRAGMENT" make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" olddefconfig echo "==> Applying kernel-container.fragment (pass 2)..." apply_fragment "$CONFIG_FRAGMENT" make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" olddefconfig # --- ARM64 virt-host specific enables --- # These are needed for the generic UEFI/virtio boot path but are arch-specific # so they live in this script rather than the shared fragment. echo "==> Enabling ARM64 virt-host configs..." ./scripts/config --enable CONFIG_EFI ./scripts/config --enable CONFIG_EFI_STUB ./scripts/config --enable CONFIG_VIRTIO ./scripts/config --enable CONFIG_VIRTIO_PCI ./scripts/config --enable CONFIG_VIRTIO_BLK ./scripts/config --enable CONFIG_VIRTIO_NET ./scripts/config --enable CONFIG_VIRTIO_CONSOLE ./scripts/config --enable CONFIG_VIRTIO_MMIO ./scripts/config --enable CONFIG_HW_RANDOM_VIRTIO # NVMe for cloud / bare-metal ARM64 hosts that don't use virtio ./scripts/config --enable CONFIG_BLK_DEV_NVME make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" olddefconfig # --- Verify critical configs --- echo "==> Verifying critical configs..." for cfg in CGROUP_BPF SECURITY_APPARMOR AUDIT VIRTIO_BLK EFI_STUB; do if ! grep -q "CONFIG_${cfg}=y" .config; then echo "ERROR: CONFIG_${cfg} not set after olddefconfig" grep "CONFIG_${cfg}" .config || echo " (not found)" exit 1 fi echo " CONFIG_${cfg}=y confirmed" done # --- Build kernel + modules (no DTBs — UEFI hosts use ACPI/virtio) --- NPROC=$(nproc 2>/dev/null || echo 4) echo "" echo "==> Building ARM64 kernel (${NPROC} parallel jobs)..." echo " This may take 20-40 minutes on a 6-core Odroid..." make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" -j"$NPROC" Image modules 2>&1 echo "==> Kernel build complete" # --- Install to staging --- echo "==> Installing Image..." cp arch/arm64/boot/Image "$CUSTOM_IMAGE" echo "==> Installing modules (stripped)..." rm -rf "$CUSTOM_MODULES" mkdir -p "$CUSTOM_MODULES" make ARCH=arm64 CROSS_COMPILE="$CROSS_COMPILE" \ INSTALL_MOD_STRIP=1 modules_install INSTALL_MOD_PATH="$CUSTOM_MODULES" # Pick up actual kernel version (e.g. 6.12.10 if KVER differs from package suffix) ACTUAL_KVER=$(ls "$CUSTOM_MODULES/lib/modules/" | head -1) rm -f "$CUSTOM_MODULES/lib/modules/$ACTUAL_KVER/build" rm -f "$CUSTOM_MODULES/lib/modules/$ACTUAL_KVER/source" depmod -a -b "$CUSTOM_MODULES" "$ACTUAL_KVER" 2>/dev/null || true cp .config "$CUSTOM_KERNEL_DIR/.config" # --- Clean up --- echo "==> Cleaning kernel build directory..." cd / rm -rf "$KERNEL_BUILD_DIR" # --- Summary --- echo "" echo "==> Generic ARM64 kernel build complete:" echo " Image: $CUSTOM_IMAGE ($(du -h "$CUSTOM_IMAGE" | cut -f1))" echo " Kernel ver: $ACTUAL_KVER" MOD_COUNT=$(find "$CUSTOM_MODULES/lib/modules/$ACTUAL_KVER" -name '*.ko*' 2>/dev/null | wc -l) echo " Modules: $MOD_COUNT" echo " Modules size: $(du -sh "$CUSTOM_MODULES/lib/modules/$ACTUAL_KVER" 2>/dev/null | cut -f1)" echo ""