Files
kubesolo-os/build/scripts/create-rpi-image.sh
Adolfo Delorenzo a2764218fc
Some checks failed
CI / Go Tests (push) Has been cancelled
CI / Build Go Binaries (amd64, linux, linux-amd64) (push) Has been cancelled
CI / Build Go Binaries (arm64, linux, linux-arm64) (push) Has been cancelled
CI / Shellcheck (push) Has been cancelled
Release / Test (push) Has been cancelled
Release / Build Binaries (amd64, linux, linux-amd64) (push) Has been cancelled
Release / Build Binaries (arm64, linux, linux-arm64) (push) Has been cancelled
Release / Build ISO (amd64) (push) Has been cancelled
Release / Create Release (push) Has been cancelled
fix: make RPi partition 1 self-sufficient boot fallback
The autoboot.txt A/B redirect requires newer RPi EEPROM firmware.
On older EEPROMs, autoboot.txt is silently ignored and the firmware
tries to boot from partition 1 directly — failing with a rainbow
screen because partition 1 had no kernel or initramfs.

Changes:
- Increase partition 1 from 32 MB to 384 MB
- Populate partition 1 with full boot files (kernel, initramfs,
  config.txt with kernel= directive, DTBs, overlays)
- Keep autoboot.txt for A/B redirect on supported EEPROMs
- When autoboot.txt works: boots from partition 2 (A/B scheme)
- When autoboot.txt is unsupported: boots from partition 1 (fallback)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:52:21 -06:00

253 lines
8.0 KiB
Bash
Executable File

