mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Strict re-probes a cached .gch, probe diagnostics get the exit-code fallback, nobuild detection narrows to ImportError
This commit is contained in:
@@ -401,18 +401,7 @@ def prepare_pch(
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"identity unknown: {err}")
|
||||
return
|
||||
if gch.is_file() and _read_stamp(sum_path) == checksum:
|
||||
_log_pch_in_use()
|
||||
return
|
||||
failed_marker = Path(f"{gch}.failed")
|
||||
if _read_stamp(failed_marker) == checksum:
|
||||
_LOGGER.info(
|
||||
"Precompiled header disabled after an earlier failure; delete %s to retry",
|
||||
failed_marker,
|
||||
)
|
||||
pch_degraded("earlier failure latched")
|
||||
return
|
||||
_log_pch_in_use()
|
||||
|
||||
def _run(run_cmd: list[str], what: str) -> subprocess.CompletedProcess | None:
|
||||
"""Spawn one pch tool step; environmental failures discard and
|
||||
@@ -463,6 +452,35 @@ def prepare_pch(
|
||||
os.utime(header)
|
||||
pch_degraded(f"{reason}: {error[:200]}")
|
||||
|
||||
def _probe() -> 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."""
|
||||
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",
|
||||
)
|
||||
|
||||
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()
|
||||
return
|
||||
if _read_stamp(failed_marker) == checksum:
|
||||
_LOGGER.info(
|
||||
"Precompiled header disabled after an earlier failure; delete %s to retry",
|
||||
failed_marker,
|
||||
)
|
||||
pch_degraded("earlier failure latched")
|
||||
return
|
||||
_log_pch_in_use()
|
||||
result = _run(cmd, "compile")
|
||||
if result is None:
|
||||
return
|
||||
@@ -474,14 +492,9 @@ def prepare_pch(
|
||||
if error is not None:
|
||||
_fail(error, "compile failed")
|
||||
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. cmd ends with the fixed "-x c++-header -c -o" tail.
|
||||
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(), "toolchain cannot load the pch")
|
||||
_probe()
|
||||
if not gch.is_file():
|
||||
# The probe discarded a rejected or unrunnable .gch
|
||||
return
|
||||
failed_marker.unlink(missing_ok=True)
|
||||
sum_path.write_text(checksum + "\n", encoding="utf-8")
|
||||
|
||||
@@ -164,12 +164,12 @@ def _setup_pch() -> bool | None:
|
||||
print(f"ESPHome: projenv unavailable ({_projenv_error}); skipping pch")
|
||||
try:
|
||||
from SCons.Script import COMMAND_LINE_TARGETS
|
||||
|
||||
# Expected under -t nobuild (nothing compiles); a missing
|
||||
# projenv on a real compile must not pass strict
|
||||
return "nobuild" in [str(t) for t in COMMAND_LINE_TARGETS]
|
||||
except Exception: # noqa: BLE001 -- no SCons: cannot tell, stay lenient
|
||||
except ImportError:
|
||||
# No SCons: cannot tell, stay lenient
|
||||
return True
|
||||
# Expected under -t nobuild (nothing compiles); a missing
|
||||
# projenv on a real compile must not pass strict
|
||||
return "nobuild" in [str(t) for t in COMMAND_LINE_TARGETS]
|
||||
# Project root: SCons compiles run here, so the relative -include
|
||||
# resolves; an absolute path would break cross-device ccache sharing.
|
||||
proj_dir = Path(env.subst("$PROJECT_DIR")) # noqa: F821
|
||||
|
||||
@@ -1257,3 +1257,42 @@ def test_prepare_pch_probe_rejection_latches_and_degrades(
|
||||
pytest.raises(EsphomeError, match="cannot load the pch"),
|
||||
):
|
||||
prepare_pch()
|
||||
|
||||
|
||||
def test_prepare_pch_strict_reprobes_cached_gch(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Rejection is per-process: strict must re-prove a cached .gch loads."""
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
dev = _make_pch_device(tmp_path, "dev_rc")
|
||||
CORE.build_path = dev
|
||||
gch = dev / "build" / "esphome_pch.h.gch"
|
||||
|
||||
def ok(cmd, **kwargs):
|
||||
if "-fsyntax-only" not in cmd:
|
||||
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=ok),
|
||||
):
|
||||
prepare_pch()
|
||||
assert gch.exists()
|
||||
|
||||
def reject(cmd, **kwargs):
|
||||
assert "-fsyntax-only" in cmd, "cached path must not recompile"
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 0, "", "warning: esphome_pch.h.gch: had text segment "
|
||||
)
|
||||
|
||||
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
|
||||
with (
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch("esphome.build_helpers.pch.subprocess.run", side_effect=reject),
|
||||
pytest.raises(EsphomeError, match="cannot load the pch"),
|
||||
):
|
||||
prepare_pch()
|
||||
assert not gch.exists()
|
||||
|
||||
@@ -472,8 +472,15 @@ def test_pch_script_strict_reraises_internal_errors(tmp_path: Path) -> None:
|
||||
_run_script(tmp_path, env_vars={"ESPHOME_PCH_STRICT": "1"}, platform_cls=None)
|
||||
|
||||
|
||||
def test_pch_script_strict_allows_nobuild_skip(tmp_path: Path) -> None:
|
||||
"""-t nobuild compiles nothing; the skip is expected even in strict."""
|
||||
@pytest.mark.parametrize(("targets", "passes"), [(["nobuild"], True), ([], False)])
|
||||
def test_pch_script_strict_projenv_skip_gated_on_nobuild(
|
||||
tmp_path: Path, targets: list[str], passes: bool
|
||||
) -> None:
|
||||
"""-t nobuild compiles nothing, so the skip passes strict; a missing
|
||||
projenv on a real compile must not."""
|
||||
import sys
|
||||
import types
|
||||
|
||||
proj = tmp_path / "dev"
|
||||
(proj / "src").mkdir(parents=True)
|
||||
|
||||
@@ -481,12 +488,23 @@ def test_pch_script_strict_allows_nobuild_skip(tmp_path: Path) -> None:
|
||||
if "projenv" in names:
|
||||
raise RuntimeError("Import of non-existent variable 'projenv'")
|
||||
|
||||
scons = types.ModuleType("SCons")
|
||||
scons_script = types.ModuleType("SCons.Script")
|
||||
scons_script.COMMAND_LINE_TARGETS = targets
|
||||
env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"])
|
||||
with patch.dict(os.environ, {"ESPHOME_PCH_STRICT": "1"}, clear=True):
|
||||
exec( # noqa: S102
|
||||
with (
|
||||
patch.dict(sys.modules, {"SCons": scons, "SCons.Script": scons_script}),
|
||||
patch.dict(os.environ, {"ESPHOME_PCH_STRICT": "1"}, clear=True),
|
||||
):
|
||||
run = lambda: exec( # noqa: S102, E731
|
||||
compile(_SCRIPT.read_text(), "pch.py", "exec"),
|
||||
{"Import": strict_import, "env": env},
|
||||
)
|
||||
if passes:
|
||||
run()
|
||||
else:
|
||||
with pytest.raises(RuntimeError, match="not used"):
|
||||
run()
|
||||
assert not (proj / "esphome_pch.h").exists()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user