Files
forgefirm/scripts/ffboot
T
ScottW514 58d4d91948 Build and release engineering: teardown order, slot safety, release gates
- Controllers stop at K80, before forgectrl at K90: runlevel 0/6 no
  longer tears down the cooling engine, fire gates, and broker while a
  controller may still be executing a job.
- The grblhal/gfcloud init scripts are real emergency levers: stop
  routes through the supervisor (POST /controller/stop - a bare pkill
  was safed and respawned seconds later), start resumes supervision,
  status exists, and the pkill fallback matches full executable paths
  instead of truncated names or bare substrings.
- slotmigrate: the partition grow gets the same 2048-sector tolerance
  as the filesystem branch (an exact compare rewrote the MBR at S02 on
  every boot on disks where the grow cannot land on the last sector),
  verifies it made progress, and the resize2fs retry is bounded at
  three attempts with the counter kept on p3 itself.
- Installer: archive product/platform are verified after the signature,
  and a validly signed OLDER release now requires an explicit yes
  instead of installing as a silent downgrade. All predictable /tmp
  paths in the installer and ffboot are mktemp now.
- release.sh rejects multiple positional versions (the last one used to
  win silently) and a release without factory-era verification dies
  unless explicitly bypassed; mkfw.sh refuses to pack when the public
  key for the post-sign self-check is missing.
- forgefirm-logrotate: size-capped rotation (boot + hourly) for the
  /data logs - a full /data breaks settings, update staging, and the
  controllers own writes.
- Bench build scripts derive every path from their own location or
  FF_SRC_TOP/FF_BUILD_TOP and log to mktemp files.
2026-08-14 18:23:13 -04:00

237 lines
7.9 KiB
Bash

