From ed48f36fa8a99e490abb633889cb17e62830169a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 15:53:58 -0500 Subject: [PATCH] Route native dispatch through a platform hook, hoist the pio-options warner, adopt shared toolchain factories --- esphome/__main__.py | 13 +++---- esphome/arduino8266/toolchain.py | 32 ++++----------- esphome/build_helpers/pio_options.py | 28 +++++++++++++ esphome/components/esp8266/__init__.py | 39 ++++++++++--------- .../unit_tests/test_arduino8266_toolchain.py | 16 ++++---- 5 files changed, 71 insertions(+), 57 deletions(-) create mode 100644 esphome/build_helpers/pio_options.py diff --git a/esphome/__main__.py b/esphome/__main__.py index 42c43c5e11..6e0ede5617 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1920,18 +1920,17 @@ def command_update_all(args: ArgsProtocol) -> int | None: 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. + Platform-owned toolchains resolve through the target platform's + ``native_toolchain_module`` hook (the same per-platform module seam + ``compile_program`` uses), so shared dispatch never names a backend. """ 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 + module = importlib.import_module("esphome.components." + CORE.target_platform) + get_native = getattr(module, "native_toolchain_module", None) + return get_native() if get_native is not None else None def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: diff --git a/esphome/arduino8266/toolchain.py b/esphome/arduino8266/toolchain.py index cc34a75ec4..2460940052 100644 --- a/esphome/arduino8266/toolchain.py +++ b/esphome/arduino8266/toolchain.py @@ -7,6 +7,7 @@ from pathlib import Path import subprocess from esphome.arduino8266 import framework +from esphome.build_helpers.pio_options import warn_ignored_platformio_options from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, @@ -27,23 +28,6 @@ _MAX_RAM_SIZE = 81920 _CONSUMED_PIO_OPTIONS = frozenset({"lib_ignore", "upload_speed"}) -def _warn_ignored_platformio_options() -> None: - """Warn for component-added platformio options the native build drops. - - YAML ``esphome: platformio_options:`` keys are warned about during code - generation and never reach ``CORE.platformio_options`` under the native - toolchain, so anything left here came from ``cg.add_platformio_option()`` - calls (e.g. an external component) and would be silently ignored. - """ - for key in sorted(CORE.platformio_options or {}): - if key not in _CONSUMED_PIO_OPTIONS: - _LOGGER.warning( - "platformio_options->%s is ignored when building with the " - "native 'arduino' toolchain", - key, - ) - - _RAM_SECTIONS = (".data", ".rodata", ".bss") _FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata") @@ -81,21 +65,21 @@ def get_readelf_path() -> Path: def run_compile(config: ConfigType, verbose: bool) -> int: from esphome.build_gen import arduino8266 as build_gen - _warn_ignored_platformio_options() + warn_ignored_platformio_options(_CONSUMED_PIO_OPTIONS, "arduino") paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]) ninja_changed = build_gen.write_project(paths) build_dir = get_build_dir() - env = framework.get_build_env(paths["toolchain_path"]) + env = framework.get_build_env(paths.toolchain) # The compile database is a pure function of build.ninja (no compilation # involved), so regenerate it before the build: a failed build can then # never leave a stale database behind. Skip the ninja spawn plus MBs of # text on unchanged builds. if ninja_changed or not (build_dir / "compile_commands.json").is_file(): - _write_compile_commands(paths["ninja_path"], build_dir, env) + _write_compile_commands(paths.ninja, build_dir, env) - cmd = [str(paths["ninja_path"]), "-C", str(build_dir)] + cmd = [str(paths.ninja), "-C", str(build_dir)] if verbose: cmd.append("-v") if jobs := config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT): @@ -186,11 +170,9 @@ def _print_size_summary(build_dir: Path) -> None: try: sections[parts[0]] = int(parts[1]) except ValueError: + # An unparsed RAM/Flash section trips the missing-sections + # guard below, so no total is built on a dropped value _LOGGER.warning("Unparsable size output for section %s", parts[0]) - if parts[0] in _RAM_SECTIONS or parts[0] in _FLASH_SECTIONS: - # A confident total built on a dropped section would feed - # a wrong number to CI's memory-impact metric - return if missing := set(_RAM_SECTIONS + _FLASH_SECTIONS) - set(sections): # A defaulted 0 would print a confidently wrong total for CI's metric _LOGGER.warning( diff --git a/esphome/build_helpers/pio_options.py b/esphome/build_helpers/pio_options.py new file mode 100644 index 0000000000..cf0d6f1979 --- /dev/null +++ b/esphome/build_helpers/pio_options.py @@ -0,0 +1,28 @@ +"""Warn about platformio options a native build backend drops.""" + +from __future__ import annotations + +import logging + +from esphome.core import CORE + +_LOGGER = logging.getLogger(__name__) + + +def warn_ignored_platformio_options(consumed: frozenset[str], toolchain: str) -> None: + """Warn for component-added platformio options the native build drops. + + YAML ``esphome: platformio_options:`` keys are warned about during code + generation and never reach ``CORE.platformio_options`` under a native + toolchain, so anything left here came from ``cg.add_platformio_option()`` + calls (e.g. an external component) and would be silently ignored. + ``consumed`` names the keys the backend honors. + """ + for key in sorted(CORE.platformio_options or {}): + if key not in consumed: + _LOGGER.warning( + "platformio_options->%s is ignored when building with the " + "native '%s' toolchain", + key, + toolchain, + ) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 7afa1b1f9c..6dace98891 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -109,18 +109,9 @@ def set_core_data(config): return config -def _validate_toolchain(value: str) -> Toolchain: - return Toolchain( - cv.one_of(Toolchain.PLATFORMIO, Toolchain.ARDUINO, lower=True)(value) - ) - - -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) - cv.check_supported_toolchain("ESP8266", (Toolchain.PLATFORMIO, Toolchain.ARDUINO)) - return config +_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ARDUINO) +_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS) +_resolve_toolchain = cv.resolve_toolchain("ESP8266", _TOOLCHAINS, Toolchain.PLATFORMIO) def _validate_native_toolchain(config: ConfigType) -> ConfigType: @@ -321,6 +312,18 @@ CONFIG_SCHEMA = cv.All( ) +def native_toolchain_module(): + """The native build backend for the resolved toolchain, if any. + + Hook for ``__main__``'s shared dispatch (idedata, analyze_memory). + """ + if not CORE.using_toolchain_arduino: + return None + from esphome.arduino8266 import toolchain + + return toolchain + + def check_rosetta() -> None: """Fail fast when the x86_64 ESP8266 toolchain cannot run on this Mac. @@ -627,12 +630,12 @@ def _decode_pc(config, addr): addr2line = native_toolchain.get_addr2line_path() elf = native_toolchain.get_elf_path() - missing = addr2line if not addr2line.is_file() else elf - if not missing.is_file(): - _warn_decode_problem( - str(missing), "Cannot decode crash addresses: %s missing", missing - ) - return + for path in (addr2line, elf): + if not path.is_file(): + _warn_decode_problem( + str(path), "Cannot decode crash addresses: %s missing", path + ) + return addr2line, elf = str(addr2line), str(elf) else: from esphome.platformio import toolchain diff --git a/tests/unit_tests/test_arduino8266_toolchain.py b/tests/unit_tests/test_arduino8266_toolchain.py index aa452a34f6..5f15b5b698 100644 --- a/tests/unit_tests/test_arduino8266_toolchain.py +++ b/tests/unit_tests/test_arduino8266_toolchain.py @@ -8,6 +8,7 @@ from unittest.mock import MagicMock, patch import pytest from esphome.arduino8266 import framework, toolchain +from esphome.build_helpers.pio_options import warn_ignored_platformio_options import esphome.config_validation as cv from esphome.const import ( CONF_COMPILE_PROCESS_LIMIT, @@ -39,12 +40,12 @@ def _setup_core(tmp_path: Path) -> None: CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)} -def _paths(tmp_path: Path) -> dict[str, Path]: - return { - "framework_path": tmp_path / "framework", - "toolchain_path": tmp_path / "toolchain", - "ninja_path": tmp_path / "ninja", - } +def _paths(tmp_path: Path) -> framework.InstalledPaths: + return framework.InstalledPaths( + framework=tmp_path / "framework", + toolchain=tmp_path / "toolchain", + ninja=tmp_path / "ninja", + ) def test_path_getters(tmp_path: Path) -> None: @@ -331,7 +332,8 @@ def test_warn_ignored_platformio_options(caplog: pytest.LogCaptureFixture) -> No "lib_ignore": ["Updater"], "upload_speed": "460800", } - toolchain._warn_ignored_platformio_options() + warn_ignored_platformio_options(toolchain._CONSUMED_PIO_OPTIONS, "arduino") assert "platformio_options->board_build.ldscript is ignored" in caplog.text + assert "native 'arduino' toolchain" in caplog.text assert "lib_ignore" not in caplog.text assert "upload_speed" not in caplog.text