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.
This commit is contained in:
ScottW514
2026-08-14 18:23:13 -04:00
parent cbabf7d111
commit 58d4d91948
15 changed files with 301 additions and 45 deletions
@@ -5,7 +5,10 @@ require forgefirm-app.inc
inherit update-rc.d
INITSCRIPT_NAME = "gfcloud"
INITSCRIPT_PARAMS = "defaults 92"
# stop 80 < forgectrl's 90: at runlevel 0/6 the controller goes down
# BEFORE the daemon that carries the cooling engine, fire gates, and
# broker - never the other way around.
INITSCRIPT_PARAMS = "start 92 2 3 4 5 . stop 80 0 1 6 ."
do_install() {
install -Dm 0755 ${S}/forgefirm-app/gfcloud.py ${D}${sbindir}/gfcloud.py
@@ -0,0 +1,43 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: forgefirm-logrotate
# Required-Start: $local_fs
# Required-Stop:
# Default-Start: 2 3 4 5
# Default-Stop:
# Short-Description: rotate the /data logs at boot and hourly
### END INIT INFO
# The image runs no cron daemon, so rotation is driven here: once at
# every boot (bounds carry-over bloat), then hourly from a small
# background loop (bounds growth across long uptimes - a full /data
# breaks settings, update staging, and the controllers' own writes).
LOGROTATE=/usr/sbin/logrotate
CONF=/etc/logrotate.conf
PIDFILE=/var/run/forgefirm-logrotate.pid
case "$1" in
start)
[ -x "$LOGROTATE" ] || exit 0
"$LOGROTATE" "$CONF" 2>/dev/null
( while :; do
sleep 3600
"$LOGROTATE" "$CONF" 2>/dev/null
done ) &
echo $! > "$PIDFILE"
;;
stop)
[ -r "$PIDFILE" ] && kill "$(cat "$PIDFILE")" 2>/dev/null
rm -f "$PIDFILE"
;;
restart)
$0 stop
$0 start
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
exit 0
@@ -0,0 +1,12 @@
# ForgeFIRM logs on the persistent /data partition. copytruncate: the
# daemon and both controllers keep their log fds open for their whole
# lifetime, so the file must be truncated in place, never moved out
# from under them.
/data/forgectrl.log /data/glowforge.log /data/gfcloud.log /data/gfhome.log {
size 1M
rotate 3
copytruncate
compress
missingok
notifempty
}
@@ -0,0 +1,26 @@
SUMMARY = "Log rotation for the ForgeFIRM logs on /data"
DESCRIPTION = "Size-capped rotation for the daemon and controller logs \
on the persistent /data partition: a full /data breaks settings, update \
staging, and the controllers' own writes. Rotation runs at every boot \
and hourly while the machine is up."
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
SRC_URI = " \
file://forgefirm.logrotate \
file://forgefirm-logrotate.init \
"
S = "${WORKDIR}"
inherit update-rc.d
INITSCRIPT_NAME = "forgefirm-logrotate"
INITSCRIPT_PARAMS = "defaults 40"
RDEPENDS:${PN} = "logrotate"
do_install() {
install -Dm 0644 ${WORKDIR}/forgefirm.logrotate ${D}${sysconfdir}/logrotate.d/forgefirm
install -Dm 0755 ${WORKDIR}/forgefirm-logrotate.init ${D}${sysconfdir}/init.d/forgefirm-logrotate
}
@@ -20,7 +20,10 @@ S = "${WORKDIR}/git"
inherit cmake update-rc.d
INITSCRIPT_NAME = "grblhal"
INITSCRIPT_PARAMS = "defaults 92"
# stop 80 < forgectrl's 90: at runlevel 0/6 the controller goes down
# BEFORE the daemon that carries the cooling engine, fire gates, and
# broker - never the other way around.
INITSCRIPT_PARAMS = "start 92 2 3 4 5 . stop 80 0 1 6 ."
do_install:append() {
install -d ${D}${sysconfdir}/init.d
@@ -11,21 +11,54 @@
# The controller lifecycle belongs to the forgectrl supervisor: it
# spawns the controller selected by controller_mode as a direct child
# (that relationship carries the pulse-device fd and death detection).
# This script remains only as a manual emergency stop.
# This script is the manual emergency lever, routed THROUGH the
# supervisor: stop asks it to stop the child and hold it down (a bare
# pkill would be safed and respawned seconds later); start resumes
# supervision.
DAEMON_BIN=/usr/bin/grblHAL_glowforge
TOKEN_FILE=/data/forgefirm/panel.token
CTRL_URL="http://127.0.0.1:${FORGECTRL_PORT:-8080}"
super_post() {
[ -r "$TOKEN_FILE" ] || return 1
curl -sf -m 20 -X POST \
-H "X-ForgeFIRM-Token: $(cat "$TOKEN_FILE")" \
"$CTRL_URL/controller/$1" >/dev/null 2>&1
}
case "$1" in
start)
echo "grblHAL is managed by forgectrl (POST /mode to switch controllers)"
if super_post start; then
echo "grblHAL supervision resumed (forgectrl spawns the selected controller)"
else
echo "grblHAL is managed by forgectrl (POST /mode to switch controllers)"
fi
;;
stop)
echo "Stopping grblHAL"
pkill -x grblHAL_glowfor 2>/dev/null
if ! super_post stop; then
# Supervisor unreachable: kill the controller directly. If a
# supervisor is alive after all, the child's exit still lands on
# its safing path. Match the full executable path, not the
# 15-char comm truncation.
pkill -f "^$DAEMON_BIN" 2>/dev/null
fi
;;
status)
if pgrep -f "^$DAEMON_BIN" >/dev/null 2>&1; then
echo "grblHAL is running"
exit 0
else
echo "grblHAL is not running"
exit 3
fi
;;
restart)
$0 stop
;;
*)
echo "Usage: $0 {start|stop|restart}"
echo "Usage: $0 {start|stop|status|restart}"
exit 1
;;
esac
@@ -23,7 +23,9 @@ IMAGE_INSTALL:remove = "gfui-client"
# to the inactive rootfs slot.
# ffboot: boot-slot inventory and switching (also ships fw_env.config).
# slotmigrate: boot-time reclaim of the legacy p4 layout (grows /data).
IMAGE_INSTALL:append = " grblhal-glowforge forgectrl gfhome gfcloud v4l-utils fwup ffboot slotmigrate"
# forgefirm-logrotate: size-capped rotation of the /data logs (boot +
# hourly; a full /data breaks settings, updates, and controller writes).
IMAGE_INSTALL:append = " grblhal-glowforge forgectrl gfhome gfcloud v4l-utils fwup ffboot slotmigrate forgefirm-logrotate"
# NXP's firmware EULA covers the i.MX VPU/EPDC blobs the BSP installs, so the
# image ships the license text with them (/usr/share/licenses/firmware-imx).
@@ -57,11 +57,20 @@ P3_SIZE=$(sfdisk -d "$DISK" 2>/dev/null | sed -n "s|^${P3} .*size=[ ]*\([0-9]*\)
[ -n "$DISK_SECT" ] && [ -n "$P3_START" ] && [ -n "$P3_SIZE" ] \
|| { log "cannot read disk/p3 geometry"; exit 0; }
if [ $((P3_START + P3_SIZE)) -lt "$DISK_SECT" ]; then
log "growing p3 to the end of the disk ($((P3_START + P3_SIZE)) -> $DISK_SECT sectors)"
# 2048-sector tolerance (mirrors the filesystem branch): on a disk where
# the grow cannot land exactly on the last sector, an exact comparison
# would rewrite the MBR at S02 on EVERY boot - and a power loss inside
# that window costs the partition table and /data.
if [ $((P3_START + P3_SIZE)) -lt $((DISK_SECT - 2048)) ]; then
log "growing p3 toward the end of the disk ($((P3_START + P3_SIZE)) -> $DISK_SECT sectors)"
echo ", +" | sfdisk --no-reread --force -N 3 "$DISK" >/dev/null 2>&1 \
|| { log "p3 grow FAILED"; exit 0; }
partx -u --nr 3 "$DISK" 2>/dev/null
P3_NEW=$(sfdisk -d "$DISK" 2>/dev/null | sed -n "s|^${P3} .*size=[ ]*\([0-9]*\),.*|\1|p")
if [ -n "$P3_NEW" ] && [ "$P3_NEW" = "$P3_SIZE" ]; then
log "p3 grow made no progress ($P3_SIZE sectors); leaving the table alone"
exit 0
fi
fi
# --- filesystem ----------------------------------------------------------
@@ -71,8 +80,42 @@ FS_BLOCKS=$(tune2fs -l "$P3" 2>/dev/null | sed -n 's/^Block count:[ ]*//p')
FS_BSIZE=$(tune2fs -l "$P3" 2>/dev/null | sed -n 's/^Block size:[ ]*//p')
[ -n "$FS_BLOCKS" ] && [ -n "$FS_BSIZE" ] || { log "cannot read p3 filesystem"; exit 0; }
# Bounded retry: a resize that keeps failing must not cost a full
# e2fsck pass on every boot forever. Nothing else is mounted this
# early, so the attempt counter lives on p3 itself.
TRY_FILE=.slotmigrate-resize-tries
read_tries () {
TRIES=0
T=$(mktemp -d) || return
if mount "$P3" "$T" 2>/dev/null; then
TRIES=$(cat "$T/$TRY_FILE" 2>/dev/null)
umount "$T" 2>/dev/null
fi
rmdir "$T" 2>/dev/null
case "$TRIES" in
''|*[!0-9]*) TRIES=0 ;;
esac
}
write_tries () {
T=$(mktemp -d) || return
if mount "$P3" "$T" 2>/dev/null; then
if [ "$1" -gt 0 ]; then
echo "$1" > "$T/$TRY_FILE"
else
rm -f "$T/$TRY_FILE"
fi
umount "$T" 2>/dev/null
fi
rmdir "$T" 2>/dev/null
}
FS_SECT=$((FS_BLOCKS * (FS_BSIZE / 512)))
if [ "$FS_SECT" -lt $((PART_SECT - 2048)) ]; then
read_tries
if [ "$TRIES" -ge 3 ]; then
log "resize2fs failed $TRIES times; giving up (grow /data manually with resize2fs $P3)"
exit 0
fi
log "growing /data filesystem ($FS_SECT -> $PART_SECT sectors)"
e2fsck -f -p "$P3" >/dev/null 2>&1
RC=$?
@@ -80,9 +123,13 @@ if [ "$FS_SECT" -lt $((PART_SECT - 2048)) ]; then
log "e2fsck found errors (rc=$RC), NOT resizing"
exit 0
fi
resize2fs "$P3" >/dev/null 2>&1 \
&& log "/data grown to full size" \
|| log "resize2fs FAILED (will retry next boot)"
if resize2fs "$P3" >/dev/null 2>&1; then
log "/data grown to full size"
write_tries 0
else
log "resize2fs FAILED (attempt $((TRIES + 1)) of 3)"
write_tries $((TRIES + 1))
fi
fi
exit 0
+11 -1
View File
@@ -1,7 +1,17 @@
#!/bin/bash
# Cross-compiles the bench feeder for the factory board, borrowing the
# Yocto cross toolchain from the ulfius recipe work directory.
#
# Environment (defaults derive from this script's location, assuming
# the standard multi-repo checkout layout):
# FF_SRC_TOP checkout holding the sibling repos
# FF_BUILD_TOP Yocto build tree (default: $FF_SRC_TOP/forgefirm/build)
set -e
TC=/home/builder/dev/openglow-forgefirm/forgefirm/build/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15
SP="$(cd "$(dirname "$0")" && pwd)"
FF_SRC_TOP="${FF_SRC_TOP:-$(cd "$SP/../../.." && pwd)}"
FF_BUILD_TOP="${FF_BUILD_TOP:-$FF_SRC_TOP/forgefirm/build}"
TC="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15"
[ -d "$TC/recipe-sysroot" ] || { echo "toolchain not staged at $TC (run: bitbake ulfius)"; exit 1; }
"$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi/arm-fslc-linux-gnueabi-gcc" \
--sysroot="$TC/recipe-sysroot" \
-mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 \
+19 -10
View File
@@ -1,14 +1,22 @@
#!/bin/bash
# Cross-compiles forgectrl (the canonical repo, ../../../forgectrl -> synced
# to ~/dev/openglow-forgefirm/forgectrl) for the factory board, borrowing the
# Yocto cross toolchain + sysroot from the forgectrl recipe work directory
# (which carries ulfius and libjpeg). If that path ages out after a clean,
# regenerate it with: bitbake forgectrl. Run from PowerShell:
# wsl -d forge-yocto -- bash <path>/build-forgectrl.sh
# Cross-compiles forgectrl (the sibling repo) for the factory board,
# borrowing the Yocto cross toolchain + sysroot from the forgectrl
# recipe work directory (which carries ulfius and libjpeg). If that
# path ages out after a clean, regenerate it with: bitbake forgectrl.
#
# Environment (defaults derive from this script's location, assuming
# the standard multi-repo checkout layout):
# FF_SRC_TOP checkout holding the sibling repos
# FF_BUILD_TOP Yocto build tree (default: $FF_SRC_TOP/forgefirm/build)
set -e
TC=/home/builder/dev/openglow-forgefirm/forgefirm/build/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/forgectrl/0.1.0
SP="$(cd "$(dirname "$0")" && pwd)"
FF_SRC_TOP="${FF_SRC_TOP:-$(cd "$SP/../../.." && pwd)}"
FF_BUILD_TOP="${FF_BUILD_TOP:-$FF_SRC_TOP/forgefirm/build}"
TC="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/forgectrl/0.1.0"
[ -d "$TC/recipe-sysroot" ] || { echo "toolchain not staged at $TC (run: bitbake forgectrl)"; exit 1; }
export PATH="$TC/recipe-sysroot-native/usr/bin:$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi:$PATH"
cd /home/builder/dev/openglow-forgefirm/forgectrl
LOG=$(mktemp -t fcbuild.XXXXXX)
cd "$FF_SRC_TOP/forgectrl"
rm -rf build-arm
cmake -B build-arm \
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm \
@@ -16,7 +24,8 @@ cmake -B build-arm \
-DCMAKE_BUILD_TYPE=None \
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -O2 -g" \
"-DCMAKE_EXE_LINKER_FLAGS=--sysroot=$TC/recipe-sysroot" \
> /tmp/cmake-fc.log 2>&1
cmake --build build-arm -j8 > /tmp/fcbuild.log 2>&1 || { tail -30 /tmp/fcbuild.log; exit 1; }
> "$LOG" 2>&1
cmake --build build-arm -j8 >> "$LOG" 2>&1 || { tail -30 "$LOG"; exit 1; }
rm -f "$LOG"
echo BUILD-OK
file build-arm/forgectrl 2>/dev/null || ls build-arm/
+19 -7
View File
@@ -1,11 +1,22 @@
#!/bin/bash
# Cross-compiles grblHAL-glowforge (the canonical driver repo) for the
# factory board, borrowing the Yocto cross toolchain from the ulfius work
# directory. Run from PowerShell: wsl -d forge-yocto -- bash <path>.
# Cross-compiles grblHAL-glowforge (the sibling driver repo) for the
# factory board, borrowing the Yocto cross toolchain from the ulfius
# recipe work directory. Regenerate a cleaned toolchain path with:
# bitbake ulfius.
#
# Environment (defaults derive from this script's location, assuming
# the standard multi-repo checkout layout):
# FF_SRC_TOP checkout holding the sibling repos
# FF_BUILD_TOP Yocto build tree (default: $FF_SRC_TOP/forgefirm/build)
set -e
TC=/home/builder/dev/openglow-forgefirm/forgefirm/build/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15
SP="$(cd "$(dirname "$0")" && pwd)"
FF_SRC_TOP="${FF_SRC_TOP:-$(cd "$SP/../../.." && pwd)}"
FF_BUILD_TOP="${FF_BUILD_TOP:-$FF_SRC_TOP/forgefirm/build}"
TC="$FF_BUILD_TOP/tmp/work/cortexa9t2hf-neon-fslc-linux-gnueabi/ulfius/2.7.15"
[ -d "$TC/recipe-sysroot" ] || { echo "toolchain not staged at $TC (run: bitbake ulfius)"; exit 1; }
export PATH="$TC/recipe-sysroot-native/usr/bin:$TC/recipe-sysroot-native/usr/bin/arm-fslc-linux-gnueabi:$PATH"
cd /home/builder/dev/openglow-forgefirm/grblHAL-glowforge
LOG=$(mktemp -t gfbuild.XXXXXX)
cd "$FF_SRC_TOP/grblHAL-glowforge"
rm -rf build-arm
cmake -B build-arm \
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=arm \
@@ -13,7 +24,8 @@ cmake -B build-arm \
-DCMAKE_BUILD_TYPE=None \
"-DCMAKE_C_FLAGS=--sysroot=$TC/recipe-sysroot -mthumb -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a9 -O1 -g" \
"-DCMAKE_EXE_LINKER_FLAGS=--sysroot=$TC/recipe-sysroot" \
> /tmp/cmake-gf.log 2>&1
cmake --build build-arm -j8 > /tmp/gfbuild.log 2>&1 || { tail -30 /tmp/gfbuild.log; exit 1; }
> "$LOG" 2>&1
cmake --build build-arm -j8 >> "$LOG" 2>&1 || { tail -30 "$LOG"; exit 1; }
rm -f "$LOG"
echo BUILD-OK
file build-arm/grblHAL_glowforge 2>/dev/null || ls build-arm/
+2 -3
View File
@@ -61,9 +61,8 @@ probe_part () {
# 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
ROOT_DIR="/tmp/ffboot.probe.$$"
P_MOUNTED=yes
mkdir -p "$ROOT_DIR"
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
@@ -132,7 +131,7 @@ env_verify () {
}
set_env () {
SCRIPT="/tmp/ffboot.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
+56 -8
View File
@@ -52,7 +52,7 @@ stop_gf_services () {
# dd's exit status is captured via a file so a read failure is not
# masked by gzip succeeding on truncated input.
archive_dev () {
RC_FILE="/tmp/ffinstall.rc.$$"
RC_FILE=$(mktemp /tmp/ffinstall.rc.XXXXXX) || return 1
rm -f "$RC_FILE"
( dd if="$1" bs=1M 2>/dev/null; echo $? > "$RC_FILE" ) | gzip -1 > "$2" &
GZPID=$!
@@ -83,9 +83,8 @@ slot_probe () {
else
RD=$(sed -n "s|^/dev/mmcblk2p$1 \([^ ]*\).*|\1|p" /proc/mounts | head -n 1)
if [ -z "$RD" ]; then
RD="/tmp/ffinstall.probe.$$"
S_MOUNTED=yes
mkdir -p "$RD" || return 1
RD=$(mktemp -d /tmp/ffinstall.probe.XXXXXX) || return 1
mount -o ro -t ext4 "/dev/mmcblk2p$1" "$RD" 2>/dev/null \
|| { rmdir "$RD" 2>/dev/null; S_TYPE=unknown; return 0; }
fi
@@ -116,6 +115,25 @@ slot_desc () {
esac
}
# ver_lt A B: true when semantic version A < B (leading v ignored).
# Returns false on any non-numeric component (e.g. a dev datetime
# stamp) - no verdict means no downgrade prompt, never a refusal.
ver_lt () {
VA=${1#v}; VB=${2#v}
[ "$VA" = "$VB" ] && return 1
VI=1
while [ "$VI" -le 3 ]; do
A=$(echo "$VA" | cut -d. -f$VI)
B=$(echo "$VB" | cut -d. -f$VI)
A=${A:-0}; B=${B:-0}
case "$A$B" in *[!0-9]*) return 1 ;; esac
[ "$A" -lt "$B" ] && return 0
[ "$A" -gt "$B" ] && return 1
VI=$((VI + 1))
done
return 1
}
# Verified atomic env flip (all four variables, classic u-boot-tools script
# format first - that is what factory firmware ships - then libubootenv
# format, then per-variable writes; read-back verified in every case).
@@ -136,7 +154,7 @@ env_verify () {
}
set_env () {
SCRIPT="/tmp/ffinstall.env.$$"
SCRIPT=$(mktemp /tmp/ffinstall.env.XXXXXX) || return 1
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
@@ -257,12 +275,43 @@ else
fi
# --- verify signature ---------------------------------------------------------
KEYFILE="/tmp/forgefirm.pub.$$"
KEYFILE=$(mktemp /tmp/forgefirm.pub.XXXXXX) || die "cannot create temp file"
printf "$PUBKEY" > "$KEYFILE"
[ "$(wc -c < "$KEYFILE")" = "32" ] || die "embedded public key corrupt"
echo -e "${ASTERISK}Verifying firmware signature:"
fwup -V -i "$FW_FILE" -p "$KEYFILE" || { rm -f "$KEYFILE"; die "signature verification FAILED - refusing to install"; }
fwup -m -i "$FW_FILE" | grep meta-version
# --- archive identity + downgrade gate ----------------------------------------
META=$(fwup -m -i "$FW_FILE")
M_PRODUCT=$(echo "$META" | sed -n 's/^meta-product="\(.*\)"$/\1/p')
M_PLATFORM=$(echo "$META" | sed -n 's/^meta-platform="\(.*\)"$/\1/p')
M_VERSION=$(echo "$META" | sed -n 's/^meta-version="\(.*\)"$/\1/p')
[ "$M_PRODUCT" = "ForgeFIRM firmware" ] \
|| { rm -f "$KEYFILE"; die "archive product is '$M_PRODUCT', not ForgeFIRM firmware - wrong archive"; }
[ "$M_PLATFORM" = "glowforge" ] \
|| { rm -f "$KEYFILE"; die "archive platform is '$M_PLATFORM', not glowforge - wrong archive"; }
echo -e "${ASTERISK}Archive: $M_PRODUCT $M_VERSION ($M_PLATFORM)"
# A validly signed OLDER release must never install silently; downgrades
# need an explicit yes (rollback stays possible, just deliberate).
INSTALLED=""
for S in 1 2; do
slot_probe "$S"
if [ "$S_TYPE" = "forgefirm" ] && [ -n "$S_VER" ]; then
if [ -z "$INSTALLED" ] || ver_lt "$INSTALLED" "$S_VER"; then
INSTALLED="$S_VER"
fi
fi
done
if [ -n "$INSTALLED" ] && ver_lt "$M_VERSION" "$INSTALLED"; then
echo -e "${ASTERISK}This archive ($M_VERSION) is OLDER than the installed ForgeFIRM ($INSTALLED)."
read -n1 -p "Install the downgrade anyway? [y/N] " YN
echo
case "$YN" in
y|Y) ;;
*) rm -f "$KEYFILE"; die "downgrade declined" ;;
esac
fi
# --- apply to the inactive slot -----------------------------------------------
echo -e "${ASTERISK}Writing ForgeFIRM to slot $TARGET (/dev/mmcblk2p$TARGET):"
@@ -274,8 +323,7 @@ fwup -a -d "/dev/mmcblk2p$TARGET" -i "$FW_FILE" -t "$TASK" -p "$KEYFILE" \
rm -f "$KEYFILE"
# --- post-write verify --------------------------------------------------------
MP="/tmp/ffinstall.verify.$$"
mkdir -p "$MP"
MP=$(mktemp -d /tmp/ffinstall.verify.XXXXXX) || die "cannot create temp dir"
mount -o ro -t ext4 "/dev/mmcblk2p$TARGET" "$MP" || die "new rootfs does not mount"
NEWVER=$(cat "$MP/etc/forgefirm-version" 2>/dev/null)
[ -n "$NEWVER" ] || { umount "$MP"; die "new rootfs has no ForgeFIRM version stamp"; }
+4 -3
View File
@@ -72,10 +72,11 @@ EOF
if [ -n "$KEY" ]; then
[ -f "$KEY" ] || { echo "ERROR: key '$KEY' not found" >&2; exit 1; }
"$FWUP" -S -s "$KEY" -i "$WORK/unsigned.fw" -o "$OUT"
# The post-sign self-check is mandatory: packing without it means a
# release could ship with a signature nothing ever verified.
PUB="${KEY%.priv}.pub"
if [ -f "$PUB" ]; then
"$FWUP" -V -i "$OUT" -p "$PUB" || { echo "ERROR: signature self-check failed" >&2; exit 1; }
fi
[ -f "$PUB" ] || { echo "ERROR: public key '$PUB' not found - cannot self-check the signature; refusing to pack unverified" >&2; exit 1; }
"$FWUP" -V -i "$OUT" -p "$PUB" || { echo "ERROR: signature self-check failed" >&2; exit 1; }
echo "signed: $OUT"
else
cp "$WORK/unsigned.fw" "$OUT"
+9 -1
View File
@@ -46,7 +46,11 @@ for ARG in "$@"; do
--dev) MODE=dev ;;
--publish) PUBLISH=1 ;;
-*) die "unknown option $ARG" ;;
*) VERSION="$ARG" ;;
*)
[ -z "$VERSION" ] \
|| die "multiple versions given ('$VERSION' and '$ARG')"
VERSION="$ARG"
;;
esac
done
@@ -163,6 +167,10 @@ if [ -n "${FWUP_COMPAT:-}" ]; then
|| { rm -f "$RAW"; die "factory-era fwup rejects the archive"; }
rm -f "$RAW"
echo "factory-era fwup verification OK"
elif [ "$MODE" = release ] && [ -z "${FWUP_COMPAT_SKIP:-}" ]; then
# The factory-compat guarantee is a release property: a public release
# must not skip it silently. FWUP_COMPAT_SKIP=1 bypasses deliberately.
die "FWUP_COMPAT not set - factory-era verification is required for a release (set FWUP_COMPAT_SKIP=1 to bypass deliberately)"
else
warn "FWUP_COMPAT not set - factory-era verification skipped"
fi