Add firmware verification harness
CI / Non-hardware verification (push) Has been cancelled

This commit is contained in:
2026-07-13 11:10:19 +02:00
parent def09160d0
commit 99c07ecda6
16 changed files with 1004 additions and 32 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
from __future__ import annotations
import configparser
import os
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
EXPECTED_PLATFORMIO_CORE = "6.1.18"
EXPECTED_PLATFORM = "https://github.com/pioarduino/platform-espressif32/releases/download/51.03.07/platform-espressif32.zip"
EXPECTED_ENVS = {"production", "debug", "test"}
EXPECTED_BOARD = "ttgo-lora32-v1"
EXPECTED_FRAMEWORK = "arduino"
PROPOSED_EXACT_PINS = {
"sandeepmistry/LoRa": "0.8.0",
"bblanchon/ArduinoJson": "6.21.6",
"adafruit/Adafruit SSD1306": "2.5.17",
"adafruit/Adafruit GFX Library": "1.12.6",
"knolleary/PubSubClient": "2.8.0",
"throwtheswitch/Unity": "2.6.1",
}
def command_text(command: list[str]) -> str:
return " ".join(f'"{part}"' if any(ch.isspace() for ch in part) else part for part in command)
def run(command: list[str]) -> subprocess.CompletedProcess[str]:
env = os.environ.copy()
env["PYTHONIOENCODING"] = "utf-8"
env["PYTHONUTF8"] = "1"
env["PIO_FORCE_COLOR"] = "0"
print(f"$ {command_text(command)}", flush=True)
result = subprocess.run(
command,
cwd=ROOT,
env=env,
text=True,
encoding="utf-8",
errors="replace",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
print(result.stdout, end="")
return result
def split_multiline(value: str) -> list[str]:
return [line.strip() for line in value.splitlines() if line.strip()]
def parse_platformio() -> configparser.RawConfigParser:
parser = configparser.RawConfigParser()
parser.optionxform = str
with (ROOT / "platformio.ini").open(encoding="utf-8") as handle:
parser.read_file(handle)
return parser
def main() -> int:
errors: list[str] = []
warnings: list[str] = []
version_result = run([sys.executable, "-m", "platformio", "--version"])
if version_result.returncode != 0:
errors.append("PlatformIO version command failed.")
else:
match = re.search(r"version\s+([0-9.]+)", version_result.stdout)
observed = match.group(1) if match else "unknown"
if observed != EXPECTED_PLATFORMIO_CORE:
errors.append(f"Expected PlatformIO Core {EXPECTED_PLATFORMIO_CORE}, observed {observed}.")
config_result = run([sys.executable, "-m", "platformio", "project", "config", "--json-output"])
if config_result.returncode != 0:
errors.append("PlatformIO project config command failed.")
pkg_result = run([sys.executable, "-m", "platformio", "pkg", "list"])
if pkg_result.returncode != 0:
errors.append("PlatformIO package resolution failed.")
parser = parse_platformio()
env_sections = {section.split(":", 1)[1] for section in parser.sections() if section.startswith("env:")}
missing = EXPECTED_ENVS - env_sections
if missing:
errors.append(f"Missing required PlatformIO environments: {', '.join(sorted(missing))}")
base_platform = parser.get("env", "platform", fallback="")
if base_platform != EXPECTED_PLATFORM:
errors.append("Base PlatformIO platform is not the expected pinned pioarduino release URL.")
base_board = parser.get("env", "board", fallback="")
base_framework = parser.get("env", "framework", fallback="")
if base_board != EXPECTED_BOARD:
errors.append(f"Expected board {EXPECTED_BOARD}, observed {base_board}.")
if base_framework != EXPECTED_FRAMEWORK:
errors.append(f"Expected framework {EXPECTED_FRAMEWORK}, observed {base_framework}.")
lib_deps = split_multiline(parser.get("env", "lib_deps", fallback=""))
for dep in lib_deps:
if "@" not in dep:
errors.append(f"Dependency lacks an explicit version requirement: {dep}")
elif "@^" in dep:
warnings.append(f"Ranged dependency allowed for now, exact pin proposed separately: {dep}")
for env_name in EXPECTED_ENVS:
section = f"env:{env_name}"
if not parser.has_section(section):
continue
if parser.get(section, "platform", fallback=base_platform) != EXPECTED_PLATFORM:
errors.append(f"{section} platform differs from the pinned base platform.")
if parser.get(section, "board", fallback=base_board) != EXPECTED_BOARD:
errors.append(f"{section} board differs from {EXPECTED_BOARD}.")
if parser.get(section, "framework", fallback=base_framework) != EXPECTED_FRAMEWORK:
errors.append(f"{section} framework differs from Arduino.")
print("Proposed exact direct dependency pins for a separate commit:")
for package, version in PROPOSED_EXACT_PINS.items():
print(f"- {package}@{version}")
for warning in warnings:
print(f"WARNING: {warning}")
if errors:
for error in errors:
print(f"ERROR: {error}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())