Merge branch 'esp8266-native-framework-installer' into esp8266-native-library-backend

This commit is contained in:
J. Nick Koston
2026-08-22 22:39:48 -05:00
4 changed files with 51 additions and 24 deletions
+29 -14
View File
@@ -161,6 +161,10 @@ def parse_entry(
raw = os.path.normpath(directory / raw)
return raw.replace("\\", "/")
if not tokens:
# _split_command("") is [] by design; fail like _pick_entry does
# instead of an IndexError traceback
raise ValueError(f"empty compile command for {entry.get('file')}")
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
if launcher is not None and tokens[0] == launcher:
tokens = tokens[1:]
@@ -290,6 +294,19 @@ def load_or_build_idedata(
return data
def reject_launcher_compiler(cxx_path: str) -> None:
"""Reject a compile DB that names a launcher (ccache) as the compiler.
Reject before the toolchain probe, which would fail opaquely on a
launcher; the unusable compile DB must never be cached or consumed.
"""
if _is_launcher(cxx_path):
raise EsphomeError(
f"compile_commands.json names the launcher {cxx_path} as the "
"compiler; the compile database is unusable"
)
def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> dict:
"""Parse compile_commands.json into the idedata fields consumers expect.
@@ -304,17 +321,12 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d
representative = _pick_entry(entries)
cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher)
if _is_launcher(cxx_path):
# Reject before the toolchain probe, which would fail opaquely on
# a launcher; never cache the unusable compile DB
raise EsphomeError(
f"compile_commands.json names the launcher {cxx_path} as the "
"compiler; the compile database is unusable"
)
reject_launcher_compiler(cxx_path)
# Seed with the representative's includes so it is not parsed twice
has_esphome_tu = _is_esphome_src(representative["file"])
build_includes: dict[str, None] = dict.fromkeys(
rep_includes if _is_esphome_src(representative["file"]) else ()
rep_includes if has_esphome_tu else ()
)
def _shape(entry: dict) -> str:
@@ -331,18 +343,21 @@ def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> d
for entry in entries:
if entry is representative or not _is_esphome_src(entry["file"]):
continue
has_esphome_tu = True
if (shape := _shape(entry)) in seen_shapes:
continue
seen_shapes.add(shape)
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,
if not has_esphome_tu:
# _pick_entry fell back to an arbitrary C++ entry: idedata built
# from it breaks clang-tidy/IDE consumers, and a one-time warning
# would be cached into permanence. The best-effort call sites
# downgrade this to a build warning.
raise EsphomeError(
f"No ESPHome translation unit found in {compile_commands}; "
"refusing to cache unusable idedata"
)
return {
+6 -1
View File
@@ -23,7 +23,11 @@ from dataclasses import dataclass
import os
from pathlib import Path
from esphome.build_helpers.idedata import get_toolchain_includes, parse_entry
from esphome.build_helpers.idedata import (
get_toolchain_includes,
parse_entry,
reject_launcher_compiler,
)
TIDY_PROJECT_NAME = "esphome_tidy"
@@ -422,6 +426,7 @@ def _idedata_from_tidy_project(compile_commands: Path) -> dict:
if entry is None:
raise RuntimeError(f"tidy.cpp not found in {compile_commands}")
cxx_path, defines, includes, cxx_flags = parse_entry(entry)
reject_launcher_compiler(cxx_path)
return {
"cxx_path": cxx_path,
+1
View File
@@ -1123,6 +1123,7 @@ def test_should_run_esp32_platformio_with_branch() -> None:
# Shared native-build modules the IDF build imports -> trigger
(["esphome/build_helpers/idedata.py"], True),
(["esphome/platformio/library.py"], True),
(["esphome/framework_helpers.py"], True),
(["esphome/platformio/extra_script.py"], True),
# PlatformIO build gen, its toolchain, and the esp32 component are
# NOT IDF-infra triggers
+15 -9
View File
@@ -151,11 +151,16 @@ 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."""
def test_parse_entry_empty_command_raises() -> None:
"""A blank command fails with a named ValueError, not an IndexError."""
entry = {"directory": "/b", "file": "/b/src/x.cpp", "command": ""}
with pytest.raises(ValueError, match="empty compile command"):
idedata.parse_entry(entry)
def test_idedata_from_build_empty_includes_raises(tmp_path: Path) -> None:
"""A compile DB with no ESPHome TU is never usable idedata and must
not be cached (call sites downgrade the raise to a build warning)."""
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(
json.dumps(
@@ -168,10 +173,11 @@ def test_idedata_from_build_empty_includes_warns(
]
)
)
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
with (
patch.object(idedata, "get_toolchain_includes", return_value=[]),
pytest.raises(EsphomeError, match="No ESPHome translation unit found"),
):
idedata.idedata_from_build(compile_commands)
def test_idedata_from_build_dedupes_identical_command_shapes(