mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Merge branch 'esp32-idf-pch' into platformio-pch-rp2
This commit is contained in:
@@ -1223,6 +1223,11 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
# C++ src edges swap the force-includes for one precompiled prefix
|
||||
# header holding the same content plus defines.h; C and assembly
|
||||
# edges keep srcflags (a .gch is a C++ artifact)
|
||||
# The opt-out hint matters when a toolchain rejects its own .gch:
|
||||
# the build stays correct but every TU warns via -Winvalid-pch
|
||||
_LOGGER.info(
|
||||
"Compiling with a precompiled header (set ESPHOME_PCH_ENABLE=0 to disable)"
|
||||
)
|
||||
pch_header = build_dir / PCH_HEADER_NAME
|
||||
pch_includes = (*src_includes, PCH_CORE_HEADER)
|
||||
write_file_if_changed(pch_header, pch_header_text(pch_includes))
|
||||
|
||||
@@ -200,13 +200,10 @@ def parse_entry(
|
||||
for tok in it:
|
||||
if tok in ("-c", "-o"):
|
||||
next(it, None) # drop the flag and its argument (input/output)
|
||||
continue
|
||||
if tok == "-include":
|
||||
# Drop only the injected pch include, whose relative path does
|
||||
# not resolve outside the build dir; keep other force-includes
|
||||
inc = next(it, "")
|
||||
if not inc.endswith("esphome_pch.h"):
|
||||
cxx_flags.extend(("-include", inc))
|
||||
elif tok == "-include":
|
||||
# Resolve like -I so cached idedata works from any cwd (the pch
|
||||
# include is emitted relative to the build dir)
|
||||
cxx_flags.extend(("-include", _include(next(it, ""))))
|
||||
elif tok.startswith("-D"):
|
||||
# ``.strip()`` handles tokens like ``-D CONFIGURED=1`` (a single
|
||||
# quoted arg with a space after -D) that some flags arrive as.
|
||||
|
||||
+5
-4
@@ -7,6 +7,7 @@ import re
|
||||
import time
|
||||
|
||||
from esphome import loader
|
||||
from esphome.build_helpers.pch import PCH_HEADER_NAME
|
||||
from esphome.compiled_config import save_compiled_config
|
||||
from esphome.config import iter_component_configs, iter_components
|
||||
from esphome.const import (
|
||||
@@ -612,10 +613,10 @@ def clean_build(clear_pio_cache: bool = True, *, full: bool = False):
|
||||
# The PlatformIO pch artifacts live at the project root so the
|
||||
# relative -include resolves; a partial clean must drop them too
|
||||
for name in (
|
||||
"esphome_pch.h",
|
||||
"esphome_pch.h.gch",
|
||||
"esphome_pch.h.gch.sum",
|
||||
"esphome_pch.h.gch.failed",
|
||||
PCH_HEADER_NAME,
|
||||
f"{PCH_HEADER_NAME}.gch",
|
||||
f"{PCH_HEADER_NAME}.gch.sum",
|
||||
f"{PCH_HEADER_NAME}.gch.failed",
|
||||
):
|
||||
pch_path = CORE.relative_build_path(name)
|
||||
if pch_path.is_file():
|
||||
|
||||
@@ -85,6 +85,24 @@ def test_parse_entry_resolves_relative_includes() -> None:
|
||||
assert all(Path(inc).is_absolute() for inc in includes)
|
||||
|
||||
|
||||
def test_parse_entry_resolves_force_include_path() -> None:
|
||||
"""The pch -include is emitted relative to the build dir; idedata must
|
||||
resolve it so cached flags work from any cwd."""
|
||||
directory = f"{ABS}build/proj"
|
||||
entry = _entry(
|
||||
directory,
|
||||
f"{directory}/src/esphome/x.cpp",
|
||||
"g++ -include esphome_pch.h -c x.cpp",
|
||||
)
|
||||
|
||||
_, _, _, cxx_flags = idedata.parse_entry(entry)
|
||||
|
||||
idx = cxx_flags.index("-include")
|
||||
resolved = cxx_flags[idx + 1]
|
||||
assert Path(resolved).is_absolute()
|
||||
assert resolved.endswith("build/proj/esphome_pch.h")
|
||||
|
||||
|
||||
def test_parse_entry_skips_dependency_flags() -> None:
|
||||
"""Dependency-generation flags (and their args) are dropped."""
|
||||
entry = _entry(
|
||||
|
||||
@@ -27,10 +27,22 @@ class _FakePlatform:
|
||||
return "1.2.3"
|
||||
|
||||
|
||||
class _BrokenPlatform(_FakePlatform):
|
||||
def get_package_version(self, name: str) -> str:
|
||||
raise RuntimeError("manifest parse error")
|
||||
|
||||
|
||||
class _FakeSConsEnv(dict):
|
||||
"""Just enough of a SCons construction environment for pch.py."""
|
||||
|
||||
def __init__(self, proj_dir: Path, src_dir: Path, cxx: str, flags: list[str]):
|
||||
def __init__(
|
||||
self,
|
||||
proj_dir: Path,
|
||||
src_dir: Path,
|
||||
cxx: str,
|
||||
flags: list[str],
|
||||
platform_cls: type[_FakePlatform] = _FakePlatform,
|
||||
):
|
||||
super().__init__(ENV={})
|
||||
self._subst = {
|
||||
"$PROJECT_DIR": str(proj_dir),
|
||||
@@ -38,6 +50,7 @@ class _FakeSConsEnv(dict):
|
||||
"$CXX": cxx,
|
||||
}
|
||||
self._flags = flags
|
||||
self._platform_cls = platform_cls
|
||||
self.prepended: list[str] = []
|
||||
|
||||
def subst(self, expr: str) -> str: # noqa: N802
|
||||
@@ -47,14 +60,18 @@ class _FakeSConsEnv(dict):
|
||||
return [self._flags]
|
||||
|
||||
def PioPlatform(self) -> _FakePlatform: # noqa: N802
|
||||
return _FakePlatform()
|
||||
return self._platform_cls()
|
||||
|
||||
def Prepend(self, CXXFLAGS: list[str]) -> None: # noqa: N802, N803
|
||||
self.prepended = CXXFLAGS
|
||||
|
||||
|
||||
def _fake_cxx(tmp_path: Path, fail: bool = False) -> Path:
|
||||
"""A compiler stand-in that records its argv and writes the -o target."""
|
||||
def _fake_cxx(tmp_path: Path, fail: bool = False, reject_pch: bool = False) -> Path:
|
||||
"""A compiler stand-in that records its argv and writes the -o target.
|
||||
|
||||
With reject_pch it builds the .gch fine but, like GCC 10 on macOS arm64,
|
||||
warns on any consuming compile that the .gch cannot be loaded.
|
||||
"""
|
||||
cxx = tmp_path / "fake-gxx"
|
||||
body = (
|
||||
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
|
||||
@@ -62,7 +79,12 @@ def _fake_cxx(tmp_path: Path, fail: bool = False) -> Path:
|
||||
if fail:
|
||||
body += "echo boom >&2\nexit 1\n"
|
||||
else:
|
||||
body += 'out=""; prev=""\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\necho gch > "$out"\n'
|
||||
# Only the c++-header compile has a -o; the load probe has none
|
||||
body += 'out=""; prev=""\nfor a in "$@"; do [ "$prev" = "-o" ] && out="$a"; prev="$a"; done\n'
|
||||
body += '[ -n "$out" ] && echo gch > "$out"\n'
|
||||
if reject_pch:
|
||||
body += 'case " $* " in *c++-header*) ;; *) echo "warning: esphome_pch.h.gch: had text segment at different address" >&2;; esac\n'
|
||||
body += "exit 0\n"
|
||||
cxx.write_text("#!/bin/sh\n" + body)
|
||||
cxx.chmod(cxx.stat().st_mode | stat.S_IEXEC)
|
||||
return cxx
|
||||
@@ -72,22 +94,28 @@ def _run_script(
|
||||
tmp_path: Path,
|
||||
flags: list[str] | None = None,
|
||||
fail: bool = False,
|
||||
reject_pch: bool = False,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
name: str = "dev",
|
||||
platform_cls: type[_FakePlatform] = _FakePlatform,
|
||||
) -> _FakeSConsEnv:
|
||||
proj = tmp_path / name
|
||||
src = proj / "src"
|
||||
(src / "esphome" / "core").mkdir(parents=True, exist_ok=True)
|
||||
(src / "esphome" / "core" / "defines.h").write_text("#define USE_X\n")
|
||||
cxx = _fake_cxx(tmp_path, fail=fail)
|
||||
scons_env = _FakeSConsEnv(proj, src, str(cxx), flags or ["-DX=1"])
|
||||
cxx = _fake_cxx(tmp_path, fail=fail, reject_pch=reject_pch)
|
||||
args = (proj, src, str(cxx), flags or ["-DX=1"], platform_cls)
|
||||
# Distinct objects: the script must scope ccache/flags to projenv only
|
||||
global_env = _FakeSConsEnv(*args)
|
||||
projenv = _FakeSConsEnv(*args)
|
||||
projenv.global_env = global_env
|
||||
source = _SCRIPT.read_text()
|
||||
with patch.dict(os.environ, env_vars or {}, clear=True):
|
||||
exec( # noqa: S102
|
||||
compile(source, "pch.py", "exec"),
|
||||
{"Import": lambda *_names: None, "env": scons_env, "projenv": scons_env},
|
||||
{"Import": lambda *_names: None, "env": global_env, "projenv": projenv},
|
||||
)
|
||||
return scons_env
|
||||
return projenv
|
||||
|
||||
|
||||
def test_pch_script_builds_and_prepends_relative_include(tmp_path: Path) -> None:
|
||||
@@ -98,9 +126,12 @@ def test_pch_script_builds_and_prepends_relative_include(tmp_path: Path) -> None
|
||||
assert len((proj / "esphome_pch.h.gch.sum").read_text().strip()) == 64
|
||||
# Relative include: an absolute path would poison ccache keys
|
||||
assert scons_env.prepended == ["-Winvalid-pch", "-include", "esphome_pch.h"]
|
||||
# ccache settings land on the SCons ENV only, never os.environ
|
||||
# ccache settings land on projenv's ENV only: framework/library TUs
|
||||
# compile under the global env and must keep strict hashing
|
||||
assert scons_env["ENV"]["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
|
||||
assert scons_env["ENV"]["CCACHE_PCH_EXTSUM"] == "true"
|
||||
assert scons_env.global_env["ENV"] == {}
|
||||
assert scons_env.global_env.prepended == []
|
||||
assert "CCACHE_SLOPPINESS" not in os.environ
|
||||
|
||||
|
||||
@@ -154,6 +185,29 @@ def test_pch_script_failure_marker_suppresses_retry(
|
||||
assert "delete esphome_pch.h.gch.failed to retry" in out
|
||||
|
||||
|
||||
def test_pch_script_probe_rejection_falls_back(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A toolchain that cannot load its own .gch (GCC 10 on macOS arm64)
|
||||
must not leave consumers paying for a pch every compile rejects."""
|
||||
scons_env = _run_script(tmp_path, reject_pch=True)
|
||||
proj = tmp_path / "dev"
|
||||
assert not (proj / "esphome_pch.h.gch").exists()
|
||||
assert not (proj / "esphome_pch.h.gch.sum").exists()
|
||||
assert (proj / "esphome_pch.h.gch.failed").is_file()
|
||||
assert scons_env.prepended == []
|
||||
assert "toolchain cannot load the pch" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_pch_script_package_version_error_skips_pch(tmp_path: Path) -> None:
|
||||
"""Without trustworthy package identity a stale .gch could survive an
|
||||
upgrade, so the script must not build one at all."""
|
||||
scons_env = _run_script(tmp_path, platform_cls=_BrokenPlatform)
|
||||
proj = tmp_path / "dev"
|
||||
assert not (proj / "esphome_pch.h.gch").exists()
|
||||
assert scons_env.prepended == []
|
||||
|
||||
|
||||
def test_pch_script_rebuilds_when_header_missing(tmp_path: Path) -> None:
|
||||
_run_script(tmp_path)
|
||||
proj = tmp_path / "dev"
|
||||
|
||||
Reference in New Issue
Block a user