#!/usr/bin/env python3 """Verify official candidate archives; optionally fetch or prepare an isolated smoke project.""" import argparse import hashlib import json import pathlib import platform import tarfile import urllib.parse import urllib.request ROOT = pathlib.Path(__file__).resolve().parents[2] BASE = ROOT / ".pio/idf-candidate-5.5.3" LOCK = pathlib.Path(__file__).with_name("artifacts.json") class RegistryRedirects(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): check_url(newurl) return super().redirect_request(req, fp, code, msg, headers, newurl) def check_url(url): parsed = urllib.parse.urlsplit(url) if parsed.scheme != "https" or parsed.hostname not in { "dl.registry.platformio.org", "dl.registry.nm1.platformio.org", }: raise ValueError("Unapproved artifact host: " + url) def verify(path, item): digest = hashlib.sha256() with path.open("rb") as stream: for block in iter(lambda: stream.read(1024 * 1024), b""): digest.update(block) if path.stat().st_size != item["size"] or digest.hexdigest() != item["checksum"]["sha256"]: raise ValueError("Artifact size/hash mismatch: " + str(path)) def member(archive, name): matches = [m for m in archive.getmembers() if m.name.removeprefix("./") == name and m.isfile()] if len(matches) != 1: raise ValueError("Missing/ambiguous archive member: " + name) return archive.extractfile(matches[0]).read() def require(condition): if not condition: raise ValueError("Candidate package contract mismatch") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--fetch", action="store_true", help="Download absent pinned archives; never install shared packages") parser.add_argument("--prepare", action="store_true", help="Create a fresh, separate smoke project after verifying all four archives") parser.add_argument("--sdk-only", action="store_true", help="Verify/download platform and SDK only, not toolchains; cannot prepare") args = parser.parse_args() if args.prepare and args.sdk_only: parser.error("--prepare requires both toolchain archives") if platform.system() != "Linux" or platform.machine() != "x86_64": parser.error("This lock is for Linux x86_64 only") # Do not follow a user-created candidate/cache symlink outside the workspace. for path in (ROOT / ".pio", BASE, BASE / "archives"): if path.is_symlink(): raise ValueError("Refusing symlink: " + str(path)) path.mkdir(exist_ok=True) items = json.loads(LOCK.read_text())["artifacts"] selected = items[:2] if args.sdk_only else items opener = urllib.request.build_opener(RegistryRedirects()) for item in selected: path = BASE / "archives" / item["name"] if path.is_symlink(): raise ValueError("Refusing symlink: " + str(path)) if not path.exists() and args.fetch: check_url(item["download_url"]) temporary = path.with_suffix(".partial") with temporary.open("xb") as target: try: with opener.open(item["download_url"], timeout=60) as source: total = 0 while block := source.read(1024 * 1024): total += len(block) if total > item["size"]: raise ValueError("Download exceeds pinned size") target.write(block) except BaseException: temporary.unlink() raise try: verify(temporary, item) temporary.rename(path) finally: temporary.unlink(missing_ok=True) verify(path, item) print("SHA256 PASS", item["name"], flush=True) with tarfile.open(BASE / "archives" / items[0]["name"]) as archive: manifest = json.loads(member(archive, "platform.json")) require(manifest["version"] == "6.13.0") require(manifest["packages"]["framework-espidf"]["version"] == "~3.50503.0") require(manifest["packages"]["toolchain-xtensa-esp-elf"]["version"] == "14.2.0+20251107") with tarfile.open(BASE / "archives" / items[1]["name"]) as archive: require(json.loads(member(archive, "package.json"))["version"] == "3.50503.0") tools = json.loads(member(archive, "tools/tools.json")) xtensa = next(t for t in tools["tools"] if t["name"] == "xtensa-esp-elf") require(any(v["name"] == "esp-14.2.0_20251107" and v["status"] == "recommended" for v in xtensa["versions"])) header = member(archive, "components/wpa_supplicant/esp_supplicant/src/esp_wifi_driver.h").decode() require("(*wpa_ap_get_wpa_ie)(size_t *len)" in header) expected = { "core": "9f7b14a8bf6eec64973da8adc65d35b5ba9bee49", "espnow": "132b4f67e339ca2081d2add91c14eefa39476ff9", "mesh": "2e9dc1c8c7afbf033337b4175032e9b1161e3262", "net80211": "2800d447ec385d33869373696ba8191292647694", "pp": "8944bcad7371621045f376cf74c62fde6f368cbb", "smartconfig": "4dc759e25617aa00b9e12887fd092a1d5780a170", "wapi": "65655b6feab0572a6e8a1200946d53a21f3f4722", } for name, digest in expected.items(): data = member(archive, "components/esp_wifi/lib/esp32s3/lib" + name + ".a") require(hashlib.sha1(b"blob " + str(len(data)).encode() + b"\0" + data).hexdigest() == digest) print("Official manifest/toolchain, fixed callback ABI and all seven release Wi-Fi archives PASS") if args.prepare: project = BASE / "smoke" project.mkdir(exist_ok=False) (project / "src").mkdir() (project / "src/main.c").write_text("void app_main(void) {}\n") (project / "src/CMakeLists.txt").write_text('idf_component_register(SRCS "main.c" INCLUDE_DIRS ".")\n') (project / "CMakeLists.txt").write_text('cmake_minimum_required(VERSION 3.16)\ninclude($ENV{IDF_PATH}/tools/cmake/project.cmake)\nproject(idf_candidate_smoke)\n') config = "[platformio]\ncore_dir = " + str(BASE / "core") + "\n\n[env:candidate]\n" config += "platform = " + (BASE / "archives" / items[0]["name"]).as_uri().replace("%2B", "+").replace("%", "%%") + "\n" config += "board = esp32-s3-devkitc-1\nframework = espidf\nplatform_packages =\n" for item in items[1:]: config += " " + item["package"].split("/")[-1] + " @ " + (BASE / "archives" / item["name"]).as_uri().replace("%2B", "+").replace("%", "%%") + "\n" (project / "platformio.ini").write_text(config) print("Prepared", project) print("Smoke only: not production sources, board customization, managed dependencies or security overrides.") if __name__ == "__main__": main()