Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny

This commit is contained in:
J. Nick Koston
2026-08-26 00:46:51 -05:00
6 changed files with 89 additions and 20 deletions
+15 -2
View File
@@ -1217,7 +1217,15 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
# edge (hundreds of edges in a real project)
lines.append(f"srcflags = {' '.join(src_other + include_flags)}")
src_cxx_override = None
if pch_enabled():
if pch_enabled() and "-include" in cxxflags:
# GCC only loads a .gch while no tokens precede it, and the cxx rule
# expands $cxxflags before $flags: a user -include in build_flags
# means every TU would silently skip the .gch
_LOGGER.warning(
"A -include in build_flags prevents the precompiled header from "
"loading; compiling without it"
)
elif pch_enabled():
# 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)
@@ -1233,7 +1241,12 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
# depfile handles staleness. Mirror CCACHE_BASEDIR: strip the
# per-device build path so identically-configured devices
# produce identical .sum files and share cache entries
flags_id = " ".join(cxxflags).replace(effective_ccache_basedir(), "")
# Raw path too: a symlinked build dir resolves differently
flags_id = (
" ".join(cxxflags)
.replace(effective_ccache_basedir(), "")
.replace(str(CORE.build_path), "")
)
# The header text covers include order, which the sorted
# closure alone does not
checksum = pch_checksum(
+18 -11
View File
@@ -69,7 +69,9 @@ _CCACHE_PCH_ENV = {
"CCACHE_PCH_EXTSUM": "true",
}
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+"([^"]+)"', re.MULTILINE)
# Both include forms: an angle include resolving under src/ must enter the
# digest too; ones that do not resolve simply end the walk
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
def pch_enabled() -> bool:
@@ -87,7 +89,11 @@ def ccache_pch_env() -> dict[str, str]:
user_sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if user_sloppiness is not None and (
missing := [
t for t in ("pch_defines", "time_macros") if t not in user_sloppiness
t
for t in ("pch_defines", "time_macros")
# Set membership: substring matching could be fooled by a token
# that merely contains one of ours
if t not in {tok.strip() for tok in user_sloppiness.split(",")}
]
):
# Without these ccache declines every pch-consuming compile; union
@@ -260,6 +266,14 @@ def _log_pch_in_use() -> None:
)
def _read_stamp(path: Path) -> str:
"""A corrupt sidecar must read as stale, not kill the pch forever."""
try:
return path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
return ""
def discard_pch(build_dir: Path) -> None:
"""Remove the pch sidecars so a stale .gch is never consumed.
@@ -324,18 +338,11 @@ def prepare_pch(
)
discard_pch(build_dir)
return
if (
gch.is_file()
and sum_path.is_file()
and sum_path.read_text(encoding="utf-8").strip() == checksum
):
if gch.is_file() and _read_stamp(sum_path) == checksum:
_log_pch_in_use()
return
failed_marker = Path(f"{gch}.failed")
if (
failed_marker.is_file()
and failed_marker.read_text(encoding="utf-8").strip() == checksum
):
if _read_stamp(failed_marker) == checksum:
_LOGGER.info(
"Precompiled header disabled after an earlier failure; delete %s to retry",
failed_marker,
+13 -7
View File
@@ -25,7 +25,7 @@ except Exception as err: # noqa: BLE001 -- not exported under -t nobuild
# 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)
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
_CORE_HEADER = "esphome/core/defines.h"
@@ -122,6 +122,14 @@ def _compile_gch(cxx, flags, header: Path, gch: Path, proj_dir: Path):
return None
def _read_stamp(path: Path) -> str:
"""A corrupt sidecar must read as stale, not kill the pch forever."""
try:
return path.read_text(encoding="utf-8").strip()
except (OSError, UnicodeDecodeError):
return ""
def _setup_pch() -> None:
if projenv is None:
# Expected under -t nobuild; anything else must leave a trail
@@ -249,13 +257,10 @@ def _setup_pch() -> None:
not header.is_file()
or not gch.is_file()
or not sum_path.is_file()
or (sum_path.read_text(encoding="utf-8").strip() != checksum)
or (_read_stamp(sum_path) != checksum)
):
failed_marker = Path(f"{gch}.failed")
if (
failed_marker.is_file()
and failed_marker.read_text(encoding="utf-8").strip() == checksum
):
if _read_stamp(failed_marker) == checksum:
print(
"ESPHome: skipping precompiled header (previous attempt "
f"failed); delete {failed_marker.name} to retry"
@@ -292,7 +297,8 @@ def _setup_pch() -> None:
projenv["ENV"][key] = value # noqa: F821
sloppiness = os.environ.get("CCACHE_SLOPPINESS")
if sloppiness is not None:
missing = [t for t in ("pch_defines", "time_macros") if t not in sloppiness]
tokens = {tok.strip() for tok in sloppiness.split(",")}
missing = [t for t in ("pch_defines", "time_macros") if t not in tokens]
if missing:
# Without these ccache declines every pch-consuming compile;
# union rather than override so the user's own tokens survive
@@ -416,6 +416,21 @@ def test_write_project_pch_identity_unknown_skips_pch(
assert "Could not establish the pch identity" in caplog.text
def test_write_project_pch_skipped_when_user_force_include_precedes(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A -include in build_flags lands ahead of the pch include, so GCC
would never load the .gch; skip it and say so."""
paths = _make_framework(tmp_path)
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", "-include foo.h"
)
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "esphome_pch" not in content
assert "srccxxflags" not in content
assert "prevents the precompiled header" in caplog.text
def test_write_project_pch_disabled(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -45,6 +45,15 @@ def test_ccache_pch_env_disabled() -> None:
assert pch.ccache_pch_env() == {}
def test_ccache_pch_env_token_check_is_membership_not_substring(
caplog: pytest.LogCaptureFixture,
) -> None:
"""A token merely containing ours must not suppress the union."""
with patch.dict(os.environ, {"CCACHE_SLOPPINESS": "pch_defines_extra"}, clear=True):
env = pch.ccache_pch_env()
assert env["CCACHE_SLOPPINESS"] == "pch_defines_extra,pch_defines,time_macros"
def test_ccache_pch_env_unions_user_sloppiness(
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -181,3 +190,12 @@ def test_pch_checksum_survives_surrogate_extra(tmp_path: Path) -> None:
"""Install paths from non-UTF-8 filesystems carry surrogates; hashing
them must not raise past the caller's identity-unknown guard."""
assert pch.pch_checksum(tmp_path, [], ["/opt/bad\udcff/framework"])
def test_include_closure_walks_angle_includes_under_src(tmp_path: Path) -> None:
"""An angle include resolving under src/ must enter the digest; one
that does not simply ends the walk."""
_write(tmp_path, "a.h", "#include <local.h>\n#include <Arduino.h>\n")
(tmp_path / "local.h").write_text("")
closure = pch._include_closure(tmp_path, ["a.h"])
assert set(closure) == {"a.h", "local.h"}
@@ -284,6 +284,16 @@ def test_pch_script_package_version_error_skips_pch(tmp_path: Path) -> None:
assert scons_env.prepended == []
def test_pch_script_corrupt_sidecar_reads_as_stale(tmp_path: Path) -> None:
"""A truncated/corrupt .failed marker must not disable the pch forever."""
_run_script(tmp_path, fail=True)
proj = tmp_path / "dev"
(proj / "esphome_pch.h.gch.failed").write_bytes(b"\xff\xfe corrupt")
_run_script(tmp_path)
assert (proj / "esphome_pch.h.gch").is_file()
assert (proj / "esphome_pch.h.gch.sum").is_file()
def test_pch_script_rebuilds_when_header_missing(tmp_path: Path) -> None:
_run_script(tmp_path)
proj = tmp_path / "dev"