From 558acf6900485933b7942d841426d6204d3e2666 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 14:04:29 -0500 Subject: [PATCH 1/3] Make the PlatformIO pch script platform-agnostic --- esphome/build_helpers/pch.py | 2 +- esphome/components/esp8266/__init__.py | 4 ++-- .../esp8266 => platformio}/pch.py.script | 23 +++++++++++-------- esphome/platformio/toolchain.py | 9 ++++++++ 4 files changed, 26 insertions(+), 12 deletions(-) rename esphome/{components/esp8266 => platformio}/pch.py.script (86%) diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 6a013259f8..22c18aea89 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -24,7 +24,7 @@ PCH_CORE_HEADER = "esphome/core/defines.h" # ccache cannot hash through a .gch; CCACHE_PCH_EXTSUM makes it hash the # .sum sidecar instead of the .gch bytes, which are not reproducible. -# Keep in sync with the literals in components/esp8266/pch.py.script. +# Keep in sync with the literals in platformio/pch.py.script. _CCACHE_PCH_ENV = { "CCACHE_SLOPPINESS": "pch_defines,time_macros", "CCACHE_PCH_EXTSUM": "true", diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 6624ce8aff..3ba74a250e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -35,7 +35,7 @@ from esphome.core import ( ) from esphome.core.config import BOARD_MAX_LENGTH from esphome.helpers import IS_MACOS, copy_file_if_changed -from esphome.platformio.toolchain import copy_ccache_script +from esphome.platformio.toolchain import copy_ccache_script, copy_pch_script from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -579,7 +579,6 @@ def copy_files() -> None: dir = Path(__file__).parent for script in ( "post_build", - "pch", "testing_mode", "exclude_updater", "exclude_waveform", @@ -591,6 +590,7 @@ def copy_files() -> None: CORE.relative_build_path(f"{script}.py"), ) copy_ccache_script() + copy_pch_script() # ESP logs stack trace decoder, based on https://github.com/me-no-dev/EspExceptionDecoder diff --git a/esphome/components/esp8266/pch.py.script b/esphome/platformio/pch.py.script similarity index 86% rename from esphome/components/esp8266/pch.py.script rename to esphome/platformio/pch.py.script index 444fa1aa69..11b991cf95 100644 --- a/esphome/components/esp8266/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -10,12 +10,13 @@ import subprocess Import("env", "projenv") # noqa: F821 # Precompile the src force-includes plus defines.h (which pulls in -# Arduino.h) and force-include the result into C++ src compiles only; those -# TUs already include this content first, so their preprocessed output is -# unchanged. Post script: the final src flags exist, nothing compiled yet. -# Registration is gated host-side (esp8266/__init__.py, pch_enabled()). -# Keep the header tail, ccache values, include-closure recipe, and the -# checksum/failed-marker stamp flow in sync with build_helpers/pch.py. +# Arduino.h on Arduino platforms) and force-include the result into C++ src +# compiles only; those TUs already include this content first, so their +# preprocessed output is unchanged. Post script: the final src flags exist, +# nothing compiled yet. Registration is gated host-side (the platform's +# __init__.py, pch_enabled()). Keep the header tail, ccache values, +# include-closure recipe, and the checksum/failed-marker stamp flow in sync +# with build_helpers/pch.py. _INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+"([^"]+)"', re.MULTILINE) _CORE_HEADER = "esphome/core/defines.h" @@ -82,10 +83,14 @@ def _esp8266_setup_pch() -> None: flags_id = flags_id.replace(basedir, "") digest.update(flags_id.encode()) # GCC never validates a .gch against its source headers, and PlatformIO - # package paths carry no version, so a platform bump must invalidate here + # package paths carry no version, so a package bump must invalidate here platform = env.PioPlatform() # noqa: F821 - for package in ("framework-arduinoespressif8266", "toolchain-xtensa"): - digest.update(str(platform.get_package_version(package)).encode()) + for package in sorted(platform.packages): + try: + version = platform.get_package_version(package) + except Exception: # noqa: BLE001 -- absent optional package + version = None + digest.update(f"{package}={version}".encode()) digest.update(b"\0") closure = _include_closure(src_dir, [*include_headers, _CORE_HEADER]) for rel in sorted(closure): diff --git a/esphome/platformio/toolchain.py b/esphome/platformio/toolchain.py index cf2094dfe0..21a9bb5443 100644 --- a/esphome/platformio/toolchain.py +++ b/esphome/platformio/toolchain.py @@ -289,6 +289,15 @@ def copy_ccache_script() -> None: ) +def copy_pch_script() -> None: + """Copy the shared precompiled-header SCons post-script into the build + dir; platform components pair it with ``post:pch.py`` in extra_scripts.""" + copy_file_if_changed( + Path(__file__).parent / "pch.py.script", + CORE.relative_build_path("pch.py"), + ) + + def run_platformio_cli(*args, **kwargs) -> str | int: # Re-provision the PlatformIO cache if the interpreter's major.minor changed # since it was last built; a stale platform otherwise rejects the new Python From ed59080f62f9329c70efb06e6a8e0407c082bf41 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 14:09:22 -0500 Subject: [PATCH 2/3] Tokenize SCons flags per element so spaced defines survive --- esphome/platformio/pch.py.script | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index 11b991cf95..fa034f25a2 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -63,7 +63,14 @@ def _esp8266_setup_pch() -> None: # not see them; consumers keep theirs, which the .gch then satisfies. flags = [] include_headers = [] - flag_it = iter(shlex.split(projenv.subst("$CXXFLAGS $CCFLAGS $_CCCOMCOM"))) # noqa: F821 + # subst_list keeps SCons's element boundaries; a flattened-string + # shlex.split would shred defines with embedded spaces (-DX=\"A B\") + # into stray tokens gcc reads as input files + flag_it = iter( + tok + for element in projenv.subst_list("$CXXFLAGS $CCFLAGS $_CCCOMCOM")[0] # noqa: F821 + for tok in shlex.split(str(element)) + ) for tok in flag_it: if tok == "-include": include_headers.append(next(flag_it, "")) From 7d91938b795867db8898f0da9d55b2f9908279d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 14:16:04 -0500 Subject: [PATCH 3/3] Reconstruct compiler argv per SCons element for the gch compile --- esphome/platformio/pch.py.script | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index fa034f25a2..4ad5f368ac 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -47,7 +47,16 @@ def _include_closure(src_dir: Path, roots: list) -> dict: return seen -def _esp8266_setup_pch() -> None: +def _shell_arg(element) -> str: + """One compiler argv from one SCons element, matching the real spawn: + SCons whole-quotes spaced elements, the shell unquotes the rest.""" + arg = str(element) + if " " in arg: + return arg.replace('\\"', '"') + return shlex.split(arg)[0] if arg else arg + + +def _setup_pch() -> None: # Project root, not $BUILD_DIR: SCons compiles run with the project dir # as cwd, so "-include esphome_pch.h" resolves here as a relative path. # An absolute path would put the per-device build path on every compile @@ -63,13 +72,9 @@ def _esp8266_setup_pch() -> None: # not see them; consumers keep theirs, which the .gch then satisfies. flags = [] include_headers = [] - # subst_list keeps SCons's element boundaries; a flattened-string - # shlex.split would shred defines with embedded spaces (-DX=\"A B\") - # into stray tokens gcc reads as input files flag_it = iter( - tok + _shell_arg(element) for element in projenv.subst_list("$CXXFLAGS $CCFLAGS $_CCCOMCOM")[0] # noqa: F821 - for tok in shlex.split(str(element)) ) for tok in flag_it: if tok == "-include": @@ -154,4 +159,4 @@ def _esp8266_setup_pch() -> None: print("ESPHome: Compiling with precompiled header") -_esp8266_setup_pch() +_setup_pch()