#!/usr/bin/env bash # Bake the golden NixOS qcow2: boot the minimal ISO headless under qemu, drive a # scripted nixos-install over the serial console, power off. Output is a golden # disk image; Kiln (or boot-overlay.sh) boots throwaway overlays from it. # # Usage: build-golden.sh # Tunables via env: DISK_GB (20), MEM_MB (4096), SMP (2), INSTALL_TIMEOUT (2400) set -euo pipefail ISO="${1:?usage: build-golden.sh }" NINA="${2:?need nina linux binary}" OUT="${3:?need output qcow2 path}" HERE="$(cd "$(dirname "$0")" && pwd)" DISK_GB="${DISK_GB:-20}" MEM_MB="${MEM_MB:-4096}" SMP="${SMP:-2}" INSTALL_TIMEOUT="${INSTALL_TIMEOUT:-2400}" for f in "$ISO" "$NINA"; do [ -f "$f" ] || { echo "missing: $f" >&2; exit 1; }; done work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT # Seed media: our config + install script + nina, as an ISO the installer mounts. seed="$work/seed" mkdir -p "$seed" cp "$HERE/configuration.nix" "$seed/configuration.nix" cp "$HERE/install.sh" "$seed/install.sh" cp "$NINA" "$seed/nina" SEED_ISO="$work/seed.iso" hdiutil makehybrid -quiet -iso -joliet -default-volume-name NINASEED -o "$SEED_ISO" "$seed" # Fresh blank disk for the install target. qemu-img create -f qcow2 "$OUT" "${DISK_GB}G" >/dev/null # Direct kernel boot: the ISO's own cmdline omits console=ttyS0 (kernel + getty # land on VGA, invisible under -nographic). Extract the kernel/initrd and the # installer's APPEND line, then add a serial console so the install is drivable. bsdtar -xOf "$ISO" boot/bzImage > "$work/bzImage" bsdtar -xOf "$ISO" boot/initrd > "$work/initrd" APPEND_BASE="$(bsdtar -xOf "$ISO" isolinux/isolinux.cfg 2>/dev/null \ | awk '/^[[:space:]]*APPEND/ {sub(/^[[:space:]]*APPEND[[:space:]]*/,""); print; exit}')" [ -n "$APPEND_BASE" ] || { echo "could not read installer APPEND line" >&2; exit 1; } APPEND="$APPEND_BASE console=ttyS0,115200" echo "golden target: $OUT (${DISK_GB}G) ยท seed: $SEED_ISO" echo "kernel append: $APPEND" # Pass values to expect via the environment ($env(...)); `expect -c` does not # populate $argv from trailing args. export GI_ISO="$ISO" GI_SEED="$SEED_ISO" GI_OUT="$OUT" GI_MEM="$MEM_MB" \ GI_SMP="$SMP" GI_TIMEOUT="$INSTALL_TIMEOUT" \ GI_KERNEL="$work/bzImage" GI_INITRD="$work/initrd" GI_APPEND="$APPEND" expect -c ' set timeout 600 set iso $env(GI_ISO) set seed_iso $env(GI_SEED) set out $env(GI_OUT) set mem $env(GI_MEM) set smp $env(GI_SMP) set install_timeout $env(GI_TIMEOUT) set kernel $env(GI_KERNEL) set initrd $env(GI_INITRD) set append $env(GI_APPEND) spawn qemu-system-x86_64 \ -machine q35 -accel tcg -cpu max -smp $smp -m $mem \ -kernel $kernel -initrd $initrd -append $append \ -drive file=$out,if=virtio,format=qcow2 \ -drive file=$iso,media=cdrom,readonly=on \ -drive file=$seed_iso,media=cdrom,readonly=on \ -netdev user,id=n0 -device virtio-net-pci,netdev=n0 \ -nographic expect { timeout { puts "\n!! installer never reached autologin"; exit 2 } "automatic login" {} } sleep 5 send "sudo -i\r" sleep 2 send "mkdir -p /mnt-seed && mount /dev/sr1 /mnt-seed && bash /mnt-seed/install.sh\r" set timeout $install_timeout expect { timeout { puts "\n!! install never finished"; exit 4 } "NINA_INSTALL_FAIL" { puts "\n!! install.sh reported failure"; exit 5 } "NINA_INSTALL_DONE" {} "NINA_INSTALL_START" { exp_continue } } set timeout 120 expect eof ' echo "=== golden image built: $OUT ===" qemu-img info "$OUT" | sed 's/^/ /'