diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index d34d3b0029..112bf2d2eb 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -212,6 +212,18 @@ _PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"}) _PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"}) +def _db_source_path(entry: dict, build_dir: Path) -> Path: + """A compile database entry's source file as a resolved absolute path.""" + path = Path(entry["file"]) + if not path.is_absolute(): + directory = entry.get("directory") + base = ( + Path(directory) if isinstance(directory, str) and directory else build_dir + ) + path = base / path + return path.resolve() + + def pch_compile_command( build_dir: Path, header: Path, gch: Path ) -> tuple[list[str], Path] | None: @@ -232,16 +244,17 @@ def pch_compile_command( if not isinstance(entries, list): _LOGGER.warning("Malformed compile database, skipping pch") return None - # Windows compile DBs use backslashes; normalize both sides - src_prefix = str(CORE.relative_src_path()).replace("\\", "/") + # CMake may spell paths through a symlink differently than CORE does + # (macOS /tmp vs /private/tmp), so compare resolved paths + src_root = Path(CORE.relative_src_path()).resolve() entry = next( ( e for e in entries if isinstance(e, dict) and isinstance(e.get("file"), str) - and e["file"].replace("\\", "/").startswith(src_prefix) and e["file"].endswith(CXX_SOURCE_SUFFIXES) + and _db_source_path(e, build_dir).is_relative_to(src_root) ), None, ) diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index dca97c5fde..31035e0145 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -657,6 +657,31 @@ def test_pch_compile_command_variants(tmp_path: Path) -> None: assert cmd_dir == build +@pytest.mark.skipif(os.name == "nt", reason="symlinks need privileges on Windows") +def test_pch_compile_command_matches_src_through_symlink(tmp_path: Path) -> None: + """Find the src TU when CMake spells paths through a different symlink (macOS /tmp).""" + from esphome.build_helpers.pch import pch_compile_command + + real = tmp_path / "real" + (real / "src" / "esphome").mkdir(parents=True) + link = tmp_path / "link" + link.symlink_to(real, target_is_directory=True) + CORE.build_path = str(real) + build = real / "build" + build.mkdir() + header = build / "esphome_pch.h" + gch = build / "esphome_pch.h.gch" + src_file = str(link / "src" / "esphome" / "a.cpp") + (build / "compile_commands.json").write_text( + json.dumps([{"command": f"g++ -DX=1 -o a.obj -c {src_file}", "file": src_file}]) + ) + + cmd, cmd_dir = pch_compile_command(build, header, gch) + + assert cmd[:2] == ["g++", "-DX=1"] + assert cmd_dir == build + + def test_pch_compile_command_rejects_unusable_entries(tmp_path: Path) -> None: """Malformed DB shapes and command-less entries skip cleanly instead of producing a compiler-less argv retried every build."""