Compare commits

...
Author SHA1 Message Date
Jesse Hills f7f52dc6ef Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584
# Conflicts:
#	esphome/components/api/__init__.py
#	esphome/platformio/library.py
#	platformio.ini
#	tests/unit_tests/test_espidf_clang_tidy.py
2026-09-01 14:35:37 +12:00
Jesse Hills 63336ed377 Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584
# Conflicts:
#	esphome/components/api/__init__.py
#	platformio.ini
2026-08-18 12:00:59 +12:00
Jesse Hills eb53ed5558 [api] Cover the PlatformIO toolchain in the managed component tests
The ESP-IDF framework can also be built with the PlatformIO toolchain, and the
managed components are used on both. Record why that choice is deliberately
toolchain-independent: wireguard splits on the same condition, and if the two
ever disagree one of them converts a second libsodium next to the managed one.

The test now sets the toolchain explicitly and asserts the PlatformIO one takes
the managed path too, so the condition cannot narrow without a test failing.
2026-08-18 07:24:25 +12:00
Jesse Hills 6d14778123 Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584
# Conflicts:
#	esphome/components/api/__init__.py
#	platformio.ini
2026-08-18 07:11:48 +12:00
Jesse Hills eec17043bc Merge remote-tracking branch 'origin/dev' into jesserockz-2026-584 2026-08-13 23:12:59 +12:00
Jesse Hills 041123b14c [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.
2026-08-13 23:12:47 +12:00
11 changed files with 560 additions and 24 deletions
+6 -1
View File
@@ -3342,7 +3342,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)
}
+31 -6
View File
@@ -5,12 +5,18 @@ from typing import Any
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_KEY
from esphome.core import CORE
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
noise_ns = cg.esphome_ns.namespace("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.21"
LIBSODIUM_VERSION = "1.10021.4"
CONFIG_SCHEMA = cv.Schema({})
@@ -63,12 +69,31 @@ def encryption_schema(config: ConfigType | None) -> ConfigType:
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.21")
# 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.4")
# 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. 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.
#
# libsodium is declared alongside noise-c rather than left to noise-c's own
# manifest either way: it lets the library manager see the full set up front
# instead of discovering libsodium only after noise-c has downloaded, and it
# keeps other components that depend on it (wireguard) from converting a
# second copy next to the managed one. The version must match the one
# noise-c pins.
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)
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")
+21 -8
View File
@@ -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,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
@@ -273,19 +292,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)
+13 -3
View File
@@ -287,12 +287,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)
+13
View File
@@ -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.21
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.4
rules:
- if: "$ESPHOME_ARDUINO_COMPONENT == 0"
+16 -4
View File
@@ -1102,7 +1102,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``.
@@ -1123,14 +1125,24 @@ 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 = lib_ignore_set()
# Libraries the toolchain supplies by other means are excluded exactly like
# lib_ignore, 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
# 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]
+3 -1
View File
@@ -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.21 ; noise (api, ota)
improv/Improv@1.2.7 ; 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.21 ; api
SPI ; spi (Arduino built-in)
Wire ; i2c (Arduino built-int)
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
@@ -0,0 +1,131 @@
"""Tests for the noise-c/libsodium library wiring in the noise component.
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 they still go 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
import asyncio
import pytest
import esphome.codegen as cg
from esphome.components import esp32, noise
from esphome.const import (
KEY_CORE,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
Framework,
Platform,
Toolchain,
)
from esphome.core import CORE
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),
}
if platform == Platform.ESP32:
CORE.data[esp32.KEY_ESP32] = {esp32.KEY_VARIANT: "ESP32"}
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:
"""On ESP32 + ESP-IDF both libraries are declared as managed IDF components
rather than converted PlatformIO libraries. The choice is deliberately the
same on either toolchain, because wireguard splits on the same condition."""
_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_uses_add_library(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""On the Arduino framework arduino-esp32 depends on espressif/libsodium of
its own, so declaring esphome/libsodium as a managed component too would
leave the component manager unable to pick between them."""
_setup_core(Platform.ESP32, Framework.ARDUINO, Toolchain.ESP_IDF)
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_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:
"""The pins are duplicated in platformio.ini and esphome/idf_component.yml;
a bump that misses one would ship two different 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
assert f"esphome/noise-c@{noise.NOISE_C_VERSION}" in (
repo_root / "platformio.ini"
).read_text(encoding="utf-8")
@@ -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()
+113 -1
View File
@@ -3,12 +3,22 @@
import json
import os
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import yaml
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]
@@ -69,6 +79,108 @@ def test_setup_core_sets_arduino_env(
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()
def test_idedata_from_tidy_project(tmp_path) -> None:
"""The tidy TU's compile entry is assembled into consumer-shaped idedata."""
compile_commands = tmp_path / "compile_commands.json"
+106
View File
@@ -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,