From c1ec1f3ee72b3d8dc983dad772650359670f9300 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 16:03:46 -0500 Subject: [PATCH] Deduplicate the pch identity digest across backends --- esphome/build_gen/arduino8266.py | 50 ++++--------- esphome/build_helpers/pch.py | 75 ++++++++++++------- .../unit_tests/build_gen/test_arduino8266.py | 2 +- 3 files changed, 65 insertions(+), 62 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 2a55b552b1..6cb304eb23 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -27,7 +27,6 @@ import sys from typing import TYPE_CHECKING, NamedTuple from esphome.arduino8266.framework import toolchain_tool -from esphome.build_helpers.ccache import effective_ccache_basedir from esphome.build_helpers.idedata import is_joined_include from esphome.build_helpers.ninja import ( escape as _e, @@ -37,13 +36,14 @@ from esphome.build_helpers.ninja import ( from esphome.build_helpers.pch import ( PCH_CORE_HEADER, PCH_HEADER_NAME, + log_pch_in_use, mark_pch_emitted, - pch_checksum, pch_consumer_escalation, pch_degraded, pch_disabled_degraded, pch_enabled, pch_header_text, + pch_identity, pch_probe_args, pch_strict, ) @@ -1243,39 +1243,21 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: pch_header = build_dir / PCH_HEADER_NAME pch_includes = (*src_includes, PCH_CORE_HEADER) pch_text = pch_header_text(pch_includes) - checksum = None - try: - if ccache: - # The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile - # handles staleness. Strip resolved and raw build paths - # (symlinks) so identical configs share cache entries - flags_id = ( - " ".join(cxxflags) - .replace(effective_ccache_basedir(), "") - .replace(str(CORE.build_path), "") - ) - # The header text covers include order - checksum = pch_checksum( - src_dir, - pch_includes, - ( - pch_text, - str(paths.framework), - str(paths.toolchain), - flags_id, - ), - ) - except (OSError, UnicodeError) as err: - # Identity unknown: a stale cache entry must never be served - _LOGGER.warning( - "Could not establish the pch identity; compiling without it: %s", err - ) - pch_degraded(f"identity unknown: {err}") - else: - _LOGGER.info( - "Compiling with a precompiled header " - "(set ESPHOME_PCH_ENABLE=0 to disable)" + # The .sum exists only for CCACHE_PCH_EXTSUM; ninja's depfile + # handles staleness + checksum = ( + pch_identity( + cxxflags, + src_dir, + pch_includes, + (str(paths.framework), str(paths.toolchain)), ) + if ccache + else None + ) + # Identity unknown under ccache: pch_identity warned and degraded + if not ccache or checksum is not None: + log_pch_in_use() write_file_if_changed(pch_header, pch_text) sum_path = build_dir / f"{PCH_HEADER_NAME}.gch.sum" if checksum is not None: diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 6277520e7d..daa1383753 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -389,7 +389,50 @@ def pch_compile_command( return [*args, "-x", "c++-header", "-c", str(header), "-o", str(gch)], cmd_dir -def _log_pch_in_use() -> None: +def flags_identity(tokens: Iterable[str]) -> str: + """Flag string normalized for digest use: strip like ccache's rewriting + (user CCACHE_BASEDIR wins); the raw build path covers unresolved + (symlinked) spellings.""" + from esphome.core import CORE + + return ( + " ".join(tokens) + .replace(effective_ccache_basedir(), "") + .replace(str(CORE.build_path), "") + ) + + +def pch_identity( + tokens: Iterable[str], + src_dir: Path, + include_headers: tuple[str, ...], + extra: Iterable[str], +) -> str | None: + """The .sum digest naming this exact pch build: include closure, header + text, backend identity strings, and the normalized compile command or + flags (``tokens``). None (warned and degraded) when the identity + cannot be established.""" + try: + return pch_checksum( + src_dir, + include_headers, + ( + # The closure is sorted, so root order only enters via the text + pch_header_text(include_headers), + *extra, + flags_identity(tokens), + ), + ) + except (OSError, UnicodeError) as err: + # Identity unknown: a stale cache entry must never be served + _LOGGER.warning( + "Could not establish the pch identity; compiling without it: %s", err + ) + pch_degraded(f"identity unknown: {err}") + return None + + +def log_pch_in_use() -> None: # The only place a user can discover the knob; emitted only once a # .gch is actually fresh or being built _LOGGER.info( @@ -459,31 +502,9 @@ def prepare_pch( pch_degraded("no usable compile command") return cmd, cmd_dir = cmd_and_dir - # Strip like ccache's rewriting (user CCACHE_BASEDIR wins); the raw - # build path covers unresolved (symlinked) spellings - cmd_id = ( - " ".join(cmd) - .replace(effective_ccache_basedir(), "") - .replace(str(CORE.build_path), "") - ) - try: - checksum = pch_checksum( - CORE.relative_src_path(), - include_headers, - ( - # The closure is sorted, so root order only enters via the text - pch_header_text(include_headers), - *extra, - cmd_id, - ), - ) - except (OSError, UnicodeError) as err: - # Identity unknown: a stale cache entry must never be served - _LOGGER.warning( - "Could not establish the pch identity; compiling without it: %s", err - ) + checksum = pch_identity(cmd, CORE.relative_src_path(), include_headers, extra) + if checksum is None: discard_pch(build_dir) - pch_degraded(f"identity unknown: {err}") return failed_marker = Path(f"{gch}.failed") @@ -566,7 +587,7 @@ def prepare_pch( _fail(error, "probe cannot run at all", latch=latch) if gch.is_file() and _read_stamp(sum_path) == checksum: - _log_pch_in_use() + 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); no latch, @@ -580,7 +601,7 @@ def prepare_pch( ) pch_degraded("earlier failure latched") return - _log_pch_in_use() + log_pch_in_use() result = _run(cmd, "compile") if result is None: return diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 9e29156155..84c58af785 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -410,7 +410,7 @@ def test_write_project_pch_identity_unknown_skips_pch( paths = _make_framework(tmp_path) _set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH") with patch( - "esphome.build_gen.arduino8266.pch_checksum", + "esphome.build_helpers.pch.pch_checksum", side_effect=OSError("stat failed"), ): content = _write_ninja(paths, ccache="/usr/bin/ccache")