diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index cbe7fec81f..251b6e19eb 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -1313,6 +1313,9 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: pch_dep = f"{gch} esphome_pch.probe" src_cxx_override = ("$srccxxflags", pch_dep) mark_pch_emitted() + else: + # Strict CI must not read "no pch at all" as success + pch_degraded("pch disabled by ESPHOME_PCH_ENABLE") src_objs = _ninja_compile_edges( lines, _collect_sources(src_dir), diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 79f032238a..7feb6c4c4e 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -323,6 +323,7 @@ def prepare_pch() -> None: if not pch_enabled(): # Self-cleaning escape hatch: drop any previously built .gch pch.discard_pch(CORE.relative_build_path("build")) + pch.pch_degraded("pch disabled by ESPHOME_PCH_ENABLE") return sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}") try: diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 233bd43cb9..d886d37b0e 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -148,7 +148,11 @@ def ccache_pch_env() -> dict[str, str]: def pch_extra_scripts() -> list[str]: """The extra_scripts entries a PlatformIO platform registers for the pch; empty when disabled (the script itself has no enable check).""" - return ["post:pch.py"] if pch_enabled() else [] + if not pch_enabled(): + # Strict CI must not read "no pch at all" as success + pch_degraded("pch disabled by ESPHOME_PCH_ENABLE") + return [] + return ["post:pch.py"] def pch_header_text(include_headers: Iterable[str]) -> str: diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index 7a3b8884d2..4c737460b7 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -160,10 +160,15 @@ def _read_stamp(path: Path) -> str: def _setup_pch() -> bool | None: if projenv is None: - # Expected under -t nobuild (nothing compiles), so not a degrade - # even in strict mode; anything else still leaves a trail print(f"ESPHome: projenv unavailable ({_projenv_error}); skipping pch") - return True + 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 + return True # 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 diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 450a2b673c..1c97de14d5 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -869,6 +869,61 @@ def test_prepare_pch_signal_kill_is_transient(tmp_path: Path) -> None: assert len(calls) == 2 +def test_prepare_pch_signal_kill_strict_raises( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from esphome.build_gen.espidf import prepare_pch + from esphome.core import EsphomeError + + monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") + dev = _make_pch_device(tmp_path, "dev_ks") + CORE.build_path = dev + with ( + patch.object(CORE, "name", "test"), + patch( + "esphome.build_helpers.pch.subprocess.run", + side_effect=lambda cmd, **kw: subprocess.CompletedProcess(cmd, -9, "", ""), + ), + pytest.raises(EsphomeError, match="killed by signal"), + ): + prepare_pch() + + +def test_prepare_pch_strict_raises_when_disabled( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Strict must not read a disabled pch as success.""" + from esphome.build_gen.espidf import prepare_pch + from esphome.core import EsphomeError + + monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0") + monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") + dev = _make_pch_device(tmp_path, "dev_ds") + CORE.build_path = dev + with ( + patch.object(CORE, "name", "test"), + pytest.raises(EsphomeError, match="disabled"), + ): + prepare_pch() + + +def test_prepare_pch_missing_sdkconfig_strict_raises( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from esphome.build_gen.espidf import prepare_pch + from esphome.core import EsphomeError + + monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") + dev = _make_pch_device(tmp_path, "dev_ss") + (dev / "sdkconfig.test").unlink() + CORE.build_path = dev + with ( + patch.object(CORE, "name", "test"), + pytest.raises(EsphomeError, match="sdkconfig unreadable"), + ): + prepare_pch() + + def test_prepare_pch_without_compile_commands(tmp_path: Path) -> None: """Stale checksum but no configured TU yet: no compile, no sidecars.""" from esphome.build_gen.espidf import prepare_pch diff --git a/tests/unit_tests/build_helpers/test_pch.py b/tests/unit_tests/build_helpers/test_pch.py index 1d8423eb1c..3b5650fae8 100644 --- a/tests/unit_tests/build_helpers/test_pch.py +++ b/tests/unit_tests/build_helpers/test_pch.py @@ -242,3 +242,14 @@ def test_pch_degraded_raises_only_in_strict( monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") with pytest.raises(EsphomeError, match="reason"): pch.pch_degraded("reason") + + +def test_pch_extra_scripts_strict_raises_when_disabled( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from esphome.core import EsphomeError + + monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0") + monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") + with pytest.raises(EsphomeError, match="disabled"): + pch.pch_extra_scripts()