From 54bff55642afd654180649dc649edb3a45e0d19d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 22:42:03 -0500 Subject: [PATCH] Reraise in strict through the IDF flow, funnel the remaining degrade paths, exempt nobuild --- esphome/build_gen/espidf.py | 1 + esphome/build_helpers/pch.py | 1 + esphome/espidf/toolchain.py | 4 +++ esphome/platformio/pch.py.script | 5 ++-- tests/unit_tests/test_espidf_toolchain.py | 23 ++++++++++++++++ .../unit_tests/test_platformio_pch_script.py | 26 ++++++++++++++++++- 6 files changed, 57 insertions(+), 3 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 6c52f41810..79f032238a 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -334,6 +334,7 @@ def prepare_pch() -> None: "Could not read %s; compiling without the pch: %s", sdkconfig_path, err ) pch.discard_pch(CORE.relative_build_path("build")) + pch.pch_degraded(f"sdkconfig unreadable: {err}") return pch.prepare_pch( CORE.relative_build_path("build"), diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 96b663dd93..59dc8165f6 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -407,6 +407,7 @@ def prepare_pch( -result.returncode, ) discard_pch(build_dir) + pch_degraded(f"compile killed by signal {-result.returncode}") return if result.returncode != 0: error = result.stderr.strip() or f"exit code {result.returncode}" diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 82f64defca..3db8e21757 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -539,6 +539,10 @@ def run_compile(config, verbose: bool) -> int: # Discard so a stale .gch can never be consumed with suppress(OSError): discard_pch() + from esphome.build_helpers.pch import pch_strict + + if pch_strict(): + raise _LOGGER.warning( "Precompiled header setup failed; compiling without it", exc_info=True ) diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index 505ba1c74a..7a3b8884d2 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -160,9 +160,10 @@ def _read_stamp(path: Path) -> str: def _setup_pch() -> bool | None: if projenv is None: - # Expected under -t nobuild; anything else must leave a trail + # 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 + 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/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 7458d44764..4f0b24d077 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -669,6 +669,29 @@ def test_get_core_framework_version_from_core_data(): assert toolchain._get_core_framework_version() == "5.5.4" +def test_run_compile_strict_reraises_pch_failure( + setup_core: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """ESPHOME_PCH_STRICT must reach through the real compile flow.""" + from esphome.core import EsphomeError + + monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1") + monkeypatch.setenv("ESPHOME_PCH_STRICT", "1") + _setup_build(setup_core) + + with ( + patch.object(toolchain, "need_reconfigure", return_value=False), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary"), + patch( + "esphome.build_gen.espidf.prepare_pch", + side_effect=EsphomeError("ESPHOME_PCH_STRICT: no usable compile command"), + ), + pytest.raises(EsphomeError, match="no usable compile command"), + ): + toolchain.run_compile({CONF_ESPHOME: {}}, verbose=False) + + def test_run_compile_invokes_prepare_pch_and_survives_failure( setup_core: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/tests/unit_tests/test_platformio_pch_script.py b/tests/unit_tests/test_platformio_pch_script.py index 60c07bdb9d..6b25c3c989 100644 --- a/tests/unit_tests/test_platformio_pch_script.py +++ b/tests/unit_tests/test_platformio_pch_script.py @@ -462,10 +462,34 @@ def test_pch_script_unreadable_local_header_skips_pch( def test_pch_script_strict_raises_when_pch_not_used(tmp_path: Path) -> None: """ESPHOME_PCH_STRICT fails the build instead of degrading.""" - with pytest.raises(Exception, match="ESPHOME_PCH_STRICT|boom"): + with pytest.raises(RuntimeError, match="ESPHOME_PCH_STRICT"): _run_script(tmp_path, fail=True, env_vars={"ESPHOME_PCH_STRICT": "1"}) +def test_pch_script_strict_reraises_internal_errors(tmp_path: Path) -> None: + """The catch-all must not swallow programming errors in strict mode.""" + with pytest.raises(TypeError): + _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.""" + proj = tmp_path / "dev" + (proj / "src").mkdir(parents=True) + + def strict_import(*names: str) -> None: + if "projenv" in names: + raise RuntimeError("Import of non-existent variable 'projenv'") + + env = _FakeSConsEnv(proj, proj / "src", "g++", ["-DX=1"]) + with patch.dict(os.environ, {"ESPHOME_PCH_STRICT": "1"}, clear=True): + exec( # noqa: S102 + compile(_SCRIPT.read_text(), "pch.py", "exec"), + {"Import": strict_import, "env": env}, + ) + assert not (proj / "esphome_pch.h").exists() + + 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