Adopt the shared toolchain check, dedupe the native-module dispatch, and complete the trigger set

This commit is contained in:
J. Nick Koston
2026-08-20 14:32:31 -05:00
parent 421f648a07
commit d6a9fd5238
6 changed files with 71 additions and 135 deletions
+21 -12
View File
@@ -813,7 +813,7 @@ def write_cpp_file() -> int:
from esphome.build_gen import espidf
espidf.write_project()
elif CORE.using_toolchain_arduino and CORE.is_esp8266:
elif CORE.using_toolchain_arduino:
# The ninja project is generated at compile time by
# esphome.arduino8266.toolchain (it needs the downloaded framework).
pass
@@ -968,7 +968,7 @@ def upload_using_esptool(
flash_images = [
FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0")
]
elif CORE.using_toolchain_arduino and CORE.is_esp8266:
elif CORE.using_toolchain_arduino:
# The native backend writes PlatformIO-compatible output paths, so the
# shared property already points at the right file.
flash_images = [FlashImage(path=CORE.firmware_bin, offset="0x0")]
@@ -1917,14 +1917,27 @@ def command_update_all(args: ArgsProtocol) -> int | None:
return run_multiple_configs(files, build_command)
def _native_toolchain_module():
"""The native build backend module for the resolved toolchain, if any.
Platform toolchain validation rejects values a platform cannot serve, so
using_toolchain_arduino by itself implies the native ESP8266 build.
"""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return toolchain
if CORE.using_toolchain_arduino:
from esphome.arduino8266 import toolchain
return toolchain
return None
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
import json
native_toolchain = None
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain as native_toolchain
elif CORE.using_toolchain_arduino and CORE.is_esp8266:
from esphome.arduino8266 import toolchain as native_toolchain
native_toolchain = _native_toolchain_module()
if native_toolchain is not None:
# Native toolchains derive idedata from the build's
@@ -1979,11 +1992,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
# Get idedata for analysis
idedata = None
native_toolchain = None
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain as native_toolchain
elif CORE.using_toolchain_arduino and CORE.is_esp8266:
from esphome.arduino8266 import toolchain as native_toolchain
native_toolchain = _native_toolchain_module()
if native_toolchain is not None:
objdump_path = str(native_toolchain.get_objdump_path())
+2 -3
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
import os
from pathlib import Path
import subprocess
@@ -15,7 +14,7 @@ from esphome.const import (
KEY_FRAMEWORK_VERSION,
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import write_file_if_changed
from esphome.helpers import IS_WINDOWS, write_file_if_changed
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -58,7 +57,7 @@ def get_elf_path() -> Path:
# Windows binutils carry the executable suffix; is_file() checks need it
_EXE_SUFFIX = ".exe" if os.name == "nt" else ""
_EXE_SUFFIX = ".exe" if IS_WINDOWS else ""
def _toolchain_tool(name: str) -> Path:
+6 -9
View File
@@ -118,11 +118,7 @@ def _resolve_toolchain(config: ConfigType) -> ConfigType:
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
if CORE.toolchain not in (Toolchain.PLATFORMIO, Toolchain.ARDUINO):
raise cv.Invalid(
f"Unsupported toolchain '{CORE.toolchain.value}' for ESP8266. "
"Supported toolchains are 'platformio' and 'arduino'."
)
cv.check_supported_toolchain("ESP8266", (Toolchain.PLATFORMIO, Toolchain.ARDUINO))
return config
@@ -152,6 +148,8 @@ def _validate_native_toolchain(config: ConfigType) -> ConfigType:
"'toolchain: arduino' does not support a custom framework source; "
"use 'toolchain: platformio'"
)
# BOARDS is a subset of ESP8266_BOARD_BUILD today; the second clause is
# a drift guard for the independently regenerated tables
if (
config[CONF_BOARD] not in BOARDS
or config[CONF_BOARD] not in ESP8266_BOARD_BUILD
@@ -617,10 +615,9 @@ def _decode_pc(config, addr):
addr2line = native_toolchain.get_addr2line_path()
elf = native_toolchain.get_elf_path()
if not addr2line.is_file() or not elf.is_file():
_warn_missing_decode_tool(
str(addr2line if not addr2line.is_file() else elf)
)
missing = addr2line if not addr2line.is_file() else elf
if not missing.is_file():
_warn_missing_decode_tool(str(missing))
return
addr2line, elf = str(addr2line), str(elf)
else:
+4 -2
View File
@@ -642,8 +642,8 @@ ESP8266_NATIVE_TEST_COMPONENTS = frozenset(
)
# Infrastructure whose changes always trigger the native ESP8266 compile
# test. esphome/espidf/ is included because the backend shares its idedata,
# extra-script, and size-summary helpers.
# test. esphome/build_helpers/ holds the idedata and size-summary helpers
# the backend shares with the native ESP-IDF build.
ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = (
"esphome/arduino8266/",
"esphome/build_helpers/",
@@ -651,6 +651,8 @@ ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = (
ESP8266_NATIVE_TRIGGER_FILES = frozenset(
{
"esphome/build_gen/arduino8266.py",
"esphome/build_gen/build_tool.py",
"esphome/platformio/extra_script.py",
"esphome/components/esp8266/build_surgery.py",
"esphome/components/esp8266/boards.py",
"esphome/platformio/library.py",
@@ -3,11 +3,13 @@
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from esphome.components.esp8266 import (
ARDUINO_FRAMEWORK_SCHEMA,
_resolve_toolchain,
_validate_native_toolchain,
)
import esphome.config_validation as cv
@@ -16,6 +18,7 @@ from esphome.const import (
CONF_FRAMEWORK,
CONF_PLATFORM_VERSION,
CONF_SOURCE,
CONF_TOOLCHAIN,
CONF_VERSION,
Toolchain,
)
@@ -101,9 +104,6 @@ def test_unsupported_board_rejected() -> None:
def test_yaml_toolchain_key_resolves() -> None:
"""The documented `toolchain: arduino` YAML key selects the native path."""
from esphome.components.esp8266 import _resolve_toolchain
from esphome.const import CONF_TOOLCHAIN
CORE.toolchain = None
_resolve_toolchain({CONF_TOOLCHAIN: Toolchain.ARDUINO})
assert CORE.toolchain == Toolchain.ARDUINO
@@ -112,8 +112,6 @@ def test_yaml_toolchain_key_resolves() -> None:
def test_yaml_toolchain_key_defaults_to_platformio() -> None:
CORE.toolchain = None
from esphome.components.esp8266 import _resolve_toolchain
_resolve_toolchain({})
assert CORE.toolchain == Toolchain.PLATFORMIO
@@ -122,8 +120,6 @@ def test_decode_pc_native_missing_tools_warns_once(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A stack dump of many addresses produces one missing-tool warning."""
from unittest.mock import patch
from esphome.components import esp8266
esp8266._warn_missing_decode_tool.cache_clear()
+35 -102
View File
@@ -7150,54 +7150,24 @@ def test_upload_using_esptool_arduino_toolchain(
assert cmd_list[firmware_offset_idx + 1] == str(CORE.firmware_bin)
def test_write_cpp_file_arduino_toolchain_writes_no_project(tmp_path: Path) -> None:
"""The native ESP8266 Arduino toolchain generates its project at compile
time, so write_cpp_file must not write a platformio.ini."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
CORE.toolchain = Toolchain.ARDUINO
with (
patch("esphome.writer.write_cpp") as mock_write_cpp,
patch("esphome.build_gen.platformio.write_project") as mock_pio_project,
patch.object(
type(CORE), "cpp_main_section", new_callable=PropertyMock
) as mock_section,
):
mock_section.return_value = ""
assert main.write_cpp_file() == 0
mock_write_cpp.assert_called_once()
mock_pio_project.assert_not_called()
def test_write_cpp_file_platformio_toolchain_writes_project(tmp_path: Path) -> None:
"""The default toolchain writes the PlatformIO project files."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
with (
patch("esphome.writer.write_cpp") as mock_write_cpp,
patch("esphome.build_gen.platformio.write_project") as mock_pio_project,
patch.object(
type(CORE), "cpp_main_section", new_callable=PropertyMock
) as mock_section,
):
mock_section.return_value = ""
assert main.write_cpp_file() == 0
mock_write_cpp.assert_called_once()
mock_pio_project.assert_called_once()
def test_write_cpp_file_arduino_toolchain_other_platform_falls_through(
tmp_path: Path,
@pytest.mark.parametrize(
("toolchain", "pio_project_written"),
[
# The native toolchain generates its project at compile time, so
# write_cpp_file must not write a platformio.ini; the default
# toolchain writes the PlatformIO project files.
(Toolchain.ARDUINO, False),
(None, True),
],
)
def test_write_cpp_file_project_generation_follows_toolchain(
tmp_path: Path, toolchain: Toolchain | None, pio_project_written: bool
) -> None:
"""The 'arduino' toolchain is ESP8266-only; other platforms keep the
PlatformIO project generation."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test")
CORE.toolchain = Toolchain.ARDUINO
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test")
CORE.toolchain = toolchain
with (
patch("esphome.writer.write_cpp"),
patch("esphome.writer.write_cpp") as mock_write_cpp,
patch("esphome.build_gen.platformio.write_project") as mock_pio_project,
patch.object(
type(CORE), "cpp_main_section", new_callable=PropertyMock
@@ -7206,7 +7176,8 @@ def test_write_cpp_file_arduino_toolchain_other_platform_falls_through(
mock_section.return_value = ""
assert main.write_cpp_file() == 0
mock_pio_project.assert_called_once()
mock_write_cpp.assert_called_once()
assert mock_pio_project.called is pio_project_written
def test_command_idedata_arduino_prints_json(
@@ -7238,33 +7209,34 @@ def test_command_idedata_arduino_no_build_errors(tmp_path: Path) -> None:
assert result == 1
def test_command_analyze_memory_arduino_toolchain(
@pytest.mark.parametrize(
("platform", "toolchain", "module"),
[
(PLATFORM_ESP8266, Toolchain.ARDUINO, "esphome.arduino8266.toolchain"),
(PLATFORM_ESP32, Toolchain.ESP_IDF, "esphome.espidf.toolchain"),
],
)
def test_command_analyze_memory_native_toolchains(
tmp_path: Path,
mock_write_cpp: Mock,
mock_compile_program: Mock,
mock_get_esphome_components: Mock,
mock_memory_analyzer_cli: Mock,
mock_ram_strings_analyzer: Mock,
platform: str,
toolchain: Toolchain,
module: str,
) -> None:
"""analyze-memory uses the native toolchain's binutils under
'toolchain: arduino' instead of falling into the PlatformIO branch."""
setup_core(platform=PLATFORM_ESP8266, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ARDUINO
"""analyze-memory uses the native toolchain's binutils instead of
falling into the PlatformIO branch."""
setup_core(platform=platform, tmp_path=tmp_path, name="test_device")
CORE.toolchain = toolchain
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
with (
patch(
"esphome.arduino8266.toolchain.get_objdump_path",
return_value=Path("/tc/objdump"),
),
patch(
"esphome.arduino8266.toolchain.get_readelf_path",
return_value=Path("/tc/readelf"),
),
patch(
"esphome.arduino8266.toolchain.get_elf_path",
return_value=Path("/build/firmware.elf"),
),
patch(f"{module}.get_objdump_path", return_value=Path("/tc/objdump")),
patch(f"{module}.get_readelf_path", return_value=Path("/tc/readelf")),
patch(f"{module}.get_elf_path", return_value=Path("/build/firmware.elf")),
):
result = command_analyze_memory(MockArgs(), config)
@@ -7279,45 +7251,6 @@ def test_command_analyze_memory_arduino_toolchain(
)
def test_command_analyze_memory_esp_idf_toolchain(
tmp_path: Path,
mock_write_cpp: Mock,
mock_compile_program: Mock,
mock_get_esphome_components: Mock,
mock_memory_analyzer_cli: Mock,
mock_ram_strings_analyzer: Mock,
) -> None:
"""analyze-memory uses the ESP-IDF toolchain's binutils natively."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path, name="test_device")
CORE.toolchain = Toolchain.ESP_IDF
config = {CONF_ESPHOME: {CONF_NAME: "test_device"}}
with (
patch(
"esphome.espidf.toolchain.get_objdump_path",
return_value=Path("/tc/objdump"),
),
patch(
"esphome.espidf.toolchain.get_readelf_path",
return_value=Path("/tc/readelf"),
),
patch(
"esphome.espidf.toolchain.get_elf_path",
return_value=Path("/build/firmware.elf"),
),
):
result = command_analyze_memory(MockArgs(), config)
assert result == 0
mock_memory_analyzer_cli.assert_called_once_with(
str(Path("/build/firmware.elf")),
str(Path("/tc/objdump")),
str(Path("/tc/readelf")),
set(),
idedata=None,
)
def test_command_idedata_incompatible_toolchain(tmp_path: Path) -> None:
"""A non-native, non-platformio toolchain errors out cleanly."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)