diff --git a/esphome/build_helpers/tools_cache.py b/esphome/build_helpers/tools_cache.py index f23c859b34..e7193a8e2a 100644 --- a/esphome/build_helpers/tools_cache.py +++ b/esphome/build_helpers/tools_cache.py @@ -16,7 +16,11 @@ def tools_cache_path(env_var: str, subdir: str) -> Path: from esphome.helpers import get_str_env if prefix := get_str_env(env_var, "").strip(): + # resolve(): symlinked prefixes otherwise trip idf.py's + # venv-mismatch warning on every build return Path(prefix).expanduser().resolve() + # appauthor=False keeps the Windows path short (no vendor segment); + # deep IDF trees run into MAX_PATH otherwise return ( Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / subdir ).resolve() diff --git a/esphome/helpers.py b/esphome/helpers.py index fe3c383f59..b926979e04 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -31,8 +31,9 @@ SockAddr = IPv4SockAddr | IPv6SockAddr _LOGGER = logging.getLogger(__name__) -# cv.boolean's closed spelling tables; shared so env-knob parsers cannot -# drift from what configs accept +# cv.boolean's closed spelling tables, shared with the strict env-knob +# parser (build_helpers.ccache.parse_enable_env). The legacy get_bool_env +# below keeps its own laxer table for backward compatibility. TRUTHY_BOOL_STRINGS = frozenset({"true", "yes", "on", "enable"}) FALSY_BOOL_STRINGS = frozenset({"false", "no", "off", "disable"}) diff --git a/esphome/platformio/registry.py b/esphome/platformio/registry.py index d7c9a92a2c..c4b79f5c8e 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -211,8 +211,14 @@ def prefetch_packages( [(entry[0], _fetch(entry)) for entry in pending], ) for name, err in failures: - # install_package retries this one itself, with a visible bar - _LOGGER.debug("Prefetch of %s failed: %s", name, err) + if isinstance(err, (EsphomeError, OSError)): + # Expected download failures: install_package retries this one + # itself, with a visible bar + _LOGGER.debug("Prefetch of %s failed: %s", name, err) + 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) def install_package( diff --git a/tests/unit_tests/test_platformio_registry.py b/tests/unit_tests/test_platformio_registry.py index 3f2b739ca9..46409ba62e 100644 --- a/tests/unit_tests/test_platformio_registry.py +++ b/tests/unit_tests/test_platformio_registry.py @@ -625,3 +625,26 @@ def test_prefetch_packages_download_failure_is_debug( assert mock_download.call_count == 2 assert "Prefetch of a failed" in caplog.text assert "Prefetch of b failed" in caplog.text + + +def test_prefetch_packages_unexpected_failure_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A programming error (not a download failure) surfaces at WARNING + instead of becoming a permanent silent no-op.""" + with ( + patch.object( + registry, "download_with_resume", side_effect=TypeError("bad call") + ), + patch.object( + registry, "registry_download", side_effect=_resolve_for({"a": 10, "b": 20}) + ), + ): + registry.prefetch_packages( + [ + ("a", "1.0", tmp_path / "a", []), + ("b", "2.0", tmp_path / "b", []), + ], + tmp_path / "dl", + ) + assert "TypeError" in caplog.text