From 2861cbb7491484316e61f54580233867986b5291 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 14:41:23 -0500 Subject: [PATCH] Harden the shared toolchain check and pin every platform family's rejection --- esphome/components/libretiny/__init__.py | 2 +- esphome/config_validation.py | 15 +++++++++---- esphome/core/__init__.py | 8 +++++++ esphome/core/config.py | 11 +++++----- tests/unit_tests/test_config_validation.py | 25 ++++++++++++++++++++++ 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index f83593269e..6ba0b6e834 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -314,8 +314,8 @@ BASE_SCHEMA = cv.Schema( ) BASE_SCHEMA.add_extra(_detect_variant) -BASE_SCHEMA.add_extra(_update_core_data) BASE_SCHEMA.add_extra(cv.require_platformio_toolchain("LibreTiny")) +BASE_SCHEMA.add_extra(_update_core_data) def _configure_lwip(config: dict) -> None: diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 009b844986..df0c152b13 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -107,6 +107,9 @@ from esphome.util import parse_esphome_version # noqa: F401 from esphome.voluptuous_schema import _Schema from esphome.yaml_util import SensitiveStr, make_data_base +if typing.TYPE_CHECKING: + from esphome.types import ConfigType + _LOGGER = logging.getLogger(__name__) # pylint: disable=invalid-name @@ -2533,17 +2536,21 @@ def platformio_version_constraint(value): return constraints -def check_supported_toolchain(platform_name: str, supported: tuple) -> None: +def check_supported_toolchain( + platform_name: str, supported: tuple[Toolchain, ...] +) -> None: """Raise when the resolved ``CORE.toolchain`` is not in ``supported``. One message shape for every platform, so a ``--toolchain`` a platform cannot serve always fails by name instead of silently building with a different backend. """ - if CORE.toolchain not in supported: + toolchain = CORE.toolchain + if toolchain is None or toolchain not in supported: names = ", ".join(f"'{tc.value}'" for tc in supported) raise Invalid( - f"Unsupported toolchain '{CORE.toolchain.value}' for " + f"Unsupported toolchain " + f"'{toolchain.value if toolchain else 'unresolved'}' for " f"{platform_name}. Supported: {names}." ) @@ -2555,7 +2562,7 @@ def require_platformio_toolchain(platform_name: str): ``--toolchain`` they cannot serve would silently build with PlatformIO. """ - def validator(config): + def validator(config: ConfigType) -> ConfigType: if CORE.toolchain is None: CORE.toolchain = Toolchain.PLATFORMIO check_supported_toolchain(platform_name, (Toolchain.PLATFORMIO,)) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 8e9f8e9751..afe57f29b4 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -984,6 +984,12 @@ class EsphomeCore: @property def using_toolchain_arduino(self): + """The native (PlatformIO-free) ESP8266 Arduino build backend. + + Unlike ``using_arduino`` (the target *framework*, true for any + platform compiling Arduino code), this is a build *toolchain* + choice, like its ``using_toolchain_*`` siblings. + """ return self.toolchain == Toolchain.ARDUINO @property @@ -1099,6 +1105,8 @@ class EsphomeCore: return build_flag def add_build_unflag(self, build_unflag: str) -> None: + # No warning for using_toolchain_arduino: the native ESP8266 build + # honors build_unflags (token-level, matching PlatformIO). if self.using_toolchain_esp_idf: # The native ESP-IDF build generator does not consume build_unflags _LOGGER.warning( diff --git a/esphome/core/config.py b/esphome/core/config.py index 9fef173b48..350e4fd557 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -576,14 +576,15 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No for flag in vals: cg.add_build_flag(flag) elif key == "lib_deps": - # Routed through the regular library mechanism so the libraries - # are converted to IDF components like any other PIO library + # Routed through the regular library mechanism so the + # libraries reach the native backend's converter (IDF + # components, or the ESP8266 native library resolution) for lib in vals: _add_library_str(lib) elif key == "lib_ignore": - # Read by the PIO-library-to-IDF-component conversion - # (generate_idf_components); filters both top-level libraries - # and dependencies discovered during conversion + # Read by the shared library conversion (lib_ignore_set in + # platformio/library.py) on both native backends; filters + # top-level libraries and discovered dependencies cg.add_platformio_option(key, vals) elif key != "upload_speed": # upload_speed needs no handling: it is read from the raw diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index a7ddb931b3..19106f4e1b 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -3181,3 +3181,28 @@ def test_require_platformio_toolchain() -> None: CORE.toolchain = Toolchain.ARDUINO with pytest.raises(Invalid, match="Unsupported toolchain 'arduino' for RP2"): validator(config) + + +@pytest.mark.parametrize( + ("platform", "minimal_config"), + [ + ("host", {}), + ("rp2", {"board": "rpipicow"}), + ("bk72xx", {"board": "generic-bk7231n-qfn32-tuya"}), + ], +) +def test_every_platformio_only_platform_rejects_arduino_toolchain( + platform: str, minimal_config: dict +) -> None: + """The invariant every native-toolchain gate relies on: a platform that + cannot serve a CLI toolchain rejects it at validation (esp32, esp8266, + and nrf52 pin this in their own suites).""" + import importlib + + from esphome.const import Toolchain + from esphome.core import CORE + + module = importlib.import_module(f"esphome.components.{platform}") + CORE.toolchain = Toolchain.ARDUINO + with pytest.raises(Invalid, match="Unsupported toolchain 'arduino'"): + module.CONFIG_SCHEMA(dict(minimal_config))