From 95ea7b954cad10bfe4f287813a81e2cd52237904 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 14:03:44 -0500 Subject: [PATCH] Probe forced-on ccache directly so the warnings cannot contradict, carry the traceback on unexpected prefetch failures --- esphome/espidf/framework.py | 34 ++++++++++++----------- esphome/platformio/registry.py | 2 +- tests/unit_tests/test_espidf_framework.py | 18 ++++++++++-- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 82445fbacf..239d874dbd 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -33,6 +33,7 @@ from esphome.framework_helpers import ( run_command, run_command_ok, str_to_lst_of_str, + tool_version_runs, ) from esphome.helpers import write_file_if_changed @@ -1210,22 +1211,23 @@ def _ccache_env() -> dict[str, str]: return {"IDF_CCACHE_ENABLE": "0"} if idf_knob is True: # Forced on ignores the runnability verdict, but the outcome is - # worth saying out loud. Only the truly-missing case means idf.py - # compiles without ccache; a present-but-rejected binary (probe - # failure or shared opt-out) is still used, since idf.py does its - # own PATH lookup. - if resolve_ccache_path() is None: - if shutil.which("ccache") is None: - _LOGGER.warning( - "IDF_CCACHE_ENABLE=1 but no ccache binary is on PATH; " - "idf.py will compile without ccache" - ) - else: - _LOGGER.warning( - "IDF_CCACHE_ENABLE=1 forces ccache on even though it " - "was rejected here (probe failure or " - "ESPHOME_CCACHE_ENABLE=0); idf.py will use it anyway" - ) + # worth saying out loud. Probed directly (not via the resolver, + # whose failure message says "compiling without ccache" -- exactly + # what forced-on does NOT do): only the truly-missing case means + # idf.py compiles without ccache; a broken binary is still used, + # since idf.py does its own PATH lookup. + if (ccache := shutil.which("ccache")) is None: + _LOGGER.warning( + "IDF_CCACHE_ENABLE=1 but no ccache binary is on PATH; " + "idf.py will compile without ccache" + ) + else: + # The probe warns with this message iff the binary fails + tool_version_runs( + ccache, + "IDF_CCACHE_ENABLE=1 forces on the ccache at %s even though " + "it failed to run; idf.py will use it anyway", + ) elif resolve_ccache_path() is None: # ESP-IDF silently skips ccache without the binary; export the # canonical off spelling so an unparsable inherited value (or a diff --git a/esphome/platformio/registry.py b/esphome/platformio/registry.py index 6393a0d582..9538a28ff4 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -249,7 +249,7 @@ def prefetch_packages( else: # Anything else is a programming error that would otherwise # become a permanent silent no-op - _LOGGER.warning("Prefetch of %s failed: %r", name, err) + _LOGGER.warning("Prefetch of %s failed: %r", name, err, exc_info=err) def install_package( diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index 2dc102fd9e..8ef231aa3a 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -1623,7 +1623,14 @@ def test_ccache_env_opt_in_with_working_binary( ccache = tmp_path / "ccache" ccache.touch() p1, p2, p3 = _ccache_patches(tmp_path, str(ccache), tmp_path / "build") - with patch.dict("os.environ", {"IDF_CCACHE_ENABLE": "1"}, clear=True), p1, p2, p3: + with ( + patch.dict("os.environ", {"IDF_CCACHE_ENABLE": "1"}, clear=True), + patch("esphome.espidf.framework.shutil.which", return_value=str(ccache)), + patch("esphome.espidf.framework.tool_version_runs", return_value=True), + p1, + p2, + p3, + ): env = _ccache_env() assert env["IDF_CCACHE_ENABLE"] == "1" assert "ccache" not in caplog.text.lower() or "Not decoded" in caplog.text @@ -1635,10 +1642,14 @@ def test_ccache_env_opt_in_with_rejected_binary( # Forced on with a present-but-rejected binary: idf.py does its own # PATH lookup and uses it anyway; the warning must say so, not claim # the build runs without ccache. + # A present but non-executable file: the real probe fails and logs + # the forced-on message (patching the probe would silence it) + broken = tmp_path / "broken-ccache" + broken.touch() p1, p2, p3 = _ccache_patches(tmp_path, None, tmp_path / "build") with ( patch.dict("os.environ", {"IDF_CCACHE_ENABLE": "1"}, clear=True), - patch("esphome.espidf.framework.shutil.which", return_value="/usr/bin/ccache"), + patch("esphome.espidf.framework.shutil.which", return_value=str(broken)), p1, p2, p3, @@ -1646,6 +1657,9 @@ def test_ccache_env_opt_in_with_rejected_binary( env = _ccache_env() assert env["IDF_CCACHE_ENABLE"] == "1" assert "idf.py will use it anyway" in caplog.text + # Exactly one story: the resolver's contradictory "compiling without + # ccache" must not precede it + assert "compiling without ccache" not in caplog.text def test_ccache_env_honors_shared_esphome_opt_out(tmp_path: Path) -> None: