Warn when a compile entry does not lead with a compiler and fail on an empty core archive

This commit is contained in:
J. Nick Koston
2026-08-20 10:48:58 -05:00
parent 785260626e
commit 7a2eaad9ce
4 changed files with 55 additions and 1 deletions
+7
View File
@@ -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")
+13
View File
@@ -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("\\", "/")
@@ -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)
+16 -1
View File
@@ -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(