[nrf52] Share the ccache between devices (#20028)

This commit is contained in:
J. Nick Koston
2026-10-05 10:53:35 -05:00
committed by GitHub
parent bc388b1de5
commit 417fdd9dc7
4 changed files with 183 additions and 38 deletions
+58 -17
View File
@@ -5,9 +5,11 @@ from unittest.mock import Mock, patch
import pytest
from esphome.build_helpers import pch
from esphome.components import nrf52
from esphome.components.nrf52 import framework
from esphome.components.zephyr.const import KEY_BOARD
from esphome.components.nrf52.toolchain import get_elf_path
from esphome.components.zephyr.const import KEY_BOARD, KEY_SYSBUILD
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
from esphome.core import CORE, EsphomeError
@@ -55,8 +57,6 @@ def test_cmake_lists_pch_block_disabled(tmp_path: Path) -> None:
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")),
@@ -66,8 +66,16 @@ def test_the_zephyr_compiler_decides_on_windows(
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:
def _write_checksum(
tmp_path: Path,
app: str,
conf: str = "CONFIG_X=y\n",
configured: bool = True,
sysbuild: bool = False,
version: cv.Version | None = None,
) -> Path:
"""Write the checksum for a build dir whose app image sits in ``app``."""
version = version or cv.Version(2, 9, 2)
CORE.build_path = tmp_path
header = tmp_path / "src" / "esphome" / "core" / "pch_prefix.h"
header.parent.mkdir(parents=True, exist_ok=True)
@@ -77,33 +85,65 @@ def _write_checksum(tmp_path: Path, app: str, conf: str = "CONFIG_X=y\n") -> Pat
(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("")
if configured:
(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"}),
patch.dict(CORE.data, {KEY_CORE: {KEY_FRAMEWORK_VERSION: version}}),
patch.object(
nrf52,
"zephyr_data",
return_value={KEY_BOARD: "board", KEY_SYSBUILD: sysbuild},
),
):
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)
@pytest.mark.parametrize(
("app", "version"), [("zephyr", cv.Version(2, 9, 2)), (".", cv.Version(2, 9, 1))]
)
def test_pch_checksum_is_written_next_to_the_gch(
tmp_path: Path, app: str, version: cv.Version
) -> None:
"""The SDK version decides the layout, like get_elf_path."""
sum_path = _write_checksum(tmp_path, app, version=version)
assert len(sum_path.read_text().strip()) == 64
def test_pch_checksum_tracks_the_kconfig_side_inputs(tmp_path: Path) -> None:
"""West projects and the sysbuild flag reach autoconf without a .conf
line; the sum must move with them or stale objects get served."""
first = _write_checksum(tmp_path, "zephyr").read_text()
with patch.object(nrf52, "wanted_west_projects", return_value={"extra"}):
second = _write_checksum(tmp_path, "zephyr").read_text()
assert first != second
third = _write_checksum(tmp_path, "zephyr", sysbuild=True).read_text()
assert first != third
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.mark.parametrize("version", [cv.Version(2, 9, 2), cv.Version(2, 9, 1)])
def test_pch_checksum_lands_where_the_build_writes_the_image(
tmp_path: Path, version: cv.Version
) -> None:
"""One layout rule: the sum must sit in get_elf_path's app dir, or a
layout drift silently costs the first build's sharing."""
app = "zephyr" if version >= cv.Version(2, 9, 2) else "."
sum_path = _write_checksum(tmp_path, app, version=version)
CORE.name = "livingroom"
with patch.dict(CORE.data, {KEY_CORE: {KEY_FRAMEWORK_VERSION: version}}):
expected = get_elf_path().parent.parent / SUM
assert sum_path.resolve() == expected.resolve()
def test_pch_checksum_written_before_the_first_configure(tmp_path: Path) -> None:
"""The first build's compiles hash the sum in place of the .gch."""
assert _write_checksum(tmp_path, "zephyr", configured=False).is_file()
def _fake_build_env(ccache: str | None) -> dict[str, str]:
@@ -130,6 +170,7 @@ def run_cmd(tmp_path: Path) -> Mock:
CORE.name = "livingroom"
CORE.toolchain = Toolchain.SDK_NRF
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 2, 0)}
(tmp_path / "build" / "zephyr").mkdir(parents=True)
with (
patch.dict("os.environ", {}, clear=True),
patch.object(nrf52, "check_and_install"),
+46 -3
View File
@@ -11,7 +11,7 @@ from unittest.mock import ANY, call, patch
import platformdirs
import pytest
from esphome.components.nrf52 import _resolve_toolchain
from esphome.components.nrf52 import _resolve_toolchain, framework
from esphome.components.nrf52.framework import (
_PLATFORMIO_PENV_REQUIREMENTS,
_REQUIREMENTS,
@@ -22,12 +22,12 @@ from esphome.components.nrf52.framework import (
_get_toolchain_platform_info,
_install_toolchain,
_needs_venv_rebuild,
_wanted_west_projects,
check_and_install,
get_build_env,
get_sdk_nrf_tools_path,
include_west_project,
setup_platformio_python_env,
wanted_west_projects,
)
from esphome.components.zephyr.const import KEY_SYSBUILD, KEY_ZEPHYR
import esphome.config_validation as cv
@@ -539,7 +539,7 @@ class TestCheckAndInstall:
"""Zephyr 4.1 moved the Cortex-M core headers to the cmsis_6 module."""
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse(sdk_version)}
assert ("cmsis_6" in _wanted_west_projects()) is has_cmsis_6
assert ("cmsis_6" in wanted_west_projects()) is has_cmsis_6
def test_default_projects_never_read_the_stamp(
self,
@@ -1121,6 +1121,22 @@ def test_get_build_env_with_ccache(
assert env["CCACHE_DEPEND"] == "1"
assert env["CCACHE_BASEDIR"] == str((tmp_path / "build").resolve())
assert "CCACHE_DISABLE" not in env
# Only the per build map entry leaves the hash; user maps stay in
assert env["CCACHE_IGNOREOPTIONS"] == (
f"-fmacro-prefix-map={(tmp_path / 'build' / 'zephyr').as_posix()}"
"=CMAKE_SOURCE_DIR"
)
def test_get_build_env_skips_the_map_entry_on_whitespace(
nrf52_dirs: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""The ignore list splits on spaces; a spaced path cannot be
expressed, so the entry is left hashed rather than emitted broken."""
monkeypatch.delenv("CCACHE_IGNOREOPTIONS", raising=False)
CORE.build_path = tmp_path / "with space" / "build"
env = get_build_env("/usr/bin/ccache")
assert "CCACHE_IGNOREOPTIONS" not in env
def test_get_build_env_sdk_3_4_0_uses_toolchain_root(
@@ -1250,3 +1266,30 @@ def test_resolve_toolchain_rejects_unsupported() -> None:
CORE.toolchain = Toolchain.ARDUINO
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
_resolve_toolchain({})
def test_patch_gen_defines_relativizes_the_dts_path(tmp_path: Path) -> None:
"""The absolute dts.pre path is the only per device byte in the
devicetree header; the patch makes gen_defines emit the basename."""
gen = tmp_path / "zephyr" / "scripts" / "dts" / "gen_defines.py"
gen.parent.mkdir(parents=True)
gen.write_text("s = f'DTS input file:\\n {edt.dts_path}\\n'\n")
framework._patch_gen_defines_dts_path(tmp_path)
assert "{os.path.basename(edt.dts_path)}" in gen.read_text()
assert not list(gen.parent.glob("*.tmp")) # no leftovers
before = gen.read_text()
framework._patch_gen_defines_dts_path(tmp_path) # idempotent
assert gen.read_text() == before
framework._patch_gen_defines_dts_path(tmp_path / "absent") # tolerant
def test_patch_gen_defines_warns_when_the_anchor_is_gone(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A reformatted upstream must not silently cost the sharing."""
gen = tmp_path / "zephyr" / "scripts" / "dts" / "gen_defines.py"
gen.parent.mkdir(parents=True)
gen.write_text("s = 'something else entirely'\n")
with caplog.at_level("WARNING"):
framework._patch_gen_defines_dts_path(tmp_path)
assert "gen_defines.py no longer matches" in caplog.text