mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 08:50:22 +00:00
[core] Leave the precompiled header off on Windows for a GCC with bug 14940 (#19875)
This commit is contained in:
@@ -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