Probe the built .gch on the native paths so strict CI reds on an unloadable pch

This commit is contained in:
J. Nick Koston
2026-08-26 22:47:42 -05:00
parent 54bff55642
commit 4a3b095172
4 changed files with 117 additions and 1 deletions
+22 -1
View File
@@ -42,6 +42,7 @@ from esphome.build_helpers.pch import (
pch_degraded,
pch_enabled,
pch_header_text,
pch_strict,
)
from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import (
@@ -1290,7 +1291,27 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
f"-Winvalid-pch -Wno-error=invalid-pch -include {PCH_HEADER_NAME}"
]
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
src_cxx_override = ("$srccxxflags", gch)
pch_dep = gch
if pch_strict():
# Consumers wait on the probe stamp, so an unloadable .gch
# reds the build here instead of warning ~100 times
lines.append("rule pchprobe")
lines.append(
" command = $cxx $cxxflags $flags -Winvalid-pch"
" -Werror=invalid-pch"
f" -include {PCH_HEADER_NAME} -fsyntax-only -x c++"
f" {_q(Path(os.devnull))} && $stamp"
)
lines.append(" description = PCHPROBE $out")
lines.append(f"build esphome_pch.probe: pchprobe {gch}")
if src_other:
lines.append(f" flags = {' '.join(src_other)}")
stamp = (
"cmd /c copy /y nul $out >nul" if os.name == "nt" else "touch $out"
)
lines.append(f" stamp = {stamp}")
pch_dep = f"{gch} esphome_pch.probe"
src_cxx_override = ("$srccxxflags", pch_dep)
mark_pch_emitted()
src_objs = _ninja_compile_edges(
lines,
+36
View File
@@ -435,6 +435,42 @@ def prepare_pch(
os.utime(header)
pch_degraded(f"compile failed: {error[:200]}")
return
# Load probe: some toolchains build a .gch they then refuse to load
# (per-process ASLR); dep flags are already stripped from cmd, so no
# -MF is needed
probe_cmd = [
*cmd[: cmd.index("-x")],
"-Winvalid-pch",
"-include",
str(header),
"-fsyntax-only",
"-x",
"c++",
os.devnull,
]
try:
probe = subprocess.run(
probe_cmd,
cwd=cmd_dir,
env={**os.environ, "LC_ALL": "C"},
capture_output=True,
text=True,
check=False,
timeout=300,
)
except (OSError, subprocess.SubprocessError) as err:
_LOGGER.warning("Precompiled header probe did not run: %s", err)
discard_pch(build_dir)
pch_degraded(f"probe did not run: {err}")
return
if probe.returncode != 0 or ".gch" in probe.stderr:
error = f"toolchain cannot load the pch: {probe.stderr.strip()[:400]}"
_LOGGER.warning("Precompiled header failed; compiling without it: %s", error)
discard_pch(build_dir)
failed_marker.write_text(checksum + "\n", encoding="utf-8")
os.utime(header)
pch_degraded(error)
return
failed_marker.unlink(missing_ok=True)
sum_path.write_text(checksum + "\n", encoding="utf-8")
# Consumers depend on the header (depfiles cannot see through a .gch);
@@ -1856,3 +1856,21 @@ def test_write_project_pch_strict_raises_on_skip(
)
with pytest.raises(EsphomeError, match="precedes the pch"):
_write_ninja(paths, ccache="/usr/bin/ccache")
def test_write_project_pch_strict_emits_probe_edge(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Strict mode gates C++ src edges on a hard-failing load probe."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "pchprobe" not in content
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "build esphome_pch.probe: pchprobe esphome_pch.h.gch" in content
assert "-Werror=invalid-pch" in content
for line in content.splitlines():
if line.startswith("build obj/src/main.cpp.o:"):
assert line.endswith("| esphome_pch.h.gch esphome_pch.probe")
+41
View File
@@ -550,6 +550,9 @@ def test_prepare_pch_writes_header_and_sum(tmp_path: Path) -> None:
gch = dev / "build" / "esphome_pch.h.gch"
def fake_compile(cmd, **kwargs):
if "-fsyntax-only" in cmd:
# The load probe follows a successful .gch build
return subprocess.CompletedProcess(cmd, 0, "", "")
# The compile must target the header, not the stub TU
assert cmd[-5:-3] == ["c++-header", "-c"]
gch.write_bytes(b"gch")
@@ -1097,3 +1100,41 @@ def test_prepare_pch_strict_raises_on_missing_db(
pytest.raises(EsphomeError, match="no usable compile command"),
):
prepare_pch()
def test_prepare_pch_probe_rejection_latches_and_degrades(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A toolchain that cannot load its own .gch discards it, latches the
marker, and fails strict mode."""
from esphome.build_gen.espidf import prepare_pch
from esphome.core import EsphomeError
dev = _make_pch_device(tmp_path, "dev_p")
CORE.build_path = dev
gch = dev / "build" / "esphome_pch.h.gch"
def rejecting(cmd, **kwargs):
if "-fsyntax-only" in cmd:
return subprocess.CompletedProcess(
cmd, 0, "", "warning: esphome_pch.h.gch: had text segment "
)
gch.write_bytes(b"gch")
return subprocess.CompletedProcess(cmd, 0, "", "")
with (
patch.object(CORE, "name", "test"),
patch("esphome.build_helpers.pch.subprocess.run", side_effect=rejecting),
):
prepare_pch()
assert not gch.exists()
assert (dev / "build" / "esphome_pch.h.gch.failed").exists()
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
(dev / "build" / "esphome_pch.h.gch.failed").unlink()
with (
patch.object(CORE, "name", "test"),
patch("esphome.build_helpers.pch.subprocess.run", side_effect=rejecting),
pytest.raises(EsphomeError, match="cannot load the pch"),
):
prepare_pch()