Files
esphome/esphome/components/esp8266/pch.py.script
T

146 lines
5.6 KiB
Plaintext

import hashlib
import os
from pathlib import Path
import posixpath
import re
import shlex
import subprocess
# pylint: disable=E0602
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.
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+"([^"]+)"', re.MULTILINE)
_CORE_HEADER = "esphome/core/defines.h"
def _include_closure(src_dir: Path, roots: list) -> dict:
"""Quoted-include closure: src-relative name -> contents (mirror of
build_helpers/pch.py)."""
seen = {}
stack = [(name, "") for name in roots]
while stack:
name, from_dir = stack.pop()
for candidate in (f"{from_dir}/{name}" if from_dir else name, name):
rel = posixpath.normpath(candidate)
if not rel.startswith("..") and (src_dir / rel).is_file():
break
else:
continue
if rel in seen:
continue
try:
data = (src_dir / rel).read_bytes()
except OSError:
continue
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend((inc.decode(), parent) for inc in _INCLUDE_RE.findall(data))
return seen
def _esp8266_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
# command and defeat cross-device ccache sharing.
proj_dir = Path(env.subst("$PROJECT_DIR")) # noqa: F821
src_dir = Path(env.subst("$PROJECT_SRC_DIR")) # noqa: F821
header = proj_dir / "esphome_pch.h"
gch = Path(f"{header}.gch")
sum_path = Path(f"{gch}.sum")
cxx = projenv.subst("$CXX") # noqa: F821
# The header holds the -include entries itself, so the .gch compile must
# 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
for tok in flag_it:
if tok == "-include":
include_headers.append(next(flag_it, ""))
else:
flags.append(tok)
content = "".join(
f'#include "{name}"\n' for name in (*include_headers, _CORE_HEADER)
)
digest = hashlib.sha256()
digest.update(content.encode())
digest.update(cxx.encode())
# Mirror CCACHE_BASEDIR: strip the per-device build path so identical
# configs produce identical .sum files and share cache entries
flags_id = " ".join(flags)
if basedir := os.environ.get("CCACHE_BASEDIR"):
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
platform = env.PioPlatform() # noqa: F821
for package in ("framework-arduinoespressif8266", "toolchain-xtensa"):
digest.update(str(platform.get_package_version(package)).encode())
digest.update(b"\0")
closure = _include_closure(src_dir, [*include_headers, _CORE_HEADER])
for rel in sorted(closure):
digest.update(rel.encode())
digest.update(closure[rel])
digest.update(b"\0")
checksum = digest.hexdigest()
# The ccache .sum sidecar doubles as the freshness stamp
if (
not gch.is_file()
or not sum_path.is_file()
or (sum_path.read_text(encoding="utf-8").strip() != checksum)
):
failed_marker = Path(f"{gch}.failed")
if (
failed_marker.is_file()
and failed_marker.read_text(encoding="utf-8").strip() == checksum
):
return
header.write_text(content, encoding="utf-8")
try:
result = subprocess.run( # noqa: PLW1510
[cxx, "-x", "c++-header", *flags, "-c", str(header), "-o", str(gch)],
capture_output=True,
text=True,
)
error = result.stderr if result.returncode != 0 else None
except OSError as err:
error = str(err)
if error is not None:
print("ESPHome: precompiled header failed; compiling without it")
print(error)
gch.unlink(missing_ok=True)
sum_path.unlink(missing_ok=True)
# Skip retries until a flag/header/platform change alters the checksum
failed_marker.write_text(checksum + "\n", encoding="utf-8")
return
failed_marker.unlink(missing_ok=True)
sum_path.write_text(checksum + "\n", encoding="utf-8")
# Scoped to src compiles: framework/library TUs never consume the .gch
# and keep strict ccache hashing. User-set values win.
for key, value in (
("CCACHE_SLOPPINESS", "pch_defines,time_macros"),
("CCACHE_PCH_EXTSUM", "true"),
):
if key not in os.environ:
projenv["ENV"][key] = value # noqa: F821
# Prepended so it is processed before the build_src_flags -include
# entries: GCC only uses a .gch while no other tokens have been seen.
projenv.Prepend(CXXFLAGS=["-include", header.name]) # noqa: F821
print("ESPHome: Compiling with precompiled header")
_esp8266_setup_pch()