diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 96fe9fed6f..22d38e36b4 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -1,3 +1,4 @@ +import functools import logging from pathlib import Path import platform @@ -604,6 +605,12 @@ 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) + + def _decode_pc(config, addr): if CORE.using_toolchain_arduino: from esphome.arduino8266 import toolchain as native_toolchain @@ -611,9 +618,8 @@ def _decode_pc(config, addr): addr2line = native_toolchain.get_addr2line_path() elf = native_toolchain.get_elf_path() if not addr2line.is_file() or not elf.is_file(): - _LOGGER.warning( - "Cannot decode crash addresses: %s missing", - addr2line if not addr2line.is_file() else elf, + _warn_missing_decode_tool( + str(addr2line if not addr2line.is_file() else elf) ) return addr2line, elf = str(addr2line), str(elf) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 450a809b2a..c21a2ad716 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -528,6 +528,8 @@ def get_idedata() -> dict | None: """ from esphome.espidf.idedata import load_or_build_idedata + # No launcher: CMake excludes CMAKE__COMPILER_LAUNCHER (ccache) + # from the exported compile database, unlike ninja's compdb dump. return load_or_build_idedata( CORE.relative_build_path("build", "compile_commands.json"), get_elf_path(), diff --git a/tests/unit_tests/components/esp8266/test_toolchain_validation.py b/tests/unit_tests/components/esp8266/test_toolchain_validation.py index 0a82822de4..f8a02cd410 100644 --- a/tests/unit_tests/components/esp8266/test_toolchain_validation.py +++ b/tests/unit_tests/components/esp8266/test_toolchain_validation.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pathlib import Path + import pytest from esphome.components.esp8266 import ( @@ -114,3 +116,28 @@ def test_yaml_toolchain_key_defaults_to_platformio() -> None: _resolve_toolchain({}) assert CORE.toolchain == Toolchain.PLATFORMIO + + +def test_decode_pc_native_missing_tools_warns_once( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A stack dump of many addresses produces one missing-tool warning.""" + from unittest.mock import patch + + from esphome.components import esp8266 + + esp8266._warn_missing_decode_tool.cache_clear() + with ( + patch( + "esphome.arduino8266.toolchain.get_addr2line_path", + return_value=tmp_path / "missing-addr2line", + ), + patch( + "esphome.arduino8266.toolchain.get_elf_path", + return_value=tmp_path / "missing.elf", + ), + ): + esp8266._decode_pc({}, "40201234") + esp8266._decode_pc({}, "40201238") + assert caplog.text.count("Cannot decode crash addresses") == 1 + esp8266._warn_missing_decode_tool.cache_clear()