From 18206967a1b54dbf3aa2b70c5be14491608aa186 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:40:57 -0500 Subject: [PATCH 1/2] Validate captured CPPDEFINES, warn on empty idedata includes, name the idedata consumers --- esphome/__main__.py | 6 ++++- esphome/build_helpers/idedata.py | 8 +++++++ esphome/platformio/extra_script.py | 8 +++++-- .../unit_tests/build_helpers/test_idedata.py | 23 +++++++++++++++++++ .../test_platformio_extra_script.py | 19 +++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c96179b56d..0da86b3ec0 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -865,7 +865,11 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int: except IDEDATA_BEST_EFFORT_ERRORS as err: # The firmware already built; an idedata failure must not fail # a successful build. - _LOGGER.warning("Could not generate idedata: %s", err) + _LOGGER.warning( + "Could not generate idedata: %s (IDE, clang-tidy, and " + "memory-analysis data will be unavailable for this build)", + err, + ) _LOGGER.debug("Idedata failure detail", exc_info=True) else: from esphome.platformio import toolchain diff --git a/esphome/build_helpers/idedata.py b/esphome/build_helpers/idedata.py index 567a5fa29e..efee461a0a 100644 --- a/esphome/build_helpers/idedata.py +++ b/esphome/build_helpers/idedata.py @@ -337,6 +337,14 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d for inc in parse_entry(entry, launcher)[2]: build_includes.setdefault(inc, None) + if not build_includes: + # No ESPHome translation unit contributed includes: idedata with an + # empty build include set breaks clang-tidy/IDE consumers silently + _LOGGER.warning( + "No ESPHome source includes found in %s; idedata will be incomplete", + compile_commands, + ) + return { "cc_path": _cc_path_from_cxx(cxx_path), "cxx_path": cxx_path, diff --git a/esphome/platformio/extra_script.py b/esphome/platformio/extra_script.py index a88201266d..2d5667a3e5 100644 --- a/esphome/platformio/extra_script.py +++ b/esphome/platformio/extra_script.py @@ -240,10 +240,14 @@ def captured_as_build_flags( flags.append(f"-L{resolved}") flags.extend(f"-l{lib}" for lib in result.libs) for define in result.cppdefines: - if isinstance(define, tuple) and len(define) == 2: + # SCons also accepts dict/list CPPDEFINES; formatting those blind + # would hand the compiler garbage like -D{'FOO': '1'} + if isinstance(define, (tuple, list)) and len(define) == 2: flags.append(f"-D{define[0]}={define[1]}") - else: + elif isinstance(define, str): flags.append(f"-D{define}") + else: + _LOGGER.warning("Ignoring unsupported CPPDEFINES entry %r", define) flags.extend(result.linkflags) flags.extend(result.cppflags) return flags diff --git a/tests/unit_tests/build_helpers/test_idedata.py b/tests/unit_tests/build_helpers/test_idedata.py index ff91ca34c1..2998d46ec3 100644 --- a/tests/unit_tests/build_helpers/test_idedata.py +++ b/tests/unit_tests/build_helpers/test_idedata.py @@ -151,6 +151,29 @@ def test_is_esphome_src_handles_backslash_paths() -> None: assert not idedata._is_esphome_src(r"C:\b\src\esphome\core\app.h") +def test_idedata_from_build_empty_includes_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A compile DB with no ESPHome TU yields no build includes; that is + never a usable idedata, so it must be diagnosable.""" + compile_commands = tmp_path / "compile_commands.json" + compile_commands.write_text( + json.dumps( + [ + _entry( + f"{ABS}build", + f"{ABS}build/other/lib.cpp", + "/tools/g++ -c other/lib.cpp -o lib.o", + ) + ] + ) + ) + with patch.object(idedata, "get_toolchain_includes", return_value=[]): + data = idedata.idedata_from_build(compile_commands) + assert data["includes"]["build"] == [] + assert "idedata will be incomplete" in caplog.text + + def test_idedata_from_build_dedupes_identical_command_shapes( tmp_path: Path, ) -> None: diff --git a/tests/unit_tests/test_platformio_extra_script.py b/tests/unit_tests/test_platformio_extra_script.py index 73523f4bc0..5df8e3c9c5 100644 --- a/tests/unit_tests/test_platformio_extra_script.py +++ b/tests/unit_tests/test_platformio_extra_script.py @@ -169,6 +169,25 @@ def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None: assert c.data["build"]["flags"] == ["-DBASE=1", "-lesp8266"] +def test_captured_dict_cppdefines_warn_and_skip(tmp_path, caplog) -> None: + """A dict CPPDEFINES entry (legal SCons) must warn and skip; formatting + it blind would hand the compiler -D{'FOO': '1'} garbage.""" + (tmp_path / "src").mkdir() + script = tmp_path / "extra.py" + script.write_text( + "env.Append(CPPDEFINES=[{'FOO': '1'}, ('BAR', 2), ['BAZ', 3], 'PLAIN'])\n" + ) + + c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy")) + c.path = tmp_path + c.data = {"build": {"extraScript": "extra.py"}} + + apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266") + + assert c.data["build"]["flags"] == ["-DBAR=2", "-DBAZ=3", "-DPLAIN"] + assert "Ignoring unsupported CPPDEFINES entry" in caplog.text + + def test_apply_extra_script_subscript_env_read(tmp_path) -> None: """Scripts also read env["BOARD_MCU"]; the subscript form must work or the broad handler discards every flag the script captured.""" From ba1ad765ea6bb0664acd74cf64903691853a0d14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 21:41:50 -0500 Subject: [PATCH 2/2] Name the probe failure cause in the tool-version warning --- esphome/framework_helpers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 7911acb111..b473354f82 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -215,8 +215,10 @@ def tool_version_runs(binary: str, warning: str) -> bool: # Repo-wide convention (posix_spawn fast path) close_fds=False, ) - except (OSError, subprocess.SubprocessError): - _LOGGER.warning(warning, binary) + except (OSError, subprocess.SubprocessError) as err: + # The cause (permission denied, missing DLL, timeout) is the one + # detail the user needs to fix it + _LOGGER.warning("%s (%s)", warning % binary, err) return False return True