mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
[api] Import noise-c and libsodium as ESP-IDF managed components
Both libraries now ship their own CMakeLists.txt, so on ESP-IDF they can be pulled straight from the component registry (noise-c 0.1.15, libsodium 1.10021.2) instead of going through ESPHome's PlatformIO library converter. A library must not be both converted and managed, or IDF refuses component discovery, so the converter now takes a set of names the toolchain already provides. Arduino keeps the converted path: arduino-esp32 brings its own espressif/libsodium and IDF cannot pick between two managed components whose names differ only by namespace.
This commit is contained in:
@@ -43,6 +43,11 @@ DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
# Keep in sync with platformio.ini and esphome/idf_component.yml.
|
||||
# LIBSODIUM_VERSION must match the version noise-c pins in its idf_component.yml.
|
||||
NOISE_C_VERSION = "0.1.15"
|
||||
LIBSODIUM_VERSION = "1.10021.2"
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Conditionally auto-load json only when capture_response is used."""
|
||||
@@ -497,7 +502,23 @@ 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")
|
||||
cg.add_library("esphome/noise-c", "0.1.11")
|
||||
# Both libraries build themselves as ESP-IDF components, so on ESP-IDF
|
||||
# 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.
|
||||
if CORE.is_esp32 and not CORE.using_arduino:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(name="esphome/noise-c", ref=NOISE_C_VERSION)
|
||||
# noise-c pulls libsodium in itself, but declaring it here too keeps
|
||||
# other components that depend on it (wireguard, via esp_wireguard)
|
||||
# from converting a second copy of the PlatformIO library alongside
|
||||
# this managed one, which IDF rejects as a duplicate requirement.
|
||||
add_idf_component(name="esphome/libsodium", ref=LIBSODIUM_VERSION)
|
||||
else:
|
||||
cg.add_library("esphome/noise-c", NOISE_C_VERSION)
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
|
||||
@@ -3235,7 +3235,12 @@ def _write_idf_component_yml():
|
||||
# Don't process arduino libraries
|
||||
if name not in ARDUINO_DISABLED_LIBRARIES
|
||||
]
|
||||
for component in generate_idf_components(libraries):
|
||||
# A library that is also declared as a managed component must not be
|
||||
# converted as well, or IDF sees the same requirement from two
|
||||
# components and refuses to build. Converted components still link
|
||||
# against it via ${ESPHOME_PROJECT_MANAGED_COMPONENTS}.
|
||||
managed = set(CORE.data[KEY_ESP32].get(KEY_COMPONENTS, {}))
|
||||
for component in generate_idf_components(libraries, managed=managed):
|
||||
dependencies[component.get_sanitized_name()] = {
|
||||
"override_path": str(component.path)
|
||||
}
|
||||
|
||||
@@ -232,6 +232,17 @@ def _parse_lib_deps(platformio_ini: Path, framework: str):
|
||||
return libs
|
||||
|
||||
|
||||
def _esphome_manifest_deps() -> set[str]:
|
||||
"""Names of the managed components declared in ``esphome/idf_component.yml``."""
|
||||
import yaml
|
||||
|
||||
esphome_dir = Path(__file__).resolve().parent.parent
|
||||
manifest = yaml.safe_load(
|
||||
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
return set(manifest.get("dependencies") or {})
|
||||
|
||||
|
||||
def _convert_pio_libs(
|
||||
platformio_ini: Path, framework: str
|
||||
) -> dict[str, dict[str, str]]:
|
||||
@@ -244,12 +255,20 @@ def _convert_pio_libs(
|
||||
The whole library set is resolved as a single batch so a shared transitive
|
||||
dependency (e.g. esphome/libsodium pulled by both noise-c and esp_wireguard)
|
||||
is deduplicated to one component instead of clashing override_path entries.
|
||||
|
||||
Libraries ESPHome's own manifest already provides as managed components
|
||||
(noise-c, libsodium, ...) are skipped, mirroring what the real esp32 build
|
||||
does -- converting them too would make IDF see the same requirement twice.
|
||||
On Arduino those entries are rule-disabled in the manifest (arduino-esp32
|
||||
brings its own libsodium), so nothing provides them there and they have to
|
||||
go through the converter as before.
|
||||
"""
|
||||
from esphome.espidf.component import generate_idf_components
|
||||
|
||||
libraries = _parse_lib_deps(platformio_ini, framework)
|
||||
managed = set() if framework == "arduino" else _esphome_manifest_deps()
|
||||
deps: dict[str, dict[str, str]] = {}
|
||||
for component in generate_idf_components(libraries):
|
||||
for component in generate_idf_components(libraries, managed=managed):
|
||||
deps[component.get_sanitized_name()] = {"override_path": str(component.path)}
|
||||
return deps
|
||||
|
||||
@@ -267,19 +286,13 @@ def _arduino_excluded_stubs(work_dir: Path) -> dict[str, dict]:
|
||||
ethernet) are NOT stubbed -- those are real deps we need, and arduino-esp32
|
||||
resolves to the same component rather than conflicting.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
ARDUINO_EXCLUDED_IDF_COMPONENTS,
|
||||
_idf_component_dep_name,
|
||||
_idf_component_stub_name,
|
||||
)
|
||||
|
||||
esphome_dir = Path(__file__).resolve().parent.parent
|
||||
base_manifest = yaml.safe_load(
|
||||
(esphome_dir / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
esphome_deps = set(base_manifest.get("dependencies") or {})
|
||||
esphome_deps = _esphome_manifest_deps()
|
||||
|
||||
stubs_dir = work_dir / "component_stubs"
|
||||
stubs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -310,12 +310,22 @@ def _emit_idf_component(component: IDFComponent) -> None:
|
||||
)
|
||||
|
||||
|
||||
def generate_idf_components(libraries: list[Library]) -> list[IDFComponent]:
|
||||
"""Resolve and convert a batch of PlatformIO libraries to IDF components."""
|
||||
def generate_idf_components(
|
||||
libraries: list[Library], managed: set[str] | None = None
|
||||
) -> list[IDFComponent]:
|
||||
"""Resolve and convert a batch of PlatformIO libraries to IDF components.
|
||||
|
||||
``managed`` names the registry components already declared in the project
|
||||
manifest (via ``add_idf_component``). Those are skipped by the converter --
|
||||
a library must not be both converted and managed, or IDF fails component
|
||||
discovery with "Requirement <owner>__<name> and requirement <name> are both
|
||||
added as project_managed_components". Converted components pick the managed
|
||||
one up through ``${ESPHOME_PROJECT_MANAGED_COMPONENTS}`` in their REQUIRES.
|
||||
"""
|
||||
backend = LibraryBackend(
|
||||
platform=ESP32_PLATFORM,
|
||||
framework=_idf_framework(),
|
||||
emit=_emit_idf_component,
|
||||
cache_key="idf",
|
||||
)
|
||||
return convert_libraries(libraries, backend)
|
||||
return convert_libraries(libraries, backend, provided=managed)
|
||||
|
||||
@@ -106,3 +106,16 @@ dependencies:
|
||||
version: d44c800a9e876a8394caefc2ce4915dd96dac77b
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 1"
|
||||
# api. Not on Arduino: arduino-esp32 pulls espressif/libsodium, and IDF
|
||||
# refuses to build two managed components whose names differ only by
|
||||
# namespace. The Arduino envs get noise-c as a PlatformIO library instead.
|
||||
esphome/noise-c:
|
||||
version: 0.1.15
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 0"
|
||||
# Declared even though noise-c depends on it, so that the PlatformIO-library
|
||||
# converter knows to skip the copy esp_wireguard would otherwise pull in.
|
||||
esphome/libsodium:
|
||||
version: 1.10021.2
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 0"
|
||||
|
||||
@@ -689,7 +689,9 @@ def _node_key(
|
||||
|
||||
|
||||
def convert_libraries(
|
||||
libraries: list[Library], backend: LibraryBackend
|
||||
libraries: list[Library],
|
||||
backend: LibraryBackend,
|
||||
provided: set[str] | None = None,
|
||||
) -> list[ConvertedLibrary]:
|
||||
"""Resolve and convert a batch of PlatformIO libraries for ``backend``.
|
||||
|
||||
@@ -710,28 +712,36 @@ def convert_libraries(
|
||||
``lib_ignore`` from ``esphome->platformio_options`` excludes libraries by
|
||||
short name (part after the ``/``), matched against both the top-level
|
||||
libraries and every dependency discovered during the graph walk.
|
||||
|
||||
``provided`` names libraries the toolchain already supplies by other means
|
||||
(for ESP-IDF: registry-managed components declared via
|
||||
``add_idf_component``). They are excluded exactly like ``lib_ignore``, so a
|
||||
library is never both converted and managed -- ESP-IDF refuses to build when
|
||||
two components claim the same requirement.
|
||||
"""
|
||||
nodes: dict[str, _LibNode] = {}
|
||||
|
||||
lib_ignore = {
|
||||
excluded = {
|
||||
name.split("/")[-1].lower()
|
||||
for name in CORE.platformio_options.get("lib_ignore", [])
|
||||
for name in itertools.chain(
|
||||
CORE.platformio_options.get("lib_ignore", []), provided or ()
|
||||
)
|
||||
}
|
||||
|
||||
# The generated build files inside the shared cache bake in the dependency
|
||||
# wiring, which lib_ignore changes; salt the cache path so configs with
|
||||
# different lib_ignore values don't fight over (and constantly rewrite) the
|
||||
# wiring, which the exclusion set changes; salt the cache path so configs
|
||||
# with different exclusions don't fight over (and constantly rewrite) the
|
||||
# same converted component files.
|
||||
salt = (
|
||||
hashlib.sha256(",".join(sorted(lib_ignore)).encode()).hexdigest()[:8]
|
||||
if lib_ignore
|
||||
hashlib.sha256(",".join(sorted(excluded)).encode()).hexdigest()[:8]
|
||||
if excluded
|
||||
else ""
|
||||
)
|
||||
|
||||
def is_ignored(name: str | None) -> bool:
|
||||
if not lib_ignore or name is None:
|
||||
if not excluded or name is None:
|
||||
return False
|
||||
return name.split("/")[-1].lower() in lib_ignore
|
||||
return name.split("/")[-1].lower() in excluded
|
||||
|
||||
def add_spec(name: str | None, version: str | None, repository: str | None) -> str:
|
||||
key, kind, locator = _node_key(name, version, repository)
|
||||
|
||||
+5
-3
@@ -45,7 +45,6 @@ lib_deps_base =
|
||||
lib_deps =
|
||||
${common.lib_deps_base}
|
||||
https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea
|
||||
esphome/noise-c@0.1.11 ; api
|
||||
improv/Improv@1.2.6 ; improv_serial / esp32_improv
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
@@ -77,6 +76,9 @@ lib_compat_mode = strict
|
||||
extends = common
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
; api -- on the ESP-IDF framework this comes from the component registry
|
||||
; instead (see esphome/idf_component.yml), so it is not in [common].
|
||||
esphome/noise-c@0.1.15 ; api
|
||||
SPI ; spi (Arduino built-in)
|
||||
Wire ; i2c (Arduino built-int)
|
||||
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
|
||||
@@ -244,7 +246,7 @@ lib_deps =
|
||||
${common:idf-component-libs.lib_deps}
|
||||
ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base
|
||||
droscy/esp_wireguard@0.4.5 ; wireguard
|
||||
esphome/noise-c@0.1.11 ; api
|
||||
esphome/noise-c@0.1.15 ; api
|
||||
ESP32Async/AsyncTCP@3.4.5 ; async_tcp
|
||||
DNSServer ; captive_portal
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
@@ -641,7 +643,7 @@ build_unflags =
|
||||
extends = common
|
||||
platform = platformio/native
|
||||
lib_deps =
|
||||
esphome/noise-c@0.1.11 ; used by api
|
||||
esphome/noise-c@0.1.15 ; used by api
|
||||
lvgl/lvgl@9.5.0 ; lvgl
|
||||
build_flags =
|
||||
${common.build_flags}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import api, esp32
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
Framework,
|
||||
Platform,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
|
||||
|
||||
def _build_config(encryption_key: str) -> dict:
|
||||
"""A minimal, already-validated api config with encryption enabled."""
|
||||
return {
|
||||
api.CONF_ID: ID("api_id", is_declaration=True, type=api.APIServer),
|
||||
api.CONF_PORT: 6053,
|
||||
api.CONF_REBOOT_TIMEOUT: cv.positive_time_period_milliseconds("15min"),
|
||||
api.CONF_BATCH_DELAY: cv.positive_time_period_milliseconds("100ms"),
|
||||
api.CONF_MAX_CONNECTIONS: 5,
|
||||
api.CONF_MAX_SEND_QUEUE: 8,
|
||||
api.CONF_CUSTOM_SERVICES: False,
|
||||
api.CONF_HOMEASSISTANT_SERVICES: False,
|
||||
api.CONF_HOMEASSISTANT_STATES: False,
|
||||
api.CONF_ENCRYPTION: {api.CONF_KEY: encryption_key},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(name="encryption_key")
|
||||
def fixture_encryption_key() -> str:
|
||||
return base64.b64encode(b"0" * 32).decode()
|
||||
|
||||
|
||||
def _setup_core(platform: Platform, framework: Framework) -> None:
|
||||
CORE.reset()
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: str(platform),
|
||||
KEY_TARGET_FRAMEWORK: str(framework),
|
||||
}
|
||||
if platform == Platform.ESP32:
|
||||
CORE.data[esp32.KEY_ESP32] = {esp32.KEY_VARIANT: "ESP32"}
|
||||
|
||||
|
||||
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)
|
||||
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_esp32_arduino_encryption_uses_add_library(
|
||||
encryption_key: str,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""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)
|
||||
config = _build_config(encryption_key)
|
||||
CORE.component_ids.add("api_id")
|
||||
|
||||
add_idf_component_mock = MagicMock()
|
||||
monkeypatch.setattr(esp32, "add_idf_component", add_idf_component_mock)
|
||||
add_library_calls: list[tuple] = []
|
||||
monkeypatch.setattr(
|
||||
cg,
|
||||
"add_library",
|
||||
lambda name, version, repository=None: add_library_calls.append(
|
||||
(name, version)
|
||||
),
|
||||
)
|
||||
|
||||
asyncio.run(api.to_code(config))
|
||||
|
||||
assert add_library_calls == [("esphome/noise-c", api.NOISE_C_VERSION)]
|
||||
add_idf_component_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)
|
||||
config = _build_config(encryption_key)
|
||||
CORE.component_ids.add("api_id")
|
||||
|
||||
add_idf_component_mock = MagicMock()
|
||||
monkeypatch.setattr(esp32, "add_idf_component", add_idf_component_mock)
|
||||
add_library_calls: list[tuple] = []
|
||||
monkeypatch.setattr(
|
||||
cg,
|
||||
"add_library",
|
||||
lambda name, version, repository=None: add_library_calls.append(
|
||||
(name, version)
|
||||
),
|
||||
)
|
||||
|
||||
asyncio.run(api.to_code(config))
|
||||
|
||||
assert add_library_calls == [("esphome/noise-c", api.NOISE_C_VERSION)]
|
||||
add_idf_component_mock.assert_not_called()
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tests for esp32's _write_idf_component_yml() managed-component wiring.
|
||||
|
||||
A library that is already declared as a managed IDF component (via
|
||||
add_idf_component(), e.g. api's noise-c/libsodium) must not also be converted
|
||||
from a PlatformIO library, or ESP-IDF sees the same requirement declared by
|
||||
two components and refuses to build. _write_idf_component_yml() passes the
|
||||
set of already-managed component names to generate_idf_components() so the
|
||||
converter excludes them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import esp32
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
Framework,
|
||||
Platform,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def _setup_core(tmp_path: Path) -> None:
|
||||
CORE.reset()
|
||||
CORE.name = "testdevice"
|
||||
CORE.build_path = tmp_path
|
||||
CORE.toolchain = Toolchain.ESP_IDF
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: str(Platform.ESP32),
|
||||
KEY_TARGET_FRAMEWORK: str(Framework.ESP_IDF),
|
||||
}
|
||||
|
||||
|
||||
def test_write_idf_component_yml_passes_managed_components(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The names already registered via add_idf_component (e.g. noise-c from
|
||||
api's encryption config) are passed through as ``managed`` so the
|
||||
PlatformIO-library converter skips them."""
|
||||
_setup_core(tmp_path)
|
||||
CORE.data[esp32.KEY_ESP32] = {
|
||||
esp32.KEY_COMPONENTS: {
|
||||
"esphome/noise-c": {
|
||||
esp32.KEY_REPO: None,
|
||||
esp32.KEY_REF: "0.1.15",
|
||||
esp32.KEY_PATH: None,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
captured: dict[str, set[str] | None] = {}
|
||||
|
||||
# A converted (non-managed) library the batch still resolves, so the loop
|
||||
# wiring its override_path into the manifest is exercised for real too.
|
||||
converted = MagicMock()
|
||||
converted.get_sanitized_name.return_value = "esphome/other-lib"
|
||||
converted.path = tmp_path / "pio_components" / "other-lib"
|
||||
|
||||
def fake_generate_idf_components(libraries, managed=None):
|
||||
captured["managed"] = managed
|
||||
return [converted]
|
||||
|
||||
monkeypatch.setattr(esp32, "generate_idf_components", fake_generate_idf_components)
|
||||
|
||||
esp32._write_idf_component_yml()
|
||||
|
||||
assert captured["managed"] == {"esphome/noise-c"}
|
||||
# The managed component itself is still written into the manifest deps
|
||||
# directly (from KEY_COMPONENTS), just not converted a second time.
|
||||
yml_path = tmp_path / "src" / "idf_component.yml"
|
||||
assert yml_path.is_file()
|
||||
contents = yml_path.read_text(encoding="utf-8")
|
||||
assert "esphome/noise-c" in contents
|
||||
assert "0.1.15" in contents
|
||||
# The converted library the batch DID return is still wired in.
|
||||
assert "esphome/other-lib" in contents
|
||||
assert str(converted.path) in contents
|
||||
|
||||
|
||||
def test_write_idf_component_yml_empty_managed_when_no_components(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""No managed components registered yet (no add_idf_component calls) ->
|
||||
an empty managed set, matching the pre-existing (unfiltered) behavior."""
|
||||
_setup_core(tmp_path)
|
||||
CORE.data[esp32.KEY_ESP32] = {esp32.KEY_COMPONENTS: {}}
|
||||
|
||||
captured: dict[str, set[str] | None] = {}
|
||||
|
||||
def fake_generate_idf_components(libraries, managed=None):
|
||||
captured["managed"] = managed
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(esp32, "generate_idf_components", fake_generate_idf_components)
|
||||
|
||||
esp32._write_idf_component_yml()
|
||||
|
||||
assert captured["managed"] == set()
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from esphome.espidf.clang_tidy import _Settings, _setup_core, _write_tidy_project
|
||||
from esphome.espidf import clang_tidy
|
||||
from esphome.espidf.clang_tidy import (
|
||||
_arduino_excluded_stubs,
|
||||
_convert_pio_libs,
|
||||
_esphome_manifest_deps,
|
||||
_Settings,
|
||||
_setup_core,
|
||||
_write_tidy_project,
|
||||
)
|
||||
import esphome.espidf.component as espidf_component
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -64,3 +75,105 @@ def test_setup_core_sets_arduino_env(
|
||||
_setup_core(tmp_path / "proj", _settings(target_framework=target_framework))
|
||||
|
||||
assert os.environ["ESPHOME_ARDUINO_COMPONENT"] == expected
|
||||
|
||||
|
||||
def test_esphome_manifest_deps_reads_repo_manifest() -> None:
|
||||
"""Returns the top-level dependency names from esphome/idf_component.yml,
|
||||
independent of any per-dependency framework rules."""
|
||||
manifest = yaml.safe_load(
|
||||
(REPO_ROOT / "esphome" / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
deps = _esphome_manifest_deps()
|
||||
|
||||
assert isinstance(deps, set)
|
||||
assert "esphome/noise-c" in deps
|
||||
assert "esphome/libsodium" in deps
|
||||
# Cross-check against a fresh parse instead of hardcoding the manifest's
|
||||
# whole key list, so this doesn't need updating whenever a dependency is
|
||||
# added or removed.
|
||||
assert deps == set(manifest["dependencies"])
|
||||
|
||||
|
||||
def test_convert_pio_libs_arduino_framework_passes_empty_managed(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""On Arduino, ESPHome's manifest entries for noise-c/libsodium are
|
||||
rule-gated off (arduino-esp32 brings its own libsodium), so nothing
|
||||
provides them there -- managed must be empty and they go through the
|
||||
PlatformIO-library converter as before."""
|
||||
monkeypatch.setattr(clang_tidy, "_parse_lib_deps", lambda ini, framework: [])
|
||||
|
||||
captured: dict[str, set[str] | None] = {}
|
||||
|
||||
# A converted library the batch resolves, so the loop wiring its
|
||||
# override_path into the returned deps mapping is exercised for real too.
|
||||
converted = SimpleNamespace(
|
||||
get_sanitized_name=lambda: "esphome/other-lib",
|
||||
path=tmp_path / "other-lib",
|
||||
)
|
||||
|
||||
def fake_generate_idf_components(libraries, managed=None):
|
||||
captured["managed"] = managed
|
||||
return [converted]
|
||||
|
||||
monkeypatch.setattr(
|
||||
espidf_component, "generate_idf_components", fake_generate_idf_components
|
||||
)
|
||||
|
||||
result = _convert_pio_libs(tmp_path / "platformio.ini", "arduino")
|
||||
|
||||
assert captured["managed"] == set()
|
||||
assert result == {
|
||||
"esphome/other-lib": {"override_path": str(tmp_path / "other-lib")}
|
||||
}
|
||||
|
||||
|
||||
def test_convert_pio_libs_espidf_framework_passes_manifest_deps(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""On ESP-IDF, libraries ESPHome's own manifest already provides as
|
||||
managed components (noise-c, libsodium, ...) must be passed through as
|
||||
``managed`` so the converter skips them -- converting them too would make
|
||||
IDF see the same requirement twice."""
|
||||
monkeypatch.setattr(clang_tidy, "_parse_lib_deps", lambda ini, framework: [])
|
||||
|
||||
captured: dict[str, set[str] | None] = {}
|
||||
|
||||
def fake_generate_idf_components(libraries, managed=None):
|
||||
captured["managed"] = managed
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
espidf_component, "generate_idf_components", fake_generate_idf_components
|
||||
)
|
||||
|
||||
result = _convert_pio_libs(tmp_path / "platformio.ini", "espidf")
|
||||
|
||||
assert captured["managed"] == _esphome_manifest_deps()
|
||||
assert "esphome/noise-c" in captured["managed"]
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_arduino_excluded_stubs_skips_components_esphome_manifest_provides(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A component ESPHome's own idf_component.yml declares for real (e.g.
|
||||
espressif/lan867x for ethernet) must not be stubbed away -- stubbing it
|
||||
would silently disable ethernet on Arduino. A component that is only ever
|
||||
bundled by arduino-esp32 (never in ESPHome's own manifest) still gets a
|
||||
stub so the arduino-bundled copy doesn't clash with noise-c's libsodium."""
|
||||
deps = _arduino_excluded_stubs(tmp_path)
|
||||
|
||||
# lan867x is a real ESPHome dependency (esphome/idf_component.yml), so it
|
||||
# must be excluded from the stub set.
|
||||
assert "espressif/lan867x" not in deps
|
||||
# espressif/libsodium (arduino-esp32's bundled copy) is a different
|
||||
# package from ESPHome's own esphome/libsodium, so it's still stubbed.
|
||||
assert "espressif/libsodium" in deps
|
||||
stub_info = deps["espressif/libsodium"]
|
||||
assert stub_info["version"] == "*"
|
||||
stub_path = Path(stub_info["override_path"])
|
||||
assert (stub_path / "CMakeLists.txt").is_file()
|
||||
|
||||
@@ -876,6 +876,112 @@ def test_generate_idf_components_lib_ignore_filters_top_level_and_dependencies(
|
||||
assert download_salts == [hashlib.sha256(b"b,c").hexdigest()[:8]]
|
||||
|
||||
|
||||
def test_generate_idf_components_managed_filters_top_level_and_dependencies(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
esp32_idf_core: None,
|
||||
) -> None:
|
||||
# managed (e.g. noise-c/libsodium already declared via add_idf_component)
|
||||
# must drop B at the top level and C when discovered as a dependency of A,
|
||||
# exactly like lib_ignore -- neither may be resolved, downloaded, or wired
|
||||
# into a manifest.
|
||||
manifests = {
|
||||
"esphome/A": {
|
||||
"name": "A",
|
||||
"dependencies": [
|
||||
{"owner": "esphome", "name": "C", "version": "==1.10021.0"}
|
||||
],
|
||||
},
|
||||
"esphome/B": {"name": "B"},
|
||||
}
|
||||
|
||||
download_salts: list[str] = []
|
||||
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
download_salts.append(salt)
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
|
||||
|
||||
monkeypatch.setattr(IDFComponent, "download", fake_download)
|
||||
|
||||
resolve_calls: list[str] = []
|
||||
|
||||
def fake_resolve(owner, pkgname, requirements):
|
||||
resolve_calls.append(pkgname)
|
||||
return owner, pkgname, "1.0.0", f"http://x/{pkgname}.tar.gz"
|
||||
|
||||
monkeypatch.setattr(
|
||||
esphome.platformio.library, "_resolve_registry_version", fake_resolve
|
||||
)
|
||||
|
||||
top = generate_idf_components(
|
||||
[Library("esphome/A", "1.0.0", None), Library("esphome/B", "1.0.0", None)],
|
||||
managed={"esphome/B", "esphome/C"},
|
||||
)
|
||||
|
||||
assert [c.name for c in top] == ["esphome/A"]
|
||||
# Managed libraries were never resolved (and therefore never downloaded).
|
||||
assert resolve_calls == ["A"]
|
||||
# The managed dependency is not wired into A's manifest.
|
||||
assert top[0].dependencies == []
|
||||
# managed changes the generated wiring just like lib_ignore, so the cache
|
||||
# path is salted the same way.
|
||||
assert download_salts == [hashlib.sha256(b"b,c").hexdigest()[:8]]
|
||||
|
||||
|
||||
def test_generate_idf_components_lib_ignore_and_managed_combine_into_salt(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
esp32_idf_core: None,
|
||||
) -> None:
|
||||
# lib_ignore and managed both contribute to the same exclusion set, so a
|
||||
# config using both gets a salt reflecting the union of the two sources
|
||||
# rather than either alone.
|
||||
manifests = {
|
||||
"esphome/A": {"name": "A"},
|
||||
"esphome/D": {"name": "D"},
|
||||
"esphome/E": {"name": "E"},
|
||||
}
|
||||
|
||||
download_salts: list[str] = []
|
||||
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
download_salts.append(salt)
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
|
||||
|
||||
monkeypatch.setattr(IDFComponent, "download", fake_download)
|
||||
|
||||
resolve_calls: list[str] = []
|
||||
|
||||
def fake_resolve(owner, pkgname, requirements):
|
||||
resolve_calls.append(pkgname)
|
||||
return owner, pkgname, "1.0.0", f"http://x/{pkgname}.tar.gz"
|
||||
|
||||
monkeypatch.setattr(
|
||||
esphome.platformio.library, "_resolve_registry_version", fake_resolve
|
||||
)
|
||||
monkeypatch.setattr(CORE, "platformio_options", {"lib_ignore": ["D"]})
|
||||
|
||||
top = generate_idf_components(
|
||||
[
|
||||
Library("esphome/A", "1.0.0", None),
|
||||
Library("esphome/D", "1.0.0", None),
|
||||
Library("esphome/E", "1.0.0", None),
|
||||
],
|
||||
managed={"esphome/E"},
|
||||
)
|
||||
|
||||
assert [c.name for c in top] == ["esphome/A"]
|
||||
assert resolve_calls == ["A"]
|
||||
# The salt reflects BOTH lib_ignore's "D" and managed's "E" together.
|
||||
assert download_salts == [hashlib.sha256(b"d,e").hexdigest()[:8]]
|
||||
|
||||
|
||||
def test_generate_idf_components_handles_dependency_cycle(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
|
||||
Reference in New Issue
Block a user