Toolchain-suffixed idedata cache, surfaced idedata failures, and per-dump decode warnings

This commit is contained in:
J. Nick Koston
2026-08-20 14:48:18 -05:00
parent 258417d7d3
commit 2c883a2c26
4 changed files with 46 additions and 11 deletions
+8 -2
View File
@@ -107,7 +107,11 @@ def run_compile(config: ConfigType, verbose: bool) -> int:
return rc
_print_size_summary(build_dir)
get_idedata()
if get_idedata() is None:
_LOGGER.warning(
"Could not generate idedata from %s",
build_dir / "compile_commands.json",
)
return 0
@@ -218,7 +222,9 @@ def get_idedata() -> dict | None:
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
CORE.relative_internal_path("idedata", f"{CORE.name}.json"),
# Suffixed so a platformio->arduino->platformio round trip on one
# config never serves the other toolchain's cache shape
CORE.relative_internal_path("idedata", f"{CORE.name}.arduino.json"),
# The compile DB's commands carry the same ccache prefix the ninja
# rules were generated with
launcher=str(ccache) if ccache else None,
+26 -6
View File
@@ -1,9 +1,10 @@
import functools
import logging
import math
from pathlib import Path
import platform
import re
import subprocess
import time
import esphome.codegen as cg
import esphome.config_validation as cv
@@ -603,10 +604,21 @@ ESP8266_EXCEPTION_CODES = {
}
@functools.cache
def _warn_missing_decode_tool(path: str) -> None:
# Cached so a stack dump of dozens of addresses warns once, not per line
_LOGGER.warning("Cannot decode crash addresses: %s missing", path)
_DECODE_WARNED_AT: dict[str, float] = {}
def _warn_decode_problem(key: str, message: str, *args) -> None:
"""Warn, deduplicated per stack dump but not per process.
A dump decodes dozens of addresses in a burst; one warning per burst is
enough. A long-running dashboard must still warn on the next dump, so
the suppression expires instead of living for the process lifetime.
"""
now = time.monotonic()
if now - _DECODE_WARNED_AT.get(key, -math.inf) < 30:
return
_DECODE_WARNED_AT[key] = now
_LOGGER.warning(message, *args)
def _decode_pc(config, addr):
@@ -617,7 +629,9 @@ def _decode_pc(config, addr):
elf = native_toolchain.get_elf_path()
missing = addr2line if not addr2line.is_file() else elf
if not missing.is_file():
_warn_missing_decode_tool(str(missing))
_warn_decode_problem(
str(missing), "Cannot decode crash addresses: %s missing", missing
)
return
addr2line, elf = str(addr2line), str(elf)
else:
@@ -632,6 +646,12 @@ def _decode_pc(config, addr):
try:
translation = subprocess.check_output(command, close_fds=False).decode().strip()
except Exception: # noqa: BLE001 # pylint: disable=broad-except
if CORE.using_toolchain_arduino:
# A present-but-failing addr2line (stale ELF, bad install) must
# be visible, matching the missing-tool warning above
_warn_decode_problem(
"addr2line-failed", "Could not decode crash address %s", addr
)
_LOGGER.debug("Caught exception for command %s", command, exc_info=1)
return
@@ -122,7 +122,7 @@ def test_decode_pc_native_missing_tools_warns_once(
"""A stack dump of many addresses produces one missing-tool warning."""
from esphome.components import esp8266
esp8266._warn_missing_decode_tool.cache_clear()
esp8266._DECODE_WARNED_AT.clear()
with (
patch(
"esphome.arduino8266.toolchain.get_addr2line_path",
@@ -136,4 +136,13 @@ def test_decode_pc_native_missing_tools_warns_once(
esp8266._decode_pc({}, "40201234")
esp8266._decode_pc({}, "40201238")
assert caplog.text.count("Cannot decode crash addresses") == 1
esp8266._warn_missing_decode_tool.cache_clear()
esp8266._DECODE_WARNED_AT.clear()
def test_resolve_toolchain_rejects_unsupported() -> None:
"""ESP8266 rejects a CLI toolchain it cannot serve, like every platform."""
from esphome.components.esp8266 import _resolve_toolchain
CORE.toolchain = Toolchain.SDK_NRF
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'sdk-nrf'"):
_resolve_toolchain({})
@@ -212,7 +212,7 @@ def test_get_idedata_delegates(tmp_path: Path) -> None:
compile_commands, elf, cache = mock_load.call_args[0]
assert compile_commands.name == "compile_commands.json"
assert elf.name == "firmware.elf"
assert cache.name == "test8266.json"
assert cache.name == "test8266.arduino.json"
# The exact configured launcher is passed for compile DB parsing
assert mock_load.call_args.kwargs["launcher"] == str(Path("/cc/ccache"))