Escalate all strict consumers, keep cached-probe rejections unlatched, drop the dead probe knob

This commit is contained in:
J. Nick Koston
2026-08-27 00:40:37 -05:00
parent c89c2d975d
commit d38542e48b
6 changed files with 43 additions and 17 deletions
+1 -1
View File
@@ -1303,7 +1303,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
if pch_strict():
# Consumers wait on the probe stamp, so an unloadable .gch
# reds the build here instead of warning ~100 times
probe = " ".join(pch_probe_args(PCH_HEADER_NAME, fatal=True))
probe = " ".join(pch_probe_args(PCH_HEADER_NAME))
lines.append("rule pchprobe")
# $out only expands in rule text, hence the inline stamp
lines.append(
+3
View File
@@ -298,12 +298,15 @@ def _pch_cmake() -> str:
"""
if not pch_enabled():
return ""
# Strict inverts: a per-process consumer rejection reds the build
escalation = "-Werror=invalid-pch" if pch.pch_strict() else "-Wno-error=invalid-pch"
return f"""
# ESPHome precompiled header (see esphome/build_helpers/pch.py).
# OBJECT_DEPENDS is on the header, not the .gch: pch-baked headers drop
# out of TU depfiles, and prepare_pch() touches the header on rebuild.
target_compile_options(${{COMPONENT_LIB}} PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:-Winvalid-pch>"
"$<$<COMPILE_LANGUAGE:CXX>:{escalation}>"
"$<$<COMPILE_LANGUAGE:CXX>:-include>"
"$<$<COMPILE_LANGUAGE:CXX>:{PCH_HEADER_NAME}>"
)
+21 -14
View File
@@ -120,15 +120,15 @@ def pch_disabled_degraded() -> None:
pch_degraded("pch disabled by ESPHOME_PCH_ENABLE")
def pch_probe_args(header: str, fatal: bool = False) -> list[str]:
def pch_probe_args(header: str) -> list[str]:
"""Flags that load-check a built .gch via a syntax-only compile.
``fatal`` escalates a rejected pch to an error for consumers that
cannot inspect stderr (the ninja probe edge).
Rejection must be a nonzero exit (never just a wording match), so the
invalid-pch class is always escalated.
"""
return [
"-Winvalid-pch",
*(["-Werror=invalid-pch"] if fatal else []),
"-Werror=invalid-pch",
"-include",
header,
"-fsyntax-only",
@@ -452,30 +452,37 @@ def prepare_pch(
os.utime(header)
pch_degraded(f"{reason}: {error[:200]}")
def _probe() -> None:
def _probe(latch: bool = True) -> None:
"""Load-check the built .gch: some toolchains build one they then
refuse to load (per-process ASLR). Dep flags are already stripped
from cmd, so no -MF is needed; cmd ends with the fixed
"-x c++-header -c -o" tail."""
"-x c++-header -c -o" tail. A cached-header rejection may not
reproduce (per-process), so that caller passes latch=False."""
# The fixed tail pch_compile_command appends; the slice below
# depends on it
assert cmd[-6:-4] == ["-x", "c++-header"], cmd[-6:]
# fatal: rejection must be a nonzero exit, not a wording match
probe = _run([*cmd[:-6], *pch_probe_args(str(header), fatal=True)], "probe")
probe = _run([*cmd[:-6], *pch_probe_args(str(header))], "probe")
if probe is None:
return
if probe.returncode != 0 or ".gch" in probe.stderr:
_fail(
probe.stderr.strip() or f"exit code {probe.returncode}",
"toolchain cannot load the pch",
)
error = probe.stderr.strip() or f"exit code {probe.returncode}"
if latch:
_fail(error, "toolchain cannot load the pch")
else:
_LOGGER.warning(
"Precompiled header failed; compiling without it: %s",
error[:400],
)
discard_pch(build_dir)
pch_degraded(f"toolchain cannot load the pch: {error[:200]}")
if gch.is_file() and _read_stamp(sum_path) == checksum:
_log_pch_in_use()
if pch_strict():
# Rejection is per-process, so a cached .gch must re-prove
# loadability for the strict gate (CI-only cost)
_probe()
# loadability for the strict gate (CI-only cost); no latch,
# since the rejection may not reproduce either
_probe(latch=False)
return
if _read_stamp(failed_marker) == checksum:
_LOGGER.info(
+7 -1
View File
@@ -383,7 +383,13 @@ def _setup_pch() -> bool | None:
# -Wno-error: the per-process probe can pass while a later cc1plus
# rejects the .gch; that must stay a warning under user -Werror.
projenv.Prepend( # noqa: F821
CXXFLAGS=["-Winvalid-pch", "-Wno-error=invalid-pch", "-include", header.name]
CXXFLAGS=[
"-Winvalid-pch",
# Strict inverts: a per-process consumer rejection reds the build
"-Werror=invalid-pch" if _STRICT else "-Wno-error=invalid-pch",
"-include",
header.name,
]
)
projenv["ENV"].update(ccache_updates) # noqa: F821
print("ESPHome: Compiling with precompiled header")
@@ -604,6 +604,11 @@ def test_component_cmakelists_pch_block(monkeypatch: pytest.MonkeyPatch) -> None
content = get_component_cmakelists()
assert '"$<$<COMPILE_LANGUAGE:CXX>:-include>"' in content
assert "-Wno-error=invalid-pch" in content
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
strict_content = get_component_cmakelists()
assert "-Werror=invalid-pch" in strict_content
monkeypatch.delenv("ESPHOME_PCH_STRICT")
assert '"$<$<COMPILE_LANGUAGE:CXX>:esphome_pch.h>"' in content
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
assert "-include" not in get_component_cmakelists()
@@ -1296,3 +1301,6 @@ def test_prepare_pch_strict_reprobes_cached_gch(
):
prepare_pch()
assert not gch.exists()
# Per-process rejection may not reproduce: the cached path must not
# latch the pch off for later non-strict builds
assert not (dev / "build" / "esphome_pch.h.gch.failed").exists()
@@ -542,4 +542,6 @@ def test_pch_script_strict_projenv_skip_gated_on_nobuild(
def test_pch_script_strict_passes_on_success(tmp_path: Path) -> None:
scons_env = _run_script(tmp_path, env_vars={"ESPHOME_PCH_STRICT": "1"})
assert scons_env.prepended
# Strict escalates the consumer edges too
assert "-Werror=invalid-pch" in scons_env.prepended
assert "-Wno-error=invalid-pch" not in scons_env.prepended