Migrate to IDF 5.5.3 candidate

Pin PlatformIO packages and toolchains, rebase protected SDK
overrides, and add WebSocket receive regression coverage. Document
isolated candidate validation, archive provenance, and remaining gates.
This commit is contained in:
2026-09-18 14:23:13 +02:00
parent cdc4d4a8df
commit 797d2681ac
32 changed files with 1394 additions and 123 deletions
+70 -6
View File
@@ -3,6 +3,8 @@
import argparse
from dataclasses import dataclass
import math
import hashlib
import json
import os
from pathlib import Path
import signal
@@ -31,6 +33,10 @@ def positive_seconds(value):
def parser():
cli = argparse.ArgumentParser(description=__doc__)
cli.add_argument('--build-dir', type=Path, default=BUILD, help='existing firmware build directory')
cli.add_argument('--idf-path', type=Path, help='explicit ESP-IDF SDK for SDK-aware checks')
cli.add_argument('--platformio-core-dir', type=Path, help='explicit isolated PlatformIO core directory')
cli.add_argument('--web-performance', action='store_true', help='also validate generated WebSocket performance contracts')
cli.add_argument('--build', action='store_true', help='run pio run before host checks (may fetch dependencies)')
cli.add_argument('--interop', action='store_true', help='enable ordering OpenSSH local Unix-socket matrix')
cli.add_argument('--dry-run', action='store_true', help='print plan only; no execution or prerequisite validation')
@@ -50,12 +56,15 @@ def plan(options):
commands.append(Command(label or name, (sys.executable, '-B', str(path), *map(str, args)),
options.timeout, (path, *required)))
header = BUILD / 'config/sdkconfig.h'
database = BUILD / 'compile_commands.json'
build = options.build_dir
idf_args = ('--idf-path', options.idf_path) if options.idf_path else ()
header = build / 'config/sdkconfig.h'
database = build / 'compile_commands.json'
suite('security_build_policy', '--sdkconfig-header', header, required=(header,))
for name in ('ssh_auth_policy', 'ssh_auth_transport', 'hidden_input', 'ssh_memory'):
for name in ('ssh_auth_policy', 'ssh_auth_transport', 'hidden_input'):
suite(name)
suite('sdk_security_overrides', '--build-dir', BUILD, required=(database,))
suite('ssh_memory', *idf_args)
suite('sdk_security_overrides', '--build-dir', build, *idf_args, required=(database,))
for name in ('wolfssh_auth_contract', 'ssh_protocol_policy', 'wolf_crypto_policy'):
suite(name, '--compile-commands', database, required=(database,))
suite('wolfssh_parser_contract')
@@ -68,9 +77,47 @@ def plan(options):
for mode in ('base', 'admin', 'accounts', 'ssh', 'lifecycle'):
suite('web_cookie_auth', *([] if mode == 'base' else ['--' + mode]),
label='web_cookie_auth:' + mode)
if options.web_performance:
suite('web_serial_performance', '--build-dir', build, *idf_args, required=(database,))
return commands
def verify_snapshot(root, build):
"""Compare source inputs, not path-dependent generated build configuration."""
entries = json.loads((build / 'compile_commands.json').read_text())
mains = [(Path(e['directory']) / e['file']).resolve() for e in entries
if Path(e['file']).parts[-2:] == ('src', 'main.c')]
if len(mains) != 1:
raise RuntimeError('expected exactly one application main.c compilation input')
snapshot = mains[0].parents[1]
scopes = ('src', 'cmake', 'boards', 'managed_components', 'tools/wolfssh_order',
'tools/security_overrides.py', 'CMakeLists.txt', 'extra_script.py',
'sdkconfig.defaults', 'partitions.csv')
digest = hashlib.sha256()
count = 0
for scope in scopes:
def files(base):
path = base / scope
if path.is_file():
return {Path(scope)}
if not path.is_dir():
raise RuntimeError(f'missing snapshot input: {path}')
return {p.relative_to(base) for p in path.rglob('*') if p.is_file()
and not {'.git', '__pycache__'} & set(p.parts)}
paths = files(root)
if paths != files(snapshot):
raise RuntimeError(f'root/build snapshot file set differs: {scope}')
for relative in sorted(paths):
data = (root / relative).read_bytes()
if data != (snapshot / relative).read_bytes():
raise RuntimeError(f'root/build snapshot content differs: {relative}')
digest.update(str(relative).encode() + b'\0' + hashlib.sha256(data).digest())
count += 1
evidence = (snapshot, count, digest.hexdigest())
print(f'SNAPSHOT source equality: {snapshot}; {count} files; SHA256 {evidence[2]}', flush=True)
return evidence
def kill_group(process):
try:
os.killpg(process.pid, signal.SIGKILL)
@@ -105,12 +152,16 @@ def run_command(command, root, env):
return ('PASS', 'exit 0') if code == 0 else ('FAIL', f'exit {code}')
def execute(commands, root, *, dry_run=False, fail_fast=False):
def execute(commands, root, *, dry_run=False, fail_fast=False, idf_path=None, platformio_core_dir=None):
if os.name != 'posix':
print('PREREQ: POSIX process groups required', flush=True)
return 1
env = os.environ.copy()
env['CCACHE_DISABLE'] = '1'
if idf_path is not None:
env['IDF_PATH'] = str(idf_path.resolve())
if platformio_core_dir is not None:
env['PLATFORMIO_CORE_DIR'] = str(platformio_core_dir.resolve())
results = []
stopped = False
for command in commands:
@@ -143,7 +194,20 @@ def main(argv=None):
print('SKIP optional build: --build not requested; existing artifacts required', flush=True)
if not options.interop:
print('SKIP optional interop: --interop not requested', flush=True)
return execute(plan(options), ROOT, dry_run=options.dry_run, fail_fast=options.fail_fast)
if options.build and options.build_dir != BUILD:
parser().error('--build cannot be combined with a non-default --build-dir; build it separately')
try:
evidence = None
if not options.dry_run and options.build_dir != BUILD:
evidence = verify_snapshot(ROOT, options.build_dir.resolve())
result = execute(plan(options), ROOT, dry_run=options.dry_run, fail_fast=options.fail_fast,
idf_path=options.idf_path, platformio_core_dir=options.platformio_core_dir)
if evidence is not None and verify_snapshot(ROOT, options.build_dir.resolve()) != evidence:
raise RuntimeError('snapshot changed during validation')
return result
except (OSError, ValueError, RuntimeError) as error:
print(f'PREREQ snapshot: {error}', flush=True)
return 1
if __name__ == '__main__':