From 7a2eaad9ceb117ad1f5c5498ab76d7362aea11bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 10:48:58 -0500 Subject: [PATCH] Warn when a compile entry does not lead with a compiler and fail on an empty core archive --- esphome/build_gen/arduino8266.py | 7 +++++++ esphome/espidf/idedata.py | 13 +++++++++++++ .../unit_tests/build_gen/test_arduino8266.py | 19 +++++++++++++++++++ tests/unit_tests/test_espidf_idedata.py | 17 ++++++++++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 50781d29da..e29ffca66d 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -646,6 +646,13 @@ def write_project(paths: dict[str, Path]) -> bool: core_objs = _ninja_compile_edges( lines, _collect_sources(core_dir, core_exclude), core_dir, "core" ) + if not core_objs: + # An empty archive would link into a wall of undefined references + # (app_entry, the exception vectors) far from the cause + raise EsphomeError( + f"Arduino toolchain install is incomplete: no core sources in " + f"{core_dir}; run 'esphome clean-all' and retry" + ) lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}") archives.append("libFrameworkArduino.a") diff --git a/esphome/espidf/idedata.py b/esphome/espidf/idedata.py index ae15888649..fd01b4fb28 100644 --- a/esphome/espidf/idedata.py +++ b/esphome/espidf/idedata.py @@ -15,6 +15,7 @@ import json import logging import os from pathlib import Path +import re import shlex import subprocess @@ -120,6 +121,11 @@ def _pick_entry(entries: list[dict]) -> dict: raise ValueError("no C++ translation unit found in compile_commands.json") +# The compiler basename a compile_commands entry must lead with (an +# optional target-triple prefix ends in one of these) +_COMPILER_STEM = re.compile(r"(?:gcc|g\+\+|cc|c\+\+|clang|clang\+\+)$") + + def _parse_entry( entry: dict, launcher: str | None = None ) -> tuple[str, list[str], list[str], list[str]]: @@ -143,6 +149,13 @@ def _parse_entry( # build, so this is a comparison, not a guess by name. if launcher is not None and tokens[0] == launcher: tokens = tokens[1:] + if not _COMPILER_STEM.search(Path(tokens[0]).stem): + # A stale compile DB built with a launcher the current run no longer + # configures would otherwise cache the launcher as the compiler path + _LOGGER.warning( + "compile_commands entry does not start with a compiler: %s", + tokens[0], + ) # token0 is the compiler path; the rest of the command already uses forward # slashes on Windows, so normalize it too for a consistent idedata file. cxx_path = tokens[0].replace("\\", "/") diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index e2931ec3a4..f5d1368c94 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -646,3 +646,22 @@ def test_shell_token_escaping() -> None: assert arduino8266._shell_token('-DX=a\\"b c') == '"-DX=a\\\\\\"b c"' # A trailing backslash run doubles before the closing quote assert arduino8266._shell_token("a b\\") == '"a b\\\\"' + + +def test_write_project_empty_core_raises(tmp_path: Path) -> None: + """A framework tree with no core sources fails at generation, not link.""" + paths = _make_framework(tmp_path) + core = paths["framework_path"] / "cores" / "esp8266" + for f in core.iterdir(): + f.unlink() + _set_flags() + src = CORE.relative_src_path() + (src / "esphome" / "components" / "esp8266").mkdir(parents=True, exist_ok=True) + (src / "main.cpp").write_text("") + with ( + patch.object(arduino8266, "generate_ld_scripts"), + patch("esphome.arduino8266.component.resolve_libraries", return_value=[]), + patch("esphome.arduino8266.framework.ccache_path", return_value=None), + pytest.raises(EsphomeError, match="no core sources"), + ): + arduino8266.write_project(paths) diff --git a/tests/unit_tests/test_espidf_idedata.py b/tests/unit_tests/test_espidf_idedata.py index 7036e893a2..6d218f09b7 100644 --- a/tests/unit_tests/test_espidf_idedata.py +++ b/tests/unit_tests/test_espidf_idedata.py @@ -279,11 +279,26 @@ def test_parse_entry_strips_launcher_prefix() -> None: assert cxx_path == "/tools/xtensa-lx106-elf-g++" assert defines == ["USE_ESP8266"] # Without a configured launcher nothing is stripped, even a token that - # happens to be named ccache + # happens to be named ccache -- but the surprise is warned about cxx_path, _, _, _ = idedata._parse_entry(entry) assert cxx_path == "/opt/homebrew/bin/ccache" +def test_parse_entry_warns_when_first_token_is_not_a_compiler( + caplog: pytest.LogCaptureFixture, +) -> None: + entry = _entry( + f"{ABS}build", + f"{ABS}build/src/esphome/core/application.cpp", + "/opt/homebrew/bin/ccache /tools/xtensa-lx106-elf-g++ -c a.cpp -o a.o", + ) + idedata._parse_entry(entry) + assert "does not start with a compiler" in caplog.text + caplog.clear() + idedata._parse_entry(entry, launcher="/opt/homebrew/bin/ccache") + assert "does not start with a compiler" not in caplog.text + + def _write_compile_commands(tmp_path: Path) -> Path: compile_commands = tmp_path / "compile_commands.json" compile_commands.write_text(