[core] Leave the precompiled header off on Windows for a GCC with bug 14940 (#19875)

This commit is contained in:
J. Nick Koston
2026-09-29 18:51:12 +02:00
committed by GitHub
parent a7e0afeb46
commit b22f2e6b4c
17 changed files with 410 additions and 35 deletions
+3 -5
View File
@@ -1053,12 +1053,9 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
+ ["stdc++-exc" if config.exceptions else "stdc++", "m", "c", "gcc"]
)
cxx = (toolchain_tool(paths.toolchain, "g++"),)
lines = [
*tool_lines(
(toolchain_tool(paths.toolchain, "gcc"),),
(toolchain_tool(paths.toolchain, "g++"),),
ccache,
),
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
*compile_rule_lines(),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(),
@@ -1146,6 +1143,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
flag_sets.cxxflags,
src_other,
(str(paths.framework), str(paths.toolchain)),
cxx,
)
src_objs = compile_edges(
lines,
+22 -4
View File
@@ -3,6 +3,7 @@
import json
import logging
from pathlib import Path
import textwrap
from esphome.build_helpers import pch
from esphome.components.esp32 import (
@@ -289,6 +290,8 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
# Where CMake puts the .gch of the src component; ccache reads the checksum
# next to it in place of the .gch
_PCH_SUM_PATH = "build/esp-idf/src/CMakeFiles/__idf_src.dir/cmake_pch.hxx.gch.sum"
# Where the Windows gate records its choice
_PCH_CHOICE_VAR = "ESPHOME_PCH"
def _pch_cmake_block() -> str:
@@ -300,11 +303,21 @@ def _pch_cmake_block() -> str:
f' "$<$<COMPILE_LANGUAGE:CXX>:${{CMAKE_CURRENT_SOURCE_DIR}}/{header}>"'
for header in pch.PCH_DEFAULT_HEADERS
)
return f"""
# ESPHome precompiled header
target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
{headers}
)
)"""
if not pch.pch_needs_gcc_check():
return f"\n# ESPHome precompiled header\n{block}\n"
# Before the first configure only CMake knows the compiler version
return f"""
# ESPHome precompiled header, unless GCC bug 14940 keeps it from loading
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set({_PCH_CHOICE_VAR} OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)
else()
set({_PCH_CHOICE_VAR} ON CACHE BOOL "ESPHome precompiled header in use" FORCE)
{textwrap.indent(block, " ")}
endif()
"""
@@ -315,8 +328,13 @@ def _read_if_exists(path: Path) -> str:
def write_pch_checksum() -> None:
"""Write the checksum ccache uses in place of the .gch: the core headers,
the framework version, the sdkconfig and the managed component versions."""
from esphome.espidf.toolchain import get_cmake_cache_value
if not pch.pch_enabled():
return
# The gate's choice, cached by configure
if pch.pch_needs_gcc_check() and get_cmake_cache_value(_PCH_CHOICE_VAR) != "ON":
return
pch.log_pch_in_use()
checksum = pch.pch_checksum(
CORE.relative_src_path(),
+1
View File
@@ -274,6 +274,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
[*compilers.cxx[1:], *cxxflags],
(),
(compilers.cxx[0], _compiler_version(compilers.cxx)) if pch_enabled() else (),
compilers.cxx,
)
src_objs = compile_edges(
lines,
+7 -5
View File
@@ -24,8 +24,8 @@ from esphome.build_helpers.pch import (
PCH_SUM_NAME,
log_pch_in_use,
pch_consumer_flags,
pch_enabled,
pch_identity,
pch_usable,
write_pch_headers,
)
from esphome.framework_helpers import strip_win_long_path_prefix
@@ -125,15 +125,17 @@ def pch_edges(
cxxflags: Sequence[str],
src_flags: Sequence[str],
identity: Sequence[str],
cxx: Sequence[Path | str],
) -> tuple[str, str] | None:
"""Emit the precompiled header for the C++ src edges.
``headers`` are folded into one prefix header, ``src_flags`` are the
flags every src edge carries and ``identity`` names what else the
compile depends on, the compiler included. Returns the ``cxx_override``
for ``compile_edges``, or None without a pch.
flags every src edge carries, ``identity`` names what else the compile
depends on, the compiler included, and ``cxx`` is what the host rule
asks. Returns the ``cxx_override`` for ``compile_edges``, or None
without a pch.
"""
if not pch_enabled():
if not pch_usable(cxx):
return None
if any(
tok == "-include" or tok.startswith("--include") or is_joined_include(tok)
+73 -1
View File
@@ -2,13 +2,15 @@
from __future__ import annotations
from collections.abc import Iterable
from collections.abc import Iterable, Sequence
import hashlib
import logging
import os
from pathlib import Path
import posixpath
import re
import subprocess
import sys
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.const import PLATFORM_NRF52
@@ -63,6 +65,76 @@ def pch_enabled() -> bool:
return parse_enable_env("ESPHOME_PCH_ENABLE") is not False
def pch_forced() -> bool:
"""``ESPHOME_PCH_ENABLE=1``: wanted even where the host rule says no."""
return parse_enable_env("ESPHOME_PCH_ENABLE") is True
# GCC bug 14940: before these releases the Windows loader maps a .gch only
# at its saved address. First fixed release per major, 16 on always fixed;
# PCH_WINDOWS_CMAKE_OLD_GCC and the pch_usable message spell the same table
PCH_WINDOWS_GCC_FIXED = {14: (14, 4), 15: (15, 3)}
PCH_WINDOWS_GCC_FIXED_DEFAULT = (16, 0)
# The same rule for CMake, which alone knows the version before configure
PCH_WINDOWS_CMAKE_OLD_GCC = (
"CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14.4 OR "
"(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15 AND "
"CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15.3)"
)
def gcc_relocates_pch_on_windows(version: Sequence[int]) -> bool:
"""Whether a GCC of this version loads a .gch on Windows."""
if not version:
return False
fixed = PCH_WINDOWS_GCC_FIXED.get(version[0], PCH_WINDOWS_GCC_FIXED_DEFAULT)
return tuple(version[:2]) >= fixed
# GCC ends the first --version line with its version; clang names itself
_VERSION_RE = re.compile(r"\d+(?:\.\d+)+")
def gcc_version(cxx: Sequence[Path | str]) -> tuple[int, ...] | None:
"""The GCC version from ``--version``: () when it cannot be read, None
for a compiler that is not GCC."""
try:
result = subprocess.run(
[*cxx, "--version"], capture_output=True, text=True, check=False
)
except OSError as err:
_LOGGER.debug("Cannot run %s: %s", cxx[0], err)
return ()
banner = result.stdout.partition("\n")[0]
if "clang" in banner.lower():
return None
found = _VERSION_RE.findall(banner)
return tuple(int(part) for part in found[-1].split(".")) if found else ()
def pch_needs_gcc_check() -> bool:
"""Windows host with the knob unset: the compiler version decides."""
return sys.platform == "win32" and parse_enable_env("ESPHOME_PCH_ENABLE") is None
def pch_usable(cxx: Sequence[Path | str]) -> bool:
"""The knob plus the host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
if not pch_enabled():
return False
if not pch_needs_gcc_check():
return True
version = gcc_version(cxx)
if version is None or gcc_relocates_pch_on_windows(version):
return True
_LOGGER.info(
"GCC %s cannot load a precompiled header on Windows (GCC bug 14940, "
"fixed in 14.4, 15.3 and 16); compiling without it "
"(set ESPHOME_PCH_ENABLE=1 to force)",
".".join(map(str, version)) or "of unknown version",
)
return False
def pch_consumer_flags() -> list[str]:
"""Flags a C++ src compile loads the pch with. The -include stays
relative: an absolute path would enter the ccache key."""
+10 -4
View File
@@ -75,6 +75,7 @@ from .framework import (
get_build_env,
get_build_paths,
setup_platformio_python_env,
toolchain_tool,
)
# force import gpio to register pin schema
@@ -843,7 +844,11 @@ def _write_pch_checksum(build_dir: Path, source_dir: Path) -> None:
write_file_if_changed(app_dir / _PCH_SUM_PATH, checksum + "\n")
def _generate_cmake_lists() -> bool:
def _pch_usable() -> bool:
return pch.pch_usable((toolchain_tool("g++"),))
def _generate_cmake_lists(pch_on: bool) -> bool:
"""Write the project CMakeLists.txt, returning True if it changed."""
compile_flags = get_project_compile_flags()
link_flags = get_project_link_flags()
@@ -886,7 +891,7 @@ def _generate_cmake_lists() -> bool:
")",
]
if pch.pch_enabled():
if pch_on:
lines += _PCH_CMAKE_LINES
if link_flags:
@@ -945,7 +950,8 @@ def run_compile(args, config: ConfigType) -> bool:
paths = get_build_paths()
env = get_build_env()
cmake_lists_changed = _generate_cmake_lists()
pch_on = _pch_usable()
cmake_lists_changed = _generate_cmake_lists(pch_on)
board = zephyr_data()[KEY_BOARD]
build_dir = CORE.relative_pioenvs_path(CORE.name)
@@ -962,7 +968,7 @@ def run_compile(args, config: ConfigType) -> bool:
_LOGGER.info("Build inputs changed, cleaning %s", build_dir)
rmtree(build_dir)
if pch.pch_enabled():
if pch_on:
pch.log_pch_in_use()
# Zephyr turns ccache on by itself when it is installed
env.update(pch.ccache_pch_env())
+7
View File
@@ -605,6 +605,13 @@ def get_elf_path() -> Path:
return build_dir / "firmware.elf"
def get_cmake_cache_value(var_name: str) -> str | None:
"""One entry of the configured build's CMake cache, or None when unset."""
cmake_output = _get_cmake_output(CORE.relative_build_path("build"))
match = re.search(rf"^{var_name}:\w+=(.*)$", cmake_output, re.MULTILINE)
return match.group(1).strip() if match else None
def get_objdump_path() -> Path:
return _get_cmake_tool_path("CMAKE_OBJDUMP")
+31 -1
View File
@@ -60,6 +60,10 @@ rc = libc.posix_spawn(None, argv[0], None, attr, (ctypes.c_char_p * len(argv))(*
libc.strerror.restype = ctypes.c_char_p
sys.exit("ESPHome: cannot start " + real + ": " + libc.strerror(rc).decode())
"""
# GCC bug 14940: before these releases the Windows loader maps a .gch only
# at its saved address. First fixed release per major, 16 on always fixed
_WINDOWS_GCC_FIXED = {14: (14, 4), 15: (15, 3)}
_WINDOWS_GCC_FIXED_DEFAULT = (16, 0)
_CXX_SUFFIXES = (".cpp", ".cc", ".cxx")
_HEADER_SUFFIXES = (".h", ".hpp", ".hh", ".inc")
_INCLUDE_DIR_OPTIONS = ("-I", "-isystem", "-iquote", "-idirafter")
@@ -113,7 +117,11 @@ def _src_envs(src_dir: Path) -> list:
def _driver_output(cxx: str, option: str) -> str:
result = subprocess.run([cxx, option], capture_output=True, text=True) # noqa: PLW1510
"""What the driver prints for ``option``, or "" when it cannot run."""
try:
result = subprocess.run([cxx, option], capture_output=True, text=True) # noqa: PLW1510
except OSError:
return ""
return result.stdout.strip()
@@ -141,6 +149,26 @@ def _old_gcc_flags(proj_dir: Path, pio_platform: str, cxx: str):
return [f"-B{_CC1_DIR}/"]
def _gcc_loads_pch_here(cxx: str) -> bool:
"""Whether a .gch loads here; ESPHOME_PCH_ENABLE=1, as esphome
normalizes it, insists."""
if sys.platform != "win32" or os.environ.get("ESPHOME_PCH_ENABLE") == "1":
return True
# GCC ends the first --version line with its version; clang names itself
banner = _driver_output(cxx, "--version").partition("\n")[0]
if "clang" in banner.lower():
return True
found = re.findall(r"\d+(?:\.\d+)+", banner)
version = tuple(int(p) for p in found[-1].split(".")) if found else ()
if version and version[:2] >= _WINDOWS_GCC_FIXED.get(version[0], _WINDOWS_GCC_FIXED_DEFAULT):
return True
print(
f"ESPHome: GCC {'.'.join(map(str, version)) or 'of unknown version'} cannot load a "
"precompiled header on Windows (GCC bug 14940); compiling without it"
)
return False
def _setup_pch() -> None:
if projenv is None:
if "nobuild" in COMMAND_LINE_TARGETS: # noqa: F821
@@ -157,6 +185,8 @@ def _setup_pch() -> None:
src_envs = _src_envs(src_dir)
cxx = src_envs[0].subst("$CXX")
if not _gcc_loads_pch_here(cxx):
return
pio_platform = env.PioPlatform() # noqa: F821
extra = _old_gcc_flags(proj_dir, pio_platform.name, cxx)
if extra is None:
+8 -1
View File
@@ -336,10 +336,17 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
env = dict(os.environ if base_env is None else base_env)
env.update(_ccache_env())
if CORE.is_configured:
from esphome.build_helpers.pch import ccache_pch_env, pch_script_enabled
from esphome.build_helpers.pch import (
ccache_pch_env,
pch_forced,
pch_script_enabled,
)
if pch_script_enabled():
env.update(ccache_pch_env())
if pch_forced():
# Normalized for the script, like ESPHOME_CCACHE_ENABLE
env["ESPHOME_PCH_ENABLE"] = "1"
# The runner offers the out-of-flash tip but has no configured CORE, so
# tell it. Ask CORE, not is_esp32_arduino_build(), which reads this same
# variable; clear an inherited one so it cannot reach the wrong build.
@@ -447,6 +447,19 @@ def test_write_project_pch_disabled(
assert " flags = $srcflags" in content
def test_write_project_pch_asks_the_toolchain_compiler_on_windows(
windows_gcc_rule: None, tmp_path: Path
) -> None:
from esphome.build_helpers import pch
paths = _make_framework(tmp_path)
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
with patch.object(pch, "gcc_version", return_value=(10, 3, 0)) as asked:
content = _write_ninja(paths)
assert asked.call_args.args[0] == (toolchain_tool(paths.toolchain, "g++"),)
assert "esphome_pch" not in content
def test_write_project_scanf_float_and_waveform_kept(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = True
+48
View File
@@ -538,6 +538,54 @@ def test_component_cmakelists_pch_block(monkeypatch: pytest.MonkeyPatch) -> None
assert "target_precompile_headers" not in get_component_cmakelists()
def test_component_cmakelists_pch_gate_on_windows(
windows_gcc_rule: None, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The block carries the rule and records its choice; the knob drops
the gate."""
from esphome.build_gen.espidf import get_component_cmakelists
content = get_component_cmakelists()
assert (
'if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND '
"(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 14.4 OR "
"(CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 15 AND "
"CMAKE_CXX_COMPILER_VERSION VERSION_LESS 15.3)))\n"
" message(STATUS " in content
)
assert (
' set(ESPHOME_PCH OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)\nelse()\n'
in content
)
assert (
' set(ESPHOME_PCH ON CACHE BOOL "ESPHome precompiled header in use" FORCE)\n target_precompile_headers(${COMPONENT_LIB} PRIVATE\n'
in content
)
assert "endif()" in content
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1")
content = get_component_cmakelists()
assert "if(CMAKE_CXX_COMPILER_VERSION" not in content
assert "\ntarget_precompile_headers(${COMPONENT_LIB} PRIVATE\n" in content
@pytest.mark.parametrize(("choice", "written"), [("OFF", False), ("ON", True)])
def test_pch_checksum_follows_the_cmake_choice_on_windows(
windows_gcc_rule: None, tmp_path: Path, choice: str, written: bool
) -> None:
from esphome.build_gen.espidf import _PCH_SUM_PATH, write_pch_checksum
_make_pch_project(tmp_path)
with (
patch(
"esphome.espidf.toolchain.get_cmake_cache_value", return_value=choice
) as asked,
patch.object(CORE, "name", "test"),
):
write_pch_checksum()
assert asked.call_args.args == ("ESPHOME_PCH",)
assert CORE.relative_build_path(_PCH_SUM_PATH).exists() is written
@pytest.mark.parametrize(
("file", "content"),
[
@@ -2,8 +2,10 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import sys
from unittest.mock import patch
import pytest
@@ -35,6 +37,71 @@ def test_pch_enabled(value: str | None, expected: bool) -> None:
assert pch.pch_enabled() is expected
@pytest.mark.parametrize(
("version", "expected"),
[
((), False),
((10, 3), False),
((12, 2, 1), False),
((14, 2, 0), False),
((14, 3), False),
((14, 4), True),
((14,), False),
((15, 2, 0), False),
((15, 3), True),
((16, 0), True),
((17, 1), True),
],
)
def test_gcc_relocates_pch_on_windows(version: tuple[int, ...], expected: bool) -> None:
assert pch.gcc_relocates_pch_on_windows(version) is expected
@pytest.mark.parametrize(
("banner", "expected"),
[
("xtensa-esp32-elf-g++ (crosstool-NG esp-14.2.0_20260121) 14.2.0", (14, 2, 0)),
("arm-zephyr-eabi-g++ (Zephyr SDK 0.16.8) 12.2.0", (12, 2, 0)),
("g++.exe (Rev3, Built by MSYS2 project) 14.2.0", (14, 2, 0)),
("clang version 15.0.0", None),
("Apple clang version 17.0.0 (clang-1700.0.13.3)", None),
("something else", ()),
],
)
def test_gcc_version_reads_the_banner(banner: str, expected: object) -> None:
cxx = (sys.executable, "-c", f"print({banner!r}); print('more')")
assert pch.gcc_version(cxx) == expected
def test_gcc_version_of_a_compiler_that_cannot_run() -> None:
assert pch.gcc_version(("/nonexistent/g++",)) == ()
def test_pch_usable_asks_the_compiler_on_windows_only(
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
) -> None:
caplog.set_level(logging.INFO, logger=pch.__name__)
monkeypatch.delenv("ESPHOME_PCH_ENABLE")
monkeypatch.setattr(pch.sys, "platform", "darwin")
with patch.object(pch, "gcc_version", side_effect=AssertionError("off Windows")):
assert pch.pch_usable(("g++",))
monkeypatch.setattr(pch.sys, "platform", "win32")
with patch.object(pch, "gcc_version", return_value=(14, 2, 0)):
assert not pch.pch_usable(("g++",))
assert "GCC 14.2.0 cannot load a precompiled header on Windows" in caplog.text
with patch.object(pch, "gcc_version", return_value=(14, 4, 0)):
assert pch.pch_usable(("g++",))
# The bug is GCC's; another compiler is not held to its table
with patch.object(pch, "gcc_version", return_value=None):
assert pch.pch_usable(("clang++",))
# The knob overrides the rule both ways
with patch.object(pch, "gcc_version", side_effect=AssertionError("forced")):
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1")
assert pch.pch_usable(("g++",))
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
assert not pch.pch_usable(("g++",))
def test_ccache_pch_env_enabled() -> None:
with patch.dict(os.environ, {}, clear=True):
env = pch.ccache_pch_env()
+22 -9
View File
@@ -13,7 +13,7 @@ from esphome.core import CORE, EsphomeError
SUM = "CMakeFiles/app.dir/cmake_pch.hxx.gch.sum"
def _generate_cmake(tmp_path: Path) -> str:
def _generate_cmake(tmp_path: Path, pch_on: bool = True) -> str:
CORE.config_path = tmp_path / "test.yaml"
CORE.build_path = tmp_path / "build"
CORE.name = "livingroom"
@@ -25,7 +25,7 @@ def _generate_cmake(tmp_path: Path) -> str:
patch.object(nrf52, "get_project_compile_flags", return_value=["-Os"]),
patch.object(nrf52, "get_project_link_flags", return_value=[]),
):
nrf52._generate_cmake_lists()
nrf52._generate_cmake_lists(pch_on)
return (tmp_path / "build" / "zephyr" / "CMakeLists.txt").read_text()
@@ -43,15 +43,27 @@ def test_cmake_lists_precompile_the_core_headers(tmp_path: Path) -> None:
assert "if(NOT esphome_pch_headers)\n message(FATAL_ERROR" in text
def test_cmake_lists_pch_block_disabled(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
text = _generate_cmake(tmp_path)
def test_cmake_lists_pch_block_disabled(tmp_path: Path) -> None:
text = _generate_cmake(tmp_path, pch_on=False)
assert "precompile" not in text
assert "zephyr_interface" not in text
@pytest.mark.parametrize(("version", "on"), [((12, 2, 0), False), ((14, 4, 0), True)])
def test_the_zephyr_compiler_decides_on_windows(
windows_gcc_rule: None, version: tuple[int, ...], on: bool
) -> None:
from esphome.build_helpers import pch
# platformdirs would pick its Windows backend from the patched sys.platform
with (
patch.object(nrf52, "toolchain_tool", lambda name: Path(f"/sdk/{name}.exe")),
patch.object(pch, "gcc_version", return_value=version) as asked,
):
assert nrf52._pch_usable() is on
assert asked.call_args.args[0] == (Path("/sdk/g++.exe"),)
def _write_checksum(tmp_path: Path, app: str, conf: str = "CONFIG_X=y\n") -> Path:
"""Write the checksum for a build dir whose app image sits in ``app``."""
CORE.build_path = tmp_path
@@ -114,9 +126,10 @@ def run_cmd(tmp_path: Path) -> Mock:
def test_ccache_pch_settings_reach_west(run_cmd: Mock) -> None:
# clear=True also drops ambient CCACHE_* and ESPHOME_PCH_* overrides
# clear=True also drops ambient CCACHE_* overrides; the header is on
# explicitly since Windows hosts start with it off
with (
patch.dict("os.environ", {}, clear=True),
patch.dict("os.environ", {"ESPHOME_PCH_ENABLE": "1"}, clear=True),
pytest.raises(EsphomeError, match="nRF52 native build failed"),
):
nrf52.run_compile(None, {})
+12 -3
View File
@@ -178,6 +178,15 @@ def held_lock() -> Callable[..., Callable[..., None]]:
@pytest.fixture(autouse=True)
def _default_pch_knobs(monkeypatch: pytest.MonkeyPatch) -> None:
"""The precompiled header knobs change what the generators emit; a value
exported in the developer's or CI's shell must not reach the tests."""
monkeypatch.delenv("ESPHOME_PCH_ENABLE", raising=False)
"""The knob changes what the generators emit: no ambient value, and on,
so the Windows runner asks no compiler."""
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "1")
@pytest.fixture
def windows_gcc_rule(monkeypatch: pytest.MonkeyPatch) -> None:
"""Windows host, knob unset: the compiler version decides."""
from esphome.build_helpers import pch
monkeypatch.delenv("ESPHOME_PCH_ENABLE")
monkeypatch.setattr(pch.sys, "platform", "win32")
@@ -725,3 +725,11 @@ def test_get_core_framework_version_from_core_data():
CORE.data = {KEY_ESP32: {KEY_IDF_VERSION: cv.Version(5, 5, 4)}}
assert toolchain._get_core_framework_version() == "5.5.4"
def test_get_cmake_cache_value_reads_the_configured_cache(setup_core: Path) -> None:
_setup_build(setup_core)
output = "CMAKE_OBJDUMP:FILEPATH=/tools/objdump\nESPHOME_PCH:BOOL=OFF\n"
with patch.object(toolchain, "_get_cmake_output", return_value=output):
assert toolchain.get_cmake_cache_value("ESPHOME_PCH") == "OFF"
assert toolchain.get_cmake_cache_value("ESPHOME_MISSING") is None
+53 -2
View File
@@ -81,7 +81,9 @@ def _fake_cxx(tmp_path: Path, fail: bool = False) -> Path:
'case "$1" in -print-prog-name=*) n=${1#*=};'
' p="$(dirname "$0")/../libexec/gcc/arm-none-eabi/10.3.1/$n";'
' [ -x "$p" ] && echo "$p" || echo "$n"; exit 0;;'
' -dumpversion) echo "${FAKE_GCC_VERSION:-10.3.1}"; exit 0;; esac\n'
' -dumpversion) echo "${FAKE_GCC_VERSION:-10.3.1}"; exit 0;;'
' --version) echo "${FAKE_GCC_BANNER:-fake-g++ (test) ${FAKE_GCC_VERSION:-10.3.1}}";'
" exit 0;; esac\n"
'printf -- ---call---\\\\n >> "$0.argv"; printf \'%s\\n\' "$@" >> "$0.argv"\n'
)
if fail:
@@ -239,7 +241,8 @@ def test_pch_script_gcc10_skipped_elsewhere(
("linux", "aarch64", _LibreTinyPlatform, "10.3.1"),
# From GCC 12 the .gch loads at any address, so the rule retires itself
("darwin", "arm64", _LibreTinyPlatform, "12.2.0"),
("win32", "AMD64", _LibreTinyPlatform, "14.2.0"),
# Windows needs the fix for GCC bug 14940 as well
("win32", "AMD64", _LibreTinyPlatform, "14.4.0"),
],
)
def test_pch_script_no_wrapper_where_the_gch_loads(
@@ -272,6 +275,54 @@ def test_pch_script_names_match_the_python_side(tmp_path: Path) -> None:
assert namespace["_GUARD_TEXT"] == pch.PCH_GUARD_TEXT
assert namespace["_INCLUDE_RE"].pattern == pch._INCLUDE_RE.pattern
assert namespace["_CC1_DIR"] == pch.PCH_CC1_DIR
assert namespace["_WINDOWS_GCC_FIXED"] == pch.PCH_WINDOWS_GCC_FIXED
assert namespace["_WINDOWS_GCC_FIXED_DEFAULT"] == pch.PCH_WINDOWS_GCC_FIXED_DEFAULT
@pytest.mark.parametrize(
("version", "env_vars", "on"),
[
("14.2.0", {}, False),
("14.4.0", {}, True),
("14.2.0", {"ESPHOME_PCH_ENABLE": "1"}, True),
],
)
def test_pch_script_asks_the_compiler_on_windows(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
version: str,
env_vars: dict[str, str],
on: bool,
) -> None:
"""The Python rule again, plus the normalized knob."""
monkeypatch.setattr(sys, "platform", "win32")
scons_env = _run_script(
tmp_path, env_vars={"FAKE_GCC_VERSION": version, **env_vars}
)
assert (scons_env.prepended == pch.pch_consumer_flags()) is on
assert (tmp_path / "dev" / "esphome_pch.h.gch").is_file() is on
out = capsys.readouterr().out
assert ("cannot load a precompiled header on Windows" in out) is not on
def test_pch_script_spares_another_compiler_the_gcc_rule(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(sys, "platform", "win32")
scons_env = _run_script(
tmp_path, env_vars={"FAKE_GCC_BANNER": "clang version 15.0.0"}
)
assert scons_env.prepended == pch.pch_consumer_flags()
def test_pch_script_leaves_the_header_off_when_the_compiler_cannot_run(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
monkeypatch.setattr(sys, "platform", "win32")
scons_env = _run_script(tmp_path, cxx=tmp_path / "missing-g++")
assert scons_env.prepended == []
assert "GCC of unknown version cannot load" in capsys.readouterr().out
def test_pch_script_compile_failure_stops_the_build(tmp_path: Path) -> None:
@@ -354,6 +354,31 @@ def test_run_platformio_cli_exports_the_pch_ccache_settings(
assert env.get("CCACHE_PCH_EXTSUM") == expected
@pytest.mark.parametrize(
("value", "expected"), [("yes", "1"), ("0", "0"), (None, None)]
)
def test_run_platformio_cli_normalizes_a_forced_pch_for_the_script(
setup_core: Path,
mock_run_external_process: Mock,
value: str | None,
expected: str | None,
) -> None:
"""The script cannot import the knob parser, so it only reads a ``1``."""
CORE.build_path = str(setup_core / "build" / "test")
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: "esp32",
KEY_TARGET_FRAMEWORK: "esp-idf",
}
env_vars = {} if value is None else {"ESPHOME_PCH_ENABLE": value}
with patch.dict(os.environ, env_vars, clear=True):
mock_run_external_process.return_value = 0
toolchain.run_platformio_cli("test", "arg")
env = mock_run_external_process.call_args[1]["env"]
assert env.get("ESPHOME_PCH_ENABLE") == expected
@pytest.mark.parametrize(
("platform", "framework", "expected"),
[