diff --git a/esphome/writer.py b/esphome/writer.py index b0a42cf5d5..42ca0c938a 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -338,27 +338,13 @@ def copy_src_tree(): # Defensively force a rebuild if the build_info files don't exist, or if # there was a config change which didn't actually cause a source change - if not build_info_data_h_path.exists() or not build_info_data_cpp_path.exists(): - _LOGGER.debug("Build info files missing; regenerating") + if _build_info_stale( + build_info_data_h_path, + build_info_data_cpp_path, + build_info_json_path, + config_hash, + ): sources_changed = True - else: - try: - existing = json.loads(build_info_json_path.read_text(encoding="utf-8")) - if ( - existing.get("config_hash") != config_hash - or existing.get("esphome_version") != __version__ - ): - _LOGGER.debug( - "Build info stale (config_hash %s -> %s, version %s -> %s)", - existing.get("config_hash"), - config_hash, - existing.get("esphome_version"), - __version__, - ) - sources_changed = True - except (json.JSONDecodeError, KeyError, OSError): - _LOGGER.debug("Build info JSON unreadable; regenerating") - sources_changed = True # Write build_info header and JSON metadata if sources_changed: @@ -412,6 +398,33 @@ def generate_version_h(): ) +def _build_info_stale( + h_path: Path, cpp_path: Path, json_path: Path, config_hash: int +) -> bool: + """Whether the build-info sources must regenerate (missing or stale).""" + if not h_path.exists() or not cpp_path.exists(): + _LOGGER.debug("Build info files missing; regenerating") + return True + try: + existing = json.loads(json_path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + _LOGGER.debug("Build info JSON unreadable; regenerating") + return True + if ( + existing.get("config_hash") != config_hash + or existing.get("esphome_version") != __version__ + ): + _LOGGER.debug( + "Build info stale (config_hash %s -> %s, version %s -> %s)", + existing.get("config_hash"), + config_hash, + existing.get("esphome_version"), + __version__, + ) + return True + return False + + def get_build_info() -> tuple[int, int, str, str]: """Calculate build_info values from current config. diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index 46e60ebd8e..2d93dc866e 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -2462,3 +2462,28 @@ def test_copy_src_tree_ignores_removed_generated_file( # file was removed and regenerated, not that it triggered sources_changed. new_json = json.loads(build_info_json_path.read_text()) assert new_json["config_hash"] == 0xDEADBEEF + + +def test_build_info_stale_branches(tmp_path: Path) -> None: + """Missing files, an unreadable JSON, a hash or version mismatch each + regenerate; a matching record does not.""" + import json as json_mod + + from esphome.const import __version__ + from esphome.writer import _build_info_stale + + h = tmp_path / "build_info_data.h" + cpp = tmp_path / "build_info_data.cpp" + info = tmp_path / "build_info.json" + assert _build_info_stale(h, cpp, info, 1) is True # files missing + h.write_text("") + cpp.write_text("") + assert _build_info_stale(h, cpp, info, 1) is True # JSON unreadable + info.write_text("not json") + assert _build_info_stale(h, cpp, info, 1) is True + info.write_text(json_mod.dumps({"config_hash": 2, "esphome_version": __version__})) + assert _build_info_stale(h, cpp, info, 1) is True # hash mismatch + info.write_text(json_mod.dumps({"config_hash": 1, "esphome_version": "0.0.0"})) + assert _build_info_stale(h, cpp, info, 1) is True # version mismatch + info.write_text(json_mod.dumps({"config_hash": 1, "esphome_version": __version__})) + assert _build_info_stale(h, cpp, info, 1) is False