diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index dd1615888c..2a0822213c 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -502,12 +502,17 @@ async def to_code(config: ConfigType) -> None: # and plaintext disabled. Only a factory reset can remove it. cg.add_define("USE_API_PLAINTEXT") cg.add_define("USE_API_NOISE") - # Both libraries build themselves as ESP-IDF components, so on ESP-IDF + # Both libraries build themselves as ESP-IDF components, so on ESP32 # they are pulled straight from the component registry instead of going - # through ESPHome's PlatformIO-library converter. Not on the Arduino - # framework: arduino-esp32 brings its own espressif/libsodium (on IDF - # < 6.0), and IDF refuses to build with two managed components whose - # names differ only by namespace. + # through ESPHome's PlatformIO-library converter. Deliberately not + # conditional on the toolchain: wireguard splits on the same condition, + # and if the two disagree one of them converts a second libsodium next + # to the managed one. + # + # Not on the Arduino framework though: arduino-esp32 depends on + # espressif/libsodium of its own (on IDF < 6.0), so the component + # manager would see two managed components whose names match once the + # namespace is stripped, and refuse to pick between them. if CORE.is_esp32 and not CORE.using_arduino: from esphome.components.esp32 import add_idf_component diff --git a/tests/unit_tests/components/api/test_api_to_code.py b/tests/unit_tests/components/api/test_api_to_code.py index a9d562ef2a..a4aab85258 100644 --- a/tests/unit_tests/components/api/test_api_to_code.py +++ b/tests/unit_tests/components/api/test_api_to_code.py @@ -1,12 +1,13 @@ """Tests for the noise-c/libsodium library wiring in api's to_code. -On ESP-IDF (not Arduino) both libraries build themselves as native ESP-IDF -managed components, so they are declared via add_idf_component() instead of -going through ESPHome's PlatformIO-library converter. On every other target -(and on the Arduino framework, where arduino-esp32 brings its own bundled -espressif/libsodium) noise-c still goes through the PlatformIO-library -converter via cg.add_library(). This drives the real to_code() coroutine so -both branches of that decision are exercised end to end, not just mocked. +On ESP32 (but not the Arduino framework) both libraries build themselves as +native ESP-IDF managed components, so they are declared via add_idf_component() +instead of going through ESPHome's PlatformIO-library converter, on either +toolchain. Elsewhere noise-c still goes through that converter via +cg.add_library(): on the Arduino framework because arduino-esp32 depends on +espressif/libsodium of its own, and off ESP32 because there are no IDF +components at all. This drives the real to_code() coroutine so every branch of +that decision is exercised end to end, not just mocked. """ from __future__ import annotations @@ -26,6 +27,7 @@ from esphome.const import ( KEY_TARGET_PLATFORM, Framework, Platform, + Toolchain, ) from esphome.core import CORE, ID @@ -51,8 +53,9 @@ def fixture_encryption_key() -> str: return base64.b64encode(b"0" * 32).decode() -def _setup_core(platform: Platform, framework: Framework) -> None: +def _setup_core(platform: Platform, framework: Framework, toolchain: Toolchain) -> None: CORE.reset() + CORE.toolchain = toolchain CORE.data[KEY_CORE] = { KEY_TARGET_PLATFORM: str(platform), KEY_TARGET_FRAMEWORK: str(framework), @@ -65,9 +68,10 @@ def test_to_code_esp32_idf_encryption_uses_managed_idf_components( encryption_key: str, monkeypatch: pytest.MonkeyPatch, ) -> None: - """On ESP32 + ESP-IDF, noise-c and libsodium are declared as managed IDF - components (add_idf_component), not converted PlatformIO libraries.""" - _setup_core(Platform.ESP32, Framework.ESP_IDF) + """On ESP32 + the ESP-IDF toolchain, noise-c and libsodium are declared as + managed IDF components (add_idf_component), not converted PlatformIO + libraries.""" + _setup_core(Platform.ESP32, Framework.ESP_IDF, Toolchain.ESP_IDF) config = _build_config(encryption_key) CORE.component_ids.add("api_id") @@ -96,7 +100,7 @@ def test_to_code_esp32_arduino_encryption_uses_add_library( """On the Arduino framework, arduino-esp32 brings its own bundled espressif/libsodium, so noise-c must still go through the PlatformIO- library converter (cg.add_library) instead of add_idf_component().""" - _setup_core(Platform.ESP32, Framework.ARDUINO) + _setup_core(Platform.ESP32, Framework.ARDUINO, Toolchain.ESP_IDF) config = _build_config(encryption_key) CORE.component_ids.add("api_id") @@ -117,13 +121,44 @@ def test_to_code_esp32_arduino_encryption_uses_add_library( add_idf_component_mock.assert_not_called() +def test_to_code_esp32_idf_platformio_toolchain_also_uses_managed_components( + encryption_key: str, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The ESP-IDF framework can also be built with the PlatformIO toolchain, + and the managed components are used there too. The choice deliberately does + not depend on the toolchain: wireguard splits on the same condition, and if + the two ever disagree one of them converts a second libsodium next to the + managed one, which IDF refuses to build.""" + _setup_core(Platform.ESP32, Framework.ESP_IDF, Toolchain.PLATFORMIO) + config = _build_config(encryption_key) + CORE.component_ids.add("api_id") + + add_idf_component_calls: list[dict] = [] + monkeypatch.setattr( + esp32, + "add_idf_component", + lambda **kwargs: add_idf_component_calls.append(kwargs), + ) + add_library_mock = MagicMock() + monkeypatch.setattr(cg, "add_library", add_library_mock) + + asyncio.run(api.to_code(config)) + + assert add_idf_component_calls == [ + {"name": "esphome/noise-c", "ref": api.NOISE_C_VERSION}, + {"name": "esphome/libsodium", "ref": api.LIBSODIUM_VERSION}, + ] + add_library_mock.assert_not_called() + + def test_to_code_non_esp32_encryption_uses_add_library( encryption_key: str, monkeypatch: pytest.MonkeyPatch, ) -> None: """Off ESP32 entirely (e.g. host), noise-c always goes through the PlatformIO-library converter -- add_idf_component is ESP-IDF-only.""" - _setup_core(Platform.HOST, Framework.NATIVE) + _setup_core(Platform.HOST, Framework.NATIVE, Toolchain.PLATFORMIO) config = _build_config(encryption_key) CORE.component_ids.add("api_id")