Strip the exact configured launcher from compile commands instead of guessing by name

This commit is contained in:
J. Nick Koston
2026-08-20 09:55:15 -05:00
parent 0d38e47646
commit 069ae156f9
4 changed files with 52 additions and 16 deletions
+4
View File
@@ -210,8 +210,12 @@ def get_idedata() -> dict | None:
"""
from esphome.espidf.idedata import load_or_build_idedata
ccache = framework.ccache_path()
return load_or_build_idedata(
get_build_dir() / "compile_commands.json",
get_elf_path(),
CORE.relative_internal_path("idedata", f"{CORE.name}.json"),
# The compile DB's commands carry the same ccache prefix the ninja
# rules were generated with
launcher=str(ccache) if ccache else None,
)
+18 -9
View File
@@ -120,7 +120,9 @@ def _pick_entry(entries: list[dict]) -> dict:
raise ValueError("no C++ translation unit found in compile_commands.json")
def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
def _parse_entry(
entry: dict, launcher: str | None = None
) -> tuple[str, list[str], list[str], list[str]]:
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
directory = Path(entry["directory"])
tokens = _expand_response_files(_split_command(entry["command"]), directory)
@@ -136,8 +138,10 @@ def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
raw = os.path.normpath(directory / raw)
return raw.replace("\\", "/")
# A ccache-wrapped command ("ccache g++ ...") names the compiler second.
if Path(tokens[0]).stem == "ccache":
# A launcher-wrapped command ("ccache g++ ...") names the compiler
# second. The caller passes the exact launcher it configured into the
# build, so this is a comparison, not a guess by name.
if launcher is not None and tokens[0] == launcher:
tokens = tokens[1:]
# 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.
@@ -223,12 +227,17 @@ def _cc_path_from_cxx(cxx_path: str) -> str:
def load_or_build_idedata(
compile_commands: Path, elf_path: Path, cache: Path
compile_commands: Path,
elf_path: Path,
cache: Path,
launcher: str | None = None,
) -> dict | None:
"""Return idedata for a compile_commands.json build, cached on mtime.
Shared by the native ESP-IDF and ESP8266 Arduino toolchains. Returns None
when the compile DB doesn't exist yet (nothing was built).
when the compile DB doesn't exist yet (nothing was built). ``launcher``
is the compiler-launcher path (ccache) the build was generated with, if
any; commands in the compile DB are prefixed with it.
"""
if not compile_commands.is_file():
_LOGGER.debug("No %s yet; skipping idedata generation", compile_commands)
@@ -247,14 +256,14 @@ def load_or_build_idedata(
if isinstance(cached, dict) and "cc_path" in cached:
return cached
data = idedata_from_build(compile_commands)
data = idedata_from_build(compile_commands, launcher)
data["prog_path"] = str(elf_path)
cache.parent.mkdir(parents=True, exist_ok=True)
cache.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return data
def idedata_from_build(compile_commands: Path) -> dict:
def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> dict:
"""Parse compile_commands.json into the idedata fields consumers expect.
A single ESP-IDF compile entry only carries its own component's REQUIRES
@@ -265,13 +274,13 @@ def idedata_from_build(compile_commands: Path) -> dict:
provides).
"""
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
cxx_path, defines, _, cxx_flags = _parse_entry(_pick_entry(entries))
cxx_path, defines, _, cxx_flags = _parse_entry(_pick_entry(entries), launcher)
build_includes: dict[str, None] = {}
for entry in entries:
if not _is_esphome_src(entry["file"]):
continue
for inc in _parse_entry(entry)[2]:
for inc in _parse_entry(entry, launcher)[2]:
build_includes.setdefault(inc, None)
return {
+20 -3
View File
@@ -200,14 +200,31 @@ def test_print_size_summary_size_tool_failure(
def test_get_idedata_delegates(tmp_path: Path) -> None:
with patch(
"esphome.espidf.idedata.load_or_build_idedata", return_value={"cc_path": "x"}
) as mock_load:
with (
patch(
"esphome.espidf.idedata.load_or_build_idedata",
return_value={"cc_path": "x"},
) as mock_load,
patch.object(framework, "ccache_path", return_value=Path("/cc/ccache")),
):
assert toolchain.get_idedata() == {"cc_path": "x"}
compile_commands, elf, cache = mock_load.call_args[0]
assert compile_commands.name == "compile_commands.json"
assert elf.name == "firmware.elf"
assert cache.name == "test8266.json"
# The exact configured launcher is passed for compile DB parsing
assert mock_load.call_args.kwargs["launcher"] == str(Path("/cc/ccache"))
def test_get_idedata_no_ccache(tmp_path: Path) -> None:
with (
patch(
"esphome.espidf.idedata.load_or_build_idedata", return_value={}
) as mock_load,
patch.object(framework, "ccache_path", return_value=None),
):
toolchain.get_idedata()
assert mock_load.call_args.kwargs["launcher"] is None
def test_run_compile_skips_compdb_when_ninja_unchanged(tmp_path: Path) -> None:
+10 -4
View File
@@ -264,18 +264,24 @@ def test_parse_entry_normalizes_windows_cxx_path() -> None:
assert "C:/inc/a" in includes
def test_parse_entry_strips_ccache_prefix() -> None:
"""A ccache-wrapped compile names the compiler second; the wrapper must
not be mistaken for the compiler path."""
def test_parse_entry_strips_launcher_prefix() -> None:
"""A launcher-wrapped compile names the compiler second; the exact
configured launcher is stripped, not anything ccache-shaped."""
entry = _entry(
f"{ABS}build",
f"{ABS}build/src/esphome/core/application.cpp",
"/opt/homebrew/bin/ccache /tools/xtensa-lx106-elf-g++ -DUSE_ESP8266 "
"-c app.cpp -o app.cpp.o",
)
cxx_path, defines, _, _ = idedata._parse_entry(entry)
cxx_path, defines, _, _ = idedata._parse_entry(
entry, launcher="/opt/homebrew/bin/ccache"
)
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
cxx_path, _, _, _ = idedata._parse_entry(entry)
assert cxx_path == "/opt/homebrew/bin/ccache"
def _write_compile_commands(tmp_path: Path) -> Path: