fix(build): clear matrix failures while preserving features

This commit is contained in:
mikecarper committed 2026-09-22 23:23:25 -07:00
1 parent 7b8ca33386
commit e6b002759d
14 files changed
+170 -12

No files matched your search

+2
View File
@@ -233,6 +233,8 @@ jobs:
run: |
bash test/test_build_size_detection.sh
python3 -B test/test_firmware_ram.py
python3 -B test/test_neighbor_scope_storage.py
python3 -B test/test_ed25519_compact.py
python3 -B test/test_cascade_release_package.py
python3 -B test/test_t096_full_memory.py
python3 -B test/test_nrf52_ble_startup.py
+8 -1
View File
@@ -4384,7 +4384,7 @@ run_pio_with_size_detection() {
build_status=1
elif [ "$build_status" -ne 0 ] \
&& grep -Eiq \
'will not fit in region|region .+ overflowed by|section .+ will not fit|sketch too big|program size is greater than maximum|exceed(s|ing).*(flash|partition|app)' \
'will not fit in region|region .+ overflowed by|section .+ will not fit|sketch too big|program size.*is greater than maximum|exceed(s|ing).*(flash|partition|app)' \
"$build_output_log"; then
build_status=42
fi
@@ -5066,6 +5066,13 @@ is_runtime_setting_alias_target() {
}
is_redundant_bulk_build_target() {
# Bench fixtures and migration utilities use their dedicated PlatformIO
# recipes; they are not node firmware for the release/OTA packaging matrix.
case "${1,,}" in
profile_switch_*|*_partition_migrator|*_partition_migrator_test_*|*_partition_legacy_seed|*_sim)
return 0
;;
esac
# Keep every legacy name available to `build-firmware` and
# `build-matching-firmwares`, but do not republish binaries that differ only
# by a saved/default setting, or roles already supplied by Full Companion.
+4
View File
@@ -2285,7 +2285,11 @@ void halt() {
clear_bonds, stealth_pair_once,
bonded_only_peer_ptr)) {
strncpy(companion_bluetooth_start_failure,
#if defined(NRF52_PLATFORM)
bluetooth_interface.beginFailure(),
#else
"Bluetooth initialization failed",
#endif
sizeof(companion_bluetooth_start_failure) - 1);
companion_bluetooth_start_failure[
sizeof(companion_bluetooth_start_failure) - 1] = 0;
+11 -9
View File
@@ -3662,7 +3662,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
loadTelemetryHistoryTxPrefs();
#endif
#ifdef SIM_WIFI_SSID
#if defined(SIM_WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
// Emulator builds (Wokwi) boot with fresh NVS every run. Seed WiFi so the
// observer auto-joins the simulator's network and brings the MQTT bridge up
// (WiFi is driven by the bridge task), instead of raising the setup AP that
@@ -12842,7 +12842,7 @@ bool MyMesh::completeNeighborDiscoverEntry() {
pubkey_hex,
entry.snr / 4.0f,
UINT32_MAX,
entry.scopes,
neighbor_discover_scopes[neighbor_discover_next],
entry.status == ND_RESPONDED ? "responded"
: (entry.status == ND_SEND_FAILED ? "send_failed" : "timeout")
};
@@ -12868,7 +12868,8 @@ bool MyMesh::completeNeighborDiscoverEntry() {
bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx,
const uint8_t* data, size_t len,
float snr, int16_t rssi) {
if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count) return false;
if (overlay_idx < 0 || overlay_idx >= neighbor_discover_count
|| (size_t)overlay_idx >= NEIGHBOR_SCOPE_RESULTS) return false;
NeighborDiscoverEntry& entry = neighbor_discover[overlay_idx];
if (entry.status != ND_PENDING || len < 8) return false;
@@ -12877,11 +12878,12 @@ bool MyMesh::handleNeighborDiscoverResponse(int overlay_idx,
if (tag != entry.tag) return false;
size_t scope_len = len - 8;
if (scope_len >= sizeof(entry.scopes)) {
scope_len = sizeof(entry.scopes) - 1;
auto& scopes = neighbor_discover_scopes[overlay_idx];
if (scope_len >= sizeof(scopes)) {
scope_len = sizeof(scopes) - 1;
}
memcpy(entry.scopes, &data[8], scope_len);
entry.scopes[scope_len] = 0;
memcpy(scopes, &data[8], scope_len);
scopes[scope_len] = 0;
entry.status = ND_RESPONDED;
// A zero-hop reply is proof we heard this neighbour now, so re-stamp both the
// snapshot and live table with this packet's measurements. A stamp taken
@@ -13028,7 +13030,7 @@ void MyMesh::finishNeighborDiscover() {
bool heard_known = neighborHeardAgeUsable(entry.heard_timestamp, now_secs);
entries[i].heard_unknown = !heard_known;
entries[i].heard_secs_ago = heard_known ? (now_secs - entry.heard_timestamp) : 0;
entries[i].scopes = entry.scopes;
entries[i].scopes = neighbor_discover_scopes[i];
switch (entry.status) {
case ND_RESPONDED: entries[i].status = "responded"; break;
case ND_SEND_FAILED: entries[i].status = "send_failed"; break;
@@ -13169,7 +13171,6 @@ bool MyMesh::startNeighborDiscover(char* reply) {
entry.heard_timestamp = neighbours[i].heard_timestamp;
entry.snr = neighbours[i].snr;
entry.rssi = neighbours[i].rssi;
entry.scopes[0] = 0;
entry.tag = 0;
entry.status = ND_UNSENT;
neighbor_discover_count++;
@@ -13193,6 +13194,7 @@ bool MyMesh::startNeighborDiscover(char* reply) {
neighbor_discover[j] = entry;
}
memset(neighbor_discover_scopes, 0, sizeof(neighbor_discover_scopes));
neighbor_discover_next = 0;
resetNeighborDiscoverJsonBudget();
neighbor_discover_active = true;
+7 -1
View File
@@ -644,10 +644,16 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
int8_t snr; // multiplied by 4
int16_t rssi; // dBm from the last packet heard from this neighbour
uint32_t tag; // anon-regions request tag we're waiting on
char scopes[96]; // scope names from the response
uint8_t status; // NeighborDiscoverStatus
};
NeighborDiscoverEntry neighbor_discover[MAX_NEIGHBOURS];
// Only the publishable prefix (plus the result that detects truncation)
// receives scope queries. Keep every neighbor snapshot, without reserving
// 96 response bytes for each entry that will never be queried.
static constexpr size_t NEIGHBOR_SCOPE_RESULTS =
MAX_NEIGHBOURS < MQTTBridge::NEIGHBORS_MAX_PUBLISH_ENTRIES + 1
? MAX_NEIGHBOURS : MQTTBridge::NEIGHBORS_MAX_PUBLISH_ENTRIES + 1;
char neighbor_discover_scopes[NEIGHBOR_SCOPE_RESULTS][96];
uint8_t neighbor_discover_count;
uint8_t neighbor_discover_next; // newest-first entry currently being queried
uint8_t neighbor_discover_publish_count; // completed prefix that fits the JSON buffer
+14
View File
@@ -115,6 +115,19 @@ static int sha512_compress(sha512_context *md, unsigned char *buf)
d += t0; \
h = t0 + t1;
#if defined(ED25519_COMPACT_SHA512)
/* Same 80 rounds, with a fixed state rotation instead of eight copies of
the round body. Small STM32 images trade loop overhead for flash space;
neither the loop bounds nor memory indexes depend on secret data. */
for (i = 0; i < 80; ++i) {
int j;
uint64_t next;
RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i);
next = S[7];
for (j = 7; j > 0; --j) S[j] = S[j - 1];
S[0] = next;
}
#else
for (i = 0; i < 80; i += 8) {
RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i+0);
RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],i+1);
@@ -125,6 +138,7 @@ static int sha512_compress(sha512_context *md, unsigned char *buf)
RND(S[2],S[3],S[4],S[5],S[6],S[7],S[0],S[1],i+6);
RND(S[1],S[2],S[3],S[4],S[5],S[6],S[7],S[0],i+7);
}
#endif
#undef RND
+7
View File
@@ -12,6 +12,13 @@ fail() {
[ "$OPTION3_BUILD_WORKERS" -eq 1 ] \
|| fail "logging matrix permits concurrent PlatformIO target builds"
for utility in profile_switch_t096_sx1262 xiao_s3_partition_migrator \
heltec_v4_partition_migrator_test_hold xiao_s3_partition_legacy_seed \
Heltec_v3_repeater_observer_mqtt_sim; do
is_redundant_bulk_build_target "$utility" \
|| fail "release matrix includes development utility $utility"
done
# Reduced RAK profiles all retain the compact INA set, but GPS depends on
# whether the bridge already owns Serial1. Keep those independent contracts so
# a valid Serial1 image cannot fail release qualification for an absent GPS
+2 -1
View File
@@ -38,7 +38,8 @@ pio_result=1
for pio_message in \
"firmware.elf section .text will not fit in region FLASH" \
"region FLASH overflowed by 43080 bytes" \
"Error: The program size is greater than maximum allowed"; do
"Error: The program size is greater than maximum allowed" \
"Error: The program size (1314113 bytes) is greater than maximum allowed (1310720 bytes)"; do
check_status 42
grep -Fq "$pio_message" "$test_root/output" || fail "compiler output was lost"
done
+20
View File
@@ -1,3 +1,5 @@
import ctypes
import hashlib
import subprocess
import tempfile
import unittest
@@ -22,6 +24,22 @@ SOURCES = [
class Ed25519CompactTest(unittest.TestCase):
def test_compact_sha512_matches_hashlib_at_block_boundaries(self):
with tempfile.TemporaryDirectory() as temp_dir:
library = Path(temp_dir) / "sha512.so"
subprocess.run(["cc", "-std=c99", "-O2", "-shared", "-fPIC",
"-DED25519_COMPACT_SHA512=1", str(ED25519 / "sha512.c"),
"-o", str(library)], check=True)
sha = ctypes.CDLL(str(library)).sha512
sha.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p]
sha.restype = ctypes.c_int
for length in (0, 1, 63, 64, 111, 112, 127, 128, 129, 255, 256, 1024, 4097):
data = bytes((i * 73 + 19) % 256 for i in range(length))
output = ctypes.create_string_buffer(64)
with self.subTest(length=length):
self.assertEqual(sha(data, len(data), output), 0)
self.assertEqual(output.raw, hashlib.sha512(data).digest())
def test_stm32_builds_enable_compact_base_table(self):
platformio = (ROOT / "platformio.ini").read_text()
stm32_base = platformio.split("[stm32_base]", 1)[1].split("\n[", 1)[0]
@@ -32,6 +50,8 @@ class Ed25519CompactTest(unittest.TestCase):
for label, extra_flags in (
("standard", []),
("compact", ["-DED25519_COMPACT_BASE=1"]),
("compact_sha512", ["-DED25519_COMPACT_BASE=1",
"-DED25519_COMPACT_SHA512=1"]),
):
executable = Path(temp_dir) / label
subprocess.run(
+91
View File
@@ -0,0 +1,91 @@
"""Exercise production scope storage and truncation with 254 neighbors."""
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class NeighborScopeStorageTest(unittest.TestCase):
def test_response_bounds_and_publish_prefix(self):
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text()
source = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text()
layout = header[header.index(" struct NeighborDiscoverEntry {"):
header.index(" uint8_t neighbor_discover_count;")]
methods = "\n".join(extract_braced(source, signature) for signature in (
"bool MyMesh::completeNeighborDiscoverEntry()",
"bool MyMesh::handleNeighborDiscoverResponse("))
code = r'''
#include <cassert>
#include <cstdint>
#include <cstring>
#include <cstddef>
#define MAX_NEIGHBOURS 254
#define PUB_KEY_SIZE 32
namespace mesh {
struct Identity { uint8_t pub_key[32]; };
struct Utils { static void toHex(char* out, const uint8_t*, size_t) { out[0] = 0; } };
}
struct MQTTBridge {
static constexpr int NEIGHBORS_MAX_PUBLISH_ENTRIES = 20;
static constexpr size_t NEIGHBORS_JSON_BUFFER_SIZE = 8192;
};
struct MQTTMessageBuilder {
struct NeighborsMessageEntry {
const char* key; float snr; uint32_t age; const char* scopes;
const char* status; int16_t rssi;
};
static size_t measureNeighborsMessageEntry(const NeighborsMessageEntry&) { return 100; }
};
struct Clock { uint32_t getCurrentTime() { return 100; } };
struct MyMesh {
enum { ND_UNSENT, ND_QUEUED, ND_PENDING, ND_RESPONDED, ND_TIMEOUT, ND_SEND_FAILED };
''' + layout + r'''
unsigned neighbor_discover_next = 0, neighbor_discover_count = 254;
unsigned neighbor_discover_publish_count = 0;
size_t neighbor_discover_json_size = 0;
bool neighbor_discover_truncated = false, finished = false;
Clock clock;
Clock* getRTCClock() { return &clock; }
void touchNeighbourHeard(const mesh::Identity&, uint32_t, float, int16_t) {}
void finishNeighborDiscover() { finished = true; }
bool completeNeighborDiscoverEntry();
bool handleNeighborDiscoverResponse(int, const uint8_t*, size_t, float, int16_t);
};
''' + methods + r'''
int main() {
MyMesh m = {};
static_assert(sizeof(m.neighbor_discover_scopes) == 21 * 96, "bounded scope storage");
static_assert(sizeof(m.neighbor_discover) / sizeof(m.neighbor_discover[0]) == 254,
"retain every neighbor snapshot");
uint8_t response[200] = {};
uint32_t tag = 123;
memcpy(response, &tag, 4);
memset(response + 8, 'x', sizeof(response) - 8);
for (int i = 0; i <= 20; ++i) {
auto& entry = m.neighbor_discover[i];
entry.status = MyMesh::ND_PENDING;
entry.tag = tag;
assert(m.handleNeighborDiscoverResponse(i, response, sizeof(response), 5, -100));
assert(strlen(m.neighbor_discover_scopes[i]) == 95);
assert(m.completeNeighborDiscoverEntry() == (i < 20));
}
assert(m.finished && m.neighbor_discover_truncated);
assert(m.neighbor_discover_publish_count == 20);
assert(!m.handleNeighborDiscoverResponse(21, response, sizeof(response), 5, -100));
assert(!m.handleNeighborDiscoverResponse(-1, response, sizeof(response), 5, -100));
}
'''
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
(path / "test.cpp").write_text(code)
subprocess.run(["g++", "-std=c++17", "-fsanitize=address,undefined",
str(path / "test.cpp"), "-o", str(path / "test")], check=True)
subprocess.run([str(path / "test")], check=True)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -66,6 +66,7 @@ extends = rak3x72
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
build_unflags = -Os
build_flags = ${rak3x72.build_flags}
-D ED25519_COMPACT_SHA512=1 ; same hash rounds, compact loop preserves the filesystem boundary
-Oz
-flto
-fno-inline-small-functions
+1
View File
@@ -72,6 +72,7 @@ extends = Tiny_Relay
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
build_unflags = -Os
build_flags = ${Tiny_Relay.build_flags}
-D ED25519_COMPACT_SHA512=1 ; same hash rounds, compact loop preserves the filesystem boundary
-Oz
-flto
-fno-inline-small-functions
+1
View File
@@ -90,6 +90,7 @@ extends = lora_e5
platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
build_unflags = -Os
build_flags = ${lora_e5.build_flags}
-D ED25519_COMPACT_SHA512=1 ; same hash rounds, compact loop preserves the filesystem boundary
-Oz
-flto
-fno-inline-small-functions
+1
View File
@@ -79,6 +79,7 @@ platform_packages = platformio/toolchain-gccarmnoneeabi@^1.140201.0
build_unflags =
-Os
build_flags = ${lora_e5_mini.build_flags}
-D ED25519_COMPACT_SHA512=1 ; same hash rounds, compact loop preserves the filesystem boundary
-Oz
-flto
-fno-inline-small-functions