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:
@@ -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__':
|
||||
|
||||
Reference in New Issue
Block a user