Publish one ordinary ESP32 Full image per board role
This commit is contained in:
1 parent
b0b9e53dd7
commit
df19760c85
6 files changed
+311
-44
No files matched your search
+126
-28
@@ -2428,7 +2428,8 @@ get_unified_full_infrastructure_target() {
|
||||
|
||||
# These plain Full recipes retain more routing capacity than their MQTT
|
||||
# siblings (T-Beam flood rules / room neighbors, TLora repeater neighbors).
|
||||
# They are intentional alternatives, not duplicate Full artifacts.
|
||||
# Keep them available for an explicit exact-identity build; the ordinary
|
||||
# release chooses the MQTT observer for these board/role groups.
|
||||
case "${mqtt_base,,}" in
|
||||
tbeam_sx1262_repeater|tbeam_sx1276_repeater|\
|
||||
tbeam_sx1262_room_server|tbeam_sx1276_room_server|\
|
||||
@@ -5412,8 +5413,10 @@ configure_effective_build_profile() {
|
||||
if [ "$EXACT_IDENTITY_FULL_BUILD" = "1" ]; then
|
||||
if [ "$command_name" != "build-firmware" ] \
|
||||
|| [ "${#RESOLVED_BUILD_TARGETS[@]}" -ne 1 ] \
|
||||
|| ! is_esp32_canonical_full_release_target "$target"; then
|
||||
echo "--full-exact requires one canonical ESP32 infrastructure target."
|
||||
|| [ "${PIO_ENV_PLATFORM_BY_NAME[$target]:-}" != ESP32_PLATFORM ] \
|
||||
|| ! supports_esp32_full_build "$target" \
|
||||
|| is_esp32_companion_build "$target"; then
|
||||
echo "--full-exact requires one supported ESP32 infrastructure target."
|
||||
return 1
|
||||
fi
|
||||
BUILD_PROFILE_EFFECTIVE="full"
|
||||
@@ -5810,6 +5813,69 @@ get_esp32_full_profile_target() {
|
||||
echo "$target"
|
||||
}
|
||||
|
||||
# The ordinary release offers one Full image per physical board and role.
|
||||
# Keep the exact environment names for direct --full-exact builds and for
|
||||
# legacy/partition migration packages; this selection applies only to bulk
|
||||
# Full passes. Include the board in the key so similarly named recipes cannot
|
||||
# collapse distinct hardware.
|
||||
get_ordinary_full_group_key() {
|
||||
local target=$1
|
||||
local base=${target%_}
|
||||
local role_base=$base
|
||||
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$target]:-}" = ESP32_PLATFORM ] || return 1
|
||||
supports_esp32_full_build "$target" || return 1
|
||||
case "${base,,}" in
|
||||
*_repeater_observer_mqtt|*_room_server_observer_mqtt)
|
||||
role_base=${base%_observer_mqtt} ;;
|
||||
*_repeater_bridge_espnow)
|
||||
role_base=${base%_bridge_espnow} ;;
|
||||
*_repeater|*_room_server) ;;
|
||||
*) role_base=$base ;;
|
||||
esac
|
||||
printf '%s|%s\n' "${PIO_ENV_BOARD_BY_NAME[$target]:-$target}" "${role_base,,}"
|
||||
}
|
||||
|
||||
get_ordinary_full_priority() {
|
||||
local target=$1
|
||||
local base=${target%_}
|
||||
case "${base,,}" in
|
||||
*_observer_mqtt) echo 20 ;;
|
||||
*_bridge_espnow) echo 10 ;;
|
||||
*)
|
||||
# G2's deployed Full identity is the observer. Other audited plain
|
||||
# targets keep their own identity, with observer sources where needed.
|
||||
if is_ordinary_partition_migration_full_target "$target" \
|
||||
|| is_esp32_full_only_bulk_target "$target"; then
|
||||
echo 30
|
||||
else
|
||||
echo 0
|
||||
fi ;;
|
||||
esac
|
||||
}
|
||||
|
||||
select_ordinary_full_targets() {
|
||||
local target candidate key priority
|
||||
local -a keys=()
|
||||
local -A choice=() best_priority=()
|
||||
for target in "$@"; do
|
||||
candidate=$(get_esp32_full_profile_target "$target")
|
||||
key=$(get_ordinary_full_group_key "$candidate") || continue
|
||||
priority=$(get_ordinary_full_priority "$candidate")
|
||||
if [ -z "${choice[$key]+x}" ]; then
|
||||
keys+=("$key")
|
||||
choice[$key]=$candidate
|
||||
best_priority[$key]=$priority
|
||||
elif [ "$priority" -gt "${best_priority[$key]}" ]; then
|
||||
choice[$key]=$candidate
|
||||
best_priority[$key]=$priority
|
||||
fi
|
||||
done
|
||||
for key in "${keys[@]}"; do
|
||||
printf '%s\n' "${choice[$key]}"
|
||||
done
|
||||
}
|
||||
|
||||
has_esp32_full_profile() {
|
||||
local target
|
||||
local candidate=""
|
||||
@@ -5845,6 +5911,15 @@ run_full_esp32_profile() {
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
full_profile_target=$(get_esp32_full_profile_target "$target")
|
||||
# An earlier exact-identity pass already emitted this release's chosen
|
||||
# Full image. Do this before resolving a plain target to its observer
|
||||
# source, which would otherwise emit a second OTA identity.
|
||||
if { [ "${PARTITION_MIGRATION_FULL_PROFILE_ACTIVE:-0}" = 1 ] \
|
||||
&& is_ordinary_partition_migration_full_target "$full_profile_target"; } \
|
||||
|| { [ "${FULL_ONLY_EXACT_PROFILE_ACTIVE:-0}" = 1 ] \
|
||||
&& is_esp32_full_only_bulk_target "$full_profile_target"; }; then
|
||||
continue
|
||||
fi
|
||||
# Use the same exact-board choice as single-target auto builds, including
|
||||
# historical trailing-underscore recipes such as Heltec T190.
|
||||
mqtt_target=$(get_unified_full_infrastructure_target "$full_profile_target") || mqtt_target=""
|
||||
@@ -5866,21 +5941,6 @@ run_full_esp32_profile() {
|
||||
if [ -z "$full_target" ] || ! supports_esp32_full_build "$full_target"; then
|
||||
continue
|
||||
fi
|
||||
# The canonical normal-target FULL artifact was already emitted by the
|
||||
# partition-migration pass. Do not rebuild it as an identical fallback.
|
||||
# Observer identities are intentionally not skipped: existing observer
|
||||
# nodes still need their exact legacy mOTA asset during the transition.
|
||||
if [ "${PARTITION_MIGRATION_FULL_PROFILE_ACTIVE:-0}" = "1" ] \
|
||||
&& is_esp32_partition_migration_full_target "$full_target"; then
|
||||
continue
|
||||
fi
|
||||
# The logging matrix has already emitted these approved targets under
|
||||
# their exact environment names. Do not replace that identity with a
|
||||
# generic MQTT/unified sibling in this legacy resolution pass.
|
||||
if [ "${FULL_ONLY_EXACT_PROFILE_ACTIVE:-0}" = "1" ] \
|
||||
&& is_esp32_full_only_bulk_target "$full_target"; then
|
||||
continue
|
||||
fi
|
||||
if [ -z "${seen_full_targets[$full_target]+x}" ]; then
|
||||
full_targets+=("$full_target")
|
||||
seen_full_targets["$full_target"]=1
|
||||
@@ -6029,9 +6089,23 @@ run_full_esp32_build_targets() {
|
||||
local profile_mode=$1
|
||||
shift
|
||||
local targets=("$@")
|
||||
local ordinary_full_targets=()
|
||||
local full_only_targets=()
|
||||
local target
|
||||
local profile_name="FULL unified"
|
||||
local build_status=0
|
||||
local pass_status=0
|
||||
local FULL_ONLY_EXACT_PROFILE_ACTIVE=0
|
||||
local PARTITION_MIGRATION_FULL_PROFILE_ACTIVE=0
|
||||
|
||||
if [ "${SINGLE_TARGET_FULL_BUILD:-0}" = 1 ]; then
|
||||
# A named single-target build is an explicit operator choice. Keep its
|
||||
# historical observer/bridge resolution rather than applying bulk release
|
||||
# identity selection to it.
|
||||
ordinary_full_targets=("${targets[@]}")
|
||||
else
|
||||
mapfile -t ordinary_full_targets < <(select_ordinary_full_targets "${targets[@]}")
|
||||
fi
|
||||
|
||||
if [ "$profile_mode" = "fallback" ]; then
|
||||
profile_name="FULL logging fallback"
|
||||
@@ -6039,15 +6113,32 @@ run_full_esp32_build_targets() {
|
||||
|
||||
LOGGING_MATRIX_FAILURES=()
|
||||
if [ "$profile_mode" = "fallback" ]; then
|
||||
run_full_esp32_profile "${profile_name}-only build" "fallback" "${targets[@]}"
|
||||
run_full_esp32_profile "${profile_name}-only build" "fallback" "${ordinary_full_targets[@]}"
|
||||
build_status=$?
|
||||
else
|
||||
run_full_esp32_profile "${profile_name} build" "unified" "${targets[@]}"
|
||||
if [ "${SINGLE_TARGET_FULL_BUILD:-0}" != 1 ]; then
|
||||
for target in "${ordinary_full_targets[@]}"; do
|
||||
if is_esp32_full_only_bulk_target "$target"; then
|
||||
full_only_targets+=("$target")
|
||||
fi
|
||||
done
|
||||
run_full_only_esp32_profile "${full_only_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
run_partition_migration_full_esp32_profile "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
FULL_ONLY_EXACT_PROFILE_ACTIVE=1
|
||||
PARTITION_MIGRATION_FULL_PROFILE_ACTIVE=1
|
||||
fi
|
||||
run_full_esp32_profile "${profile_name} build" "unified" "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
run_full_esp32_profile "FULL logging fallback build" "fallback" "${targets[@]}"
|
||||
run_full_esp32_profile "FULL logging fallback build" "fallback" "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
@@ -6070,6 +6161,7 @@ run_logging_matrix_build_targets() {
|
||||
local target
|
||||
local standard_targets=()
|
||||
local full_only_targets=()
|
||||
local ordinary_full_targets=()
|
||||
local original_meshdebug_override=$MESHDEBUG_OVERRIDE
|
||||
local original_packet_logging_override=$PACKET_LOGGING_OVERRIDE
|
||||
local original_mqtt_bridge_override=$MQTT_BRIDGE_OVERRIDE
|
||||
@@ -6094,6 +6186,7 @@ run_logging_matrix_build_targets() {
|
||||
fi
|
||||
LOGGING_MATRIX_FAILURES=()
|
||||
LOGGING_MATRIX_DEFERRED_TARGETS=()
|
||||
mapfile -t ordinary_full_targets < <(select_ordinary_full_targets "${targets[@]}")
|
||||
if [ "${REQUIRE_OTA_UPDATES:-0}" = "1" ]; then
|
||||
printf '%s\n' "${OTA_EXCLUDED_TARGETS[@]}" > "${OUTPUT_DIR}/ota-excluded-targets.txt"
|
||||
fi
|
||||
@@ -6102,14 +6195,11 @@ run_logging_matrix_build_targets() {
|
||||
|
||||
for target in "${targets[@]}"; do
|
||||
if is_esp32_full_only_bulk_target "$target"; then
|
||||
full_only_exact_count=$((full_only_exact_count + 1))
|
||||
# Full companion environments already select their complete profile in
|
||||
# build_firmware, so keep them in the ordinary pass to preserve their
|
||||
# established companion-specific recipe.
|
||||
if is_companion_radio_full_target "$target"; then
|
||||
standard_targets+=("$target")
|
||||
else
|
||||
full_only_targets+=("$target")
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
@@ -6129,6 +6219,14 @@ run_logging_matrix_build_targets() {
|
||||
fi
|
||||
done
|
||||
|
||||
for target in "${ordinary_full_targets[@]}"; do
|
||||
if is_esp32_full_only_bulk_target "$target" \
|
||||
&& ! is_companion_radio_full_target "$target"; then
|
||||
full_only_targets+=("$target")
|
||||
full_only_exact_count=$((full_only_exact_count + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Profile 1/2: building ${#standard_targets[@]} standard target(s); ${merged_usb_logging_count} embed runtime-controlled USB logging in the ordinary artifact."
|
||||
if [ "$constrained_merged_logging_count" -gt 0 ]; then
|
||||
echo "Keeping verbose MESH_DEBUG off for ${constrained_merged_logging_count} size-constrained STM32 target(s); packet logging remains available at runtime."
|
||||
@@ -6139,7 +6237,7 @@ run_logging_matrix_build_targets() {
|
||||
if [ "$full_only_exact_count" -gt 0 ]; then
|
||||
echo "Publishing ${full_only_exact_count} audited ESP32 target(s) as their exact-identity FULL release only; explicit --standard remains available for recovery."
|
||||
fi
|
||||
for target in "${targets[@]}"; do
|
||||
for target in "${ordinary_full_targets[@]}"; do
|
||||
if is_ordinary_partition_migration_full_target "$target"; then
|
||||
partition_migration_full_count=$((partition_migration_full_count + 1))
|
||||
fi
|
||||
@@ -6166,19 +6264,19 @@ run_logging_matrix_build_targets() {
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
run_partition_migration_full_esp32_profile "${targets[@]}"
|
||||
run_partition_migration_full_esp32_profile "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
PARTITION_MIGRATION_FULL_PROFILE_ACTIVE=1
|
||||
|
||||
FULL_ONLY_EXACT_PROFILE_ACTIVE=1
|
||||
run_full_esp32_profile "FULL unified pass" "unified" "${targets[@]}"
|
||||
run_full_esp32_profile "FULL unified pass" "unified" "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
run_full_esp32_profile "FULL logging fallback pass" "fallback" "${targets[@]}"
|
||||
run_full_esp32_profile "FULL logging fallback pass" "fallback" "${ordinary_full_targets[@]}"
|
||||
pass_status=$?
|
||||
if [ "$pass_status" -eq 130 ]; then return 130; fi
|
||||
if [ "$pass_status" -ne 0 ]; then build_status=1; fi
|
||||
|
||||
@@ -46,15 +46,22 @@ an old or failed artifact without a verified sidecar.
|
||||
For a single ESP32 repeater or room-server target, `auto` selects its matching
|
||||
combined Full MQTT/USB/WiFi image when that image covers the plain Full recipe.
|
||||
The direct observer and Full-matrix paths use the same `full-usb-wifi-ota`
|
||||
artifact and runtime output controls. Plain Full T-Beam SX1262/SX1276 profiles
|
||||
and the TLora V2 repeater remain alternatives because their MQTT siblings have
|
||||
lower routing-table capacities.
|
||||
artifact and runtime output controls. The ordinary bulk release publishes one
|
||||
Full OTA identity per exact ESP32 board and role. Audited plain targets keep
|
||||
their identity while compiling the combined observer/ESP-NOW sources; G2 keeps
|
||||
its deployed observer identity. T-Beam SX1262/SX1276 and TLora V2 choose the
|
||||
observer in the ordinary release. Their plain Full recipes remain available
|
||||
as explicit alternatives with different routing capacities. Build one with
|
||||
`bash build.sh build-firmware <target> --full-exact`, then deliberately install
|
||||
it over LoRa with `ota pull <MID8> flash` and `ota install` on compatible
|
||||
hardware and partition layouts.
|
||||
|
||||
Standard and reduced OTA images, including images that fit the 1.25 MiB
|
||||
portable application slot, remain available and remain in the standard matrix
|
||||
pass. Select `--build-profile standard` to retain the original target and
|
||||
partition contract. Moving from a plain target to its MQTT sibling changes the
|
||||
LoRa OTA target identity; existing nodes need an appropriate wired migration.
|
||||
LoRa OTA target identity; a partition layout change needs its matching
|
||||
migration step.
|
||||
For the normal-role partition migrations, the matrix also publishes a
|
||||
canonical Full image under the normal target identity. Install its matching
|
||||
merged image over USB once; mOTA rejects the incompatible partition signature
|
||||
|
||||
+13
-1
@@ -205,7 +205,9 @@ long ago it was seen. The fit marker:
|
||||
in-place delta. A matched external-QSPI nRF52 can accept that full codec.
|
||||
- **[rescue]** - an installable in-place nRF52 delta for the same target, but this running firmware has no
|
||||
valid app-side EndF. It requires the explicit rescue download and install flow below.
|
||||
- **[name]** - a different known board or role (for example `[ProMicro_companion_radio_usb]`). Don't install it.
|
||||
- **[name]** - a different known board or role (for example `[ProMicro_companion_radio_usb]`).
|
||||
Install it only if you deliberately chose a different role for the same hardware; the installer
|
||||
still checks the hardware identity and partition layout.
|
||||
- **[?]** - can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build).
|
||||
|
||||
Run it again after a few seconds - discovery happens in the background, so the list fills in. Nothing is
|
||||
@@ -233,6 +235,16 @@ host folder - nothing is staged on this node. That's how you grab an **exact cop
|
||||
firmware** off the mesh (to a `.mota` file) so you can later build a *delta* against firmware you don't
|
||||
otherwise have. (`ota get` is an alias.)
|
||||
|
||||
**Deliberate alternate image.** An operator can choose a different target ID
|
||||
for the same physical board by selecting its exact manifest ID from `ota ls`,
|
||||
then running `ota pull <MID8> flash` and `ota install`. The target ID controls
|
||||
which image is discovered and fetched; it does not lock manual installs to
|
||||
the current role. The hardware identity, codec, image hash, and partition
|
||||
checks still run. Automatic updates follow the firmware's selected target
|
||||
(or its declared one-way successor), so use `ota config autofetch off` and
|
||||
`ota config autoinstall off` if you want to keep an alternate image without
|
||||
automatic convergence.
|
||||
|
||||
**`validate` (warm-start, advanced).** Capturing a full image over the radio is slow. If you have a
|
||||
*similar* build on the computer (e.g. a fresh recompile of the same firmware), run motatool with
|
||||
`--seed <that.mota>` and add **`validate`**: the node fetches just the target's block fingerprints, keeps
|
||||
|
||||
@@ -97,6 +97,47 @@ def build_migration_artifacts(work: Path, version: str, short_source: str,
|
||||
raise RuntimeError(f"migration utility {bridge} failed; see {log}")
|
||||
|
||||
|
||||
def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
"""Keep one ESP32 Full OTA identity per board/role in the public matrix.
|
||||
|
||||
Explicit alternate images and old partition identities still belong in
|
||||
their separately built migration packages or direct --full-exact builds.
|
||||
This also removes stale sibling artifacts when a matrix build is resumed.
|
||||
"""
|
||||
chosen: dict[str, tuple[int, dict]] = {}
|
||||
passthrough: list[dict] = []
|
||||
for record in records:
|
||||
manifest = record["manifest"]
|
||||
if manifest["platform"] != "ESP32_PLATFORM" or manifest["build_profile"] != "full":
|
||||
passthrough.append(record)
|
||||
continue
|
||||
target = manifest["target"]
|
||||
base = target.rstrip("_")
|
||||
kind = "plain"
|
||||
if base.lower().endswith("_observer_mqtt"):
|
||||
base = base[:-len("_observer_mqtt")]
|
||||
kind = "observer"
|
||||
elif base.lower().endswith("_bridge_espnow"):
|
||||
base = base[:-len("_bridge_espnow")]
|
||||
kind = "bridge"
|
||||
key = base.lower()
|
||||
# G2's observer is the deployed Full identity. The other listed
|
||||
# observer/bridge recipes offer features that their plain images do
|
||||
# not combine, so keep the richer ordinary release choice.
|
||||
special = key in {
|
||||
"station_g2_repeater", "station_g2_room_server",
|
||||
"tbeam_sx1262_repeater", "tbeam_sx1276_repeater",
|
||||
"tbeam_sx1262_room_server", "tbeam_sx1276_room_server",
|
||||
"lilygo_tlora_v2_1_1_6_repeater",
|
||||
"meshadventurer_sx1262_repeater", "meshadventurer_sx1268_repeater",
|
||||
}
|
||||
priority = {"observer": 20, "bridge": 10, "plain": 0 if special else 30}[kind]
|
||||
old = chosen.get(key)
|
||||
if old is None or priority > old[0]:
|
||||
chosen[key] = (priority, record)
|
||||
return passthrough + [item[1] for item in chosen.values()]
|
||||
|
||||
|
||||
def stage_release(work: Path, migration_work: Path, destination: Path, version: str,
|
||||
source: str) -> None:
|
||||
if destination.exists():
|
||||
@@ -105,10 +146,7 @@ def stage_release(work: Path, migration_work: Path, destination: Path, version:
|
||||
short_source = source[:8]
|
||||
tooling_source = git("rev-parse", "HEAD")
|
||||
artifact_version = f"{version}-{short_source}"
|
||||
records = [record for record in collect_artifacts(work, artifact_version)
|
||||
if not (record["manifest"]["build_profile"] == "full"
|
||||
and record["manifest"]["target"] in {
|
||||
"Station_G2_repeater", "Station_G2_room_server"})]
|
||||
records = select_ordinary_full_records(collect_artifacts(work, artifact_version))
|
||||
if not records:
|
||||
raise ValueError("firmware matrix produced no qualified artifacts")
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@ assert_auto_unified() (
|
||||
}
|
||||
run_command build-firmware >/dev/null
|
||||
[ "${built[*]}" = "$expected" ] || fail "auto emitted duplicate Full artifacts"
|
||||
built=()
|
||||
run_full_esp32_build_targets all "$plain" "$expected" >/dev/null
|
||||
[ "${built[*]}" = "$expected" ] || fail "matrix emitted duplicate Full artifacts"
|
||||
)
|
||||
|
||||
assert_auto_unified heltec_v4_r8_repeater heltec_v4_r8_repeater_observer_mqtt
|
||||
@@ -62,6 +59,28 @@ assert_auto_unified Station_G2_room_server Station_G2_room_server_observer_mqtt
|
||||
run_command build-firmware >/dev/null
|
||||
)
|
||||
|
||||
# Operators may still build an alternate Full identity that is omitted from
|
||||
# the ordinary bulk release, including capacity-specific plain recipes.
|
||||
for alternate in Tbeam_SX1262_repeater \
|
||||
Tbeam_SX1262_repeater_bridge_espnow \
|
||||
LilyGo_TLora_V2_1_1_6_repeater; do
|
||||
(
|
||||
BUILD_PROFILE_OVERRIDE=full
|
||||
BUILD_PROFILE_EXPLICIT=1
|
||||
SINGLE_TARGET_FULL_BUILD=1
|
||||
EXACT_IDENTITY_FULL_BUILD=1
|
||||
RESOLVED_BUILD_TARGETS=("$alternate")
|
||||
configure_effective_build_profile build-firmware >/dev/null
|
||||
[ "${RESOLVED_BUILD_TARGETS[*]}" = "$alternate" ] \
|
||||
|| fail "alternate Full target changed identity: $alternate"
|
||||
run_logged_build_targets() {
|
||||
[ "$*" = "$alternate" ] || fail "alternate Full built another target"
|
||||
[ "$ESP32_FULL_BUILD" = 1 ] || fail "alternate Full lost expanded partitions"
|
||||
}
|
||||
run_command build-firmware >/dev/null
|
||||
)
|
||||
done
|
||||
|
||||
# Only targets whose standard and FULL board recipes retain the exact same
|
||||
# partition table may omit their redundant portable bulk artifact.
|
||||
full_only_count=0
|
||||
@@ -259,8 +278,8 @@ done
|
||||
heltec_v4_r8_repeater_observer_mqtt >/dev/null
|
||||
[ "$standard_built" = heltec_v4_r8_repeater_lora_ota_no_external_sensors ] \
|
||||
|| fail "matrix changed the distinct reduced OTA build"
|
||||
[ "$full_built" = 'heltec_v4_r8_repeater heltec_v4_r8_repeater_observer_mqtt' ] \
|
||||
|| fail "matrix did not retain V4's exact and legacy Full identities"
|
||||
[ "$full_built" = heltec_v4_r8_repeater ] \
|
||||
|| fail "matrix emitted duplicate V4 Full identities"
|
||||
[ "$ESP32_LORA_OTA_APP_LIMIT" = 1310720 ] || fail "changed the 1.25 MiB portable limit"
|
||||
)
|
||||
|
||||
@@ -273,8 +292,53 @@ done
|
||||
run_logging_matrix_build_targets heltec_rc32_repeater \
|
||||
heltec_rc32_repeater_bridge_espnow >/dev/null
|
||||
[ "${#calls[@]}" -eq 1 ] || fail "FULL-only targets emitted duplicate matrix artifacts"
|
||||
[ "${calls[0]}" = 'full:1:full-logging:heltec_rc32_repeater heltec_rc32_repeater_bridge_espnow' ] \
|
||||
|| fail "matrix did not build exact FULL-only identities"
|
||||
[ "${calls[0]}" = 'full:1:full-logging:heltec_rc32_repeater' ] \
|
||||
|| fail "matrix emitted duplicate RC32 Full identities"
|
||||
)
|
||||
|
||||
# A three-identity group keeps the audited plain target; a group without an
|
||||
# audited plain Full uses the feature-complete observer. Different hardware
|
||||
# variants must remain separate.
|
||||
(
|
||||
calls=()
|
||||
run_logged_build_targets() {
|
||||
[ "$ESP32_FULL_BUILD" = 1 ] && calls+=("$*")
|
||||
}
|
||||
run_logging_matrix_build_targets Heltec_v3_repeater \
|
||||
Heltec_v3_repeater_observer_mqtt Heltec_v3_repeater_bridge_espnow \
|
||||
Tbeam_SX1262_repeater_bridge_espnow Tbeam_SX1262_repeater_observer_mqtt \
|
||||
Heltec_v3_room_server >/dev/null
|
||||
[ "${#calls[@]}" -eq 2 ] || fail "multi-role matrix emitted too many Full passes"
|
||||
[ "${calls[0]}" = 'Heltec_v3_repeater Heltec_v3_room_server' ] \
|
||||
|| fail "plain V3 Full identity or separate room role was lost"
|
||||
[ "${calls[1]}" = Tbeam_SX1262_repeater_observer_mqtt ] \
|
||||
|| fail "T-Beam observer Full identity was lost"
|
||||
)
|
||||
|
||||
(
|
||||
mapfile -t matrix_targets < <(resolve_logging_matrix_firmwares)
|
||||
mapfile -t full_targets < <(select_ordinary_full_targets "${matrix_targets[@]}")
|
||||
declare -A seen=()
|
||||
for target in "${full_targets[@]}"; do
|
||||
key=$(get_ordinary_full_group_key "$target") || fail "invalid Full target $target"
|
||||
[ -z "${seen[$key]+x}" ] || fail "duplicate ordinary Full group $key"
|
||||
seen[$key]=1
|
||||
done
|
||||
[ "${#full_targets[@]}" -gt 100 ] || fail "Full selection unexpectedly dropped board roles"
|
||||
)
|
||||
|
||||
(
|
||||
calls=()
|
||||
run_logged_build_targets() { calls+=("$*"); }
|
||||
run_full_esp32_build_targets all heltec_v4_r8_repeater \
|
||||
heltec_v4_r8_repeater_observer_mqtt \
|
||||
Tbeam_SX1262_repeater_bridge_espnow \
|
||||
Tbeam_SX1262_repeater_observer_mqtt >/dev/null
|
||||
[ "${#calls[@]}" -eq 2 ] || fail "Full-only bulk command emitted duplicate roles"
|
||||
[ "${calls[0]}" = heltec_v4_r8_repeater ] \
|
||||
|| fail "Full-only bulk command lost the audited plain identity"
|
||||
[ "${calls[1]}" = Tbeam_SX1262_repeater_observer_mqtt ] \
|
||||
|| fail "Full-only bulk command lost the T-Beam observer identity"
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The ordinary release has one ESP32 Full OTA identity per board and role."""
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
from build_local_release import select_ordinary_full_records
|
||||
|
||||
|
||||
def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM") -> dict:
|
||||
return {"manifest": {"target": target, "build_profile": profile, "platform": platform},
|
||||
"files": []}
|
||||
|
||||
|
||||
class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
def test_resume_filters_sibling_full_images_only(self):
|
||||
inputs = [
|
||||
record("Heltec_v3_repeater_observer_mqtt"),
|
||||
record("Heltec_v3_repeater_bridge_espnow"),
|
||||
record("Heltec_v3_repeater"),
|
||||
record("Heltec_v3_repeater", "standard"),
|
||||
record("Heltec_v3_room_server"),
|
||||
record("Station_G2_repeater"),
|
||||
record("Station_G2_repeater_observer_mqtt"),
|
||||
record("Tbeam_SX1262_repeater_bridge_espnow"),
|
||||
record("Tbeam_SX1262_repeater_observer_mqtt"),
|
||||
record("LilyGo_TLora_V2_1_1_6_repeater"),
|
||||
record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_"),
|
||||
record("Meshadventurer_sx1262_repeater"),
|
||||
record("Meshadventurer_sx1262_repeater_bridge_espnow"),
|
||||
record("RAK_4631_repeater", platform="NRF52_PLATFORM"),
|
||||
]
|
||||
selected = select_ordinary_full_records(inputs)
|
||||
self.assertEqual({item["manifest"]["target"] for item in selected}, {
|
||||
"Heltec_v3_repeater", "Heltec_v3_room_server",
|
||||
"Station_G2_repeater_observer_mqtt",
|
||||
"Tbeam_SX1262_repeater_observer_mqtt",
|
||||
"LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_",
|
||||
"Meshadventurer_sx1262_repeater_bridge_espnow",
|
||||
"RAK_4631_repeater",
|
||||
})
|
||||
self.assertEqual(len(selected), 8) # standard V3 remains alongside Full V3
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user