mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 01:10:22 +00:00
[noise] Import noise-c and libsodium as ESP-IDF managed components (#18351)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Jonathan Swoboda
J. Nick Koston
parent
879c5cfc5b
commit
5e67258818
@@ -322,6 +322,34 @@ ARDUINO_EXCLUDED_IDF_COMPONENTS = (
|
||||
"joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem
|
||||
)
|
||||
|
||||
# Entries arduino-esp32 only declares below the given IDF version; stubbing one past
|
||||
# it clashes with ESPHome's own managed component of the same short name.
|
||||
ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF: dict[str, cv.Version] = {
|
||||
"espressif__libsodium": cv.Version(6, 0, 0),
|
||||
}
|
||||
|
||||
|
||||
def arduino_bundles_libsodium() -> bool:
|
||||
"""arduino-esp32 ships its own libsodium below IDF 6.0."""
|
||||
return (
|
||||
CORE.using_arduino
|
||||
and idf_version()
|
||||
< ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF["espressif__libsodium"]
|
||||
)
|
||||
|
||||
|
||||
def arduino_excluded_idf_components() -> set[str]:
|
||||
"""The arduino-bundled components to stub for this build's IDF version."""
|
||||
version = idf_version()
|
||||
return {
|
||||
component
|
||||
for component in ARDUINO_EXCLUDED_IDF_COMPONENTS
|
||||
if (max_version := ARDUINO_EXCLUDED_IDF_COMPONENTS_MAX_IDF.get(component))
|
||||
is None
|
||||
or version < max_version
|
||||
}
|
||||
|
||||
|
||||
# Mapping of Arduino libraries to IDF managed components they require
|
||||
# When an Arduino library is enabled via cg.add_library(), these components
|
||||
# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS.
|
||||
@@ -3513,9 +3541,7 @@ def _write_idf_component_yml():
|
||||
}
|
||||
|
||||
# Only stub components that are not required by any enabled Arduino library
|
||||
components_to_stub = (
|
||||
set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components
|
||||
)
|
||||
components_to_stub = arduino_excluded_idf_components() - required_idf_components
|
||||
|
||||
stubs_dir = CORE.relative_build_path("component_stubs")
|
||||
stubs_dir.mkdir(exist_ok=True)
|
||||
@@ -3580,7 +3606,11 @@ 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 also declared as a managed component is not converted too, or
|
||||
# IDF sees the same requirement twice; converted components reach it through
|
||||
# ${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)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ from esphome.types import ConfigType
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DOMAIN = "noise"
|
||||
|
||||
# Keep in sync with platformio.ini and esphome/idf_component.yml.
|
||||
# LIBSODIUM_VERSION must match the version noise-c pins in its manifests.
|
||||
NOISE_C_VERSION = "0.1.30"
|
||||
LIBSODIUM_VERSION = "1.10021.11"
|
||||
|
||||
noise_ns = cg.esphome_ns.namespace("noise")
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
@@ -92,6 +97,22 @@ def encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
def _use_managed_components() -> bool:
|
||||
"""Whether noise-c and libsodium come from the ESP-IDF component registry.
|
||||
|
||||
Both build as ESP-IDF components, so on ESP32 they skip the PlatformIO library
|
||||
converter unless arduino-esp32 bundles its own libsodium. Not toolchain
|
||||
dependent: the PlatformIO toolchain reads the project manifest too, and every
|
||||
consumer of libsodium must make the same choice or a second copy appears.
|
||||
"""
|
||||
if not CORE.is_esp32:
|
||||
return False
|
||||
|
||||
from esphome.components.esp32 import arduino_bundles_libsodium
|
||||
|
||||
return not arduino_bundles_libsodium()
|
||||
|
||||
|
||||
def enable_spare_ephemeral() -> None:
|
||||
"""Compile the spare ephemeral key slot; the component that refills it calls this."""
|
||||
cg.add_define("USE_NOISE_SPARE_EPHEMERAL")
|
||||
@@ -99,12 +120,16 @@ def enable_spare_ephemeral() -> None:
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.30")
|
||||
# noise-c depends on libsodium, but declaring it here too lets the
|
||||
# library manager see the full set up front instead of discovering
|
||||
# libsodium only after noise-c has downloaded, so the two can download
|
||||
# in parallel. The version must match noise-c's library.json.
|
||||
cg.add_library("esphome/libsodium", "1.10021.11")
|
||||
# libsodium is declared next to noise-c so the library manager sees both up front
|
||||
# and nothing else pulls a second copy; the version must match noise-c's own pin
|
||||
if _use_managed_components():
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(name="esphome/noise-c", ref=NOISE_C_VERSION)
|
||||
add_idf_component(name="esphome/libsodium", ref=LIBSODIUM_VERSION)
|
||||
else:
|
||||
cg.add_library("esphome/noise-c", NOISE_C_VERSION)
|
||||
cg.add_library("esphome/libsodium", LIBSODIUM_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")
|
||||
|
||||
@@ -238,6 +238,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]]:
|
||||
@@ -250,12 +261,19 @@ 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 the manifest already provides as managed components are skipped, as
|
||||
in the real esp32 build; converting them too would give IDF the same requirement
|
||||
twice. Arduino below IDF 6.0 has them rule-gated off, so there they convert.
|
||||
"""
|
||||
from esphome.components.esp32 import arduino_bundles_libsodium
|
||||
from esphome.espidf.component import generate_idf_components
|
||||
|
||||
libraries = _parse_lib_deps(platformio_ini, framework)
|
||||
# Same rule as noise._use_managed_components and the manifest
|
||||
managed = set() if arduino_bundles_libsodium() 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
|
||||
|
||||
@@ -273,24 +291,18 @@ 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,
|
||||
arduino_excluded_idf_components,
|
||||
)
|
||||
|
||||
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)
|
||||
deps: dict[str, dict] = {}
|
||||
for component in sorted(ARDUINO_EXCLUDED_IDF_COMPONENTS):
|
||||
for component in sorted(arduino_excluded_idf_components()):
|
||||
if _idf_component_dep_name(component) in esphome_deps:
|
||||
continue # ESPHome needs this one for real (don't stub it away)
|
||||
stub_path = stubs_dir / _idf_component_stub_name(component)
|
||||
|
||||
@@ -287,12 +287,19 @@ 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 registry components already in the project manifest; they
|
||||
are not converted, since IDF rejects the same requirement from two components.
|
||||
Converted components reach them through ``${ESPHOME_PROJECT_MANAGED_COMPONENTS}``.
|
||||
"""
|
||||
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,14 @@ dependencies:
|
||||
version: d44c800a9e876a8394caefc2ce4915dd96dac77b
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 1"
|
||||
# noise (api, ota). Arduino below IDF 6.0 bundles espressif/libsodium, which
|
||||
# clashes with esphome/libsodium, so those builds use the PlatformIO libraries.
|
||||
esphome/noise-c:
|
||||
version: 0.1.30
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 0 || idf_version >= 6.0.0"
|
||||
# Declared alongside noise-c so the library converter skips esp_wireguard's copy
|
||||
esphome/libsodium:
|
||||
version: 1.10021.11
|
||||
rules:
|
||||
- if: "$ESPHOME_ARDUINO_COMPONENT == 0 || idf_version >= 6.0.0"
|
||||
|
||||
@@ -1120,7 +1120,9 @@ def _prefetch_wave(
|
||||
|
||||
|
||||
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``.
|
||||
|
||||
@@ -1141,10 +1143,18 @@ 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 supplies by other means (for ESP-IDF,
|
||||
managed components from ``add_idf_component``); unlike ``backend.provides`` they
|
||||
carry an owner. They are excluded like ``lib_ignore`` so nothing is both
|
||||
converted and managed, which ESP-IDF refuses to build.
|
||||
"""
|
||||
nodes: dict[str, _LibNode] = {}
|
||||
|
||||
lib_ignore = lib_ignore_set()
|
||||
# Folded into one set so every is_lib_ignored() call site honors both.
|
||||
lib_ignore = lib_ignore_set() | {
|
||||
name.split("/")[-1].lower() for name in 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
|
||||
|
||||
+4
-1
@@ -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.30 ; noise (api, ota)
|
||||
improv/Improv@1.2.7 ; improv_serial / improv_ble
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
@@ -77,6 +76,10 @@ lib_compat_mode = strict
|
||||
extends = common
|
||||
lib_deps =
|
||||
${common.lib_deps}
|
||||
; noise (api, ota) -- on the ESP-IDF framework these come from the
|
||||
; component registry instead (see esphome/idf_component.yml), so they
|
||||
; are not in [common].
|
||||
esphome/noise-c@0.1.30 ; noise (api, ota)
|
||||
SPI ; spi (Arduino built-in)
|
||||
Wire ; i2c (Arduino built-int)
|
||||
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Tests for the noise-c/libsodium library wiring in the noise component.
|
||||
|
||||
Drives the real to_code() so every branch of the managed-versus-converted
|
||||
decision runs end to end.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, noise
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
Framework,
|
||||
Platform,
|
||||
Toolchain,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
DEFAULT_IDF_VERSION = cv.Version(5, 5, 4)
|
||||
|
||||
|
||||
def _setup_core(
|
||||
platform: Platform,
|
||||
framework: Framework,
|
||||
toolchain: Toolchain,
|
||||
idf_version: cv.Version = DEFAULT_IDF_VERSION,
|
||||
) -> None:
|
||||
CORE.reset()
|
||||
CORE.toolchain = toolchain
|
||||
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",
|
||||
esp32.KEY_IDF_VERSION: idf_version,
|
||||
}
|
||||
|
||||
|
||||
def _record_calls(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> tuple[list[dict], list[tuple]]:
|
||||
"""Capture both wiring paths so each test can assert one ran and one did not."""
|
||||
idf_calls: list[dict] = []
|
||||
lib_calls: list[tuple] = []
|
||||
monkeypatch.setattr(
|
||||
esp32, "add_idf_component", lambda **kwargs: idf_calls.append(kwargs)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
cg,
|
||||
"add_library",
|
||||
lambda name, version, repository=None: lib_calls.append((name, version)),
|
||||
)
|
||||
return idf_calls, lib_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("toolchain", [Toolchain.ESP_IDF, Toolchain.PLATFORMIO])
|
||||
def test_to_code_esp32_idf_uses_managed_idf_components(
|
||||
toolchain: Toolchain,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""ESP32 + ESP-IDF declares both as managed IDF components on either toolchain."""
|
||||
_setup_core(Platform.ESP32, Framework.ESP_IDF, toolchain)
|
||||
idf_calls, lib_calls = _record_calls(monkeypatch)
|
||||
|
||||
asyncio.run(noise.to_code({}))
|
||||
|
||||
assert idf_calls == [
|
||||
{"name": "esphome/noise-c", "ref": noise.NOISE_C_VERSION},
|
||||
{"name": "esphome/libsodium", "ref": noise.LIBSODIUM_VERSION},
|
||||
]
|
||||
assert lib_calls == []
|
||||
|
||||
|
||||
def test_to_code_esp32_arduino_below_idf6_uses_add_library(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Arduino below IDF 6.0 bundles espressif/libsodium, so both stay PlatformIO
|
||||
libraries."""
|
||||
_setup_core(
|
||||
Platform.ESP32, Framework.ARDUINO, Toolchain.ESP_IDF, cv.Version(5, 5, 4)
|
||||
)
|
||||
idf_calls, lib_calls = _record_calls(monkeypatch)
|
||||
|
||||
asyncio.run(noise.to_code({}))
|
||||
|
||||
assert lib_calls == [
|
||||
("esphome/noise-c", noise.NOISE_C_VERSION),
|
||||
("esphome/libsodium", noise.LIBSODIUM_VERSION),
|
||||
]
|
||||
assert idf_calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("idf_version", [cv.Version(6, 0, 0), cv.Version(6, 1, 0)])
|
||||
def test_to_code_esp32_arduino_idf6_uses_managed_idf_components(
|
||||
idf_version: cv.Version,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""From IDF 6.0 there is no clash, so Arduino uses managed components too."""
|
||||
_setup_core(Platform.ESP32, Framework.ARDUINO, Toolchain.ESP_IDF, idf_version)
|
||||
idf_calls, lib_calls = _record_calls(monkeypatch)
|
||||
|
||||
asyncio.run(noise.to_code({}))
|
||||
|
||||
assert idf_calls == [
|
||||
{"name": "esphome/noise-c", "ref": noise.NOISE_C_VERSION},
|
||||
{"name": "esphome/libsodium", "ref": noise.LIBSODIUM_VERSION},
|
||||
]
|
||||
assert lib_calls == []
|
||||
|
||||
|
||||
def test_to_code_non_esp32_uses_add_library(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Off ESP32 entirely (e.g. host) there are no IDF components at all."""
|
||||
_setup_core(Platform.HOST, Framework.NATIVE, Toolchain.PLATFORMIO)
|
||||
idf_calls, lib_calls = _record_calls(monkeypatch)
|
||||
|
||||
asyncio.run(noise.to_code({}))
|
||||
|
||||
assert lib_calls == [
|
||||
("esphome/noise-c", noise.NOISE_C_VERSION),
|
||||
("esphome/libsodium", noise.LIBSODIUM_VERSION),
|
||||
]
|
||||
assert idf_calls == []
|
||||
|
||||
|
||||
def test_versions_match_the_repo_manifests() -> None:
|
||||
"""A bump that misses one of the duplicated pins would ship two libsodium versions."""
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
repo_root = Path(__file__).resolve().parents[4]
|
||||
manifest = yaml.safe_load(
|
||||
(repo_root / "esphome" / "idf_component.yml").read_text(encoding="utf-8")
|
||||
)
|
||||
deps = manifest["dependencies"]
|
||||
|
||||
assert deps["esphome/noise-c"]["version"] == noise.NOISE_C_VERSION
|
||||
assert deps["esphome/libsodium"]["version"] == noise.LIBSODIUM_VERSION
|
||||
# Both are skipped on Arduino below IDF 6.0, where the PlatformIO library
|
||||
# path is used instead (see noise._use_managed_components).
|
||||
for name in ("esphome/noise-c", "esphome/libsodium"):
|
||||
assert deps[name]["rules"] == [
|
||||
{"if": "$ESPHOME_ARDUINO_COMPONENT == 0 || idf_version >= 6.0.0"}
|
||||
]
|
||||
# Every noise-c pin in platformio.ini, not just one of them
|
||||
pins = re.findall(
|
||||
r"esphome/noise-c@(\S+)",
|
||||
(repo_root / "platformio.ini").read_text(encoding="utf-8"),
|
||||
)
|
||||
assert set(pins) == {noise.NOISE_C_VERSION}
|
||||
assert len(pins) >= 1
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Tests for esp32's _write_idf_component_yml() managed-component wiring.
|
||||
|
||||
A library already declared as a managed IDF component must not also be converted,
|
||||
or ESP-IDF sees the same requirement twice and refuses to build.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import esp32
|
||||
import esphome.config_validation as cv
|
||||
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:
|
||||
"""Names registered via add_idf_component are passed as ``managed`` so the
|
||||
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 add_idf_component calls means an empty managed set, the old 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()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version", "libsodium_stubbed"),
|
||||
[
|
||||
(cv.Version(5, 5, 4), True),
|
||||
(cv.Version(5, 99, 99), True),
|
||||
(cv.Version(6, 0, 0), False),
|
||||
(cv.Version(6, 1, 0), False),
|
||||
],
|
||||
)
|
||||
def test_arduino_excluded_idf_components_depends_on_idf_version(
|
||||
version: cv.Version, libsodium_stubbed: bool
|
||||
) -> None:
|
||||
"""espressif/libsodium is stubbed below IDF 6.0 only; unmapped entries always."""
|
||||
CORE.reset()
|
||||
CORE.data[esp32.KEY_ESP32] = {esp32.KEY_IDF_VERSION: version}
|
||||
|
||||
excluded = esp32.arduino_excluded_idf_components()
|
||||
|
||||
assert ("espressif__libsodium" in excluded) is libsodium_stubbed
|
||||
assert "espressif__cbor" in excluded
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("framework", "version", "bundled"),
|
||||
[
|
||||
(Framework.ARDUINO, cv.Version(5, 5, 5), True),
|
||||
(Framework.ARDUINO, cv.Version(6, 0, 0), False),
|
||||
(Framework.ESP_IDF, cv.Version(5, 5, 5), False),
|
||||
],
|
||||
)
|
||||
def test_arduino_bundles_libsodium(
|
||||
framework: Framework, version: cv.Version, bundled: bool, tmp_path: Path
|
||||
) -> None:
|
||||
"""Only Arduino below IDF 6.0 brings its own libsodium."""
|
||||
_setup_core(tmp_path)
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = str(framework)
|
||||
CORE.data[esp32.KEY_ESP32] = {esp32.KEY_IDF_VERSION: version}
|
||||
|
||||
assert esp32.arduino_bundles_libsodium() is bundled
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("version", "libsodium_stubbed"),
|
||||
[(cv.Version(5, 5, 4), True), (cv.Version(6, 0, 0), False)],
|
||||
)
|
||||
def test_write_idf_component_yml_arduino_stubs_follow_idf_version(
|
||||
version: cv.Version,
|
||||
libsodium_stubbed: bool,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""The manifest stubs follow the IDF version: espressif/libsodium below 6.0 only."""
|
||||
_setup_core(tmp_path)
|
||||
CORE.toolchain = Toolchain.PLATFORMIO
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = str(Framework.ARDUINO)
|
||||
CORE.data[esp32.KEY_ESP32] = {
|
||||
esp32.KEY_COMPONENTS: {},
|
||||
esp32.KEY_IDF_VERSION: version,
|
||||
esp32.KEY_ARDUINO_LIBRARIES: set(),
|
||||
}
|
||||
|
||||
esp32._write_idf_component_yml()
|
||||
|
||||
contents = (tmp_path / "src" / "idf_component.yml").read_text(encoding="utf-8")
|
||||
assert ("espressif/libsodium" in contents) is libsodium_stubbed
|
||||
assert "espressif/cbor" in contents
|
||||
stub_dir = (
|
||||
tmp_path
|
||||
/ "component_stubs"
|
||||
/ esp32._idf_component_stub_name("espressif__libsodium")
|
||||
)
|
||||
assert stub_dir.is_dir() is libsodium_stubbed
|
||||
@@ -5,12 +5,26 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_IDF_VERSION
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM
|
||||
from esphome.core import CORE
|
||||
from esphome.espidf import clang_tidy
|
||||
from esphome.espidf.clang_tidy import _Settings, _setup_core, _write_tidy_project
|
||||
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]
|
||||
|
||||
@@ -137,3 +151,146 @@ def test_generate_compile_commands_configures_twice(
|
||||
)
|
||||
assert result == tmp_path / "build" / "compile_commands.json"
|
||||
assert mock_write.call_args_list[1].args[1] == ["esp_timer", "lwip"]
|
||||
|
||||
|
||||
def test_esphome_manifest_deps_reads_repo_manifest() -> None:
|
||||
"""Top-level dependency names, independent of the per-dependency 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:
|
||||
"""Below IDF 6.0 the manifest gates noise-c/libsodium off on Arduino, so they
|
||||
still go through the converter."""
|
||||
_set_idf_version(cv.Version(5, 5, 5))
|
||||
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_arduino_idf_6_passes_manifest_deps(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""From IDF 6.0 the manifest enables them on Arduino too, so the converter
|
||||
must skip them."""
|
||||
_set_idf_version(cv.Version(6, 0, 0))
|
||||
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
|
||||
)
|
||||
|
||||
assert _convert_pio_libs(tmp_path / "platformio.ini", "arduino") == {}
|
||||
assert captured["managed"] == _esphome_manifest_deps()
|
||||
|
||||
|
||||
def test_convert_pio_libs_espidf_framework_passes_manifest_deps(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Manifest-provided components are passed as ``managed`` so the converter
|
||||
skips them."""
|
||||
_set_idf_version(cv.Version(5, 5, 5), "espidf")
|
||||
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 _set_idf_version(version: cv.Version, framework: str = "arduino") -> None:
|
||||
CORE.reset()
|
||||
CORE.data[KEY_ESP32] = {KEY_IDF_VERSION: version}
|
||||
CORE.data[KEY_CORE] = {
|
||||
KEY_TARGET_PLATFORM: "esp32",
|
||||
KEY_TARGET_FRAMEWORK: framework,
|
||||
}
|
||||
|
||||
|
||||
def test_arduino_excluded_stubs_skips_components_esphome_manifest_provides(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A component ESPHome's own manifest declares (espressif/lan867x for ethernet)
|
||||
is not stubbed; one only arduino-esp32 bundles still is."""
|
||||
_set_idf_version(cv.Version(5, 5, 4))
|
||||
|
||||
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 below IDF 6.0, where
|
||||
# arduino-esp32 still declares it, it is 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()
|
||||
|
||||
|
||||
def test_arduino_excluded_stubs_skips_libsodium_from_idf_6(tmp_path: Path) -> None:
|
||||
"""From IDF 6.0 arduino-esp32 drops espressif/libsodium; a stub would clash
|
||||
with esphome/libsodium."""
|
||||
_set_idf_version(cv.Version(6, 0, 0))
|
||||
|
||||
deps = _arduino_excluded_stubs(tmp_path)
|
||||
|
||||
assert "espressif/libsodium" not in deps
|
||||
# Other arduino-bundled components are still stubbed.
|
||||
assert "espressif/cbor" in deps
|
||||
|
||||
@@ -803,6 +803,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", None
|
||||
|
||||
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", None
|
||||
|
||||
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