Files
AI_Printer/tools/esptool-factory/sync_factory_bins.py

121 lines
3.9 KiB
Python
Executable File

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