[espidf] Resolve the ESP-IDF tool paths once per build (#19996)

This commit is contained in:
J. Nick Koston
2026-10-01 19:39:06 -05:00
committed by GitHub
parent b324a7c6e0
commit ff97353767
2 changed files with 55 additions and 2 deletions
+31 -2
View File
@@ -2,6 +2,7 @@
from collections.abc import Callable
from ctypes.util import find_library
from dataclasses import dataclass, field
import json
import logging
import os
@@ -18,7 +19,7 @@ from esphome.build_helpers.ccache import (
)
from esphome.build_helpers.pch import ccache_pch_env
from esphome.build_helpers.tools_cache import IDF_TOOLS_CACHE, tools_cache_path
from esphome.core import Version
from esphome.core import CORE, Version
from esphome.framework_helpers import (
PathType,
create_venv,
@@ -42,6 +43,21 @@ _LOGGER = logging.getLogger(__name__)
_SCRIPTS_DIR = Path(__file__).parent
DOMAIN = "espidf_framework"
@dataclass
class _FrameworkCache:
tool_paths: dict[Path, tuple[list[str], dict[str, str]]] = field(
default_factory=dict
)
def _cache() -> _FrameworkCache:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = _FrameworkCache()
return CORE.data[DOMAIN]
ESPHOME_STAMP_FILE = ".esphome.stamp.json"
@@ -362,7 +378,17 @@ def _get_idf_tool_paths(
Raises:
RuntimeError: If ESP-IDF tool paths cannot be determined
The install check and the build environment both resolve the same
framework, so the result is cached per run and the helper script runs
once per build instead of once per caller. The script also reads
``IDF_TOOLS_PATH``; every caller sets it from ``get_idf_tools_path()``,
so the key leaves it out.
"""
cache = _cache().tool_paths
key = Path(idf_framework_root)
if (cached := cache.get(key)) is not None:
return cached
success, stdout, stderr = _run_idf_tools_script(
idf_framework_root, "get_idf_tool_paths.py", "ESP-IDF tool paths", env=env
@@ -373,11 +399,13 @@ def _get_idf_tool_paths(
# Extract json values
try:
data = json.loads(stdout)
return data["paths_to_export"], data["export_vars"]
result = (data["paths_to_export"], data["export_vars"])
except Exception as e:
raise RuntimeError(
f"Can't extract ESP-IDF tool paths of {idf_framework_root}"
) from e
cache[key] = result
return result
def _get_python_version(
@@ -927,6 +955,7 @@ def _check_esphome_idf_framework_install(
# Validate via the managed tool-path resolution, not ``idf_tools.py check``:
# ``check`` probes tools on the system PATH and aborts if any fail to run (e.g. a
# broken Homebrew openocd), which forced a toolchain reinstall on every build.
# The resolved paths stay cached for get_framework_env.
try:
_get_idf_tool_paths(framework_path, env)
install = False
+24
View File
@@ -1511,6 +1511,30 @@ def test_get_idf_tool_paths_raises_on_failure(tmp_path: Path) -> None:
_get_idf_tool_paths(tmp_path)
def test_get_idf_tool_paths_runs_the_script_once_per_build(tmp_path: Path) -> None:
payload = json.dumps({"paths_to_export": ["/a"], "export_vars": {"X": "1"}})
env = {"IDF_TOOLS_PATH": str(tmp_path / "tools")}
with patch(
"esphome.espidf.framework.run_command", return_value=(True, payload, "")
) as run:
first = _get_idf_tool_paths(tmp_path, env)
second = _get_idf_tool_paths(tmp_path, env)
assert run.call_count == 1
assert first == second == (["/a"], {"X": "1"})
def test_get_idf_tool_paths_does_not_cache_a_failure(tmp_path: Path) -> None:
payload = json.dumps({"paths_to_export": ["/a"], "export_vars": {}})
with patch(
"esphome.espidf.framework.run_command",
side_effect=[(False, "", "err"), (True, payload, "")],
) as run:
with pytest.raises(RuntimeError, match="Can't get ESP-IDF tool paths"):
_get_idf_tool_paths(tmp_path)
assert _get_idf_tool_paths(tmp_path) == (["/a"], {})
assert run.call_count == 2
def test_get_python_version_parses_stdout(tmp_path: Path) -> None:
with patch(
"esphome.espidf.framework.run_command", return_value=(True, "3.11.0\n", "")