#!/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())