From 522541634738b95ac91d8266be67f2dd509e97c5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Thu, 21 May 2026 12:05:27 -0400 Subject: [PATCH] [espidf] Backport ninja linux-arm64 entry into tools.json on aarch64 hosts (#16527) --- esphome/espidf/framework.py | 76 ++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 6710c632c4..ee612c4c7e 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -7,6 +7,7 @@ import json import logging import os from pathlib import Path +import platform import shutil import subprocess import sys @@ -17,7 +18,7 @@ import requests from esphome.config_validation import Version from esphome.core import CORE -from esphome.helpers import ProgressBar, get_str_env, rmtree +from esphome.helpers import ProgressBar, get_str_env, rmtree, write_file_if_changed PathType = str | os.PathLike @@ -808,6 +809,74 @@ def _write_idf_version_txt(framework_path: Path, version: str) -> None: ) +# Backport of espressif/esp-idf#18272: every ESPHome-supported IDF release +# through v6.0 ships a tools.json whose ninja 1.12.1 entry has no +# ``linux-arm64`` source. ``idf_tools.py`` then either fails to find a +# matching binary or grabs the x86_64 one, which can't execute on +# aarch64. cmake is already populated across the same release range; we +# only need to inject ninja. Values lifted verbatim from the IDF v6.0.1 +# tools.json where the fix landed natively. +_NINJA_ARM64_BACKPORT: dict[str, dict[str, str | int]] = { + "1.12.1": { + "rename_dist": "ninja-linux-arm64-v1.12.1.zip", + "sha256": "5c25c6570b0155e95fce5918cb95f1ad9870df5768653afe128db822301a05a1", + "size": 121787, + "url": "https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux-aarch64.zip", + }, +} + + +def _patch_tools_json_for_linux_arm64(framework_path: Path) -> None: + """Inject ninja linux-arm64 entries into the framework's tools.json on aarch64. + + Idempotent: a tools.json that already has the entry, or a host that + isn't aarch64, is a no-op. Applied unconditionally on every install + check so a build dir extracted before the backport got fixed up + without forcing a clean. + """ + if platform.machine() != "aarch64": + return + + tools_json = framework_path / "tools" / "tools.json" + if not tools_json.is_file(): + return + + try: + with open(tools_json, encoding="utf-8") as f: + data = json.load(f) + except (json.JSONDecodeError, OSError) as e: + _LOGGER.warning( + "Could not parse %s for linux-arm64 backport (%s); " + "skipping. A clean reinstall of the framework directory " + "may be needed.", + tools_json, + e, + ) + return + + changed = False + for tool in data.get("tools", []): + if tool.get("name") != "ninja": + continue + for ver in tool.get("versions", []): + entry = _NINJA_ARM64_BACKPORT.get(ver.get("name")) + if entry is None or ver.get("linux-arm64"): + continue + ver["linux-arm64"] = entry + changed = True + + if changed: + # write_file_if_changed stages a tempfile in the destination dir + # and atomically replaces — safe against mid-write interruption + # and concurrent invocations. + write_file_if_changed(tools_json, json.dumps(data, indent=2) + "\n") + _LOGGER.info( + "Patched %s to add ninja linux-arm64 download " + "(espressif/esp-idf#18272 backport).", + tools_json, + ) + + def _check_esphome_idf_framework_install( version: str, targets: list[str], @@ -894,6 +963,11 @@ def _check_esphome_idf_framework_install( # clean. Skips when version.txt already exists. _write_idf_version_txt(framework_path, version) + # Apply the ninja linux-arm64 backport on every invocation, not just on + # fresh extracts — idempotent and cheap, and lets a build dir carrying + # a pre-patch tools.json get fixed up without forcing a clean. + _patch_tools_json_for_linux_arm64(framework_path) + # 3. Check if the framework tools are the same and correctly installed if not install: install = True