[nrf52] Use the shared managed ccache settings for the native build (#20027)

This commit is contained in:
J. Nick Koston
2026-10-02 13:28:40 -05:00
committed by GitHub
parent 78f39c7fee
commit 6d33b0401d
5 changed files with 105 additions and 28 deletions
+7 -8
View File
@@ -9,6 +9,7 @@ import subprocess
from esphome import pins
from esphome.build_helpers import pch
from esphome.build_helpers.ccache import resolve_ccache_path
import esphome.codegen as cg
from esphome.components.zephyr import (
add_extra_script,
@@ -584,7 +585,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
raise EsphomeError("Not implemented yet")
check_and_install()
paths = get_build_paths()
env = get_build_env()
env = get_build_env(None) # no compile, just nrfutil
build_dir = CORE.relative_pioenvs_path(CORE.name)
dfu_package = build_dir / "firmware.zip"
if not dfu_package.is_file():
@@ -666,7 +667,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
if not CORE.using_toolchain_platformio:
check_and_install()
paths = get_build_paths()
env = get_build_env()
env = get_build_env(resolve_ccache_path()) # west flash may rebuild
build_dir = CORE.relative_pioenvs_path(CORE.name)
west_cmd = [
str(paths["python_executable"]),
@@ -949,7 +950,10 @@ def run_compile(args, config: ConfigType) -> bool:
check_and_install()
paths = get_build_paths()
env = get_build_env()
# Depend mode in the shared ccache settings keeps the .gch sound
# across Kconfig flips.
ccache = resolve_ccache_path()
env = get_build_env(ccache)
pch_on = _pch_usable()
cmake_lists_changed = _generate_cmake_lists(pch_on)
@@ -976,11 +980,6 @@ def run_compile(args, config: ConfigType) -> bool:
if pch_on:
pch.log_pch_in_use()
# Zephyr turns ccache on by itself when it is installed
env.update(pch.ccache_pch_env())
# Depend mode, or a Kconfig flip reuses a stale .gch: autoconf.h is
# all #defines, which vanish from the preprocessed creation hash.
env.setdefault("CCACHE_DEPEND", "1")
_write_pch_checksum(build_dir, source_dir)
west_cmd = _west_build_command(
+1 -1
View File
@@ -277,7 +277,7 @@ def generate_compile_commands(work_dir: Path, platformio_ini: Path) -> Path:
]
if not run_command_ok(
west_cmd,
env=get_build_env(),
env=get_build_env(None), # configure only, nothing compiles
stream_output=True,
cwd=str(paths["framework_path"]),
):
+15 -1
View File
@@ -10,6 +10,7 @@ import platform
import shutil
import sys
from esphome.build_helpers.ccache import ccache_env
from esphome.build_helpers.tools_cache import SDK_NRF_TOOLS_CACHE, tools_cache_path
from esphome.components.zephyr.const import KEY_SYSBUILD, KEY_ZEPHYR
import esphome.config_validation as cv
@@ -194,7 +195,14 @@ def get_build_paths() -> dict:
}
def get_build_env() -> dict:
def get_build_env(ccache: str | None) -> dict:
"""Build the west/sdk-nrf process environment.
``ccache`` is the resolved binary (resolve_ccache_path), or None when
ccache is disabled or the caller never compiles; it brings the shared
managed-ccache settings and the pch sloppiness, so every caller that
may compile gets the same cache.
"""
version = _get_version_str()
venv_bin_dir = get_python_env_executable_path(
_get_python_env_path(version), "python"
@@ -211,6 +219,12 @@ def get_build_env() -> dict:
# non-root builds failed to locate the SDK with it, while
# ZEPHYR_SDK_INSTALL_DIR fixed the same invocation.
env["ZEPHYR_SDK_INSTALL_DIR"] = str(_get_toolchain_path(_get_toolchain_version()))
if ccache is None:
# Zephyr wraps compiles with any ccache it finds; unmanaged it
# must not cache (a sysbuild image never sees USE_CCACHE=0).
env.setdefault("CCACHE_DISABLE", "1")
else:
env.update(ccache_env(ccache, SDK_NRF_TOOLS_CACHE))
return env
+55 -16
View File
@@ -6,6 +6,7 @@ from unittest.mock import Mock, patch
import pytest
from esphome.components import nrf52
from esphome.components.nrf52 import framework
from esphome.components.zephyr.const import KEY_BOARD
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain
@@ -105,6 +106,23 @@ def test_pch_checksum_waits_for_the_first_configure(tmp_path: Path) -> None:
assert not build_dir.exists()
def _fake_build_env(ccache: str | None) -> dict[str, str]:
"""The real get_build_env with only the install path lookups stubbed."""
with (
patch.object(framework, "_get_version_str", return_value="v1"),
patch.object(framework, "_get_python_env_path", return_value=Path("/penv")),
patch.object(
framework,
"get_python_env_executable_path",
return_value=Path("/penv/bin/python"),
),
patch.object(framework, "_get_framework_path", return_value=Path("/fw")),
patch.object(framework, "_get_toolchain_version", return_value="t1"),
patch.object(framework, "_get_toolchain_path", return_value=Path("/tc")),
):
return framework.get_build_env(ccache)
@pytest.fixture
def run_cmd(tmp_path: Path) -> Mock:
CORE.config_path = tmp_path / "test.yaml"
@@ -113,6 +131,7 @@ def run_cmd(tmp_path: Path) -> Mock:
CORE.toolchain = Toolchain.SDK_NRF
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 2, 0)}
with (
patch.dict("os.environ", {}, clear=True),
patch.object(nrf52, "check_and_install"),
patch.object(nrf52, "_generate_cmake_lists", return_value=False),
patch.object(
@@ -120,18 +139,18 @@ def run_cmd(tmp_path: Path) -> Mock:
"get_build_paths",
return_value={"python_executable": "python3", "framework_path": tmp_path},
),
patch.object(nrf52, "get_build_env", return_value={}),
patch.object(nrf52, "get_build_env", side_effect=_fake_build_env),
patch.object(nrf52, "resolve_ccache_path", return_value="/usr/bin/ccache"),
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_* overrides; the header is on
# explicitly since Windows hosts start with it off
def test_shared_ccache_settings_reach_west(run_cmd: Mock, tmp_path: Path) -> None:
# The header is on explicitly since Windows hosts start with it off
with (
patch.dict("os.environ", {"ESPHOME_PCH_ENABLE": "1"}, clear=True),
patch.dict("os.environ", {"ESPHOME_PCH_ENABLE": "1"}),
pytest.raises(EsphomeError, match="nRF52 native build failed"),
):
nrf52.run_compile(None, {})
@@ -140,23 +159,43 @@ def test_ccache_pch_settings_reach_west(run_cmd: Mock) -> None:
assert env["CCACHE_SLOPPINESS"] == "pch_defines,time_macros"
# Without depend mode a Kconfig flip reuses a stale .gch
assert env["CCACHE_DEPEND"] == "1"
# The full managed set, not a bespoke subset
assert env["CCACHE_DIR"].endswith("ccache")
assert env["CCACHE_NOHASHDIR"] == "true"
assert env["CCACHE_BASEDIR"] == str((tmp_path / "build").resolve())
assert "CCACHE_DISABLE" not in env
def test_ccache_depend_respects_a_user_override(run_cmd: Mock) -> None:
def test_user_exported_ccache_values_win(run_cmd: Mock) -> None:
user = {"ESPHOME_PCH_ENABLE": "1", "CCACHE_DEPEND": "0", "CCACHE_DIR": "/mine"}
with (
patch.object(nrf52, "get_build_env", return_value={"CCACHE_DEPEND": "0"}),
patch.dict("os.environ", {"ESPHOME_PCH_ENABLE": "1"}, clear=True),
patch.dict("os.environ", user),
pytest.raises(EsphomeError, match="nRF52 native build failed"),
):
nrf52.run_compile(None, {})
assert run_cmd.call_args.kwargs["env"]["CCACHE_DEPEND"] == "0"
env = run_cmd.call_args.kwargs["env"]
assert env["CCACHE_DEPEND"] == "0"
assert env["CCACHE_DIR"] == "/mine"
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"):
def test_no_ccache_disables_the_zephyr_launcher(run_cmd: Mock) -> None:
"""ESPHOME_CCACHE_ENABLE=0 must also stop Zephyr's self-enabled ccache."""
with (
patch.object(nrf52, "resolve_ccache_path", return_value=None),
pytest.raises(EsphomeError, match="nRF52 native build failed"),
):
nrf52.run_compile(None, {})
assert "CCACHE_PCH_EXTSUM" not in run_cmd.call_args.kwargs["env"]
assert "CCACHE_DEPEND" not in run_cmd.call_args.kwargs["env"]
env = run_cmd.call_args.kwargs["env"]
assert env["CCACHE_DISABLE"] == "1"
assert "CCACHE_DEPEND" not in env
def test_disabled_pch_still_gets_the_shared_settings(run_cmd: Mock) -> None:
with (
patch.dict("os.environ", {"ESPHOME_PCH_ENABLE": "0"}),
pytest.raises(EsphomeError, match="nRF52 native build failed"),
):
nrf52.run_compile(None, {})
env = run_cmd.call_args.kwargs["env"]
assert "CCACHE_PCH_EXTSUM" not in env
assert env["CCACHE_DEPEND"] == "1"
+27 -2
View File
@@ -1078,8 +1078,9 @@ def test_get_build_env(
containerized non-root builds and was removed.
"""
monkeypatch.setenv("SOME_PREEXISTING_VAR", "kept")
monkeypatch.delenv("CCACHE_DISABLE", raising=False)
env = get_build_env()
env = get_build_env(None)
tools = get_sdk_nrf_tools_path()
venv_bin_dir = get_python_env_executable_path(
@@ -1096,6 +1097,30 @@ def test_get_build_env(
assert "Zephyr-sdk_DIR" not in env
# The rest of the process environment is inherited
assert env["SOME_PREEXISTING_VAR"] == "kept"
# No managed settings without a resolved binary; the self-enabled
# Zephyr ccache must not cache
assert "CCACHE_DIR" not in env or "CCACHE_DIR" in os.environ
assert env["CCACHE_DISABLE"] == "1"
def test_get_build_env_with_ccache(
nrf52_dirs: SimpleNamespace, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""A resolved ccache brings the shared managed settings."""
for key in (
"CCACHE_DIR",
"CCACHE_DEPEND",
"CCACHE_NOHASHDIR",
"CCACHE_BASEDIR",
"CCACHE_DISABLE",
):
monkeypatch.delenv(key, raising=False)
CORE.build_path = tmp_path / "build"
env = get_build_env("/usr/bin/ccache")
assert env["CCACHE_DIR"] == str(get_sdk_nrf_tools_path() / "ccache")
assert env["CCACHE_DEPEND"] == "1"
assert env["CCACHE_BASEDIR"] == str((tmp_path / "build").resolve())
assert "CCACHE_DISABLE" not in env
def test_get_build_env_sdk_3_4_0_uses_toolchain_root(
@@ -1104,7 +1129,7 @@ def test_get_build_env_sdk_3_4_0_uses_toolchain_root(
"""For NCS >= 3.4.0, ZEPHYR_SDK_INSTALL_DIR still points at the toolchain root."""
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse("3.4.0")}
env = get_build_env()
env = get_build_env(None)
tools = get_sdk_nrf_tools_path()
assert env["ZEPHYR_SDK_INSTALL_DIR"] == str(tools / "toolchains" / "1.0.1")