[espidf] Also skip installing gdb and ULP toolchains ESPHome never runs (#17687)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2026-07-25 20:07:14 -10:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent 559614077d
commit 9dd7dcb06d
2 changed files with 71 additions and 39 deletions
+39 -24
View File
@@ -551,30 +551,46 @@ def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None:
)
def _patch_tools_json_demote_openocd(framework_path: Path) -> None:
"""Demote openocd-esp32 from ``install: always`` to ``install: on_request``.
# Tools marked ``install: always`` in tools.json that no ESPHome build ever
# runs. openocd-esp32 is a JTAG debug server (its post-install check also
# fails outright on systems without libusb-1.0, #17685). The gdb bundles are
# debuggers used only by ``idf.py gdb``/``idf.py monitor`` flows ESPHome never
# invokes; stack decoding uses addr2line from the compiler toolchains instead.
# esp32ulp-elf is the ULP coprocessor toolchain, and ESPHome excludes the IDF
# ``ulp`` component from every build. esp-rom-elfs stays required: the cmake
# gdbinit generation reads ESP_ROM_ELF_DIR during every configure and warns
# when it is missing.
_UNUSED_IDF_TOOLS: tuple[str, ...] = (
"esp32ulp-elf",
"openocd-esp32",
"riscv32-esp-elf-gdb",
"xtensa-esp-elf-gdb",
)
``idf_tools.py install required`` installs every tool marked ``always`` in
tools.json and validates each one after extraction by running its version
command. openocd links against libusb-1.0, which minimal systems (bare LXC
containers, slim images) often lack, so that one validation aborted the
whole framework install and left it permanently retrying (#17685) — even
though ESPHome never runs openocd (it is a JTAG debugging tool). Demoting
it drops it from the ``required`` set: it is no longer downloaded or
validated, and the tool-path export treats a missing ``on_request`` tool
as fine. A user who wants it can still name ``openocd-esp32`` explicitly
in ESPHOME_IDF_DEFAULT_TOOLS; explicit names bypass install-type
filtering.
Because this runs on every install check, an install stuck in the
failing state (which never wrote its stamp file) heals on the next
build without a clean.
def _patch_tools_json_demote_unused_tools(framework_path: Path) -> None:
"""Demote tools ESPHome never runs from ``install: always`` to ``on_request``.
``idf_tools.py install required`` downloads every tool marked ``always``
in tools.json and validates each one after extraction by running its
version command. Demoting the tools in ``_UNUSED_IDF_TOOLS`` drops them
from the ``required`` set: they are no longer downloaded or validated,
and the tool-path export treats a missing ``on_request`` tool as fine.
Besides the download and disk savings, this makes the openocd libusb
validation failure (#17685) impossible; because this runs on every
install check, an install stuck in that failing state (which never wrote
its stamp file) heals on the next build without a clean. A user who
wants one of these tools can still name it explicitly in
ESPHOME_IDF_DEFAULT_TOOLS; explicit names bypass install-type filtering.
"""
def apply_patch(data: dict) -> bool:
changed = False
for tool in data.get("tools", []):
if tool.get("name") == "openocd-esp32" and tool.get("install") == "always":
if (
tool.get("name") in _UNUSED_IDF_TOOLS
and tool.get("install") == "always"
):
tool["install"] = "on_request"
changed = True
return changed
@@ -582,9 +598,8 @@ def _patch_tools_json_demote_openocd(framework_path: Path) -> None:
_patch_tools_json(
framework_path,
apply_patch,
"Patched %s to make openocd-esp32 optional (not needed for "
"building, and its install check fails on systems without "
"libusb-1.0).",
"Patched %s to skip installing tools ESPHome does not use "
"(openocd, gdb, ULP toolchain).",
)
@@ -779,10 +794,10 @@ def _check_esphome_idf_framework_install(
# a pre-patch tools.json get fixed up without forcing a clean.
_patch_tools_json_for_linux_arm64(framework_path)
# Drop openocd-esp32 from the required tool set on every invocation so
# an install that previously failed on its libusb check recovers on the
# next build.
_patch_tools_json_demote_openocd(framework_path)
# Drop tools ESPHome never runs from the required tool set on every
# invocation, so an install that previously failed on the openocd libusb
# check recovers on the next build.
_patch_tools_json_demote_unused_tools(framework_path)
# 3. Check if the framework tools are the same and correctly installed
if not install:
+32 -15
View File
@@ -29,7 +29,7 @@ from esphome.espidf.framework import (
_get_python_env_path,
_get_python_version,
_parse_git_source,
_patch_tools_json_demote_openocd,
_patch_tools_json_demote_unused_tools,
_patch_tools_json_for_linux_arm64,
_prefetch_idf_tool_archives,
_windows_long_paths_enabled,
@@ -352,7 +352,7 @@ def espidf_mocks(setup_core: Path):
patch("esphome.espidf.framework._clone_idf_with_submodules") as clone,
patch("esphome.espidf.framework._write_idf_version_txt"),
patch("esphome.espidf.framework._patch_tools_json_for_linux_arm64"),
patch("esphome.espidf.framework._patch_tools_json_demote_openocd"),
patch("esphome.espidf.framework._patch_tools_json_demote_unused_tools"),
patch("esphome.espidf.framework._prefetch_idf_tool_archives"),
patch("esphome.espidf.framework._write_stamp"),
patch("esphome.espidf.framework._check_stamp", return_value=True),
@@ -952,28 +952,37 @@ def test_get_tool_downloads_inprocess_explicit_tool_specs(
# ---------------------------------------------------------------------------
# _patch_tools_json_demote_openocd (openocd-esp32 made optional)
# _patch_tools_json_demote_unused_tools (openocd, gdb, ULP toolchain optional)
# ---------------------------------------------------------------------------
def test_demote_openocd_patches_install_type(tmp_path: Path) -> None:
def test_demote_unused_tools_patches_install_type(tmp_path: Path) -> None:
tools_json = _write_tools_json(
tmp_path,
{
"tools": [
{"name": "openocd-esp32", "install": "always"},
{"name": "cmake", "install": "always"},
{"name": "xtensa-esp-elf-gdb", "install": "always"},
{"name": "riscv32-esp-elf-gdb", "install": "always"},
{"name": "esp32ulp-elf", "install": "always"},
{"name": "xtensa-esp-elf", "install": "always"},
{"name": "esp-rom-elfs", "install": "always"},
]
},
)
_patch_tools_json_demote_openocd(tmp_path)
_patch_tools_json_demote_unused_tools(tmp_path)
data = json.loads(tools_json.read_text(encoding="utf-8"))
openocd = next(t for t in data["tools"] if t["name"] == "openocd-esp32")
cmake = next(t for t in data["tools"] if t["name"] == "cmake")
assert openocd["install"] == "on_request"
# other tools are left untouched
assert cmake["install"] == "always"
install_types = {t["name"]: t["install"] for t in data["tools"]}
assert install_types == {
"openocd-esp32": "on_request",
"xtensa-esp-elf-gdb": "on_request",
"riscv32-esp-elf-gdb": "on_request",
"esp32ulp-elf": "on_request",
# the compiler toolchain and ROM ELFs stay required
"xtensa-esp-elf": "always",
"esp-rom-elfs": "always",
}
def test_patch_tools_json_unexpected_structure_warns_and_skips(
@@ -985,16 +994,24 @@ def test_patch_tools_json_unexpected_structure_warns_and_skips(
tools_json = tools_dir / "tools.json"
tools_json.write_text('["not", "a", "dict"]', encoding="utf-8")
before = tools_json.read_text(encoding="utf-8")
_patch_tools_json_demote_openocd(tmp_path) # AttributeError -> skip
_patch_tools_json_demote_unused_tools(tmp_path) # AttributeError -> skip
assert tools_json.read_text(encoding="utf-8") == before
def test_demote_openocd_already_patched_is_noop(tmp_path: Path) -> None:
def test_demote_unused_tools_already_patched_is_noop(tmp_path: Path) -> None:
tools_json = _write_tools_json(
tmp_path, {"tools": [{"name": "openocd-esp32", "install": "on_request"}]}
tmp_path,
{
"tools": [
{"name": "openocd-esp32", "install": "on_request"},
{"name": "xtensa-esp-elf-gdb", "install": "on_request"},
{"name": "riscv32-esp-elf-gdb", "install": "on_request"},
{"name": "esp32ulp-elf", "install": "on_request"},
]
},
)
before = tools_json.read_text(encoding="utf-8")
_patch_tools_json_demote_openocd(tmp_path)
_patch_tools_json_demote_unused_tools(tmp_path)
assert tools_json.read_text(encoding="utf-8") == before