[esp32] Support esphome idedata with the native ESP-IDF toolchain (#17040)

This commit is contained in:
Jonathan Swoboda
2026-06-18 15:56:21 -04:00
committed by GitHub
parent f6c78f7415
commit 53e85e07d4
4 changed files with 66 additions and 4 deletions

View File

@@ -1771,6 +1771,21 @@ def command_update_all(args: ArgsProtocol) -> int | None:
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json import json
if CORE.using_toolchain_esp_idf:
# Native ESP-IDF derives idedata from the build's compile_commands.json,
# so the configuration must already be compiled.
from esphome.espidf import toolchain as espidf_toolchain
idedata = espidf_toolchain.get_idedata()
if idedata is None:
_LOGGER.error(
"No idedata available; compile the configuration first",
)
return 1
print(json.dumps(idedata, indent=2) + "\n")
return 0
if not CORE.using_toolchain_platformio: if not CORE.using_toolchain_platformio:
_LOGGER.error( _LOGGER.error(
"The idedata command is not compatible with %s toolchain", "The idedata command is not compatible with %s toolchain",

View File

@@ -472,6 +472,7 @@ def get_idedata() -> dict | None:
pass pass
data = idedata_from_build(compile_commands) data = idedata_from_build(compile_commands)
data["prog_path"] = str(get_elf_path())
cache.parent.mkdir(parents=True, exist_ok=True) cache.parent.mkdir(parents=True, exist_ok=True)
cache.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") cache.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
return data return data

View File

@@ -89,8 +89,9 @@ def test_get_idedata_generates_and_caches(setup_core: Path) -> None:
result = toolchain.get_idedata() result = toolchain.get_idedata()
mock_transform.assert_called_once() mock_transform.assert_called_once()
assert result == {"cxx_path": "g++"} prog_path = str(toolchain.get_elf_path())
assert json.loads(cache.read_text()) == {"cxx_path": "g++"} assert result == {"cxx_path": "g++", "prog_path": prog_path}
assert json.loads(cache.read_text()) == {"cxx_path": "g++", "prog_path": prog_path}
def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None: def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None:
@@ -127,7 +128,7 @@ def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) -
result = toolchain.get_idedata() result = toolchain.get_idedata()
mock_transform.assert_called_once() mock_transform.assert_called_once()
assert result == {"cxx_path": "fresh"} assert result == {"cxx_path": "fresh", "prog_path": str(toolchain.get_elf_path())}
def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None: def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None:
@@ -147,7 +148,26 @@ def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None:
result = toolchain.get_idedata() result = toolchain.get_idedata()
mock_transform.assert_called_once() mock_transform.assert_called_once()
assert result == {"cxx_path": "regen"} assert result == {"cxx_path": "regen", "prog_path": str(toolchain.get_elf_path())}
def test_get_idedata_prog_path_points_at_firmware_elf(setup_core: Path) -> None:
"""The idedata exposes prog_path (the ELF) so consumers like build-action
can locate firmware.factory.bin / firmware.ota.bin as its siblings."""
compile_commands, _ = _setup_build(setup_core)
compile_commands.parent.mkdir(parents=True, exist_ok=True)
compile_commands.write_text("[]")
with patch(
"esphome.espidf.idedata.idedata_from_build",
return_value={"cxx_path": "g++"},
):
result = toolchain.get_idedata()
# Use Path semantics so the contract holds on Windows too (backslashes).
prog_path = Path(result["prog_path"])
assert prog_path.name == "firmware.elf"
assert prog_path.parent.name == "build"
def test_get_idf_env_sets_git_ceiling_directories(setup_core: Path) -> None: def test_get_idf_env_sets_git_ceiling_directories(setup_core: Path) -> None:

View File

@@ -32,6 +32,7 @@ from esphome.__main__ import (
command_clean_all, command_clean_all,
command_config, command_config,
command_config_hash, command_config_hash,
command_idedata,
command_rename, command_rename,
command_run, command_run,
command_update_all, command_update_all,
@@ -6257,3 +6258,28 @@ def test_command_run_defaults_subscribe_states_true(
mock_run_logs.assert_called_once_with( mock_run_logs.assert_called_once_with(
CORE.config, ["192.168.1.100"], subscribe_states=True CORE.config, ["192.168.1.100"], subscribe_states=True
) )
def test_command_idedata_esp_idf_prints_json(capsys: CaptureFixture) -> None:
"""Under the native ESP-IDF toolchain, idedata is emitted as JSON."""
setup_core()
CORE.toolchain = Toolchain.ESP_IDF
data = {"cxx_path": "g++", "prog_path": "/build/firmware.elf"}
with patch("esphome.espidf.toolchain.get_idedata", return_value=data) as mock_get:
result = command_idedata(MagicMock(), CORE.config)
assert result == 0
mock_get.assert_called_once_with()
assert json.loads(capsys.readouterr().out) == data
def test_command_idedata_esp_idf_no_build_errors() -> None:
"""Under ESP-IDF, a missing build (no idedata) returns an error, not a crash."""
setup_core()
CORE.toolchain = Toolchain.ESP_IDF
with patch("esphome.espidf.toolchain.get_idedata", return_value=None):
result = command_idedata(MagicMock(), CORE.config)
assert result == 1