#!/bin/bash # Flash Raspberry Pi image with headless config (Trixie/Bookworm compatible) # Usage: ./flash-pi.sh # Reads settings from config.json in same directory # # Uses the same firstrun.sh approach as rpi-imager for compatibility set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="$SCRIPT_DIR/config.json" # ============================================================================ # Load config # ============================================================================ if [ ! -f "$CONFIG_FILE" ]; then echo "Error: config.json not found at $CONFIG_FILE" exit 1 fi PI_USER=$(jq -r '.username' "$CONFIG_FILE") PI_PASS=$(jq -r '.password' "$CONFIG_FILE") WIFI_SSID=$(jq -r '.wifiSSID' "$CONFIG_FILE") WIFI_PASS=$(jq -r '.wifiPassword' "$CONFIG_FILE") WIFI_COUNTRY=$(jq -r '.wifiCountry // "US"' "$CONFIG_FILE") PI_HOSTNAME=$(jq -r '.hostname' "$CONFIG_FILE") PI_TIMEZONE=$(jq -r '.timezone // "America/New_York"' "$CONFIG_FILE") PI_KEYMAP=$(jq -r '.keyboardLayout // "us"' "$CONFIG_FILE") echo "Loaded config from $CONFIG_FILE" echo " Hostname: $PI_HOSTNAME" echo " User: $PI_USER" echo " WiFi: $WIFI_SSID" echo " Timezone: $PI_TIMEZONE" echo # ============================================================================ # Validate args # ============================================================================ if [ $# -ne 2 ]; then echo "Usage: $0 " echo "Example: $0 2025-12-04-raspios-trixie-arm64-lite.img.xz /dev/sdb" exit 1 fi IMAGE="$1" DEVICE="$2" if [ ! -f "$IMAGE" ]; then echo "Error: Image file not found: $IMAGE" exit 1 fi if [ ! -b "$DEVICE" ]; then echo "Error: Device not found: $DEVICE" exit 1 fi # Safety check echo "WARNING: This will ERASE all data on $DEVICE" echo "Device info:" lsblk "$DEVICE" echo read -p "Type 'yes' to continue: " confirm if [ "$confirm" != "yes" ]; then echo "Aborted." exit 1 fi # Ask about wiping echo read -p "Wipe partition table first? (recommended if having issues) [y/N] " wipe_confirm if [[ "$wipe_confirm" =~ ^[Yy]$ ]]; then echo "Wiping partition table..." sudo wipefs -a "$DEVICE" sudo dd if=/dev/zero of="$DEVICE" bs=1M count=10 status=none sync echo " Wiped clean" fi # ============================================================================ # Flash image # ============================================================================ echo "Flashing $IMAGE to $DEVICE..." if [[ "$IMAGE" == *.xz ]]; then xzcat "$IMAGE" | sudo dd of="$DEVICE" bs=4M status=progress conv=fsync elif [[ "$IMAGE" == *.zst ]]; then zstdcat "$IMAGE" | sudo dd of="$DEVICE" bs=4M status=progress conv=fsync else sudo dd if="$IMAGE" of="$DEVICE" bs=4M status=progress conv=fsync fi echo "Syncing..." sync # Wait for partitions sleep 2 sudo partprobe "$DEVICE" 2>/dev/null || true sleep 1 # ============================================================================ # Find partitions # ============================================================================ if [ -b "${DEVICE}1" ]; then BOOT_PART="${DEVICE}1" elif [ -b "${DEVICE}p1" ]; then BOOT_PART="${DEVICE}p1" else echo "Error: Could not find boot partition" exit 1 fi MOUNT_DIR=$(mktemp -d) # ============================================================================ # Configure boot partition with firstrun.sh (rpi-imager method) # ============================================================================ echo "Mounting boot partition..." sudo mount "$BOOT_PART" "$MOUNT_DIR" # Enable SSH echo "Enabling SSH..." sudo touch "$MOUNT_DIR/ssh" # Generate password hash PASS_HASH=$(echo "$PI_PASS" | openssl passwd -6 -stdin) # Create firstrun.sh - this is exactly what rpi-imager generates echo "Creating firstrun.sh..." sudo tee "$MOUNT_DIR/firstrun.sh" > /dev/null << 'EOFSCRIPT' #!/bin/bash set +e CURRENT_HOSTNAME=$(cat /etc/hostname | tr -d " \t\n\r") if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then /usr/lib/raspberrypi-sys-mods/imager_custom set_hostname PLACEHOLDER_HOSTNAME else echo PLACEHOLDER_HOSTNAME >/etc/hostname sed -i "s/127.0.1.1.*$CURRENT_HOSTNAME/127.0.1.1\tPLACEHOLDER_HOSTNAME/g" /etc/hosts fi FIRSTUSER=$(getent passwd 1000 | cut -d: -f1) FIRSTUSERHOME=$(getent passwd 1000 | cut -d: -f6) if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then /usr/lib/raspberrypi-sys-mods/imager_custom enable_ssh else systemctl enable ssh fi if [ -f /usr/lib/userconf-pi/userconf ]; then /usr/lib/userconf-pi/userconf 'PLACEHOLDER_USER' 'PLACEHOLDER_HASH' else echo "$FIRSTUSER:"'PLACEHOLDER_HASH' | chpasswd -e if [ "$FIRSTUSER" != "PLACEHOLDER_USER" ]; then usermod -l "PLACEHOLDER_USER" "$FIRSTUSER" usermod -m -d "/home/PLACEHOLDER_USER" "PLACEHOLDER_USER" groupmod -n "PLACEHOLDER_USER" "$FIRSTUSER" if grep -q "^autologin-user=" /etc/lightdm/lightdm.conf 2>/dev/null; then sed -i "s/^autologin-user=.*/autologin-user=PLACEHOLDER_USER/" /etc/lightdm/lightdm.conf fi if [ -f /etc/systemd/system/getty@tty1.service.d/autologin.conf ]; then sed -i "s/$FIRSTUSER/PLACEHOLDER_USER/" /etc/systemd/system/getty@tty1.service.d/autologin.conf fi fi fi if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then /usr/lib/raspberrypi-sys-mods/imager_custom set_keymap 'PLACEHOLDER_KEYMAP' /usr/lib/raspberrypi-sys-mods/imager_custom set_timezone 'PLACEHOLDER_TIMEZONE' fi if [ -f /usr/lib/raspberrypi-sys-mods/imager_custom ]; then /usr/lib/raspberrypi-sys-mods/imager_custom set_wlan 'PLACEHOLDER_SSID' 'PLACEHOLDER_WIFIPASS' 'PLACEHOLDER_COUNTRY' else cat >/etc/wpa_supplicant/wpa_supplicant.conf <<'WPAEOF' country=PLACEHOLDER_COUNTRY ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev ap_scan=1 update_config=1 network={ ssid="PLACEHOLDER_SSID" psk="PLACEHOLDER_WIFIPASS" } WPAEOF chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf rfkill unblock wifi for filename in /var/lib/systemd/rfkill/*:wlan ; do echo 0 > "$filename" done fi rm -f /boot/firstrun.sh rm -f /boot/firmware/firstrun.sh sed -i 's| systemd.run.*||g' /boot/cmdline.txt 2>/dev/null sed -i 's| systemd.run.*||g' /boot/firmware/cmdline.txt 2>/dev/null exit 0 EOFSCRIPT # Replace placeholders with actual values sudo sed -i "s/PLACEHOLDER_HOSTNAME/$PI_HOSTNAME/g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s/PLACEHOLDER_USER/$PI_USER/g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s|PLACEHOLDER_HASH|$PASS_HASH|g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s/PLACEHOLDER_KEYMAP/$PI_KEYMAP/g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s|PLACEHOLDER_TIMEZONE|$PI_TIMEZONE|g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s/PLACEHOLDER_SSID/$WIFI_SSID/g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s/PLACEHOLDER_WIFIPASS/$WIFI_PASS/g" "$MOUNT_DIR/firstrun.sh" sudo sed -i "s/PLACEHOLDER_COUNTRY/$WIFI_COUNTRY/g" "$MOUNT_DIR/firstrun.sh" sudo chmod +x "$MOUNT_DIR/firstrun.sh" # Update cmdline.txt to run firstrun.sh on boot echo "Updating cmdline.txt..." CMDLINE="$MOUNT_DIR/cmdline.txt" if [ -f "$CMDLINE" ]; then # Read current cmdline, strip any existing systemd.run, append new one CURRENT=$(cat "$CMDLINE" | tr -d '\n' | sed 's| systemd.run.*||g') echo "$CURRENT systemd.run=/boot/firmware/firstrun.sh systemd.run_success_action=reboot systemd.unit=kernel-command-line.target" | sudo tee "$CMDLINE" > /dev/null echo " cmdline.txt updated" fi sudo umount "$MOUNT_DIR" rmdir "$MOUNT_DIR" echo echo "Done! SD card is ready." echo " Hostname: $PI_HOSTNAME" echo " User: $PI_USER" echo " SSH: enabled" echo " WiFi: $WIFI_SSID" echo echo "Insert into Pi and boot. Find it with: ping $PI_HOSTNAME.local"