mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 00:40:21 +00:00
[nrf52] Precompile the core headers on the sdk-nrf build (#19826)
This commit is contained in:
@@ -8,6 +8,7 @@ import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome import pins
|
||||
from esphome.build_helpers import pch
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import (
|
||||
add_extra_script,
|
||||
@@ -781,6 +782,67 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
||||
return False
|
||||
|
||||
|
||||
# GCC only loads a precompiled header ahead of every other forced header, and
|
||||
# Zephyr forces two with -imacros. They hold macros only, so the C++ sources
|
||||
# of the app get them through the precompiled header.
|
||||
_PCH_CMAKE_LINES = [
|
||||
"",
|
||||
"# ESPHome precompiled header",
|
||||
"get_property(esphome_options TARGET zephyr_interface",
|
||||
" PROPERTY INTERFACE_COMPILE_OPTIONS)",
|
||||
"set(esphome_kept_options)",
|
||||
"set(esphome_pch_headers)",
|
||||
"foreach(option IN LISTS esphome_options)",
|
||||
' if(option MATCHES "imacros> (.+)$")',
|
||||
' list(APPEND esphome_pch_headers "${CMAKE_MATCH_1}")',
|
||||
" list(APPEND esphome_kept_options",
|
||||
' "$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>")',
|
||||
" else()",
|
||||
' list(APPEND esphome_kept_options "${option}")',
|
||||
" endif()",
|
||||
"endforeach()",
|
||||
"if(NOT esphome_pch_headers)",
|
||||
' message(FATAL_ERROR "ESPHome: the headers Zephyr forces were not found, so "',
|
||||
' "the precompiled header would not load (set ESPHOME_PCH_ENABLE=0)")',
|
||||
"endif()",
|
||||
"set_property(TARGET zephyr_interface",
|
||||
' PROPERTY INTERFACE_COMPILE_OPTIONS "${esphome_kept_options}")',
|
||||
*(
|
||||
f'list(APPEND esphome_pch_headers "${{CMAKE_CURRENT_LIST_DIR}}/../src/{header}")'
|
||||
for header in pch.PCH_DEFAULT_HEADERS
|
||||
),
|
||||
'list(TRANSFORM esphome_pch_headers REPLACE "(.+)" "$<$<COMPILE_LANGUAGE:CXX>:\\\\1>")',
|
||||
"target_precompile_headers(app PRIVATE ${esphome_pch_headers})",
|
||||
]
|
||||
# Where CMake puts the .gch of the app, below its binary dir
|
||||
_PCH_SUM_PATH = "CMakeFiles/app.dir/cmake_pch.hxx.gch.sum"
|
||||
|
||||
|
||||
def _write_pch_checksum(build_dir: Path, source_dir: Path) -> None:
|
||||
"""Write the checksum ccache reads in place of the .gch. The app binary
|
||||
dir only exists after the first configure; sysbuild nests it."""
|
||||
app_dir = build_dir / "zephyr"
|
||||
if not (app_dir / "CMakeCache.txt").is_file():
|
||||
app_dir = build_dir
|
||||
if not (app_dir / "CMakeCache.txt").is_file():
|
||||
return
|
||||
checksum = pch.pch_checksum(
|
||||
CORE.relative_src_path(),
|
||||
pch.PCH_DEFAULT_HEADERS,
|
||||
(
|
||||
str(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]),
|
||||
zephyr_data()[KEY_BOARD],
|
||||
# What the Zephyr configuration is generated from
|
||||
*(
|
||||
path.read_text(encoding="utf-8")
|
||||
for path in sorted(source_dir.iterdir())
|
||||
if path.suffix in (".conf", ".overlay")
|
||||
),
|
||||
),
|
||||
)
|
||||
write_file_if_changed(app_dir / _PCH_SUM_PATH, checksum + "\n")
|
||||
|
||||
|
||||
def _generate_cmake_lists() -> bool:
|
||||
"""Write the project CMakeLists.txt, returning True if it changed."""
|
||||
compile_flags = get_project_compile_flags()
|
||||
@@ -824,6 +886,9 @@ def _generate_cmake_lists() -> bool:
|
||||
")",
|
||||
]
|
||||
|
||||
if pch.pch_enabled():
|
||||
lines += _PCH_CMAKE_LINES
|
||||
|
||||
if link_flags:
|
||||
lines += [
|
||||
"",
|
||||
@@ -897,6 +962,12 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
_LOGGER.info("Build inputs changed, cleaning %s", build_dir)
|
||||
rmtree(build_dir)
|
||||
|
||||
if pch.pch_enabled():
|
||||
pch.log_pch_in_use()
|
||||
# Zephyr turns ccache on by itself when it is installed
|
||||
env.update(pch.ccache_pch_env())
|
||||
_write_pch_checksum(build_dir, source_dir)
|
||||
|
||||
west_cmd = _west_build_command(
|
||||
paths["python_executable"], board, build_dir, source_dir
|
||||
)
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""nrf52 sdk-nrf precompiled header: the CMake block and the ccache checksum."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import nrf52
|
||||
from esphome.components.zephyr.const import KEY_BOARD
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
SUM = "CMakeFiles/app.dir/cmake_pch.hxx.gch.sum"
|
||||
|
||||
|
||||
def _generate_cmake(tmp_path: Path) -> str:
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
CORE.build_path = tmp_path / "build"
|
||||
CORE.name = "livingroom"
|
||||
with (
|
||||
patch(
|
||||
"esphome.components.zephyr.library.generate_zephyr_modules",
|
||||
return_value=[],
|
||||
),
|
||||
patch.object(nrf52, "get_project_compile_flags", return_value=["-Os"]),
|
||||
patch.object(nrf52, "get_project_link_flags", return_value=[]),
|
||||
):
|
||||
nrf52._generate_cmake_lists()
|
||||
return (tmp_path / "build" / "zephyr" / "CMakeLists.txt").read_text()
|
||||
|
||||
|
||||
def test_cmake_lists_precompile_the_core_headers(tmp_path: Path) -> None:
|
||||
text = _generate_cmake(tmp_path)
|
||||
assert "target_precompile_headers(app PRIVATE ${esphome_pch_headers})" in text
|
||||
assert "/../src/esphome/core/pch_prefix.h" in text
|
||||
# The headers Zephyr forces leave the C++ compiles of the app only
|
||||
assert (
|
||||
"$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,"
|
||||
"$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>"
|
||||
) in text
|
||||
assert r'REPLACE "(.+)" "$<$<COMPILE_LANGUAGE:CXX>:\\1>"' in text
|
||||
# A Zephyr that spells them another way must not go unnoticed
|
||||
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)
|
||||
assert "precompile" not in text
|
||||
assert "zephyr_interface" not in text
|
||||
|
||||
|
||||
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
|
||||
header = tmp_path / "src" / "esphome" / "core" / "pch_prefix.h"
|
||||
header.parent.mkdir(parents=True, exist_ok=True)
|
||||
header.write_text("#define M 1\n")
|
||||
source_dir = tmp_path / "zephyr"
|
||||
source_dir.mkdir(exist_ok=True)
|
||||
(source_dir / "prj.conf").write_text(conf)
|
||||
(source_dir / "CMakeLists.txt").write_text("not part of the checksum\n")
|
||||
build_dir = tmp_path / ".pioenvs" / "livingroom"
|
||||
(build_dir / app).mkdir(parents=True, exist_ok=True)
|
||||
(build_dir / app / "CMakeCache.txt").write_text("")
|
||||
with (
|
||||
patch.dict(CORE.data, {KEY_CORE: {KEY_FRAMEWORK_VERSION: "2.9.2"}}),
|
||||
patch.object(nrf52, "zephyr_data", return_value={KEY_BOARD: "board"}),
|
||||
):
|
||||
nrf52._write_pch_checksum(build_dir, source_dir)
|
||||
return build_dir / app / SUM
|
||||
|
||||
|
||||
@pytest.mark.parametrize("app", ["zephyr", "."])
|
||||
def test_pch_checksum_is_written_next_to_the_gch(tmp_path: Path, app: str) -> None:
|
||||
"""Sysbuild nests the app image; without it the build dir is the app."""
|
||||
sum_path = _write_checksum(tmp_path, app)
|
||||
assert len(sum_path.read_text().strip()) == 64
|
||||
|
||||
|
||||
def test_pch_checksum_tracks_the_zephyr_configuration(tmp_path: Path) -> None:
|
||||
first = _write_checksum(tmp_path, "zephyr").read_text()
|
||||
assert _write_checksum(tmp_path, "zephyr", "CONFIG_X=n\n").read_text() != first
|
||||
|
||||
|
||||
def test_pch_checksum_waits_for_the_first_configure(tmp_path: Path) -> None:
|
||||
CORE.build_path = tmp_path
|
||||
build_dir = tmp_path / ".pioenvs" / "livingroom"
|
||||
nrf52._write_pch_checksum(build_dir, tmp_path / "zephyr")
|
||||
assert not build_dir.exists()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def run_cmd(tmp_path: Path) -> Mock:
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
CORE.build_path = tmp_path / "build"
|
||||
CORE.name = "livingroom"
|
||||
CORE.toolchain = Toolchain.SDK_NRF
|
||||
with (
|
||||
patch.object(nrf52, "check_and_install"),
|
||||
patch.object(nrf52, "_generate_cmake_lists", return_value=False),
|
||||
patch.object(
|
||||
nrf52,
|
||||
"get_build_paths",
|
||||
return_value={"python_executable": "python3", "framework_path": tmp_path},
|
||||
),
|
||||
patch.object(nrf52, "get_build_env", return_value={}),
|
||||
patch.object(nrf52, "zephyr_data", return_value={KEY_BOARD: "board"}),
|
||||
patch.object(nrf52, "run_command_ok", return_value=False) as run,
|
||||
):
|
||||
yield run
|
||||
|
||||
|
||||
def test_ccache_pch_settings_reach_west(run_cmd: Mock) -> None:
|
||||
# clear=True also drops ambient CCACHE_* and ESPHOME_PCH_* overrides
|
||||
with (
|
||||
patch.dict("os.environ", {}, clear=True),
|
||||
pytest.raises(EsphomeError, match="nRF52 native build failed"),
|
||||
):
|
||||
nrf52.run_compile(None, {})
|
||||
env = run_cmd.call_args.kwargs["env"]
|
||||
assert env["CCACHE_PCH_EXTSUM"] == "true"
|
||||
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
|
||||
|
||||
|
||||
def test_disabled_leaves_the_west_environment_alone(
|
||||
run_cmd: Mock, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setenv("ESPHOME_PCH_ENABLE", "0")
|
||||
with pytest.raises(EsphomeError, match="nRF52 native build failed"):
|
||||
nrf52.run_compile(None, {})
|
||||
assert "CCACHE_PCH_EXTSUM" not in run_cmd.call_args.kwargs["env"]
|
||||
Reference in New Issue
Block a user