Run the gch compile from its resolution dir, self-clean when disabled, log the opt-out hint, pin the knob in tests

This commit is contained in:
J. Nick Koston
2026-08-25 18:54:48 -05:00
parent a4dd668e69
commit 81bca96e1b
4 changed files with 33 additions and 21 deletions
+2
View File
@@ -322,6 +322,8 @@ def prepare_pch() -> None:
"""Build the .gch right before ninja, after every reconfigure, so the
compile_commands.json flags and the sdkconfig are the settled ones."""
if not pch_enabled():
# Self-cleaning escape hatch: drop any previously built .gch
pch.discard_pch(CORE.relative_build_path("build"))
return
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
try:
+17 -10
View File
@@ -156,9 +156,13 @@ _PCH_STRIP_FLAGS_WITH_ARG = frozenset({"-o", "-c", "-MT", "-MF", "-MQ"})
_PCH_STRIP_FLAGS = frozenset({"-MD", "-MMD", "-MP", "-MM", "-M"})
def pch_compile_command(build_dir: Path, header: Path, gch: Path) -> list[str] | None:
"""The exact src C++ flags from compile_commands.json, retargeted at
the header; None (logged) when no configured C++ TU is available yet."""
def pch_compile_command(
build_dir: Path, header: Path, gch: Path
) -> tuple[list[str], Path] | None:
"""The exact src C++ flags from compile_commands.json retargeted at the
header, with the directory they resolve against (relative -I paths must
be expanded and executed from the same root); None (logged) when no
configured C++ TU is available yet."""
from esphome.core import CORE
try:
@@ -187,9 +191,8 @@ def pch_compile_command(build_dir: Path, header: Path, gch: Path) -> list[str] |
if entry is None:
_LOGGER.warning("No src C++ entry in the compile database, skipping pch")
return None
tokens = expand_response_files(
split_command(entry.get("command", "")), Path(entry.get("directory", build_dir))
)
cmd_dir = Path(entry.get("directory", build_dir))
tokens = expand_response_files(split_command(entry.get("command", "")), cmd_dir)
# A DB recorded with ccache enabled prefixes the compiler with the
# launcher; the .gch must be compiled directly
if tokens and is_launcher(tokens[0]):
@@ -215,7 +218,7 @@ def pch_compile_command(build_dir: Path, header: Path, gch: Path) -> list[str] |
args.extend(("-include", inc))
continue
args.append(tok)
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)]
return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir
def discard_pch(build_dir: Path) -> None:
@@ -247,14 +250,18 @@ def prepare_pch(
"""
from esphome.core import CORE
_LOGGER.info(
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
)
header = build_dir / PCH_HEADER_NAME
gch = Path(f"{header}.gch")
sum_path = Path(f"{gch}.sum")
cmd = pch_compile_command(build_dir, header, gch)
if cmd is None:
cmd_and_dir = pch_compile_command(build_dir, header, gch)
if cmd_and_dir is None:
# Freshness cannot be validated; a leftover .gch must not be consumed
discard_pch(build_dir)
return
cmd, cmd_dir = cmd_and_dir
# Stripped like ccache's own rewriting (a user CCACHE_BASEDIR wins) so
# identical configs hash identically across devices; the raw build path
# covers unresolved spellings in the compile DB
@@ -291,7 +298,7 @@ def prepare_pch(
return
try:
result = subprocess.run(
cmd, cwd=build_dir, capture_output=True, text=True, check=False, timeout=300
cmd, cwd=cmd_dir, capture_output=True, text=True, check=False, timeout=300
)
error = None
if result.returncode != 0:
+11 -2
View File
@@ -492,6 +492,12 @@ def test_get_component_cmakelists_no_compile_features() -> None:
assert "target_compile_features" not in content
@pytest.fixture(autouse=True)
def _pch_default_on(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pin the knob so a developer's ESPHOME_PCH_ENABLE=0 cannot fail these."""
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1")
def _make_pch_device(tmp_path: Path, name: str) -> Path:
"""A device dir with the pch source headers and a stub compile_commands."""
from esphome.build_helpers.pch import PCH_DEFAULT_HEADERS
@@ -634,7 +640,8 @@ def test_pch_compile_command_variants(tmp_path: Path) -> None:
)
)
# Launcher stripped; -include/-o/-c and depfile flags removed
assert pch_compile_command(build, header, gch) == [
cmd, cmd_dir = pch_compile_command(build, header, gch)
assert cmd == [
"g++",
"-DX=1",
"-x",
@@ -644,6 +651,8 @@ def test_pch_compile_command_variants(tmp_path: Path) -> None:
"-o",
str(gch),
]
# The compile must run where the flags were resolved
assert cmd_dir == build
def test_pch_compile_command_rejects_unusable_entries(tmp_path: Path) -> None:
@@ -947,6 +956,6 @@ def test_prepare_pch_keeps_user_force_includes(tmp_path: Path) -> None:
]
)
)
cmd = pch_compile_command(build, build / "esphome_pch.h", build / "x.gch")
cmd, _ = pch_compile_command(build, build / "esphome_pch.h", build / "x.gch")
assert "user.h" in cmd
assert "esphome_pch.h" not in " ".join(cmd[:-3])
+3 -9
View File
@@ -1582,6 +1582,9 @@ def test_ccache_env_default_enabled_when_available(tmp_path: Path) -> None:
assert env["CCACHE_NOHASHDIR"] == "true"
assert env["CCACHE_DEPEND"] == "1"
assert env["CCACHE_BASEDIR"] == str((tmp_path / "build").resolve())
# The pch cannot cache under ccache without these
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
def test_ccache_env_disabled_when_binary_missing(tmp_path: Path) -> None:
@@ -1991,12 +1994,3 @@ def test_ccache_env_opt_in_with_usable_binary(
env = _ccache_env()
assert env["IDF_CCACHE_ENABLE"] == "1"
assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
def test_ccache_env_exports_pch_settings(tmp_path: Path) -> None:
# The pch cannot cache under ccache without these
p1, p2, p3 = _ccache_patches(tmp_path, "/usr/bin/ccache", tmp_path / "build")
with patch.dict("os.environ", {}, clear=True), p1, p2, p3:
env = _ccache_env()
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"