mirror of
https://github.com/esphome/esphome.git
synced 2026-08-25 15:46:20 +00:00
[core] Log which source change triggered a rebuild (#18552)
This commit is contained in:
@@ -55,8 +55,9 @@ DEFAULT_BUILD_SRC_DIRS = "src"
|
||||
DEFAULT_BUILD_INCLUDE_DIR = "include"
|
||||
DEFAULT_BUILD_FLAGS = []
|
||||
# Suffix -> compiler kind (PlatformIO's CSUFFIXES/CXXSUFFIXES/ASSUFFIXES);
|
||||
# "asm" merges SCons's AS and ASPP sets. Per CXXSUFFIXES .C/.C++ are C++
|
||||
# here, even where SCons demotes .C on case-insensitive filesystems.
|
||||
# "aspp" is SCons's preprocessed-assembly set, "asm" its plain-assembler
|
||||
# set (no preprocessor, defines, or includes). Per CXXSUFFIXES .C/.C++ are
|
||||
# C++ here, even where SCons demotes .C on case-insensitive filesystems.
|
||||
SOURCE_KIND_FOR_SUFFIX: dict[str, str] = {
|
||||
".c": "c",
|
||||
".cpp": "cxx",
|
||||
@@ -65,10 +66,10 @@ SOURCE_KIND_FOR_SUFFIX: dict[str, str] = {
|
||||
".c++": "cxx",
|
||||
".C": "cxx",
|
||||
".C++": "cxx",
|
||||
".S": "asm",
|
||||
".spp": "asm",
|
||||
".SPP": "asm",
|
||||
".sx": "asm",
|
||||
".S": "aspp",
|
||||
".spp": "aspp",
|
||||
".SPP": "aspp",
|
||||
".sx": "aspp",
|
||||
".s": "asm",
|
||||
".asm": "asm",
|
||||
".ASM": "asm",
|
||||
|
||||
+60
-28
@@ -288,11 +288,13 @@ def copy_src_tree():
|
||||
# Source file removed, delete target
|
||||
p.unlink()
|
||||
if target not in generated_files:
|
||||
_LOGGER.debug("Source removed: %s", target)
|
||||
sources_changed = True
|
||||
else:
|
||||
src_file = source_files_copy.pop(target)
|
||||
with src_file.path() as src_path:
|
||||
if copy_file_if_changed(src_path, p) and target not in generated_files:
|
||||
_LOGGER.debug("Source changed: %s", target)
|
||||
sources_changed = True
|
||||
|
||||
# Now copy new files
|
||||
@@ -303,21 +305,25 @@ def copy_src_tree():
|
||||
copy_file_if_changed(src_path, dst_path)
|
||||
and target not in generated_files
|
||||
):
|
||||
_LOGGER.debug("Source added: %s", target)
|
||||
sources_changed = True
|
||||
|
||||
# Finally copy defines
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome", "core", "defines.h"), generate_defines_h()
|
||||
):
|
||||
_LOGGER.debug("Source changed: esphome/core/defines.h")
|
||||
sources_changed = True
|
||||
write_file_if_changed(CORE.relative_build_path("README.txt"), ESPHOME_README_TXT)
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome.h"), ESPHOME_H_FORMAT.format(include_s)
|
||||
):
|
||||
_LOGGER.debug("Source changed: esphome.h")
|
||||
sources_changed = True
|
||||
if write_file_if_changed(
|
||||
CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h()
|
||||
):
|
||||
_LOGGER.debug("Source changed: esphome/core/version.h")
|
||||
sources_changed = True
|
||||
|
||||
# Generate new build_info files if needed
|
||||
@@ -332,35 +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():
|
||||
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 not isinstance(existing, dict) or (
|
||||
existing.get("config_hash") != config_hash
|
||||
or existing.get("esphome_version") != __version__
|
||||
):
|
||||
# Non-object JSON is stale like every other damage case
|
||||
sources_changed = True
|
||||
except FileNotFoundError:
|
||||
# An absent build_info.json is stale, not damaged; rebuild quietly
|
||||
sources_changed = True
|
||||
except (ValueError, OSError) as err:
|
||||
# ValueError covers both JSONDecodeError and UnicodeDecodeError;
|
||||
# unlink so the regenerating write never re-reads the bad copy.
|
||||
# "Unreadable" not "damaged": EACCES/EISDIR land here too
|
||||
_LOGGER.warning("Regenerating unreadable build_info.json: %s", err)
|
||||
try:
|
||||
# missing_ok: a concurrent clean may have removed it already
|
||||
build_info_json_path.unlink(missing_ok=True)
|
||||
except OSError as unlink_err:
|
||||
# The later write re-reads the file, so a kept unreadable copy
|
||||
# fails again with a misattributed error; name the real cause
|
||||
_LOGGER.warning(
|
||||
"Could not remove unreadable build_info.json: %s", unlink_err
|
||||
)
|
||||
sources_changed = True
|
||||
|
||||
# Write build_info header and JSON metadata
|
||||
if sources_changed:
|
||||
@@ -414,6 +398,54 @@ 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 FileNotFoundError:
|
||||
# An absent build_info.json is stale, not damaged; rebuild quietly
|
||||
_LOGGER.debug("Build info JSON missing; regenerating")
|
||||
return True
|
||||
except (ValueError, OSError) as err:
|
||||
# ValueError covers both JSONDecodeError and UnicodeDecodeError;
|
||||
# unlink so the regenerating write never re-reads the bad copy.
|
||||
# "Unreadable" not "damaged": EACCES/EISDIR land here too
|
||||
_LOGGER.warning("Regenerating unreadable build_info.json: %s", err)
|
||||
try:
|
||||
# missing_ok: a concurrent clean may have removed it already
|
||||
json_path.unlink(missing_ok=True)
|
||||
except OSError as unlink_err:
|
||||
# The later write re-reads the file, so a kept unreadable copy
|
||||
# fails again with a misattributed error; name the real cause
|
||||
_LOGGER.warning(
|
||||
"Could not remove unreadable build_info.json: %s", unlink_err
|
||||
)
|
||||
return True
|
||||
if not isinstance(existing, dict):
|
||||
# Valid JSON that is not an object (truncated or hand-edited) is
|
||||
# stale, not a traceback
|
||||
_LOGGER.debug("Build info JSON malformed; 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.
|
||||
|
||||
|
||||
@@ -929,11 +929,11 @@ def test_split_flag_entry_non_string_is_clean() -> None:
|
||||
|
||||
|
||||
def test_source_kind_map_shape() -> None:
|
||||
"""The kind values the native compile rules key on, and the deliberate
|
||||
AS/ASPP merge (.s and .S both map to asm)."""
|
||||
"""The kind values the native compile rules key on; the AS/ASPP split
|
||||
matches SCons (.S preprocessed, .s plain assembler)."""
|
||||
|
||||
assert set(SOURCE_KIND_FOR_SUFFIX.values()) == {"c", "cxx", "asm"}
|
||||
assert set(SOURCE_KIND_FOR_SUFFIX.values()) == {"c", "cxx", "asm", "aspp"}
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".s"] == "asm"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".S"] == "asm"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".S"] == "aspp"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".c"] == "c"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".cpp"] == "cxx"
|
||||
|
||||
@@ -2441,3 +2441,39 @@ 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
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("case", "content", "expected"),
|
||||
[
|
||||
("files missing", None, True),
|
||||
("json missing", "ABSENT", True),
|
||||
("json unreadable", "not json", True),
|
||||
# Valid JSON that is not an object is stale, not an AttributeError
|
||||
("json not an object", "[]", True),
|
||||
("hash mismatch", {"config_hash": 2, "esphome_version": "CURRENT"}, True),
|
||||
("version mismatch", {"config_hash": 1, "esphome_version": "0.0.0"}, True),
|
||||
("matching record", {"config_hash": 1, "esphome_version": "CURRENT"}, False),
|
||||
],
|
||||
)
|
||||
def test_build_info_stale_branches(
|
||||
tmp_path: Path, case: str, content, expected: bool
|
||||
) -> None:
|
||||
"""Missing files, an unreadable JSON, a hash or version mismatch each
|
||||
regenerate; a matching record does not."""
|
||||
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"
|
||||
if content is not None:
|
||||
h.write_text("")
|
||||
cpp.write_text("")
|
||||
if isinstance(content, dict):
|
||||
if content.get("esphome_version") == "CURRENT":
|
||||
content["esphome_version"] = __version__
|
||||
info.write_text(json.dumps(content))
|
||||
elif isinstance(content, str) and content != "ABSENT":
|
||||
info.write_text(content)
|
||||
assert _build_info_stale(h, cpp, info, 1) is expected, case
|
||||
|
||||
Reference in New Issue
Block a user