Resolve registry metadata once, canonicalize the ccache knob, disable on an empty value

registry_download memoizes per process (failures uncached), so the
prefetch and the install share one metadata round-trip per package.
The idf ccache env always hands idf.py exactly one canonical spelling:
the no-ccache path exports 0 so an unparsable inherited value or a
probe-rejected shim cannot enable it, the enabled path exports 1
whatever accepted spelling arrived, and forced-on still runs the
resolver for its no-binary warning. An empty env value keeps its
historical disable meaning instead of falling to the default policy.
This commit is contained in:
J. Nick Koston
2026-08-23 14:17:29 -05:00
parent 87a01b7ed9
commit 55323596a9
6 changed files with 67 additions and 13 deletions
+3
View File
@@ -36,6 +36,9 @@ def parse_enable_env(name: str) -> bool | None:
if raw is None:
return None
lowered = raw.strip().lower()
if not lowered:
# ENV KNOB= (Docker/CI) has always read as a disable
return False
if lowered in TRUTHY_ENV_STRINGS:
return True
if lowered in FALSY_ENV_STRINGS:
+12 -6
View File
@@ -1206,14 +1206,20 @@ def _ccache_env() -> dict[str, str]:
# os.environ, where a non-false-constant string reads as truthy;
# export the canonical off spelling instead
return {"IDF_CCACHE_ENABLE": "0"}
if idf_knob is None and resolve_ccache_path() is None:
# ESP-IDF silently skips ccache without the binary; don't enable it.
return {}
if idf_knob is True:
# Forced on skips the runnability verdict, but still resolve for
# the "no ccache binary on PATH" warning
resolve_ccache_path()
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
# probe-rejected ccache idf.py would still find) cannot enable it
return {"IDF_CCACHE_ENABLE": "0"}
env = ccache_defaults_env(get_idf_tools_path() / "ccache")
if idf_knob is None:
# An unparsable IDF_CCACHE_ENABLE must not leak to idf.py as truthy
env["IDF_CCACHE_ENABLE"] = "1"
# Exactly one canonical spelling ever reaches idf.py, whatever the
# accepted input spelling was ("enable", "yes", ...)
env["IDF_CCACHE_ENABLE"] = "1"
return env
+4 -1
View File
@@ -4,7 +4,7 @@ platformio package (identical bits, esphome's own download machinery)."""
from __future__ import annotations
from collections.abc import Callable, Collection
from functools import partial
from functools import cache, partial
import io
import json
import logging
@@ -53,11 +53,14 @@ def get_systype() -> str:
return f"{system}_{arch}" if arch else system
@cache
def registry_download(package: str, version: str) -> tuple[str, str, int | None]:
"""Resolve a package's download URL, sha256, and size via the registry.
The metadata fetch goes through ``download_from_mirrors`` so it shares
the retry, backoff, and error reporting of every other download here.
Cached per process so the prefetch and the install resolve each package
once (failures are not cached; the install retries them).
"""
buf = io.BytesIO()
download_from_mirrors([_REGISTRY_URL], {"package": package}, buf)
@@ -109,6 +109,9 @@ def test_resolve_unrecognized_value_warns_and_probes(
("disable", False),
("Off", False),
("maybe", None),
# ENV KNOB= (Docker/CI) has always read as a disable
("", False),
(" ", False),
],
)
def test_parse_enable_env_spelling_tables(
+7 -6
View File
@@ -1564,7 +1564,8 @@ def test_ccache_env_disabled_when_binary_missing(tmp_path: Path) -> None:
# build_path is None here too: a disabled cache must not require it.
p1, p2, p3 = _ccache_patches(tmp_path, None, None)
with patch.dict("os.environ", {}, clear=True), p1, p2, p3:
assert _ccache_env() == {}
# Canonical off, so an inherited/unparsable value cannot enable it
assert _ccache_env() == {"IDF_CCACHE_ENABLE": "0"}
def test_ccache_env_opt_out_via_env(tmp_path: Path) -> None:
@@ -1578,12 +1579,12 @@ def test_ccache_env_opt_out_via_env(tmp_path: Path) -> None:
def test_ccache_env_opt_in_without_binary(tmp_path: Path) -> None:
# Explicit IDF_CCACHE_ENABLE=1 forces it on without probing PATH. It's
# already in the environment, so it isn't re-emitted, but the rest is.
# Explicit IDF_CCACHE_ENABLE=1 forces it on; the probe verdict is
# ignored but the resolver still runs for its no-binary warning.
p1, p2, p3 = _ccache_patches(tmp_path, None, tmp_path / "build")
with patch.dict("os.environ", {"IDF_CCACHE_ENABLE": "1"}, clear=True), p1, p2, p3:
env = _ccache_env()
assert "IDF_CCACHE_ENABLE" not in env
assert env["IDF_CCACHE_ENABLE"] == "1"
assert env["CCACHE_DIR"] == str(tmp_path / "tools" / "ccache")
assert env["CCACHE_DEPEND"] == "1"
@@ -1595,7 +1596,7 @@ def test_ccache_env_honors_shared_esphome_opt_out(tmp_path: Path) -> None:
env_vars = {"ESPHOME_CCACHE_ENABLE": "0", "PATH": "/usr/bin"}
with patch.dict("os.environ", env_vars, clear=True), p2, p3:
# The real resolver runs so the opt-out parse is exercised
assert _ccache_env() == {}
assert _ccache_env() == {"IDF_CCACHE_ENABLE": "0"}
@pytest.mark.parametrize("value", ["off", "no"])
@@ -1627,7 +1628,7 @@ def test_ccache_env_idf_knob_wins_over_shared_opt_out(tmp_path: Path) -> None:
with patch.dict("os.environ", env_vars, clear=True), p1, p2, p3:
env = _ccache_env()
assert env["CCACHE_DIR"] == str(tmp_path / "tools" / "ccache")
assert "IDF_CCACHE_ENABLE" not in env
assert env["IDF_CCACHE_ENABLE"] == "1"
def test_ccache_env_preserves_user_overrides(tmp_path: Path) -> None:
@@ -14,6 +14,44 @@ from esphome.core import EsphomeError
from esphome.platformio import registry
def test_registry_download_resolves_once_per_process() -> None:
"""The prefetch and the install share one metadata resolve per package."""
calls: list[dict] = []
payload = {
"versions": [
{
"name": "1.0.0",
"files": [
{
"download_url": "http://x/pkg.tar.gz",
"checksum": {"sha256": "ab" * 32},
"size": 5,
}
],
}
]
}
def fake_download(mirrors, substitutions, target):
calls.append(substitutions)
target.write(json.dumps(payload).encode())
return mirrors[0]
with patch.object(registry, "download_from_mirrors", side_effect=fake_download):
first = registry.registry_download("o/pkg", "1.0.0")
second = registry.registry_download("o/pkg", "1.0.0")
assert first == second
assert len(calls) == 1
@pytest.fixture(autouse=True)
def _fresh_registry_cache():
# registry_download memoizes per process; tests reuse package names
registry.registry_download.cache_clear()
yield
registry.registry_download.cache_clear()
@pytest.mark.parametrize(
("system", "machine", "expected"),
[