diff --git a/esphome/__main__.py b/esphome/__main__.py index e8327720d5..c4f76fcc99 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -2734,12 +2734,8 @@ def run_esphome(argv): cache_write_eligible = ( args.command in ("upload", "logs") and not command_line_substitutions ) - # An explicit CLI toolchain must run the per-platform validators; the - # cache was validated under whatever the last compile used. Only the - # read is gated: the refresh below still saves the freshly validated - # config. The sidecar is only written when none exists; a - # compile-written one keeps the compile's toolchain (the firmware on - # disk was built by it), which upload/logs then restore. + # An explicit --toolchain must re-run the per-platform validators, so + # gate only the cache read; the refresh below still saves the result. cache_read_eligible = cache_write_eligible and args.toolchain is None if cache_read_eligible: from esphome.compiled_config import load_compiled_config @@ -2765,11 +2761,8 @@ def run_esphome(argv): return 2 CORE.config = config - # Every platform resolves the toolchain during validation now, but the - # compiled-config cache fast path skips validation entirely and a - # sidecar written before the toolchain field existed restores nothing; - # this fallback covers that path. Must run before the cache refresh - # below so its sidecar records the same toolchain a compile would. + # The cache fast path skips validation, and legacy sidecars lack the + # toolchain field. Must run before the cache refresh below. if CORE.toolchain is None: CORE.toolchain = Toolchain.PLATFORMIO diff --git a/esphome/compiled_config.py b/esphome/compiled_config.py index 066209b184..0d855d71db 100644 --- a/esphome/compiled_config.py +++ b/esphome/compiled_config.py @@ -105,10 +105,9 @@ def _refresh_sidecar() -> bool: and CORE.toolchain is not None and old.toolchain != CORE.toolchain.value ): - # The config was validated under a different toolchain than - # the compile's, and platforms normalize toolchain-sensitive - # keys (e.g. the esp32 board name) differently; caching it - # would disagree with the sidecar until the next compile + # Platforms normalize toolchain-sensitive keys differently; + # never cache a config validated under a different toolchain + # than the compile's _LOGGER.debug( "Not caching: config validated with toolchain %r but the " "last compile used %r", diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 102e78b3d9..98001d5d5b 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -2540,12 +2540,8 @@ def platformio_version_constraint(value): 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. - """ + """Raise when the resolved ``CORE.toolchain`` is not in ``supported`` + (one message shape for every platform).""" toolchain = CORE.toolchain if toolchain is None: # A caller ran the check before resolving; an ordering bug, not a @@ -2591,14 +2587,8 @@ def resolve_toolchain( def require_platformio_toolchain( platform_name: str, ) -> Callable[[ConfigType], ConfigType]: - """Reject a CLI-selected toolchain other than PlatformIO. - - For platforms with only the PlatformIO backend. Without this a - ``--toolchain`` they cannot serve would either build with PlatformIO - while claiming another backend, or (for a toolchain another platform - owns, like ``esp-idf``) dispatch to a native backend that cannot - build this platform at all. - """ + """Reject a CLI-selected toolchain other than PlatformIO, for platforms + with only the PlatformIO backend.""" return resolve_toolchain( platform_name, (Toolchain.PLATFORMIO,), Toolchain.PLATFORMIO ) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index a20a60e322..a4fd2ced30 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -985,12 +985,8 @@ 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. - """ + """The native ESP8266 Arduino build toolchain (unlike + ``using_arduino``, which is the target framework).""" return self.toolchain == Toolchain.ARDUINO @property diff --git a/esphome/core/config.py b/esphome/core/config.py index cbd36e8924..bade3ba9c4 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -566,11 +566,7 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No if CORE.using_native_toolchain: # The native builds don't read platformio.ini; honor the options # with a native equivalent and warn about the rest, which would - # otherwise be silently ignored. Every dispatch site that tests a - # specific using_toolchain_* as a stand-in for "native" (project - # writing, compile, upload, firmware paths) must agree with this - # gate: a toolchain treated as native here must never fall through - # to a PlatformIO code path there. + # otherwise be silently ignored. for key, val in pio_options.items(): vals = [val] if isinstance(val, str) else val if key == CONF_BUILD_FLAGS: @@ -600,12 +596,8 @@ async def _add_platformio_options(pio_options: dict[str, str | list[str]]) -> No # discovered dependencies cg.add_platformio_option(key, vals) elif key in NATIVE_ARDUINO_PIO_OPTIONS and CORE.using_toolchain_arduino: - # Real-world knobs many published ESP8266 configs rely on: - # f_cpu 160000000L for timing-sensitive integrations, and a - # custom ldscript to reserve a filesystem region or correct - # a board's flash size. The esp8266 native generator reads - # both; other native toolchains have no equivalent and fall - # through to the warning. + # The esp8266 native generator reads these; other native + # toolchains have no equivalent and fall through to the warning. cg.add_platformio_option(key, val) elif key != "upload_speed": # upload_speed needs no handling: it is read from the raw diff --git a/tests/unit_tests/test_compiled_config.py b/tests/unit_tests/test_compiled_config.py index 62d043f480..4333420a9e 100644 --- a/tests/unit_tests/test_compiled_config.py +++ b/tests/unit_tests/test_compiled_config.py @@ -643,8 +643,7 @@ def test_save_compiled_config_and_sidecar_toolchain_mismatch( tmp_path: Path, sidecar_toolchain: str | None, saved: bool ) -> None: """A config validated under a different toolchain than the compile's - must not overwrite the cache: platforms normalize toolchain-sensitive - keys differently and the sidecar keeps the compile's toolchain.""" + must not overwrite the cache.""" yaml_path = _bare_yaml(tmp_path) _prime_core(tmp_path) CORE.config = {CONF_ESPHOME: {CONF_NAME: "lite_test"}} diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 9f8633667e..0f927a6513 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,3 +1,4 @@ +import importlib import json import logging from pathlib import Path @@ -48,6 +49,7 @@ from esphome.const import ( TYPE_GIT, TYPE_LOCAL, Framework, + Toolchain, ) from esphome.core import ( CORE, @@ -3169,9 +3171,6 @@ def test_file__remapped_path_is_directory_raises(setup_core: Path) -> None: def test_require_platformio_toolchain() -> None: """Platforms with only the PlatformIO backend reject other toolchains.""" - from esphome.const import Toolchain - from esphome.core import CORE - validator = cv.require_platformio_toolchain("RP2") CORE.toolchain = None config: dict = {} @@ -3186,9 +3185,6 @@ def test_require_platformio_toolchain() -> None: def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None: """Calling the check before resolution fails naming the ordering bug, not a user-facing unsupported-toolchain error.""" - from esphome.const import Toolchain - from esphome.core import CORE - CORE.toolchain = None with pytest.raises(Invalid, match="not resolved before RP2 validation"): cv._check_supported_toolchain("RP2", (Toolchain.PLATFORMIO,)) @@ -3209,14 +3205,7 @@ def test_check_supported_toolchain_unresolved_is_an_ordering_bug() -> None: 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 - + """A platform that cannot serve a CLI toolchain rejects it at validation.""" module = importlib.import_module(f"esphome.components.{platform}") CORE.toolchain = Toolchain.ARDUINO with pytest.raises(Invalid, match="Unsupported toolchain 'arduino'"): diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index ddce559e77..b820f6b551 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -7212,8 +7212,6 @@ def test_compile_program_espidf_idedata_none_warns( def test_cli_toolchain_skips_the_validated_config_cache(tmp_path: Path) -> None: """An explicit --toolchain must run the per-platform validators, so the upload/logs fast path becomes a cache miss.""" - from esphome.__main__ import run_esphome - conf = tmp_path / "device.yaml" conf.write_text("esphome:\n name: t\n") argv = ["esphome", "--toolchain", "arduino", "logs", str(conf)] @@ -7232,8 +7230,6 @@ def test_cli_toolchain_still_refreshes_the_validated_config_cache( """An explicit --toolchain gates only the cache read; the freshly validated config is still saved so a later plain run keeps the fast path (an existing compile-written sidecar keeps its toolchain).""" - from esphome.__main__ import run_esphome - conf = tmp_path / "device.yaml" conf.write_text("esphome:\n name: t\n") argv = ["esphome", "--toolchain", "platformio", "logs", str(conf)] diff --git a/tests/unit_tests/test_nrf52_framework.py b/tests/unit_tests/test_nrf52_framework.py index b5f6b5794f..65b73e37fe 100644 --- a/tests/unit_tests/test_nrf52_framework.py +++ b/tests/unit_tests/test_nrf52_framework.py @@ -7,8 +7,10 @@ import sys from types import SimpleNamespace from unittest.mock import patch +import platformdirs import pytest +from esphome.components.nrf52 import _resolve_toolchain from esphome.components.nrf52.framework import ( _PLATFORMIO_PENV_REQUIREMENTS, _REQUIREMENTS, @@ -22,8 +24,9 @@ from esphome.components.nrf52.framework import ( get_sdk_nrf_tools_path, setup_platformio_python_env, ) +import esphome.config_validation as cv from esphome.config_validation import Version -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain from esphome.core import CORE, EsphomeError from esphome.framework_helpers import get_python_env_executable_path @@ -558,7 +561,6 @@ def testget_tools_path_blank_env_falls_back_to_default( Path("") would resolve to the working directory, which clean-all could then delete by accident. """ - import platformdirs monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", value) expected = ( @@ -570,7 +572,6 @@ def testget_tools_path_blank_env_falls_back_to_default( def testget_tools_path_default_is_global_cache( monkeypatch: pytest.MonkeyPatch, ) -> None: - import platformdirs monkeypatch.delenv("ESPHOME_SDK_NRF_PREFIX", raising=False) expected = ( @@ -623,9 +624,6 @@ def test_needs_venv_rebuild_on_dangling_interpreter_symlink(tmp_path: Path) -> N def test_resolve_toolchain_rejects_unsupported() -> None: """A --toolchain nRF52 cannot serve fails instead of degrading silently.""" - from esphome.components.nrf52 import _resolve_toolchain - import esphome.config_validation as cv - from esphome.const import Toolchain CORE.toolchain = Toolchain.ARDUINO with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):