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:
@@ -1217,7 +1217,7 @@ 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() and "-include" in cxxflags:
|
||||
if pch_enabled() and any(tok.startswith("-include") for tok 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
|
||||
@@ -1281,7 +1281,10 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
# Relative -include (resolved from the ninja cwd, where the header
|
||||
# lives): an absolute path would put the per-device build path on
|
||||
# every compile command and defeat cross-device ccache sharing
|
||||
cxx_parts = src_other + [f"-Winvalid-pch -include {PCH_HEADER_NAME}"]
|
||||
# -Wno-error keeps a rejected .gch a warning under user -Werror
|
||||
cxx_parts = src_other + [
|
||||
f"-Winvalid-pch -Wno-error=invalid-pch -include {PCH_HEADER_NAME}"
|
||||
]
|
||||
lines.append(f"srccxxflags = {' '.join(cxx_parts)}")
|
||||
src_cxx_override = ("$srccxxflags", gch)
|
||||
src_objs = _ninja_compile_edges(
|
||||
|
||||
@@ -71,6 +71,9 @@ _CCACHE_PCH_ENV = {
|
||||
|
||||
# Both include forms: an angle include resolving under src/ must enter the
|
||||
# digest too; ones that do not resolve simply end the walk
|
||||
# Compiler failures that clear on their own must not latch the .failed marker
|
||||
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
|
||||
|
||||
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
|
||||
|
||||
|
||||
@@ -379,6 +382,9 @@ def prepare_pch(
|
||||
# This path latches, so keep the full compiler output recoverable
|
||||
_LOGGER.debug("Full pch compile output: %s", error)
|
||||
discard_pch(build_dir)
|
||||
if any(m in error for m in _TRANSIENT_ERRORS):
|
||||
# Resource exhaustion clears on its own; retry next build
|
||||
return
|
||||
# Skip retries until a header/flag/backend-identity/command change
|
||||
failed_marker.write_text(checksum + "\n", encoding="utf-8")
|
||||
os.utime(header)
|
||||
|
||||
@@ -25,6 +25,9 @@ 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.
|
||||
|
||||
# Compiler failures that clear on their own must not latch the .failed marker
|
||||
_TRANSIENT_ERRORS = ("No space left", "Cannot allocate", "Resource temporarily")
|
||||
|
||||
_INCLUDE_RE = re.compile(rb'^\s*#\s*include\s+["<]([^">]+)[">]', re.MULTILINE)
|
||||
_CORE_HEADER = "esphome/core/defines.h"
|
||||
|
||||
@@ -76,9 +79,12 @@ def _shell_arg(element) -> str:
|
||||
except ValueError as err:
|
||||
print(f"ESPHome: could not lex flag {arg!r} for the pch: {err}")
|
||||
return arg
|
||||
# Anything but exactly one token means the model above is wrong for
|
||||
# this element; pass it through untouched rather than dropping flags
|
||||
return tokens[0] if len(tokens) == 1 else arg
|
||||
if len(tokens) != 1:
|
||||
# The shell-quoting model is wrong for this element; say so rather
|
||||
# than surfacing only as downstream pch warnings
|
||||
print(f"ESPHome: passing flag {arg!r} through unlexed for the pch")
|
||||
return arg
|
||||
return tokens[0]
|
||||
|
||||
|
||||
def _compile_gch(cxx, flags, header: Path, gch: Path, proj_dir: Path):
|
||||
@@ -280,6 +286,9 @@ def _setup_pch() -> None:
|
||||
print(error)
|
||||
gch.unlink(missing_ok=True)
|
||||
sum_path.unlink(missing_ok=True)
|
||||
if any(m in error for m in _TRANSIENT_ERRORS):
|
||||
# Resource exhaustion clears on its own; retry next build
|
||||
return
|
||||
# Skip retries until a flag/header/platform change alters the checksum
|
||||
failed_marker.write_text(checksum + "\n", encoding="utf-8")
|
||||
return
|
||||
|
||||
@@ -416,6 +416,19 @@ 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_for_joined_force_include_spelling(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""GCC also accepts -includefoo.h as one token; the guard must see it."""
|
||||
paths = _make_framework(tmp_path)
|
||||
_set_flags(
|
||||
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", "-includefoo.h"
|
||||
)
|
||||
content = _write_ninja(paths, ccache="/usr/bin/ccache")
|
||||
assert "esphome_pch" not in content
|
||||
assert "prevents the precompiled header" in caplog.text
|
||||
|
||||
|
||||
def test_write_project_pch_skipped_when_user_force_include_precedes(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -1799,7 +1812,10 @@ def test_write_project_pch_no_device_path_poison(tmp_path: Path) -> None:
|
||||
CORE.build_path = tmp_path / name
|
||||
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
content = _write_ninja(paths, ccache="/usr/bin/ccache")
|
||||
assert "srccxxflags = -Winvalid-pch -include esphome_pch.h" in content
|
||||
assert (
|
||||
"srccxxflags = -Winvalid-pch -Wno-error=invalid-pch "
|
||||
"-include esphome_pch.h" in content
|
||||
)
|
||||
sums.append(
|
||||
(CORE.relative_pioenvs_path(name) / "esphome_pch.h.gch.sum").read_text()
|
||||
)
|
||||
|
||||
@@ -1052,3 +1052,29 @@ def test_prepare_pch_identity_unknown_discards(tmp_path: Path) -> None:
|
||||
prepare_pch()
|
||||
assert not stale.exists()
|
||||
assert not (dev / "build" / "esphome_pch.h.gch.sum").exists()
|
||||
|
||||
|
||||
def test_prepare_pch_transient_compiler_failure_does_not_latch(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""ENOSPC-style failures clear on their own; no .failed marker."""
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
|
||||
dev = _make_pch_device(tmp_path, "dev_e")
|
||||
CORE.build_path = dev
|
||||
calls = []
|
||||
|
||||
def enospc(cmd, **kwargs):
|
||||
calls.append(cmd)
|
||||
return subprocess.CompletedProcess(
|
||||
cmd, 1, "", "fatal error: No space left on device"
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch("esphome.build_helpers.pch.subprocess.run", side_effect=enospc),
|
||||
):
|
||||
prepare_pch()
|
||||
prepare_pch()
|
||||
assert not (dev / "build" / "esphome_pch.h.gch.failed").exists()
|
||||
assert len(calls) == 2
|
||||
|
||||
@@ -82,6 +82,7 @@ class _FakeSConsEnv(dict):
|
||||
def _fake_cxx(
|
||||
tmp_path: Path,
|
||||
fail: bool = False,
|
||||
fail_msg: str | None = None,
|
||||
reject_pch: bool = False,
|
||||
probe_exit: int = 0,
|
||||
) -> Path:
|
||||
@@ -96,7 +97,7 @@ def _fake_cxx(
|
||||
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
|
||||
)
|
||||
if fail:
|
||||
body += "echo boom >&2\nexit 1\n"
|
||||
body += f"echo {fail_msg or 'boom'} >&2\nexit 1\n"
|
||||
else:
|
||||
# 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'
|
||||
@@ -113,6 +114,7 @@ def _run_script(
|
||||
tmp_path: Path,
|
||||
flags: list[str] | None = None,
|
||||
fail: bool = False,
|
||||
fail_msg: str | None = None,
|
||||
reject_pch: bool = False,
|
||||
probe_exit: int = 0,
|
||||
missing_cxx: bool = False,
|
||||
@@ -124,7 +126,13 @@ def _run_script(
|
||||
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, reject_pch=reject_pch, probe_exit=probe_exit)
|
||||
cxx = _fake_cxx(
|
||||
tmp_path,
|
||||
fail=fail,
|
||||
fail_msg=fail_msg,
|
||||
reject_pch=reject_pch,
|
||||
probe_exit=probe_exit,
|
||||
)
|
||||
if missing_cxx:
|
||||
cxx = tmp_path / "no-such-gxx"
|
||||
args = (proj, src, str(cxx), flags or ["-DX=1"], platform_cls)
|
||||
@@ -217,6 +225,16 @@ def test_pch_script_sum_is_device_independent(tmp_path: Path) -> None:
|
||||
assert sums[0] == sums[1]
|
||||
|
||||
|
||||
def test_pch_script_transient_compiler_failure_does_not_latch(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""ENOSPC-style failures clear on their own; no .failed marker."""
|
||||
scons_env = _run_script(tmp_path, fail=True, fail_msg="No space left on device")
|
||||
proj = tmp_path / "dev"
|
||||
assert not (proj / "esphome_pch.h.gch.failed").exists()
|
||||
assert scons_env.prepended == []
|
||||
|
||||
|
||||
def test_pch_script_failure_marker_suppresses_retry(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user