acceptance: a later FAIL blocks inheritance, ffboot is a component, the units fallback is valid

The inheritance walk skipped every record that was not a PASS on the
current fingerprint, so a FAIL or ERROR recorded after a PASS on the
same image was stepped over and the older PASS inherited into the next
campaign. The newest record on the fingerprint now decides: a PASS is
inherited, a FAIL or ERROR blocks it (reason failed-since), an ABORTED
run says nothing. Unit tests for all three orders.

ffboot, the tool that rewrites the boot environment on every install and
slot switch, was packaged from scripts/ outside every fingerprint. It
now lives in the recipe's files and the recipe inherits the manifest
class; the tree manifest tool fingerprints file components the same
way, and the update tests cover the component.

forgectrl.settings-bounds fell back to ui_units=mm, which the whitelist
refuses, so the always-required test failed on a fresh machine; the
fallback is metric.
This commit is contained in:
ScottW514
2026-09-02 08:00:51 -04:00
parent 0ca6c4be9f
commit 0e37b0812e
7 changed files with 92 additions and 8 deletions
-236
View File
@@ -1,236 +0,0 @@
#!/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
+33
View File
@@ -43,6 +43,11 @@ RECIPES = [
("python3-gfhardware", "meta-glowforge-bsp/recipes-devtools/python/python3-gfhardware.bb", "meta-openglow"),
("python3-gfutilities", "meta-openglow-core/recipes-devtools/python/python3-gfutilities_git.bb", "meta-openglow"),
]
# Components built from files in a layer (FORGEFIRM_MANIFEST_SRC in the
# recipe): (component, directory relative to the repo, layer, recipe).
FILE_COMPONENTS = [
("ffboot", "meta-forgefirm/recipes-forgefirm/ffboot/files", "forgefirm", "ffboot.bb"),
]
CONTENT_LAYERS = {"meta-forgefirm": ("forgefirm", "meta-forgefirm"),
"meta-glowforge-bsp": ("meta-openglow", "meta-glowforge-bsp"),
"meta-openglow-core": ("meta-openglow", "meta-openglow-core")}
@@ -138,6 +143,29 @@ def ls_tree(repo, rev, url, cache, prefix, files):
ls_tree(sub_repo, obj, sub_url, cache, prefix + path + "/", files)
def dir_files(path):
"""[path, blob-id] per regular file under path, relative paths, the
same walk and hashing as forgefirm-manifest.bbclass (hash-object, so
the ids compare with tree ids)."""
paths = []
for root, dirs, fns in os.walk(path):
dirs[:] = sorted(x for x in dirs if x not in (".git", "__pycache__"))
for fn in fns:
if fn.endswith((".pyc", ".pyo")):
continue
p = os.path.join(root, fn)
if os.path.isfile(p) and not os.path.islink(p):
paths.append(os.path.relpath(p, path).replace(os.sep, "/"))
paths.sort()
if not paths:
return []
# absolute paths: hash-object --stdin-paths resolves relative ones against
# the enclosing repository's top level, not the cwd
ids = git(["hash-object", "--stdin-paths"], cwd=path,
input=(chr(10).join(os.path.join(path, p) for p in paths) + chr(10)).encode()).decode().split()
return [[p, i] for p, i in zip(paths, ids)]
def layer_content(path):
out = git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], cwd=path)
paths = sorted(set(p.decode("utf-8", "replace") for p in out.split(b"\0") if p))
@@ -176,6 +204,11 @@ def main(argv=None):
files.sort()
components[name] = {"srcrev": rev, "source": url, "files": files, "recipes": [os.path.basename(rel)]}
print("%s: %s (%d files)" % (name, rev[:12], len(files)), file=sys.stderr)
for name, rel, layer, recipe in FILE_COMPONENTS:
base = REPO if layer == "forgefirm" else args.meta_openglow
files = dir_files(os.path.join(base, rel))
components[name] = {"srcrev": None, "source": "files", "files": files, "recipes": [recipe]}
print("%s: files (%d files)" % (name, len(files)), file=sys.stderr)
ksrc = args.kernel_srcrev
if not ksrc: