diff --git a/CMakeLists.txt b/CMakeLists.txt index c7f23ff..9049c02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,25 @@ cmake_minimum_required(VERSION 3.16) include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(TQ_printer) + +# Auto-sync required firmware bins to tools/esptool-factory/bin after IDF build. +set(_factory_sync_python "") +if(DEFINED PYTHON AND NOT "${PYTHON}" STREQUAL "") + set(_factory_sync_python "${PYTHON}") +elseif(DEFINED _Python3_EXECUTABLE AND NOT "${_Python3_EXECUTABLE}" STREQUAL "") + set(_factory_sync_python "${_Python3_EXECUTABLE}") +else() + find_package(Python3 REQUIRED COMPONENTS Interpreter) + set(_factory_sync_python "${Python3_EXECUTABLE}") +endif() + +add_custom_target(factory_sync_bins ALL + COMMAND ${_factory_sync_python} + ${CMAKE_SOURCE_DIR}/tools/esptool-factory/sync_factory_bins.py + --build-dir + ${CMAKE_BINARY_DIR} + DEPENDS app bootloader + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Sync firmware bins to tools/esptool-factory/bin" + VERBATIM +) diff --git a/tools/esptool-factory/README.md b/tools/esptool-factory/README.md index f43b7dc..a2914de 100644 --- a/tools/esptool-factory/README.md +++ b/tools/esptool-factory/README.md @@ -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` diff --git a/tools/esptool-factory/factory_common/idf_artifacts.py b/tools/esptool-factory/factory_common/idf_artifacts.py index 8113345..c5d8e38 100644 --- a/tools/esptool-factory/factory_common/idf_artifacts.py +++ b/tools/esptool-factory/factory_common/idf_artifacts.py @@ -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) diff --git a/tools/esptool-factory/package_factory_tools.py b/tools/esptool-factory/package_factory_tools.py index 2e16dd2..4598889 100644 --- a/tools/esptool-factory/package_factory_tools.py +++ b/tools/esptool-factory/package_factory_tools.py @@ -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: diff --git a/tools/esptool-factory/sync_factory_bins.py b/tools/esptool-factory/sync_factory_bins.py new file mode 100755 index 0000000..eb03f6b --- /dev/null +++ b/tools/esptool-factory/sync_factory_bins.py @@ -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: /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)