mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 10:06:03 +00:00
Strip the exact configured launcher from compile commands instead of guessing by name
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user