#!/bin/sh
# (C) Copyright 2020-2026
# Scott Wiederhold, s.e.wiederhold@gmail.com
# https://community.openglow.org
# SPDX-License-Identifier: MIT
#
# Boot-slot tool for Glowforge factory hardware running ForgeFIRM or
# factory firmware: inventories what is installed on each bootable
# partition and switches the boot target by rewriting the saved U-Boot
# environment (mmcdev/mmchwpart/mmcpart/mmcroot).
#
# The flip writes all four variables in one fw_setenv -s transaction and
# read-back verifies them, retrying with the classic u-boot-tools script
# format ("name value") if the libubootenv format ("name=value") did not
# take, and per-variable calls as a last resort. A switch target must
# pass a content probe (rootfs mounts, kernel present) unless forced.
usage () {
cat <<END
usage: ffboot -l | -s|-e[<partition>] [-n] [-f]
-l: inventory bootable partitions (key=value lines) and exit
-s: SDCARD OpenGlow/ForgeFIRM
-e[<partition>]: eMMC
1: eMMC slot 1 (factory image 1)
2: eMMC slot 2 (factory image 2)
4: legacy OpenGlow/ForgeFIRM partition
Default: factory image with the most recent firmware
-n: No reboot
-f: Force - skip the target content probe
END
}
# --- fw_env config selection -------------------------------------------------
# The env lives on the eMMC user area (0x80000/0x82000, redundant). Factory
# firmware ships per-device configs; the mmcblk2-specific override is honored
# if present, otherwise the standard /etc/fw_env.config is used.
if [ -f "/etc/fw_env_mmcblk2.config" ] && [ ! -d "/factory" ]; then
FWCONFIG="/etc/fw_env_mmcblk2.config"
else
FWCONFIG="/etc/fw_env.config"
fi
BOOTED_ROOT=$(sed -n 's/.*root=\([^ ]*\).*/\1/p' /proc/cmdline)
# --- partition probe ---------------------------------------------------------
# probe_part <device>
# Sets: P_PRESENT P_STATE(ok|empty|unreadable) P_TYPE(forgefirm|factory|unknown)
# P_VERSION (display) P_DATE (build datetime, for ordering) P_KERNEL(yes|no)
probe_part () {
P_PRESENT=no; P_STATE=unreadable; P_TYPE=unknown
P_VERSION=""; P_DATE=""; P_KERNEL=no
P_MOUNTED=""
[ -b "$1" ] || return 1
P_PRESENT=yes
if [ "$1" = "$BOOTED_ROOT" ]; then
ROOT_DIR=""
else
# Reuse an existing mount (the image keeps the factory slots mounted
# under /factory); a fresh mount is explicit -t ext4 - letting mount
# iterate types provokes a cosmetic kernel "Can't open blockdev" for
# each foreign-type claim against an already-mounted device.
ROOT_DIR=$(sed -n "s|^$1 \([^ ]*\).*|\1|p" /proc/mounts | head -n 1)
if [ -z "$ROOT_DIR" ]; then
P_MOUNTED=yes
ROOT_DIR=$(mktemp -d /tmp/ffboot.probe.XXXXXX) || return 1
if ! mount -o ro -t ext4 "$1" "$ROOT_DIR" 2>/dev/null; then
rmdir "$ROOT_DIR" 2>/dev/null
return 1
fi
fi
fi
if [ -f "$ROOT_DIR/etc/forgefirm-version" ]; then
P_TYPE=forgefirm; P_STATE=ok
P_VERSION=$(cat "$ROOT_DIR/etc/forgefirm-version")
elif [ -f "$ROOT_DIR/etc/version" ]; then
P_TYPE=factory; P_STATE=ok
# The build datetime orders releases reliably (used by -e); the
# semantic FIRMWARE_VERSION in /etc/build is what the factory calls
# the release and what we display (fall back to the datetime).
P_DATE=$(cat "$ROOT_DIR/etc/version")
FV=$(sed -n 's/^FIRMWARE_VERSION[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' \
"$ROOT_DIR/etc/build" 2>/dev/null)
if [ -n "$FV" ]; then
P_VERSION="v$FV"
else
P_VERSION="$P_DATE"
fi
else
P_STATE=empty
fi
[ -f "$ROOT_DIR/boot/zImage" ] && P_KERNEL=yes
if [ -n "$P_MOUNTED" ]; then
umount "$ROOT_DIR" 2>/dev/null
rmdir "$ROOT_DIR" 2>/dev/null
fi
return 0
}
env_get () {
fw_printenv -c "$FWCONFIG" -n "$1" 2>/dev/null
}
# --- inventory ---------------------------------------------------------------
inventory () {
E_DEV=$(env_get mmcdev); E_PART=$(env_get mmcpart); E_ROOT=$(env_get mmcroot)
echo "env.mmcdev=$E_DEV"
echo "env.mmchwpart=$(env_get mmchwpart)"
echo "env.mmcpart=$E_PART"
echo "env.mmcroot=$E_ROOT"
echo "booted.root=$BOOTED_ROOT"
for ENTRY in "sd /dev/mmcblk1p1" "a /dev/mmcblk2p1" "b /dev/mmcblk2p2" "legacy /dev/mmcblk2p4"; do
NAME=${ENTRY%% *}; DEV=${ENTRY#* }
probe_part "$DEV"
echo "slot.$NAME.device=$DEV"
echo "slot.$NAME.present=$P_PRESENT"
[ "$P_PRESENT" = "yes" ] || continue
echo "slot.$NAME.state=$P_STATE"
echo "slot.$NAME.type=$P_TYPE"
echo "slot.$NAME.version=$P_VERSION"
echo "slot.$NAME.kernel=$P_KERNEL"
[ "$DEV" = "$BOOTED_ROOT" ] && echo "slot.$NAME.booted=yes"
[ "$DEV" = "$E_ROOT" ] && echo "slot.$NAME.next=yes"
done
}
# --- verified env flip -------------------------------------------------------
# set_env <mmcdev> <mmchwpart> <mmcpart> <mmcroot>
env_verify () {
[ "$(env_get mmcdev)" = "$1" ] && [ "$(env_get mmchwpart)" = "$2" ] && \
[ "$(env_get mmcpart)" = "$3" ] && [ "$(env_get mmcroot)" = "$4" ]
}
set_env () {
SCRIPT=$(mktemp /tmp/ffboot.env.XXXXXX) || return 1
# libubootenv format
printf 'mmcdev=%s\nmmchwpart=%s\nmmcpart=%s\nmmcroot=%s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
if env_verify "$1" "$2" "$3" "$4"; then rm -f "$SCRIPT"; return 0; fi
# classic u-boot-tools format
printf 'mmcdev %s\nmmchwpart %s\nmmcpart %s\nmmcroot %s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
rm -f "$SCRIPT"
if env_verify "$1" "$2" "$3" "$4"; then return 0; fi
# last resort: per-variable
fw_setenv -c "$FWCONFIG" mmcdev "$1" && \
fw_setenv -c "$FWCONFIG" mmchwpart "$2" && \
fw_setenv -c "$FWCONFIG" mmcpart "$3" && \
fw_setenv -c "$FWCONFIG" mmcroot "$4"
env_verify "$1" "$2" "$3" "$4"
}
# --- argument parsing --------------------------------------------------------
REBOOT=1
FORCE=0
MODE=""
for ARG in "$@"; do
case "$ARG" in
-l) MODE=list ;;
-n) REBOOT=0 ;;
-f) FORCE=1 ;;
-s|-e|-e1|-e2|-e4)
[ -z "$MODE" ] || { usage; exit 2; }
MODE="$ARG" ;;
*) usage; exit 2 ;;
esac
done
[ -n "$MODE" ] || { usage; exit 2; }
if [ ! -f "$FWCONFIG" ]; then
echo "ERROR: $FWCONFIG not found; cannot access the U-Boot environment" >&2
exit 1
fi
if [ "$MODE" = "list" ]; then
inventory
exit 0
fi
# --- resolve the switch target ----------------------------------------------
case "$MODE" in
-s)
MMCDEV=0; MMCPART=1 ;;
-e)
# Pick the factory slot with the newest firmware; a slot occupied by
# ForgeFIRM is not a factory-restore target.
MMCDEV=1; MMCPART=""
BEST=0
for CAND in 1 2; do
probe_part "/dev/mmcblk2p$CAND" || continue
[ "$P_TYPE" = "factory" ] || continue
# Order by the build datetime (monotonic), not the semantic
# version string.
V=$(echo "$P_DATE" | tr -cd '0-9')
[ -n "$V" ] || V=0
if [ -z "$MMCPART" ] || [ "$V" -gt "$BEST" ]; then
MMCPART=$CAND; BEST=$V
fi
done
[ -n "$MMCPART" ] || {
echo "ERROR: no factory image found in eMMC slot 1 or 2." >&2
echo "Use -e1/-e2/-e4 to select a slot explicitly." >&2
exit 1
} ;;
-e1) MMCDEV=1; MMCPART=1 ;;
-e2) MMCDEV=1; MMCPART=2 ;;
-e4) MMCDEV=1; MMCPART=4 ;;
esac
MMCROOT="/dev/mmcblk$((MMCDEV + 1))p$MMCPART"
# --- target sanity probe -----------------------------------------------------
if [ "$FORCE" -eq 0 ]; then
if ! probe_part "$MMCROOT"; then
echo "ERROR: $MMCROOT is missing or unreadable (use -f to override)" >&2
exit 1
fi
if [ "$P_STATE" != "ok" ] || [ "$P_KERNEL" != "yes" ]; then
echo "ERROR: $MMCROOT does not look bootable (state=$P_STATE kernel=$P_KERNEL; use -f to override)" >&2
exit 1
fi
echo "Target $MMCROOT: $P_TYPE $P_VERSION"
fi
echo "Setting boot to $MMCROOT"
if ! set_env "$MMCDEV" 0 "$MMCPART" "$MMCROOT"; then
echo "ERROR: environment write did not verify; boot selection unchanged or inconsistent" >&2
exit 1
fi
if [ "$REBOOT" -gt 0 ]; then
echo "Rebooting..."
reboot
fi
exit 0