mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 00:40:21 +00:00
[core] Leave the precompiled header off on Windows for a GCC with bug 14940 (#19875)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user