From fae9eb0dd2c99ed20c9d4bca34598a211a0a50e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 15:04:55 -0500 Subject: [PATCH] Hash project-local include dirs into the pch checksum and share the registration gate --- esphome/build_helpers/pch.py | 6 +++++ esphome/components/esp8266/__init__.py | 6 ++--- esphome/platformio/pch.py.script | 22 +++++++++++++++++++ tests/unit_tests/build_helpers/test_pch.py | 7 ++++++ .../unit_tests/test_platformio_pch_script.py | 16 ++++++++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 48c70d4da8..10761cdded 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -50,6 +50,12 @@ def ccache_pch_env() -> dict[str, str]: return {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ} +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 [] + + def pch_header_text(include_headers: Iterable[str]) -> str: """The prefix-header source: exactly these includes, in order.""" return "".join(f'#include "{name}"\n' for name in include_headers) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 3ba74a250e..1eff02e60d 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -6,7 +6,7 @@ import subprocess import time from typing import Any -from esphome.build_helpers.pch import pch_enabled +from esphome.build_helpers.pch import pch_extra_scripts import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( @@ -421,9 +421,7 @@ async def to_code(config: ConfigType) -> None: ] if not enable_scanf_float: extra_scripts.append("pre:remove_float_scanf.py") - # Generation-time gate: the script itself has no enable check - if pch_enabled(): - extra_scripts.append("post:pch.py") + extra_scripts.extend(pch_extra_scripts()) extra_scripts.append("post:post_build.py") cg.add_platformio_option("extra_scripts", extra_scripts) diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index db7db9966d..03f6567285 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -115,6 +115,28 @@ def _setup_pch() -> None: digest.update(rel.encode()) digest.update(closure[rel]) digest.update(b"\0") + # Project-local include dirs outside src (e.g. rp2's lwip_override) + # hold generated headers the src closure cannot see; hash them so an + # ESPHome-side change invalidates an existing build dir + prev = "" + for tok in flags: + inc = tok[2:] if tok.startswith("-I") and len(tok) > 2 else "" + if prev == "-I": + inc = tok + prev = tok + if not inc: + continue + inc_dir = Path(inc) + if not ( + inc_dir.is_dir() + and inc_dir.is_relative_to(proj_dir) + and not inc_dir.is_relative_to(src_dir) + ): + continue + for local in sorted(inc_dir.rglob("*.h")): + digest.update(str(local.relative_to(proj_dir)).encode()) + digest.update(local.read_bytes()) + digest.update(b"\0") checksum = digest.hexdigest() # The ccache .sum sidecar doubles as the freshness stamp diff --git a/tests/unit_tests/build_helpers/test_pch.py b/tests/unit_tests/build_helpers/test_pch.py index e6a977b9d4..e2e21dcbb5 100644 --- a/tests/unit_tests/build_helpers/test_pch.py +++ b/tests/unit_tests/build_helpers/test_pch.py @@ -120,3 +120,10 @@ def test_include_closure_marks_unreadable( locked.chmod(0o644) assert closure["locked.h"] == b"" assert "Could not read locked.h" in caplog.text + + +def test_pch_extra_scripts_gated(monkeypatch: pytest.MonkeyPatch) -> None: + with patch.dict(os.environ, {}, clear=True): + assert pch.pch_extra_scripts() == ["post:pch.py"] + monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0") + assert pch.pch_extra_scripts() == [] diff --git a/tests/unit_tests/test_platformio_pch_script.py b/tests/unit_tests/test_platformio_pch_script.py index 3c4ea36860..2ff162b565 100644 --- a/tests/unit_tests/test_platformio_pch_script.py +++ b/tests/unit_tests/test_platformio_pch_script.py @@ -165,3 +165,19 @@ def test_copy_pch_script(tmp_path: Path) -> None: CORE.build_path = tmp_path toolchain.copy_pch_script() assert (tmp_path / "pch.py").read_text() == _SCRIPT.read_text() + + +def test_pch_script_hashes_project_local_include_dirs(tmp_path: Path) -> None: + """Generated headers in project-local -I dirs (e.g. rp2's lwip_override) + must invalidate the checksum when they change.""" + proj = tmp_path / "dev" + override = proj / "lwip_override" + override.mkdir(parents=True) + (override / "lwipopts.h").write_text("#define TCP_MSS 1460\n") + flags = ["-DX=1", "-I", str(override)] + _run_script(tmp_path, flags=flags) + first = (proj / "esphome_pch.h.gch.sum").read_text() + (override / "lwipopts.h").write_text("#define TCP_MSS 536\n") + (tmp_path / "fake-gxx.argv").unlink(missing_ok=True) + _run_script(tmp_path, flags=flags) + assert (proj / "esphome_pch.h.gch.sum").read_text() != first