121 lines
3.9 KiB
Python
Executable File
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)
|