#!/usr/bin/env python3 """Overwrite-sync required firmware .bin artifacts into factory bin dir.""" from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Optional from factory_common.idf_artifacts import sync_bins_from_idf_build ROOT = Path(__file__).resolve().parent REPO_ROOT = ROOT.parent.parent DEFAULT_BUILD_DIR = REPO_ROOT / "build" DEFAULT_PRIVATE_CONFIG = ROOT / "conf" / "factory_private.json" def _coerce_str(value: object, label: str) -> str: if isinstance(value, (int, float)): return str(value) if isinstance(value, str) and value.strip(): return value.strip() raise ValueError(f"无效配置: {label}") def _load_json(path: Path) -> dict: try: raw = path.read_text(encoding="utf-8") except OSError as exc: raise FileNotFoundError(f"无法读取配置文件: {path}") from exc try: data = json.loads(raw) except json.JSONDecodeError as exc: raise ValueError(f"配置不是合法 JSON: {path}") from exc if not isinstance(data, dict): raise ValueError(f"无效配置结构: {path}") return data def _parse_flash_inputs(config_path: Path) -> tuple[Path, list[str]]: raw = _load_json(config_path) flash_cfg = raw.get("flash") if not isinstance(flash_cfg, dict): raise ValueError("无效配置: 缺少 flash 段") bin_dir_raw = _coerce_str(flash_cfg.get("bin_dir"), "flash.bin_dir") bin_dir = Path(bin_dir_raw) if not bin_dir.is_absolute(): bin_dir = ROOT / bin_dir layout_raw = flash_cfg.get("layout") if not isinstance(layout_raw, list) or not layout_raw: raise ValueError("无效配置: flash.layout 不能为空") required_bins: list[str] = [] for entry in layout_raw: name: Optional[str] = None if isinstance(entry, (list, tuple)) and len(entry) == 2: name = _coerce_str(entry[1], "flash.layout.name") elif isinstance(entry, dict): name = _coerce_str(entry.get("name"), "flash.layout.name") else: raise ValueError("无效配置: flash.layout") clean_name = name.strip() if clean_name.endswith(".enc"): raise ValueError("无效配置: flash.layout 不能包含 .enc 文件") required_bins.append(clean_name) return bin_dir, required_bins def _ensure_bins_exist(bin_dir: Path, required_bins: list[str]) -> None: missing = [str(bin_dir / name) for name in required_bins if not (bin_dir / name).exists()] if missing: raise FileNotFoundError("同步后仍缺少固件文件:\n " + "\n ".join(missing)) def main() -> None: parser = argparse.ArgumentParser( description="Overwrite-sync required .bin files from ESP-IDF build outputs into tools/esptool-factory/bin." ) parser.add_argument( "--build-dir", default=str(DEFAULT_BUILD_DIR), help="ESP-IDF build directory (default: /build).", ) parser.add_argument( "--config", default=str(DEFAULT_PRIVATE_CONFIG), help="Factory private config path (default: tools/esptool-factory/conf/factory_private.json).", ) args = parser.parse_args() config_path = Path(args.config).expanduser() build_dir = Path(args.build_dir).expanduser() bin_dir, required_bins = _parse_flash_inputs(config_path) copied = sync_bins_from_idf_build( bin_dir=bin_dir, required_bins=required_bins, build_dir=build_dir, overwrite=True, ) for source, target in copied: print(f"[INFO] 覆盖同步: {source} -> {target}") if not copied: print("[INFO] 未发现可覆盖文件,保留现有 bin 产物。") _ensure_bins_exist(bin_dir, required_bins) print(f"[DONE] 已更新固件目录: {bin_dir}") if __name__ == "__main__": try: main() except Exception as exc: # noqa: BLE001 print(f"[FAIL] {exc}", file=sys.stderr) sys.exit(1)