Merge branch 'esp32-idf-pch' into platformio-pch-rp2

This commit is contained in:
J. Nick Koston
2026-08-25 19:08:37 -05:00
5 changed files with 72 additions and 10 deletions
+14 -4
View File
@@ -195,6 +195,7 @@ def parse_entry(
defines: list[str] = []
includes: list[str] = []
cxx_flags: list[str] = []
unresolved_force_includes: list[str] = []
it = iter(tokens[1:])
for tok in it:
@@ -207,11 +208,11 @@ def parse_entry(
raw = next(it, "")
if not raw:
_LOGGER.warning("Dropping -include with no argument")
elif Path(resolved := _include(raw)).is_file():
cxx_flags.extend(("-include", resolved))
else:
resolved = _include(raw)
cxx_flags.extend(
("-include", resolved if Path(resolved).is_file() else raw)
)
unresolved_force_includes.append(raw)
cxx_flags.extend(("-include", raw))
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.
@@ -230,6 +231,15 @@ def parse_entry(
pass # input/output files
else:
cxx_flags.append(tok)
for raw in unresolved_force_includes:
# A deleted build artifact (clean_build removes esphome_pch.h) would
# otherwise surface only as an opaque downstream tooling error
if not any((Path(inc) / raw).is_file() for inc in includes):
_LOGGER.warning(
"-include %s found neither next to the compile nor on the "
"include path; cached idedata may not resolve it",
raw,
)
return cxx_path, defines, includes, cxx_flags
+15 -3
View File
@@ -82,6 +82,14 @@ def ccache_pch_env() -> dict[str, str]:
export these process-wide; only time_macros affects non-pch TUs."""
if not pch_enabled():
return {}
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if user_sloppiness is not None and "pch_defines" not in user_sloppiness:
# EXTSUM without pch_defines makes ccache silently decline every
# pch-consuming compile
_LOGGER.warning(
"CCACHE_SLOPPINESS lacks pch_defines; ccache will not cache "
"compiles that use the precompiled header"
)
return {k: v for k, v in _CCACHE_PCH_ENV.items() if k not in os.environ}
@@ -121,10 +129,14 @@ def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
try:
data = (src_dir / rel).read_bytes()
except OSError as err:
# Hash a marker so an unreadable header invalidates instead of
# silently vanishing from the digest
# mtime/size keep a changed-but-unreadable header shifting the
# digest without device paths in it
_LOGGER.warning("Could not read %s for the pch checksum: %s", rel, err)
data = b"<unreadable>"
try:
st = (src_dir / rel).stat()
data = f"<unreadable:{st.st_mtime_ns}:{st.st_size}>".encode()
except OSError:
data = b"<unreadable>"
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend((inc.decode(), parent) for inc in _INCLUDE_RE.findall(data))
+7 -1
View File
@@ -46,7 +46,11 @@ def _include_closure(src_dir: Path, roots: list) -> dict:
data = (src_dir / rel).read_bytes()
except OSError as err:
print(f"ESPHome: could not read {rel} for the pch checksum: {err}")
data = b"<unreadable>"
try:
st = (src_dir / rel).stat()
data = f"<unreadable:{st.st_mtime_ns}:{st.st_size}>".encode()
except OSError:
data = b"<unreadable>"
seen[rel] = data
parent = posixpath.dirname(rel)
stack.extend((inc.decode(), parent) for inc in _INCLUDE_RE.findall(data))
@@ -262,6 +266,8 @@ def _setup_pch() -> None:
# Prepended so it is processed before the build_src_flags -include
# entries: GCC only uses a .gch while no other tokens have been seen.
# The relative name also reaches "pio run -t idedata" output; external
# consumers replaying cxx_flags must run from the project dir.
projenv.Prepend(CXXFLAGS=["-Winvalid-pch", "-include", header.name]) # noqa: F821
print("ESPHome: Compiling with precompiled header")
@@ -117,6 +117,27 @@ def test_parse_entry_keeps_search_chain_force_include(tmp_path: Path) -> None:
assert cxx_flags[cxx_flags.index("-include") + 1] == "Arduino.h"
def test_parse_entry_warns_on_vanished_force_include(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A build-dir force-include deleted by clean_build must leave a trail;
a name resolvable via the -I chain must not warn."""
inc = tmp_path / "inc"
inc.mkdir()
(inc / "Arduino.h").write_text("")
entry = _entry(
str(tmp_path),
f"{tmp_path}/src/esphome/x.cpp",
f"g++ -I{inc} -include Arduino.h -include esphome_pch.h -c x.cpp",
)
_, _, _, cxx_flags = idedata.parse_entry(entry)
assert "Arduino.h" in cxx_flags
assert "esphome_pch.h" in caplog.text
assert "Arduino.h" not in caplog.text
def test_parse_entry_drops_trailing_force_include(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
+15 -2
View File
@@ -45,11 +45,22 @@ def test_ccache_pch_env_disabled() -> None:
assert pch.ccache_pch_env() == {}
def test_ccache_pch_env_respects_user_values() -> None:
def test_ccache_pch_env_respects_user_values(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A user CCACHE_SLOPPINESS wins, but one without pch_defines silently
stops ccache from caching pch consumers, so it must warn."""
with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "locale"}, clear=True):
env = pch.ccache_pch_env()
assert "CCACHE_SLOPPINESS" not in env
assert env["CCACHE_PCH_EXTSUM"] == "true"
assert "lacks pch_defines" in caplog.text
caplog.clear()
with patch.dict(
os.environ, {"CCACHE_SLOPPINESS": "pch_defines,locale"}, clear=True
):
pch.ccache_pch_env()
assert "lacks pch_defines" not in caplog.text
def test_pch_header_text_preserves_order() -> None:
@@ -118,7 +129,9 @@ def test_include_closure_marks_unreadable(
closure = pch._include_closure(tmp_path, ["a.h"])
finally:
locked.chmod(0o644)
assert closure["locked.h"] == b"<unreadable>"
# stat still works, so the marker varies with mtime/size and a later
# edit to the unreadable file still shifts the digest
assert closure["locked.h"].startswith(b"<unreadable:")
assert "Could not read locked.h" in caplog.text