feat(factory): auto-sync latest bins after idf build
This commit is contained in:
@@ -16,8 +16,14 @@ From project root:
|
||||
idf.py build
|
||||
```
|
||||
|
||||
The packaging script auto-syncs required plaintext build outputs from `build/flasher_args.json`
|
||||
into `tools/esptool-factory/bin/`, then encrypts them into `.bin.enc` for distribution.
|
||||
`idf.py build` now auto-overwrite-syncs required plaintext build outputs
|
||||
from `build/flasher_args.json` into `tools/esptool-factory/bin/`.
|
||||
|
||||
If you already built firmware and only want to refresh bin files manually:
|
||||
|
||||
```bash
|
||||
python3 tools/esptool-factory/sync_factory_bins.py
|
||||
```
|
||||
|
||||
### 2) Prepare private config
|
||||
|
||||
@@ -44,6 +50,14 @@ From project root:
|
||||
tools/esptool-factory/.venv/bin/python3 tools/esptool-factory/package_factory_tools.py
|
||||
```
|
||||
|
||||
By default, the packager now directly uses files in `tools/esptool-factory/bin/`.
|
||||
If you still want packager-side sync from `build/`, use:
|
||||
|
||||
```bash
|
||||
tools/esptool-factory/.venv/bin/python3 tools/esptool-factory/package_factory_tools.py \
|
||||
--sync-build-bins
|
||||
```
|
||||
|
||||
The packager always uses:
|
||||
|
||||
- `tools/esptool-factory/keys/firmware/fw_key.bin`
|
||||
|
||||
@@ -63,6 +63,8 @@ def sync_bins_from_idf_build(
|
||||
bin_dir: Path,
|
||||
required_bins: Iterable[str],
|
||||
build_dir: Path,
|
||||
*,
|
||||
overwrite: bool = False,
|
||||
) -> list[tuple[Path, Path]]:
|
||||
if not build_dir.exists() or not build_dir.is_dir():
|
||||
return []
|
||||
@@ -77,7 +79,7 @@ def sync_bins_from_idf_build(
|
||||
seen.add(name)
|
||||
|
||||
target = bin_dir / name
|
||||
if target.exists():
|
||||
if target.exists() and not overwrite:
|
||||
continue
|
||||
|
||||
source = build_map.get(name)
|
||||
|
||||
@@ -359,10 +359,13 @@ def _sync_bins_from_build(
|
||||
enabled: bool,
|
||||
) -> None:
|
||||
if not enabled:
|
||||
print(f"[INFO] Skip build sync; package directly from {_rel(bin_dir)}/")
|
||||
return
|
||||
copied = sync_bins_from_idf_build(bin_dir, required_bins, build_dir)
|
||||
copied = sync_bins_from_idf_build(bin_dir, required_bins, build_dir, overwrite=True)
|
||||
for source, target in copied:
|
||||
print(f"[INFO] Sync build bin: {source} -> {_rel(target)}")
|
||||
if not copied:
|
||||
print("[INFO] Build sync enabled but no bin files were updated.")
|
||||
|
||||
|
||||
def _resolve_icon() -> Optional[Path]:
|
||||
@@ -691,12 +694,12 @@ def main() -> None:
|
||||
parser.add_argument(
|
||||
"--build-dir",
|
||||
default=str(DEFAULT_IDF_BUILD_DIR),
|
||||
help="ESP-IDF build directory for auto-syncing missing .bin files.",
|
||||
help="ESP-IDF build directory used when --sync-build-bins is enabled.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-sync-build-bins",
|
||||
"--sync-build-bins",
|
||||
action="store_true",
|
||||
help="Disable auto-sync of missing bins from build/flasher_args.json.",
|
||||
help="Overwrite-sync required bins from build/flasher_args.json before packaging.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -712,7 +715,7 @@ def main() -> None:
|
||||
bin_dir=bin_dir,
|
||||
required_bins=required_bins,
|
||||
build_dir=build_dir,
|
||||
enabled=not args.no_sync_build_bins,
|
||||
enabled=args.sync_build_bins,
|
||||
)
|
||||
ensure_flash_files(bin_dir, required_bins)
|
||||
for tool in targets:
|
||||
|
||||
120
tools/esptool-factory/sync_factory_bins.py
Executable file
120
tools/esptool-factory/sync_factory_bins.py
Executable file
@@ -0,0 +1,120 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user