diff --git a/esphome/arduino8266/toolchain.py b/esphome/arduino8266/toolchain.py index 1cb8d32436..1cc26396e1 100644 --- a/esphome/arduino8266/toolchain.py +++ b/esphome/arduino8266/toolchain.py @@ -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, diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 21cec7d4ae..7afa1b1f9c 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -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 diff --git a/tests/unit_tests/components/esp8266/test_toolchain_validation.py b/tests/unit_tests/components/esp8266/test_toolchain_validation.py index a5e41c28a0..188169efb1 100644 --- a/tests/unit_tests/components/esp8266/test_toolchain_validation.py +++ b/tests/unit_tests/components/esp8266/test_toolchain_validation.py @@ -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({}) diff --git a/tests/unit_tests/test_arduino8266_toolchain.py b/tests/unit_tests/test_arduino8266_toolchain.py index 06f8c199f9..28e4128de4 100644 --- a/tests/unit_tests/test_arduino8266_toolchain.py +++ b/tests/unit_tests/test_arduino8266_toolchain.py @@ -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"))