#!/usr/bin/env bash # Update the running Pi over SSH without reflashing. # # ./deploy-rpi.sh build rpi4.scm, copy, switch, reboot # ./deploy-rpi.sh --no-reboot ... activate only (services restart on reboot) # ./deploy-rpi.sh switch N make existing generation N the default # (roll back), activate, reboot # ./deploy-rpi.sh list show the Pi's system generations # # The system is cross-built here (--target, like the image) and sent with # `guix copy'; extlinux.conf is generated here too (scripts/extlinux-conf.scm), # because `guix system reconfigure/switch-generation' on the Pi would need # native aarch64 builds (PITFALLS #28). Old generations stay in the U-Boot # menu: pick one from the console if a new one doesn't boot. # # Needs: ~/.ssh/config entry for $PI, /etc/guix/signing-key.* on this host, # and that key in the Pi's guix-service-type authorized-keys (rpi4.scm). set -euo pipefail cd "$(dirname "$0")" export PATH=$HOME/.local/bin:$PATH export GUIX_PACKAGE_PATH="$PWD/packages:$HOME/channels/nonguix" PI=${PI:-pirouter} PROFILES=/var/guix/profiles # -n: never read stdin (it would swallow `while read` loop input). pi() { ssh -n -o BatchMode=yes "$PI" "$@"; } # "N /gnu/store/…-system" for each generation, ascending. generations() { pi "cd $PROFILES && for l in system-*-link; do n=\${l#system-}; echo \"\${n%-link} \$(readlink \$l)\"; done" | sort -n } current() { pi "readlink -f $PROFILES/system"; } # Write extlinux.conf with generation $1 (store path) as default and every # other generation as fallback entries, newest first; install it on the Pi. install_bootcfg() { local default=$1 tmp params=() n path conf tmp=$(mktemp -d) pi "cat $default/parameters" > "$tmp/default" while read -r n path; do [ "$path" = "$default" ] && continue pi "cat $path/parameters" > "$tmp/$n" params=("$path" "$tmp/$n" "${params[@]}") # newest first done < <(generations) conf=$(guix repl -L packages -L "$HOME/channels/nonguix" -- \ scripts/extlinux-conf.scm rpi4.scm \ "$default" "$tmp/default" "${params[@]}" | tail -1) rm -rf "$tmp" [ -f "$conf" ] || { echo "extlinux.conf generation failed" >&2; exit 1; } scp -q -o BatchMode=yes "$conf" "$PI:/boot/extlinux/extlinux.conf.new" pi "mv /boot/extlinux/extlinux.conf.new /boot/extlinux/extlinux.conf && sync" echo "extlinux.conf: default $default, $(( ${#params[@]} / 2 )) fallback(s)" } # Point the system profile at generation $1, activate it. switch_to() { local n=$1 path path=$(pi "readlink $PROFILES/system-$n-link") install_bootcfg "$path" pi "ln -sfn system-$n-link $PROFILES/system.new && mv -T $PROFILES/system.new $PROFILES/system" echo "activating generation $n ($path)" # activate.scm picks the system from GUIX_NEW_SYSTEM (set by reconfigure # and at boot); without it, it re-activates /run/current-system. pi "GUIX_NEW_SYSTEM=$path $path/activate" || echo "warning: activation reported errors" >&2 } reboot_pi() { echo "rebooting $PI" pi "sync; (sleep 1; reboot) >/dev/null 2>&1 &" || true } case "${1:-deploy}" in list) cur=$(current) generations | while read -r n path; do echo "$n $path$( [ "$path" = "$cur" ] && echo ' (current)')" done ;; switch) n=${2:?usage: deploy-rpi.sh switch N} switch_to "$n" [ "${3:-}" = --no-reboot ] || reboot_pi ;; deploy|--no-reboot) sys=$(guix system build --target=aarch64-linux-gnu rpi4.scm | tail -1) echo "built $sys" if [ "$sys" = "$(current)" ]; then echo "already running $sys"; exit 0; fi guix copy --to="$PI" "$sys" last=$(generations | tail -1 | cut -d' ' -f1) n=$((last + 1)) pi "ln -sfn $sys $PROFILES/system-$n-link" echo "registered generation $n" switch_to "$n" [ "${1:-deploy}" = --no-reboot ] || reboot_pi ;; *) sed -n '2,10p' "$0"; exit 1 ;; esac