mirror of
https://github.com/esphome/esphome.git
synced 2026-10-17 19:52:50 +00:00
[espidf] Fail the factory image when its bootloader is missing
This commit is contained in:
@@ -355,6 +355,11 @@ def ensure_cached_bootloader(verbose: bool = False) -> bool:
|
||||
def _install_cached(app_config: dict, compiler: str, verbose: bool) -> Path | None:
|
||||
"""The installed build/bootloader dir, building and publishing on a miss;
|
||||
None means fall back to the in-tree build."""
|
||||
from esphome.espidf.framework import read_idf_version_txt
|
||||
|
||||
if not read_idf_version_txt(toolchain._get_idf_path()):
|
||||
_LOGGER.debug("Framework version.txt missing; cannot key the bootloader")
|
||||
return None
|
||||
if (names := _load_config_names()) is not None:
|
||||
entry = _cache_root() / _compute_key(_key_payload(names, app_config, compiler))
|
||||
if (entry / "bootloader.bin").is_file():
|
||||
@@ -389,14 +394,16 @@ def _install_cached(app_config: dict, compiler: str, verbose: bool) -> Path | No
|
||||
return _install_into_build(entry)
|
||||
|
||||
|
||||
def inject_bootloader_flash_file(flash_data: dict, build_dir: Path) -> None:
|
||||
def inject_bootloader_flash_file(flash_data: dict, build_dir: Path) -> bool:
|
||||
"""Add the cached bootloader to a flasher_args ``flash_files`` map.
|
||||
|
||||
Gated on the tree's mode: IDF also omits the entry on purpose for some
|
||||
secure-boot builds, and those must stay exactly as IDF wrote them.
|
||||
False means a cached-mode tree is missing pieces and the factory image
|
||||
would not boot.
|
||||
"""
|
||||
if not tree_uses_cached_bootloader(build_dir):
|
||||
return
|
||||
return True
|
||||
app_config = toolchain._load_sdkconfig_json(build_dir)
|
||||
if (
|
||||
app_config is None
|
||||
@@ -404,6 +411,7 @@ def inject_bootloader_flash_file(flash_data: dict, build_dir: Path) -> None:
|
||||
or not (build_dir / "bootloader" / "bootloader.bin").is_file()
|
||||
):
|
||||
_LOGGER.error("Cached bootloader missing; factory image has no bootloader")
|
||||
return
|
||||
return False
|
||||
flash_files = flash_data.setdefault("flash_files", {})
|
||||
flash_files[hex(_bootloader_offset(app_config))] = "bootloader/bootloader.bin"
|
||||
return True
|
||||
|
||||
@@ -854,7 +854,8 @@ def create_factory_bin() -> bool:
|
||||
|
||||
from esphome.espidf import bootloader
|
||||
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build_dir)
|
||||
if not bootloader.inject_bootloader_flash_file(flash_data, build_dir):
|
||||
return False
|
||||
|
||||
# Get flash size from config
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
|
||||
@@ -466,6 +466,7 @@ def _orchestration_env(tmp_path: Path) -> Iterator[Path]:
|
||||
patch.object(bootloader, "_cache_root", return_value=root),
|
||||
patch.object(toolchain, "_resolved_c_compiler", return_value="/tc/gcc"),
|
||||
patch.object(bootloader, "_key_payload", return_value={}),
|
||||
patch("esphome.espidf.framework.read_idf_version_txt", return_value="v5.5.5"),
|
||||
patch.object(bootloader, "_compute_key", return_value="deadbeefdeadbeef"),
|
||||
):
|
||||
yield root
|
||||
@@ -550,6 +551,17 @@ def test_ensure_rejects_an_unreproducible_bootloader(
|
||||
assert not list(root.glob(".build-*"))
|
||||
|
||||
|
||||
def test_ensure_soft_fails_without_a_version_stamp(tmp_path: Path) -> None:
|
||||
"""An unidentifiable framework install must not be keyed or cached."""
|
||||
with (
|
||||
_orchestration_env(tmp_path),
|
||||
patch("esphome.espidf.framework.read_idf_version_txt", return_value=""),
|
||||
patch.object(bootloader, "_build_standalone") as mock_build,
|
||||
):
|
||||
assert bootloader.ensure_cached_bootloader() is False
|
||||
mock_build.assert_not_called()
|
||||
|
||||
|
||||
def test_ensure_soft_fails_when_the_probe_build_fails(tmp_path: Path) -> None:
|
||||
rcs = iter([0, 2])
|
||||
|
||||
@@ -618,14 +630,14 @@ def _flash_build(tmp_path: Path, cached: bool = True, offset: int = 0x1000) -> P
|
||||
def test_inject_adds_cached_bootloader(tmp_path: Path) -> None:
|
||||
build = _flash_build(tmp_path)
|
||||
flash_data = {"flash_files": {"0x10000": "dev.bin"}}
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build)
|
||||
assert bootloader.inject_bootloader_flash_file(flash_data, build) is True
|
||||
assert flash_data["flash_files"]["0x1000"] == "bootloader/bootloader.bin"
|
||||
|
||||
|
||||
def test_inject_uses_configured_offset(tmp_path: Path) -> None:
|
||||
build = _flash_build(tmp_path, offset=0)
|
||||
flash_data: dict = {}
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build)
|
||||
assert bootloader.inject_bootloader_flash_file(flash_data, build) is True
|
||||
assert flash_data["flash_files"] == {"0x0": "bootloader/bootloader.bin"}
|
||||
|
||||
|
||||
@@ -634,7 +646,7 @@ def test_inject_noop_on_a_stock_tree(tmp_path: Path) -> None:
|
||||
not in cached mode must stay exactly as IDF wrote it."""
|
||||
build = _flash_build(tmp_path, cached=False)
|
||||
flash_data: dict = {"flash_files": {}}
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build)
|
||||
assert bootloader.inject_bootloader_flash_file(flash_data, build) is True
|
||||
assert flash_data == {"flash_files": {}}
|
||||
|
||||
|
||||
@@ -649,7 +661,10 @@ def test_tree_uses_cached_bootloader(tmp_path: Path) -> None:
|
||||
|
||||
def test_inject_noop_without_cmakecache(tmp_path: Path) -> None:
|
||||
flash_data: dict = {"flash_files": {}}
|
||||
bootloader.inject_bootloader_flash_file(flash_data, tmp_path / "missing")
|
||||
assert (
|
||||
bootloader.inject_bootloader_flash_file(flash_data, tmp_path / "missing")
|
||||
is True
|
||||
)
|
||||
assert flash_data == {"flash_files": {}}
|
||||
|
||||
|
||||
@@ -661,9 +676,9 @@ def test_inject_missing_pieces_log_an_error(
|
||||
(build / "config" / "sdkconfig.json").write_text("{}") # no offset
|
||||
flash_data: dict = {"flash_files": {}}
|
||||
with caplog.at_level("ERROR"):
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build)
|
||||
assert bootloader.inject_bootloader_flash_file(flash_data, build) is False
|
||||
(build / "config" / "sdkconfig.json").unlink()
|
||||
with caplog.at_level("ERROR"):
|
||||
bootloader.inject_bootloader_flash_file(flash_data, build)
|
||||
assert bootloader.inject_bootloader_flash_file(flash_data, build) is False
|
||||
assert flash_data == {"flash_files": {}}
|
||||
assert caplog.text.count("factory image has no bootloader") == 2
|
||||
|
||||
@@ -1222,6 +1222,22 @@ def test_create_factory_bin_merges_cached_bootloader(setup_core: Path) -> None:
|
||||
assert offset_index < cmd.index("0x10000")
|
||||
|
||||
|
||||
def test_create_factory_bin_fails_when_the_cached_bootloader_is_missing(
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""A factory image without its bootloader would not boot; fail instead."""
|
||||
_setup_build(setup_core)
|
||||
build = CORE.relative_build_path("build")
|
||||
build.mkdir(parents=True)
|
||||
(build / "CMakeCache.txt").write_text(
|
||||
"ESPHOME_USE_CACHED_BOOTLOADER:UNINITIALIZED=1\n"
|
||||
)
|
||||
(build / "flasher_args.json").write_text('{"flash_files": {}}')
|
||||
with patch.object(toolchain.subprocess, "run") as mock_run:
|
||||
assert toolchain.create_factory_bin() is False
|
||||
mock_run.assert_not_called()
|
||||
|
||||
|
||||
def test_idf_target_from_variant(setup_core: Path) -> None:
|
||||
CORE.data[KEY_ESP32] = {KEY_VARIANT: "ESP32C6"}
|
||||
assert toolchain._idf_target() == "esp32c6"
|
||||
|
||||
Reference in New Issue
Block a user