[esp8266] Precompile the prefix header on the native toolchain (#19821)

This commit is contained in:
J. Nick Koston
2026-09-28 18:51:44 +02:00
committed by GitHub
parent 787ee418b0
commit 446c47f7f8
10 changed files with 354 additions and 16 deletions
+115 -1
View File
@@ -364,7 +364,87 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None:
line for line in content.splitlines() if line.startswith(" flags = ")
]
assert flags_lines
assert all(line == " flags = $srcflags" for line in flags_lines)
# C++ src edges consume the precompiled header; C/assembly keep srcflags
assert set(flags_lines) == {" flags = $srcflags", " flags = $srccxxflags"}
def test_write_project_pch(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths, ccache="/usr/bin/ccache")
build_dir = CORE.relative_pioenvs_path(CORE.name)
assert "rule pch" in content
# Compiled from the include list, and again when the checksum changes
gch_edge = next(
line
for line in content.splitlines()
if line.startswith("build esphome_pch.h.gch: pch ")
)
assert gch_edge.endswith("esphome_pch_src.h | esphome_pch.h.gch.sum")
for line in content.splitlines():
# C++ edges wait on the .gch; the C edge must not reference it
if line.startswith("build obj/src/main.cpp.o:"):
assert line.endswith("| esphome_pch.h.gch")
if line.startswith("build obj/src/esphome/vendor.c.o:"):
assert "esphome_pch" not in line
assert (build_dir / "esphome_pch_src.h").read_text().splitlines() == [
'#include "esphome/components/esp8266/throw_stubs.h"',
'#include "esphome/core/pch_prefix.h"',
]
assert "#error" in (build_dir / "esphome_pch.h").read_text()
assert (build_dir / "esphome_pch.h.gch.sum").read_text().strip()
def test_write_project_pch_folds_joined_src_force_include(
tmp_path: Path,
) -> None:
"""-includefoo.h in build_src_flags must fold into the pch like the
separated spelling, not precede and defeat it."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
CORE.platformio_options["build_src_flags"] = "-includeesphome/core/defines.h"
content = _write_ninja(paths, ccache="/usr/bin/ccache")
assert "build esphome_pch.h.gch: pch" in content
assert "srccxxflags" in content
assert "-includeesphome" not in content
@pytest.mark.parametrize("flag", ["-includefoo.h", "--include=foo.h"])
def test_write_project_pch_skipped_for_other_force_include_spellings(
tmp_path: Path, caplog: pytest.LogCaptureFixture, flag: str
) -> None:
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH", flag)
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:
"""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:
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
content = _write_ninja(paths)
assert "esphome_pch" not in content
assert "srccxxflags" not in content
assert " flags = $srcflags" in content
def test_write_project_scanf_float_and_waveform_kept(tmp_path: Path) -> None:
@@ -1716,3 +1796,37 @@ def test_write_project_rejects_spaced_ldscript_override(tmp_path: Path) -> None:
CORE.relative_src_path().mkdir(parents=True, exist_ok=True)
with pytest.raises(EsphomeError, match="Invalid flash linker script name"):
arduino8266.write_project(paths, None)
def test_write_project_pch_no_device_path_poison(tmp_path: Path) -> None:
"""Regression: the -include stays relative and the .sum carries no
per-device path, or cross-device ccache sharing breaks."""
paths = _make_framework(tmp_path / "shared")
sums = []
for name in ("dev_a", "dev_b"):
CORE.name = name
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 -Werror=invalid-pch "
"-include esphome_pch.h" in content
)
sums.append(
(CORE.relative_pioenvs_path(name) / "esphome_pch.h.gch.sum").read_text()
)
assert sums[0] == sums[1]
def test_write_project_pch_sum_tracks_src_flags(tmp_path: Path) -> None:
"""The header compiles with the src flags too, so they are part of what
ccache is told about it."""
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
sum_path = CORE.relative_pioenvs_path(CORE.name) / "esphome_pch.h.gch.sum"
sums = []
for value in ("1", "2"):
CORE.platformio_options["build_src_flags"] = f"-DSRC_ONLY={value}"
_write_ninja(paths)
sums.append(sum_path.read_text())
assert sums[0] != sums[1]
@@ -10,6 +10,7 @@ from unittest.mock import patch
import pytest
from esphome.build_helpers import ccache
from esphome.core import CORE
def test_resolve_opt_out() -> None:
@@ -122,6 +123,20 @@ def test_parse_enable_env_spelling_tables(
assert ccache.parse_enable_env("ESPHOME_CCACHE_ENABLE") is expected
def test_effective_ccache_basedir_prefers_user_value(tmp_path: Path) -> None:
CORE.build_path = tmp_path
# Drive-qualified on Windows: "/custom/base" is not absolute there
base = "C:\\custom\\base" if os.name == "nt" else "/custom/base"
with patch.dict(os.environ, {"CCACHE_BASEDIR": base}, clear=True):
assert ccache.effective_ccache_basedir() == base
with patch.dict(os.environ, {}, clear=True):
assert ccache.effective_ccache_basedir() == str(tmp_path.resolve())
# Degenerate values would strip substrings ccache never rewrites
for bad in ("", "/", "a/b"):
with patch.dict(os.environ, {"CCACHE_BASEDIR": bad}, clear=True):
assert ccache.effective_ccache_basedir() == str(tmp_path.resolve())
def test_resolve_absolute_ccache_path(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
@@ -165,3 +180,18 @@ def test_ccache_defaults_env_escapes_a_dollar(
env = ccache.ccache_defaults_env(tmp_path / "c$d")
assert env["CCACHE_DIR"].endswith("c$$d")
assert env["CCACHE_BASEDIR"].endswith("a$$b")
def _pch_ccache_env(tmp_path: Path, environ: dict[str, str]) -> dict[str, str]:
CORE.build_path = tmp_path / "build"
spec = ("ESPHOME_TEST_PREFIX", "test")
environ = {"ESPHOME_TEST_PREFIX": str(tmp_path / "cache"), **environ}
with patch.dict(os.environ, environ, clear=True):
return ccache.ccache_env("/usr/bin/ccache", spec)
def test_ccache_env_includes_pch_settings(tmp_path: Path) -> None:
"""A native build exports the ccache settings the pch needs."""
env = _pch_ccache_env(tmp_path, {})
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
assert env["CCACHE_PCH_EXTSUM"] == "true"
@@ -57,6 +57,11 @@ def test_ccache_pch_env_disabled() -> None:
assert pch.ccache_pch_env() == {}
def test_pch_header_text_preserves_order() -> None:
text = pch.pch_header_text(["b.h", "a.h"])
assert text == '#include "b.h"\n#include "a.h"\n'
def test_include_closure_resolves_relative_and_root(tmp_path: Path) -> None:
"""Sibling includes resolve against the includer's directory first,
full paths against the src root; unresolvable names end the walk."""