#!/bin/bash
# create-rpi-image.sh — Create a raw disk image for Raspberry Pi SD card
#
# Partition layout (MBR):
# Part 1: Boot/Control (384 MB, FAT32, label KSOLOCTL) — firmware + kernel + initramfs + autoboot.txt
# Part 2: Boot A (256 MB, FAT32, label KSOLOA) — kernel + DTBs + initramfs
# Part 3: Boot B (256 MB, FAT32, label KSOLOB) — same as Boot A (initially identical)
# Part 4: Data (remaining of 2GB, ext4, label KSOLODATA)
#
# The RPi EEPROM loads start4.elf from partition 1.
# If autoboot.txt is supported (newer EEPROM), firmware redirects to partition 2/3 for A/B boot.
# If autoboot.txt is NOT supported (older EEPROM), partition 1 has full boot files as fallback.
#
# MBR is required — GPT + autoboot.txt is not reliably supported on Pi 4.
#
# Usage: build/scripts/create-rpi-image.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=../config/versions.env
. "$SCRIPT_DIR/../config/versions.env"
ROOTFS_DIR="${ROOTFS_DIR:-$PROJECT_ROOT/build/rootfs-work}"
OUTPUT_DIR="${OUTPUT_DIR:-$PROJECT_ROOT/output}"
CACHE_DIR="${CACHE_DIR:-$PROJECT_ROOT/build/cache}"
VERSION="$(cat "$PROJECT_ROOT/VERSION")"
IMG_OUTPUT="$OUTPUT_DIR/${OS_NAME}-${VERSION}.rpi.img"
IMG_SIZE_MB="${IMG_SIZE_MB:-2048}" # 2 GB default
# ARM64 kernel (Image format, not bzImage)
KERNEL="${CACHE_DIR}/custom-kernel-arm64/Image"
INITRAMFS="${ROOTFS_DIR}/kubesolo-os.gz"
RPI_FIRMWARE_DIR="${CACHE_DIR}/rpi-firmware"
echo "==> Creating ${IMG_SIZE_MB}MB Raspberry Pi disk image..."
# --- Verify required files ---
MISSING=0
for f in "$KERNEL" "$INITRAMFS"; do
if [ ! -f "$f" ]; then
echo "ERROR: Missing $f"
MISSING=1
fi
done
if [ ! -d "$RPI_FIRMWARE_DIR" ]; then
echo "ERROR: Missing RPi firmware directory: $RPI_FIRMWARE_DIR"
echo " Run 'make fetch' to download firmware blobs."
MISSING=1
fi
if [ "$MISSING" = "1" ]; then
echo ""
echo "Required files:"
echo " Kernel: $KERNEL (run 'make kernel-arm64')"
echo " Initramfs: $INITRAMFS (run 'make initramfs')"
echo " Firmware: $RPI_FIRMWARE_DIR/ (run 'make fetch')"
exit 1
fi
mkdir -p "$OUTPUT_DIR"
# --- Create sparse image ---
dd if=/dev/zero of="$IMG_OUTPUT" bs=1M count=0 seek="$IMG_SIZE_MB" 2>/dev/null
# --- Partition table (MBR) ---
# MBR is required for reliable RPi boot with autoboot.txt.
# GPT + autoboot.txt fails on many Pi 4 EEPROM versions.
# Part 1: Boot/Control 384 MB FAT32 (firmware + kernel + initramfs + autoboot.txt)
# Part 2: Boot A 256 MB FAT32 (kernel + initramfs + DTBs)
# Part 3: Boot B 256 MB FAT32 (kernel + initramfs + DTBs)
# Part 4: Data remaining ext4
sfdisk "$IMG_OUTPUT" << EOF
label: dos
# Boot/Control partition: 384 MB, FAT32 (type 0c = W95 FAT32 LBA)
# Contains firmware + autoboot.txt for A/B redirect, PLUS full boot files as fallback
start=2048, size=786432, type=c, bootable
# Boot A partition: 256 MB, FAT32
size=524288, type=c
# Boot B partition: 256 MB, FAT32
size=524288, type=c
# Data partition: remaining, Linux
type=83
EOF
# --- Set up loop device ---
LOOP=$(losetup --show -f "$IMG_OUTPUT")
echo "==> Loop device: $LOOP"
# Use kpartx for reliable partition device nodes (works in Docker/containers)
USE_KPARTX=false
if [ ! -b "${LOOP}p1" ]; then
if command -v kpartx >/dev/null 2>&1; then
kpartx -a "$LOOP"
USE_KPARTX=true
sleep 1
LOOP_NAME=$(basename "$LOOP")
P1="/dev/mapper/${LOOP_NAME}p1"
P2="/dev/mapper/${LOOP_NAME}p2"
P3="/dev/mapper/${LOOP_NAME}p3"
P4="/dev/mapper/${LOOP_NAME}p4"
else
# Retry with -P flag
losetup -d "$LOOP"
LOOP=$(losetup --show -fP "$IMG_OUTPUT")
sleep 1
P1="${LOOP}p1"
P2="${LOOP}p2"
P3="${LOOP}p3"
P4="${LOOP}p4"
fi
else
P1="${LOOP}p1"
P2="${LOOP}p2"
P3="${LOOP}p3"
P4="${LOOP}p4"
fi
MNT_CTL=$(mktemp -d)
MNT_BOOTA=$(mktemp -d)
MNT_BOOTB=$(mktemp -d)
MNT_DATA=$(mktemp -d)
cleanup() {
umount "$MNT_CTL" 2>/dev/null || true
umount "$MNT_BOOTA" 2>/dev/null || true
umount "$MNT_BOOTB" 2>/dev/null || true
umount "$MNT_DATA" 2>/dev/null || true
if [ "$USE_KPARTX" = true ]; then
kpartx -d "$LOOP" 2>/dev/null || true
fi
losetup -d "$LOOP" 2>/dev/null || true
rm -rf "$MNT_CTL" "$MNT_BOOTA" "$MNT_BOOTB" "$MNT_DATA" 2>/dev/null || true
}
trap cleanup EXIT
# --- Format partitions ---
mkfs.vfat -F 32 -n KSOLOCTL "$P1"
mkfs.vfat -F 32 -n KSOLOA "$P2"
mkfs.vfat -F 32 -n KSOLOB "$P3"
mkfs.ext4 -q -L KSOLODATA "$P4"
# --- Mount all partitions ---
mount "$P1" "$MNT_CTL"
mount "$P2" "$MNT_BOOTA"
mount "$P3" "$MNT_BOOTB"
mount "$P4" "$MNT_DATA"
# --- Helper: populate a boot partition ---
populate_boot_partition() {
local MNT="$1"
local LABEL="$2"
echo " Populating $LABEL..."
# config.txt — Raspberry Pi boot configuration
cat > "$MNT/config.txt" << 'CFGTXT'
arm_64bit=1
kernel=kernel8.img
initramfs kubesolo-os.gz followkernel
enable_uart=1
gpu_mem=16
dtoverlay=disable-wifi
dtoverlay=disable-bt
CFGTXT
# cmdline.txt — kernel command line
# Note: must be a single line
echo "console=serial0,115200 console=tty1 kubesolo.data=LABEL=KSOLODATA quiet" > "$MNT/cmdline.txt"
# Copy kernel as kernel8.img (RPi 3/4/5 ARM64 convention)
cp "$KERNEL" "$MNT/kernel8.img"
# Copy initramfs
cp "$INITRAMFS" "$MNT/kubesolo-os.gz"
# Copy DTB overlays
if [ -d "$RPI_FIRMWARE_DIR/overlays" ]; then
cp -r "$RPI_FIRMWARE_DIR/overlays" "$MNT/"
fi
# Copy base DTBs (bcm2710-*, bcm2711-*, bcm2712-*)
if ls "$RPI_FIRMWARE_DIR"/bcm27*.dtb 1>/dev/null 2>&1; then
cp "$RPI_FIRMWARE_DIR"/bcm27*.dtb "$MNT/"
fi
# Write version marker
echo "$VERSION" > "$MNT/version.txt"
}
# --- Boot Control Partition (KSOLOCTL) ---
# Partition 1 serves dual purpose:
# 1. Contains firmware + autoboot.txt for A/B redirect (if EEPROM supports it)
# 2. Contains full boot files (kernel + initramfs) as fallback if autoboot.txt isn't supported
echo " Writing firmware + autoboot.txt + boot files to partition 1..."
# autoboot.txt — tells firmware which partition to boot from (A/B switching)
# If the EEPROM doesn't support this, it's silently ignored and the firmware
# falls back to booting from partition 1 using config.txt below.
cat > "$MNT_CTL/autoboot.txt" << 'AUTOBOOT'
[all]
tryboot_a_b=1
boot_partition=2
[tryboot]
boot_partition=3
AUTOBOOT
# Copy firmware blobs — REQUIRED on partition 1 for EEPROM to boot
if ls "$RPI_FIRMWARE_DIR"/start*.elf 1>/dev/null 2>&1; then
cp "$RPI_FIRMWARE_DIR"/start*.elf "$MNT_CTL/"
fi
if ls "$RPI_FIRMWARE_DIR"/fixup*.dat 1>/dev/null 2>&1; then
cp "$RPI_FIRMWARE_DIR"/fixup*.dat "$MNT_CTL/"
fi
if [ -f "$RPI_FIRMWARE_DIR/bootcode.bin" ]; then
cp "$RPI_FIRMWARE_DIR/bootcode.bin" "$MNT_CTL/"
fi
# Full boot files on partition 1 — fallback if autoboot.txt redirect doesn't work.
# When autoboot.txt works, firmware switches to partition 2 and reads config.txt there.
# When autoboot.txt is unsupported, firmware reads THIS config.txt and boots from here.
populate_boot_partition "$MNT_CTL" "Boot Control (KSOLOCTL)"
# --- Boot A Partition (KSOLOA) ---
populate_boot_partition "$MNT_BOOTA" "Boot A (KSOLOA)"
# --- Boot B Partition (KSOLOB, initially identical) ---
populate_boot_partition "$MNT_BOOTB" "Boot B (KSOLOB)"
# --- Data Partition (KSOLODATA) ---
echo " Preparing data partition..."
for dir in kubesolo containerd etc-kubesolo log usr-local network images; do
mkdir -p "$MNT_DATA/$dir"
done
sync
echo ""
echo "==> Raspberry Pi disk image created: $IMG_OUTPUT"
echo " Size: $(du -h "$IMG_OUTPUT" | cut -f1)"
echo " Part 1 (KSOLOCTL): Firmware + kernel + initramfs + autoboot.txt (boot/control)"
echo " Part 2 (KSOLOA): Boot A — kernel + initramfs + DTBs"
echo " Part 3 (KSOLOB): Boot B — kernel + initramfs + DTBs"
echo " Part 4 (KSOLODATA): Persistent K8s state"
echo ""
echo "Write to SD card with:"
echo " sudo dd if=$IMG_OUTPUT of=/dev/sdX bs=4M status=progress"
echo ""