mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 15:27:33 +00:00
Add ESPHOME_PCH_STRICT and enforce it in the esp8266/esp32/rp2 image compile tests
This commit is contained in:
@@ -201,6 +201,17 @@ jobs:
|
||||
- ln882x-arduino
|
||||
- nrf52
|
||||
- host
|
||||
# Fail the job if the precompiled header silently degrades on the
|
||||
# platforms where it must work (libretiny needs a toolchain bump)
|
||||
include:
|
||||
- id: esp8266-arduino
|
||||
pch_strict: "1"
|
||||
- id: esp32-idf-esp-idf
|
||||
pch_strict: "1"
|
||||
- id: esp32-arduino-esp-idf
|
||||
pch_strict: "1"
|
||||
- id: rp2040-arduino
|
||||
pch_strict: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Download image artifact
|
||||
@@ -212,6 +223,7 @@ jobs:
|
||||
- name: Compile ${{ matrix.id }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
-e ESPHOME_PCH_STRICT="${{ matrix.pch_strict || '0' }}" \
|
||||
-v "${{ github.workspace }}/docker/test_configs:/config" \
|
||||
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
|
||||
compile "${{ matrix.id }}.yaml"
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.build_helpers.pch import (
|
||||
PCH_HEADER_NAME,
|
||||
mark_pch_emitted,
|
||||
pch_checksum,
|
||||
pch_degraded,
|
||||
pch_enabled,
|
||||
pch_header_text,
|
||||
)
|
||||
@@ -1230,6 +1231,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
"A -include in build_flags prevents the precompiled header from "
|
||||
"loading; compiling without it"
|
||||
)
|
||||
pch_degraded("a user -include precedes the pch")
|
||||
elif pch_enabled():
|
||||
# C++ src edges swap the force-includes for one precompiled prefix
|
||||
# header (same content plus defines.h); C/assembly keep srcflags
|
||||
@@ -1263,6 +1265,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
|
||||
_LOGGER.warning(
|
||||
"Could not establish the pch identity; compiling without it: %s", err
|
||||
)
|
||||
pch_degraded(f"identity unknown: {err}")
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Compiling with a precompiled header "
|
||||
|
||||
@@ -102,6 +102,19 @@ def pch_enabled() -> bool:
|
||||
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
|
||||
|
||||
|
||||
def pch_strict() -> bool:
|
||||
"""CI knob: ``ESPHOME_PCH_STRICT=1`` turns pch degrade paths fatal."""
|
||||
return parse_enable_env("ESPHOME_PCH_STRICT") is True
|
||||
|
||||
|
||||
def pch_degraded(reason: str) -> None:
|
||||
"""Every degrade path funnels through here; strict mode raises."""
|
||||
if pch_strict():
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
raise EsphomeError(f"ESPHOME_PCH_STRICT: {reason}")
|
||||
|
||||
|
||||
def ccache_pch_env() -> dict[str, str]:
|
||||
"""Settings ccache needs to cache compiles that consume the .gch;
|
||||
empty unless this build actually emitted one. User-set values win.
|
||||
@@ -333,6 +346,7 @@ def prepare_pch(
|
||||
if cmd_and_dir is None:
|
||||
# Freshness cannot be validated; a leftover .gch must not be consumed
|
||||
discard_pch(build_dir)
|
||||
pch_degraded("no usable compile command")
|
||||
return
|
||||
cmd, cmd_dir = cmd_and_dir
|
||||
# Strip like ccache's rewriting (user CCACHE_BASEDIR wins); the raw
|
||||
@@ -359,6 +373,7 @@ def prepare_pch(
|
||||
"Could not establish the pch identity; compiling without it: %s", err
|
||||
)
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"identity unknown: {err}")
|
||||
return
|
||||
if gch.is_file() and _read_stamp(sum_path) == checksum:
|
||||
_log_pch_in_use()
|
||||
@@ -369,6 +384,7 @@ def prepare_pch(
|
||||
"Precompiled header disabled after an earlier failure; delete %s to retry",
|
||||
failed_marker,
|
||||
)
|
||||
pch_degraded("earlier failure latched")
|
||||
return
|
||||
_log_pch_in_use()
|
||||
try:
|
||||
@@ -400,6 +416,7 @@ def prepare_pch(
|
||||
# Transient (timeout, spawn/IO): warn and retry next build, no marker
|
||||
_LOGGER.warning("Precompiled header compile did not run: %s", err)
|
||||
discard_pch(build_dir)
|
||||
pch_degraded(f"compile did not run: {err}")
|
||||
return
|
||||
if error is not None:
|
||||
_LOGGER.warning(
|
||||
@@ -410,10 +427,12 @@ def prepare_pch(
|
||||
discard_pch(build_dir)
|
||||
if any(m in error for m in _TRANSIENT_ERRORS):
|
||||
# Resource exhaustion clears on its own; retry next build
|
||||
pch_degraded(f"transient compile failure: {error[:200]}")
|
||||
return
|
||||
# Skip retries until a header/flag/backend-identity/command change
|
||||
failed_marker.write_text(checksum + "\n", encoding="utf-8")
|
||||
os.utime(header)
|
||||
pch_degraded(f"compile failed: {error[:200]}")
|
||||
return
|
||||
failed_marker.unlink(missing_ok=True)
|
||||
sum_path.write_text(checksum + "\n", encoding="utf-8")
|
||||
|
||||
@@ -158,7 +158,7 @@ def _read_stamp(path: Path) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _setup_pch() -> None:
|
||||
def _setup_pch() -> bool | None:
|
||||
if projenv is None:
|
||||
# Expected under -t nobuild; anything else must leave a trail
|
||||
print(f"ESPHome: projenv unavailable ({_projenv_error}); skipping pch")
|
||||
@@ -355,10 +355,20 @@ def _setup_pch() -> None:
|
||||
)
|
||||
projenv["ENV"].update(ccache_updates) # noqa: F821
|
||||
print("ESPHome: Compiling with precompiled header")
|
||||
return True
|
||||
|
||||
|
||||
_strict = os.environ.get("ESPHOME_PCH_STRICT", "").strip().lower() in (
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
)
|
||||
try:
|
||||
_setup_pch()
|
||||
if not _setup_pch() and _strict:
|
||||
raise RuntimeError("ESPHOME_PCH_STRICT: precompiled header was not used")
|
||||
except Exception: # noqa: BLE001 -- a speedup must never break the build
|
||||
if _strict:
|
||||
raise
|
||||
print("ESPHome: pch internal error; compiling without it")
|
||||
traceback.print_exc()
|
||||
|
||||
@@ -1842,3 +1842,17 @@ def test_write_project_pch_no_device_path_poison(tmp_path: Path) -> None:
|
||||
(CORE.relative_pioenvs_path(name) / "esphome_pch.h.gch.sum").read_text()
|
||||
)
|
||||
assert sums[0] == sums[1]
|
||||
|
||||
|
||||
def test_write_project_pch_strict_raises_on_skip(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
|
||||
paths = _make_framework(tmp_path)
|
||||
_set_flags(
|
||||
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", "-include foo.h"
|
||||
)
|
||||
with pytest.raises(EsphomeError, match="precedes the pch"):
|
||||
_write_ninja(paths, ccache="/usr/bin/ccache")
|
||||
|
||||
@@ -1078,3 +1078,22 @@ def test_prepare_pch_transient_compiler_failure_does_not_latch(
|
||||
prepare_pch()
|
||||
assert not (dev / "build" / "esphome_pch.h.gch.failed").exists()
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_prepare_pch_strict_raises_on_missing_db(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""ESPHOME_PCH_STRICT turns the silent skip into a failure."""
|
||||
from esphome.build_gen.espidf import prepare_pch
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
|
||||
dev = _make_pch_device(tmp_path, "dev_st")
|
||||
(dev / "build" / "compile_commands.json").unlink()
|
||||
CORE.build_path = dev
|
||||
with (
|
||||
patch.object(CORE, "name", "test"),
|
||||
patch("esphome.build_helpers.pch.subprocess.run", side_effect=AssertionError),
|
||||
pytest.raises(EsphomeError, match="no usable compile command"),
|
||||
):
|
||||
prepare_pch()
|
||||
|
||||
@@ -216,3 +216,29 @@ def test_ccache_pch_env_warns_on_falsy_extsum(
|
||||
env = pch.ccache_pch_env()
|
||||
assert "CCACHE_PCH_EXTSUM" not in env
|
||||
assert "disables pch caching" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[(None, False), ("0", False), ("1", True), ("true", True)],
|
||||
)
|
||||
def test_pch_strict(
|
||||
value: str | None, expected: bool, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
if value is None:
|
||||
monkeypatch.delenv("ESPHOME_PCH_STRICT", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("ESPHOME_PCH_STRICT", value)
|
||||
assert pch.pch_strict() is expected
|
||||
|
||||
|
||||
def test_pch_degraded_raises_only_in_strict(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
monkeypatch.delenv("ESPHOME_PCH_STRICT", raising=False)
|
||||
pch.pch_degraded("reason")
|
||||
monkeypatch.setenv("ESPHOME_PCH_STRICT", "1")
|
||||
with pytest.raises(EsphomeError, match="reason"):
|
||||
pch.pch_degraded("reason")
|
||||
|
||||
@@ -458,3 +458,14 @@ def test_pch_script_unreadable_local_header_skips_pch(
|
||||
assert not (proj / "esphome_pch.h.gch.sum").exists()
|
||||
assert scons_env.prepended == []
|
||||
assert "skipping precompiled header" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_pch_script_strict_raises_when_pch_not_used(tmp_path: Path) -> None:
|
||||
"""ESPHOME_PCH_STRICT fails the build instead of degrading."""
|
||||
with pytest.raises(Exception, match="ESPHOME_PCH_STRICT|boom"):
|
||||
_run_script(tmp_path, fail=True, env_vars={"ESPHOME_PCH_STRICT": "1"})
|
||||
|
||||
|
||||
def test_pch_script_strict_passes_on_success(tmp_path: Path) -> None:
|
||||
scons_env = _run_script(tmp_path, env_vars={"ESPHOME_PCH_STRICT": "1"})
|
||||
assert scons_env.prepended
|
||||
|
||||
Reference in New Issue
Block a user