One owner for the toolchain tool layout, thread a pre-resolved ccache

toolchain_tool carries the bin/xtensa-lx106-elf-<name> pattern and the
Windows suffix that four call sites previously spelled out (only one of
which handled the suffix). ccache_env and get_build_env accept the
already-resolved ccache path so run_compile can resolve once instead of
paying the PATH scan and runnability probe three times per build.
This commit is contained in:
J. Nick Koston
2026-08-21 20:46:10 -05:00
parent 78a1e88915
commit 4101ad4fb1
2 changed files with 43 additions and 5 deletions
+25 -5
View File
@@ -20,7 +20,7 @@ from __future__ import annotations
import os
from pathlib import Path
from typing import NamedTuple
from typing import Any, NamedTuple
from esphome.build_helpers.ccache import ccache_defaults_env, resolve_ccache_path
from esphome.build_helpers.ninja import find_ninja
@@ -141,7 +141,25 @@ def check_and_install(framework_version: Version) -> InstalledPaths:
)
def get_build_env(toolchain_path: Path) -> dict[str, str]:
# Sentinel: "resolve for me" (None is a real value meaning disabled).
# run_compile resolves once and threads the result so one build never pays
# the PATH scan and runnability probe three times.
_CCACHE_UNRESOLVED: Any = object()
def toolchain_tool(toolchain_path: Path, name: str) -> Path:
"""Path to one toolchain tool (gcc, g++, ar, size, addr2line, ...).
The single owner of the ``bin/xtensa-lx106-elf-<name>`` layout and the
Windows suffix, so a toolchain package bump touches one spot.
"""
suffix = ".exe" if os.name == "nt" else ""
return toolchain_path / "bin" / f"xtensa-lx106-elf-{name}{suffix}"
def get_build_env(
toolchain_path: Path, ccache: str | None = _CCACHE_UNRESOLVED
) -> dict[str, str]:
env = os.environ.copy()
# Drop empty entries: a trailing separator from an absent PATH would
# make the shell search the current directory for tools
@@ -150,7 +168,7 @@ def get_build_env(toolchain_path: Path) -> dict[str, str]:
*filter(None, env.get("PATH", "").split(os.pathsep)),
]
env["PATH"] = os.pathsep.join(parts)
env.update(ccache_env())
env.update(ccache_env(ccache))
return env
@@ -164,7 +182,7 @@ def ccache_path() -> str | None:
return resolve_ccache_path()
def ccache_env() -> dict[str, str]:
def ccache_env(ccache: str | None = _CCACHE_UNRESOLVED) -> dict[str, str]:
"""Return ccache settings for the build subprocess (not os.environ).
Mirrors ``espidf.framework._ccache_env``: cache under the machine-global
@@ -172,6 +190,8 @@ def ccache_env() -> dict[str, str]:
scoped to the build dir so devices share framework cache entries. Values
the user already set in the environment are respected.
"""
if ccache_path() is None:
if ccache is _CCACHE_UNRESOLVED:
ccache = ccache_path()
if ccache is None:
return {}
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
@@ -152,3 +152,21 @@ def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None:
):
env = framework.get_build_env(tmp_path)
assert env["PATH"].split(os.pathsep) == [str(tmp_path / "bin"), "/usr/bin", "/bin"]
def test_ccache_env_accepts_a_preresolved_path() -> None:
"""A caller that already resolved ccache threads it through; the probe
must not run again (None means resolved-and-disabled)."""
with patch.object(framework, "ccache_path") as mock_resolve:
assert framework.ccache_env(None) == {}
env = framework.ccache_env("/usr/bin/ccache")
mock_resolve.assert_not_called()
assert env["CCACHE_DIR"].endswith("ccache")
def test_toolchain_tool_layout(tmp_path: Path) -> None:
"""One owner for the bin/xtensa-lx106-elf-<name> layout."""
tool = framework.toolchain_tool(tmp_path, "addr2line")
assert tool.parent == tmp_path / "bin"
assert tool.name.startswith("xtensa-lx106-elf-addr2line")
assert (tool.suffix == ".exe") is (os.name == "nt")