From c0d3d7c79296253fec05098f8a65f4256cbb3388 Mon Sep 17 00:00:00 2001 From: rUv Date: Sat, 27 Jun 2026 13:21:05 -0400 Subject: [PATCH] chore(firmware): add release guard against stale-sdkconfig partition mismatch (#1194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While cutting v0.8.3-esp32, an incremental 8MB build reused a leftover generated `sdkconfig` and silently linked the 4MB dual-OTA partition layout (no spiffs, ota_1 @ 0x1F0000) — the would-be released `partition-table.bin` did not match the 8MB `partitions_display.csv` it claimed. scripts/firmware-release-guard.sh regenerates the expected partition table from the CSV the named flash-size variant must use and byte-compares it to the built `partition-table.bin`, and cross-checks flash size in flasher_args.json. Fails closed so a release pipeline can't ship a mismatched table. Usage: scripts/firmware-release-guard.sh <8mb|4mb> Claude-Session: https://claude.ai/code/session_01AgpTcBLRJ32hUsKWxDXf36 --- scripts/firmware-release-guard.sh | 94 +++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 scripts/firmware-release-guard.sh diff --git a/scripts/firmware-release-guard.sh b/scripts/firmware-release-guard.sh new file mode 100644 index 00000000..f4efc4c5 --- /dev/null +++ b/scripts/firmware-release-guard.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# +# firmware-release-guard.sh — guard against shipping firmware built from a +# stale generated `sdkconfig` (the v0.8.3-esp32 release bug). +# +# Symptom it catches: an incremental build reuses a leftover `sdkconfig` +# instead of `sdkconfig.defaults`, so an "8MB" build silently links the 4MB +# dual-OTA partition layout (no spiffs, ota_1 @ 0x1F0000) and the released +# `partition-table.bin` does not match the flash-size variant it claims to be. +# +# What it does: for the named flash-size variant, regenerate the EXPECTED +# partition table from the partition CSV that variant must use, and byte-compare +# it against the freshly built `partition-table.bin`. Also cross-checks the +# flash size recorded in the build's `flasher_args.json`. Exits non-zero on any +# mismatch so a release pipeline fails closed. +# +# Usage: +# scripts/firmware-release-guard.sh <8mb|4mb> +# +# Example: +# scripts/firmware-release-guard.sh 8mb firmware/esp32-csi-node/build +# +set -euo pipefail + +VARIANT="${1:-}" +BUILD_DIR="${2:-}" + +if [[ -z "$VARIANT" || -z "$BUILD_DIR" ]]; then + echo "usage: $0 <8mb|4mb> " >&2 + exit 2 +fi + +# Firmware project root (this script lives in /scripts). +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FW_DIR="$SCRIPT_DIR/../firmware/esp32-csi-node" + +case "$VARIANT" in + 8mb) EXPECT_CSV="partitions_display.csv"; EXPECT_FLASH="8MB" ;; + 4mb) EXPECT_CSV="partitions_4mb.csv"; EXPECT_FLASH="4MB" ;; + *) echo "ERROR: unknown variant '$VARIANT' (want 8mb|4mb)" >&2; exit 2 ;; +esac + +BUILT_PT="$BUILD_DIR/partition_table/partition-table.bin" +CSV_PATH="$FW_DIR/$EXPECT_CSV" + +[[ -f "$BUILT_PT" ]] || { echo "ERROR: built partition table not found: $BUILT_PT" >&2; exit 1; } +[[ -f "$CSV_PATH" ]] || { echo "ERROR: expected CSV not found: $CSV_PATH" >&2; exit 1; } + +# Locate the ESP-IDF partition table generator. +GEN="${IDF_PATH:-}/components/partition_table/gen_esp32part.py" +if [[ ! -f "$GEN" ]]; then + GEN="C:/Users/ruv/esp/v5.4/esp-idf/components/partition_table/gen_esp32part.py" +fi +[[ -f "$GEN" ]] || { echo "ERROR: gen_esp32part.py not found (set IDF_PATH)" >&2; exit 1; } + +PY="${PYTHON:-python}" +command -v "$PY" >/dev/null 2>&1 || PY="C:/Espressif/tools/python/v5.4/venv/Scripts/python.exe" + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +EXPECT_PT="$TMP/expected-partition-table.bin" + +# Regenerate the expected table from the CSV this variant must use. +"$PY" "$GEN" --quiet "$CSV_PATH" "$EXPECT_PT" + +fail=0 + +if ! cmp -s "$EXPECT_PT" "$BUILT_PT"; then + echo "FAIL: built partition table does not match $EXPECT_CSV for the $VARIANT variant." >&2 + echo " The build likely reused a stale sdkconfig. Decoded built table:" >&2 + "$PY" "$GEN" "$BUILT_PT" 2>/dev/null | grep -vE '^#|^Parsing|^Verifying' | sed 's/^/ /' >&2 + fail=1 +fi + +# Cross-check the flash size the build actually targeted. +FA="$BUILD_DIR/flasher_args.json" +if [[ -f "$FA" ]]; then + GOT_FLASH="$("$PY" - "$FA" <<'PYEOF' +import json,sys +with open(sys.argv[1]) as f: d=json.load(f) +print(d.get("flash_settings",{}).get("flash_size","")) +PYEOF +)" + if [[ "$GOT_FLASH" != "$EXPECT_FLASH" ]]; then + echo "FAIL: flasher_args.json flash_size='$GOT_FLASH', expected '$EXPECT_FLASH'." >&2 + fail=1 + fi +fi + +if [[ "$fail" -ne 0 ]]; then + exit 1 +fi + +echo "OK: $VARIANT firmware build matches $EXPECT_CSV (flash_size=$EXPECT_FLASH)."