diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 4b7653616a..82445fbacf 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -1209,14 +1209,23 @@ def _ccache_env() -> dict[str, str]: # export the canonical off spelling instead return {"IDF_CCACHE_ENABLE": "0"} if idf_knob is True: - # Forced on ignores the runnability verdict, but a missing or - # unusable binary is worth saying out loud: idf.py silently - # compiles without ccache in that case + # 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: - _LOGGER.warning( - "IDF_CCACHE_ENABLE=1 but no usable ccache binary was " - "found; idf.py will compile without ccache" - ) + 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" + ) 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 a04caa8487..4a6fb077c9 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -218,18 +218,17 @@ def prefetch_packages( def _fetch(entry: _PendingArchive, tracker: Callable[[int], None]) -> None: entry.dest.parent.mkdir(parents=True, exist_ok=True) with FileLock(f"{entry.dest}.lock", fallback_to_soft=False): - if (entry.dest / ".esphome_extracted").is_file(): - # A concurrent build installed (and deleted the archive of) - # this package while we waited; re-downloading would orphan - # a fresh copy in downloads_dir - return - download_with_resume( - entry.url, - downloads_dir / f"{entry.name}-{entry.version}", - sha256=entry.sha256, - size=entry.size, - progress=tracker, - ) + # Marker re-check: a concurrent build may have installed (and + # deleted the archive of) this package while we waited; + # re-downloading would orphan a fresh copy in downloads_dir + if not (entry.dest / ".esphome_extracted").is_file(): + download_with_resume( + entry.url, + downloads_dir / f"{entry.name}-{entry.version}", + sha256=entry.sha256, + size=entry.size, + progress=tracker, + ) failures = run_batch_downloads( "Downloading packages", diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index ecf54349be..fe9a357c28 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -1613,7 +1613,26 @@ def test_ccache_env_opt_in_without_binary( assert env["IDF_CCACHE_ENABLE"] == "1" assert env["CCACHE_DIR"] == str(tmp_path / "tools" / "ccache") assert env["CCACHE_DEPEND"] == "1" - assert "no usable ccache binary" in caplog.text + assert "no ccache binary is on PATH" in caplog.text + + +def test_ccache_env_opt_in_with_rejected_binary( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + # 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. + 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"), + p1, + p2, + p3, + ): + env = _ccache_env() + assert env["IDF_CCACHE_ENABLE"] == "1" + assert "idf.py will use it anyway" in caplog.text def test_ccache_env_honors_shared_esphome_opt_out(tmp_path: Path) -> None: diff --git a/tests/unit_tests/test_platformio_registry.py b/tests/unit_tests/test_platformio_registry.py index 56bfe4fe74..804c36dddf 100644 --- a/tests/unit_tests/test_platformio_registry.py +++ b/tests/unit_tests/test_platformio_registry.py @@ -539,8 +539,17 @@ def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None: already installed; re-downloading would orphan an archive copy.""" dest = tmp_path / "a" dest.mkdir() - (dest / ".esphome_extracted").touch() + + from contextlib import contextmanager + + @contextmanager + def marker_appears_under_lock(path, **kwargs): + # Simulates the concurrent build finishing while we waited + (dest / ".esphome_extracted").touch() + yield + with ( + patch("filelock.FileLock", side_effect=marker_appears_under_lock), patch.object(registry, "download_with_resume") as mock_download, patch.object( registry, "registry_download", side_effect=_resolve_for({"a": 10})