mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Merge branch 'platformio-pch-rp2' into platformio-pch-libretiny
This commit is contained in:
@@ -201,10 +201,10 @@ def parse_entry(
|
||||
for tok in it:
|
||||
if tok in ("-c", "-o"):
|
||||
next(it, None) # drop the flag and its argument (input/output)
|
||||
elif tok == "-include":
|
||||
elif tok == "-include" or tok.startswith("-include"):
|
||||
# Re-anchor only names next to the compile (the pch); a name
|
||||
# meant for the -I chain must stay untouched
|
||||
raw = next(it, "")
|
||||
raw = next(it, "") if tok == "-include" else tok[len("-include") :]
|
||||
if not raw:
|
||||
_LOGGER.warning("Dropping -include with no argument")
|
||||
elif Path(resolved := _include(raw)).is_file():
|
||||
|
||||
@@ -16,6 +16,7 @@ import os
|
||||
from pathlib import Path
|
||||
import posixpath
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
|
||||
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
|
||||
@@ -138,6 +139,15 @@ def pch_header_text(include_headers: Iterable[str]) -> str:
|
||||
return "".join(f'#include "{name}"\n' for name in include_headers)
|
||||
|
||||
|
||||
def _resolves(path: Path) -> bool:
|
||||
"""False when missing; other stat failures propagate (identity unknown,
|
||||
unlike is_file(), which would silently drop the header)."""
|
||||
try:
|
||||
return stat.S_ISREG(path.stat().st_mode)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return False
|
||||
|
||||
|
||||
def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
|
||||
"""Include closure of ``roots``: src-relative name -> contents.
|
||||
|
||||
@@ -151,7 +161,7 @@ def _include_closure(src_dir: Path, roots: Iterable[str]) -> dict[str, bytes]:
|
||||
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():
|
||||
if not rel.startswith("..") and _resolves(src_dir / rel):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
import posixpath
|
||||
import re
|
||||
import shlex
|
||||
import stat
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
@@ -28,6 +29,14 @@ _INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
|
||||
_CORE_HEADER = "esphome/core/defines.h"
|
||||
|
||||
|
||||
def _resolves(path: Path) -> bool:
|
||||
"""False when missing; other stat failures propagate (identity unknown)."""
|
||||
try:
|
||||
return stat.S_ISREG(path.stat().st_mode)
|
||||
except (FileNotFoundError, NotADirectoryError):
|
||||
return False
|
||||
|
||||
|
||||
def _include_closure(src_dir: Path, roots: list) -> dict:
|
||||
"""Quoted-include closure: src-relative name -> contents (mirror of
|
||||
build_helpers/pch.py)."""
|
||||
@@ -37,7 +46,7 @@ def _include_closure(src_dir: Path, roots: list) -> dict:
|
||||
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():
|
||||
if not rel.startswith("..") and _resolves(src_dir / rel):
|
||||
break
|
||||
else:
|
||||
continue
|
||||
@@ -71,11 +80,11 @@ def _shell_arg(element) -> str:
|
||||
tokens = shlex.split(arg)
|
||||
except ValueError as err:
|
||||
print(f"ESPHome: could not lex flag {arg!r} for the pch: {err}")
|
||||
return arg
|
||||
return None
|
||||
if len(tokens) != 1:
|
||||
# The quoting model is wrong for this element; leave a trail
|
||||
print(f"ESPHome: passing flag {arg!r} through unlexed for the pch")
|
||||
return arg
|
||||
# A flag the model cannot reproduce would diverge the .gch's flags
|
||||
print(f"ESPHome: cannot model flag {arg!r} for the pch")
|
||||
return None
|
||||
return tokens[0]
|
||||
|
||||
|
||||
@@ -149,10 +158,14 @@ def _setup_pch() -> None:
|
||||
# not see them; consumers keep theirs, which the .gch then satisfies.
|
||||
flags = []
|
||||
include_headers = []
|
||||
flag_it = iter(
|
||||
raw_args = [
|
||||
_shell_arg(element)
|
||||
for element in projenv.subst_list("$CXXFLAGS $CCFLAGS $_CCCOMCOM")[0] # noqa: F821
|
||||
)
|
||||
]
|
||||
if any(arg is None for arg in raw_args):
|
||||
print("ESPHome: skipping precompiled header: unmodelable flag")
|
||||
return
|
||||
flag_it = iter(raw_args)
|
||||
for tok in flag_it:
|
||||
if tok == "-include":
|
||||
include_headers.append(next(flag_it, ""))
|
||||
@@ -166,11 +179,15 @@ def _setup_pch() -> None:
|
||||
# Fold only relative names resolving under src/: consumers keep their
|
||||
# own -include entries, so folding an unguarded user header would
|
||||
# include it twice; unfolded ones stay consumer-only.
|
||||
folded = [
|
||||
name
|
||||
for name in include_headers
|
||||
if not Path(name).is_absolute() and (src_dir / name).is_file()
|
||||
]
|
||||
try:
|
||||
folded = [
|
||||
name
|
||||
for name in include_headers
|
||||
if not Path(name).is_absolute() and _resolves(src_dir / name)
|
||||
]
|
||||
except OSError as err:
|
||||
print(f"ESPHome: skipping precompiled header: {err}")
|
||||
return
|
||||
if unfolded := [n for n in include_headers if n not in folded]:
|
||||
print(f"ESPHome: not precompiling non-src force-includes: {unfolded}")
|
||||
content = "".join(f'#include "{name}"\n' for name in (*folded, _CORE_HEADER))
|
||||
@@ -202,7 +219,11 @@ def _setup_pch() -> None:
|
||||
return
|
||||
digest.update(f"{package}={version}".encode())
|
||||
digest.update(b"\0")
|
||||
closure = _include_closure(src_dir, [*folded, _CORE_HEADER])
|
||||
try:
|
||||
closure = _include_closure(src_dir, [*folded, _CORE_HEADER])
|
||||
except OSError as err:
|
||||
print(f"ESPHome: skipping precompiled header: {err}")
|
||||
return
|
||||
for rel in sorted(closure):
|
||||
digest.update(rel.encode(errors="surrogateescape"))
|
||||
digest.update(closure[rel])
|
||||
@@ -237,10 +258,8 @@ def _setup_pch() -> None:
|
||||
try:
|
||||
data = local.read_bytes()
|
||||
except OSError as err:
|
||||
# mtime/size still shift the digest; stat failure skips the pch
|
||||
print(f"ESPHome: could not read {local} for the pch checksum: {err}")
|
||||
st = local.stat()
|
||||
data = f"<unreadable:{st.st_mtime_ns}:{st.st_size}>".encode()
|
||||
print(f"ESPHome: skipping precompiled header: {local}: {err}")
|
||||
return
|
||||
digest.update(str(local.relative_to(proj_dir)).encode())
|
||||
digest.update(data)
|
||||
digest.update(b"\0")
|
||||
|
||||
@@ -103,6 +103,21 @@ def test_parse_entry_resolves_force_include_path(tmp_path: Path) -> None:
|
||||
assert resolved == str(tmp_path / "esphome_pch.h").replace("\\", "/")
|
||||
|
||||
|
||||
def test_parse_entry_resolves_joined_force_include(tmp_path: Path) -> None:
|
||||
"""The joined -includefoo.h spelling takes the same resolve path."""
|
||||
(tmp_path / "esphome_pch.h").write_text("")
|
||||
entry = _entry(
|
||||
str(tmp_path),
|
||||
f"{tmp_path}/src/esphome/x.cpp",
|
||||
"g++ -includeesphome_pch.h -c x.cpp",
|
||||
)
|
||||
|
||||
_, _, _, cxx_flags = idedata.parse_entry(entry)
|
||||
|
||||
resolved = cxx_flags[cxx_flags.index("-include") + 1]
|
||||
assert resolved == str(tmp_path / "esphome_pch.h").replace("\\", "/")
|
||||
|
||||
|
||||
def test_parse_entry_keeps_search_chain_force_include(tmp_path: Path) -> None:
|
||||
"""-include names resolved via the -I chain (libretiny's Arduino.h) must
|
||||
not be re-anchored to a nonexistent build-dir path."""
|
||||
|
||||
@@ -165,8 +165,11 @@ def test_include_closure_raises_when_identity_unknown(
|
||||
"""An unreadable header propagates; callers compile without a pch."""
|
||||
|
||||
class _BadFile:
|
||||
def is_file(self) -> bool:
|
||||
return True
|
||||
def stat(self): # noqa: ANN202 -- regular-file mode only
|
||||
import os
|
||||
import stat as stat_mod
|
||||
|
||||
return os.stat_result((stat_mod.S_IFREG | 0o644,) + (0,) * 9)
|
||||
|
||||
def read_bytes(self) -> bytes:
|
||||
raise OSError("read failed")
|
||||
|
||||
@@ -188,6 +188,15 @@ def test_pch_script_preserves_spaced_flag_elements(tmp_path: Path) -> None:
|
||||
assert pch.splitlines()[0] == '#include "other.h"'
|
||||
|
||||
|
||||
def test_pch_script_folds_joined_force_include_spelling(tmp_path: Path) -> None:
|
||||
"""-includefoo.h folds like the separated form, matching the native path."""
|
||||
(tmp_path / "dev" / "src").mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / "dev" / "src" / "other.h").write_text("")
|
||||
_run_script(tmp_path, flags=["-DX=1", "-includeother.h"])
|
||||
pch = (tmp_path / "dev" / "esphome_pch.h").read_text()
|
||||
assert pch.splitlines()[0] == '#include "other.h"'
|
||||
|
||||
|
||||
def test_pch_script_leaves_absolute_force_includes_unfolded(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -400,21 +409,17 @@ def test_pch_script_hashes_project_local_include_dirs(tmp_path: Path) -> None:
|
||||
@pytest.mark.skipif(
|
||||
getattr(os, "geteuid", lambda: -1)() == 0, reason="root ignores file modes"
|
||||
)
|
||||
def test_pch_script_unreadable_local_header_warns_and_varies(
|
||||
def test_pch_script_unreadable_local_header_skips_pch(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""An unreadable generated header still shifts the digest via mtime/size."""
|
||||
"""An unreadable generated header means unknown identity: no pch."""
|
||||
proj = tmp_path / "dev"
|
||||
override = proj / "lwip_override"
|
||||
override.mkdir(parents=True)
|
||||
secret = override / "lwipopts.h"
|
||||
secret.write_text("#define TCP_MSS 1460\n")
|
||||
secret.chmod(0)
|
||||
flags = ["-DX=1", "-I", str(override)]
|
||||
_run_script(tmp_path, flags=flags)
|
||||
first = (proj / "esphome_pch.h.gch.sum").read_text()
|
||||
assert "could not read" in capsys.readouterr().out
|
||||
os.utime(secret, (1, 1))
|
||||
(tmp_path / "fake-gxx.argv").unlink(missing_ok=True)
|
||||
_run_script(tmp_path, flags=flags)
|
||||
assert (proj / "esphome_pch.h.gch.sum").read_text() != first
|
||||
scons_env = _run_script(tmp_path, flags=["-DX=1", "-I", str(override)])
|
||||
assert not (proj / "esphome_pch.h.gch.sum").exists()
|
||||
assert scons_env.prepended == []
|
||||
assert "skipping precompiled header" in capsys.readouterr().out
|
||||
|
||||
Reference in New Issue
Block a user