feat(factory): auto-sync latest bins after idf build

This commit is contained in:
admin
2026-03-02 13:44:56 +08:00
parent 5cd0c51be0
commit 673b257c4f
5 changed files with 169 additions and 8 deletions

View File

@@ -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`

View File

@@ -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)

View File

@@ -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:

View 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)