From 73cc3af130e9a27760b64b3aefb8cf20bef4de71 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 20:56:00 -0500 Subject: [PATCH] Union sloppiness tokens on the PlatformIO path too, survive non-UTF-8 include names --- esphome/build_helpers/pch.py | 7 ++++++- esphome/platformio/pch.py.script | 13 ++++++++++++- tests/unit_tests/build_helpers/test_pch.py | 9 +++++++++ tests/unit_tests/test_platformio_pch_script.py | 10 ++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/esphome/build_helpers/pch.py b/esphome/build_helpers/pch.py index 356befc7e2..0f65055c35 100644 --- a/esphome/build_helpers/pch.py +++ b/esphome/build_helpers/pch.py @@ -121,7 +121,12 @@ def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]: data = f"".encode() seen[rel] = data parent = posixpath.dirname(rel) - stack.extend((inc.decode(), parent) for inc in _INCLUDE_RE.findall(data)) + stack.extend( + # surrogateescape: a non-UTF-8 include name must not abort the + # build; it simply will not resolve and ends the walk + (inc.decode(errors="surrogateescape"), parent) + for inc in _INCLUDE_RE.findall(data) + ) return seen diff --git a/esphome/platformio/pch.py.script b/esphome/platformio/pch.py.script index f0126c950d..9fe84b26f3 100644 --- a/esphome/platformio/pch.py.script +++ b/esphome/platformio/pch.py.script @@ -54,7 +54,10 @@ def _include_closure(src_dir: Path, roots: list) -> dict: data = f"".encode() seen[rel] = data parent = posixpath.dirname(rel) - stack.extend((inc.decode(), parent) for inc in _INCLUDE_RE.findall(data)) + stack.extend( + (inc.decode(errors="surrogateescape"), parent) + for inc in _INCLUDE_RE.findall(data) + ) return seen @@ -264,6 +267,14 @@ def _setup_pch() -> None: ): if key not in os.environ: 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] + if missing: + # Without these ccache declines every pch-consuming compile; + # union rather than override so the user's own tokens survive + projenv["ENV"]["CCACHE_SLOPPINESS"] = ",".join((sloppiness, *missing)) # noqa: F821 + print(f"ESPHome: adding {','.join(missing)} to CCACHE_SLOPPINESS for the pch") # Prepended so it is processed before the build_src_flags -include # entries: GCC only uses a .gch while no other tokens have been seen. diff --git a/tests/unit_tests/build_helpers/test_pch.py b/tests/unit_tests/build_helpers/test_pch.py index c75d9e7b4d..a7c3c5b5dd 100644 --- a/tests/unit_tests/build_helpers/test_pch.py +++ b/tests/unit_tests/build_helpers/test_pch.py @@ -166,3 +166,12 @@ def test_include_closure_raises_when_identity_unknown( with pytest.raises(OSError, match="stat failed"): pch._include_closure(_FakeSrcDir(), ["a.h"]) assert "Could not read a.h" in caplog.text + + +def test_include_closure_survives_non_utf8_include_name(tmp_path: Path) -> None: + """A non-UTF-8 quoted include must not abort the build; it simply does + not resolve and ends the walk.""" + (tmp_path / "a.h").write_bytes(b'#include "bad\xff.h"\n#include "b.h"\n') + (tmp_path / "b.h").write_text("") + closure = pch._include_closure(tmp_path, ["a.h"]) + assert set(closure) == {"a.h", "b.h"} diff --git a/tests/unit_tests/test_platformio_pch_script.py b/tests/unit_tests/test_platformio_pch_script.py index 77302a7196..7ce2d0f644 100644 --- a/tests/unit_tests/test_platformio_pch_script.py +++ b/tests/unit_tests/test_platformio_pch_script.py @@ -273,6 +273,16 @@ def test_copy_pch_script(tmp_path: Path) -> None: assert (tmp_path / "pch.py").read_text() == _SCRIPT.read_text() +def test_pch_script_unions_user_sloppiness( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A user CCACHE_SLOPPINESS without the pch tokens gets them unioned on, + mirroring ccache_pch_env, or every src TU is a permanent miss.""" + scons_env = _run_script(tmp_path, env_vars={"CCACHE_SLOPPINESS": "locale"}) + assert scons_env["ENV"]["CCACHE_SLOPPINESS"] == "locale,pch_defines,time_macros" + assert "adding pch_defines,time_macros" in capsys.readouterr().out + + def test_pch_script_nobuild_without_projenv_is_noop(tmp_path: Path) -> None: """-t nobuild never exports projenv; the script must not abort.""" proj = tmp_path / "dev"