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 79% rename from esphome/components/esp8266/pch.py.script rename to esphome/platformio/pch.py.script index 444fa1aa69..4ad5f368ac 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" @@ -46,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 @@ -62,7 +72,10 @@ 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 + flag_it = iter( + _shell_arg(element) + for element in projenv.subst_list("$CXXFLAGS $CCFLAGS $_CCCOMCOM")[0] # noqa: F821 + ) for tok in flag_it: if tok == "-include": include_headers.append(next(flag_it, "")) @@ -82,10 +95,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): @@ -142,4 +159,4 @@ def _esp8266_setup_pch() -> None: print("ESPHome: Compiling with precompiled header") -_esp8266_setup_pch() +_setup_pch() 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