[nrf52] Build PlatformIO libraries as Zephyr modules (sdk-nrf) (#17250)

This commit is contained in:
Jonathan Swoboda
2026-07-02 10:26:34 -04:00
committed by GitHub
parent 41cf842d5d
commit 65fc10d627
7 changed files with 388 additions and 25 deletions
+27 -11
View File
@@ -481,7 +481,7 @@ def test_generate_idf_components_dedupes_shared_dependency(
"esphome/C": {"name": "C"},
}
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
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;")
@@ -543,7 +543,7 @@ def test_generate_idf_components_lib_ignore_filters_top_level_and_dependencies(
download_salts: list[str] = []
def fake_download(self, force=False, salt=""):
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)
@@ -597,7 +597,7 @@ def test_generate_idf_components_handles_dependency_cycle(
},
}
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
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;")
@@ -654,7 +654,7 @@ def test_generate_idf_components_git_overrides_registry_warns(
"esphome/shared": {"name": "shared"},
}
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
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;")
@@ -691,7 +691,7 @@ def test_generate_idf_components_missing_manifest_raises(
) -> None:
# A library with neither library.json nor library.properties is invalid;
# fail loudly rather than silently generating build files for it.
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
# no library.json / library.properties written
@@ -733,7 +733,7 @@ def test_generate_idf_components_warns_on_noncanonical_duplicate(
"owner/shared": {"name": "shared"},
}
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
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;")
@@ -766,7 +766,7 @@ def test_generate_idf_components_incompatible_top_level_raises(
) -> None:
# A top-level library that isn't ESP-IDF/esp32 compatible must fail fast,
# not be silently dropped.
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "library.json").write_text(
@@ -804,7 +804,7 @@ def test_generate_idf_components_incompatible_dependency_skipped(
"esphome/B": {"name": "B", "platforms": ["espressif8266"]},
}
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
(self.path / "src").mkdir(parents=True, exist_ok=True)
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
@@ -847,6 +847,13 @@ def test_url_source_salt_changes_cache_path(
assert source.download("lib") == expected[""]
assert source.download("lib", salt="abcd1234") == expected["abcd1234"]
# A backend namespace adds a pio_components/<namespace>/ subdir.
digest = hashlib.sha256(url.encode()).hexdigest()[:8]
ns_expected = base / "idf" / digest / "lib"
ns_expected.mkdir(parents=True)
(ns_expected / ".esphome_extracted").touch()
assert source.download("lib", namespace="idf") == ns_expected
def test_git_source_salt_scopes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
"""The salt becomes a subdirectory of the git clone domain."""
@@ -863,7 +870,14 @@ def test_git_source_salt_scopes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
source = GitSource("https://github.com/esphome/noise-c.git", "v1.0")
source.download("noise-c")
source.download("noise-c", salt="abcd1234")
assert domains == ["pio_components", "pio_components/abcd1234"]
source.download("noise-c", namespace="idf")
source.download("noise-c", namespace="zephyr", salt="abcd1234")
assert domains == [
"pio_components",
"pio_components/abcd1234",
"pio_components/idf",
"pio_components/zephyr/abcd1234",
]
def test_idf_component_download_passes_salt() -> None:
@@ -873,7 +887,9 @@ def test_idf_component_download_passes_salt() -> None:
source.download.return_value = Path("/converted/owner/name")
c = IDFComponent("owner/name", "1.0", source=source)
c.download(force=True, salt="abcd1234")
c.download(force=True, salt="abcd1234", namespace="idf")
source.download.assert_called_once_with("owner/name", force=True, salt="abcd1234")
source.download.assert_called_once_with(
"owner/name", force=True, salt="abcd1234", namespace="idf"
)
assert c.path == Path("/converted/owner/name")
+4 -2
View File
@@ -26,7 +26,9 @@ from esphome.platformio.library import (
def _backend(emit=lambda component: None) -> LibraryBackend:
return LibraryBackend(platform="espressif32", framework="espidf", emit=emit)
return LibraryBackend(
platform="espressif32", framework="espidf", emit=emit, cache_key="idf"
)
def test_check_library_data_accepts_wildcards():
@@ -134,7 +136,7 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch):
def _patch_download_with_manifests(monkeypatch, tmp_path, manifests, *, properties=()):
"""Fake ConvertedLibrary.download to materialize canned manifests on disk."""
def fake_download(self, force=False, salt=""):
def fake_download(self, force=False, salt="", namespace=""):
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
self.path.mkdir(parents=True, exist_ok=True)
if self.name in properties:
+117
View File
@@ -0,0 +1,117 @@
"""Tests for the Zephyr backend of the shared PlatformIO library converter."""
from pathlib import Path
import pytest
import esphome.components.zephyr.library as zlib
from esphome.components.zephyr.library import (
generate_cmakelists_txt,
generate_module_yml,
generate_zephyr_modules,
)
from esphome.core import EsphomeError, Library
from esphome.platformio.library import ConvertedLibrary, URLSource
def _make_component(path: Path, name: str = "mylib") -> ConvertedLibrary:
c = ConvertedLibrary(name, "1.0", source=URLSource("http://dummy"))
c.path = path
return c
def test_generate_module_yml_uses_sanitized_name():
c = ConvertedLibrary("owner/My Lib", "1.0", source=URLSource("http://dummy"))
out = generate_module_yml(c)
# "/" -> "__" and " " -> "_" so it's a valid Zephyr module name.
assert "name: owner__My_Lib" in out
assert "cmake: zephyr" in out
def test_generate_cmakelists_txt_basic(tmp_path):
c = _make_component(tmp_path)
src = tmp_path / "src"
src.mkdir()
(src / "main.c").write_text("int main() {}")
c.data = {}
out = generate_cmakelists_txt(c)
assert "zephyr_library_named(mylib)" in out
assert "zephyr_library_sources(" in out
# Sources are emitted as absolute paths (CMakeLists lives in zephyr/ subdir),
# backslash-escaped for CMake (matching the output on Windows).
assert str((src / "main.c").resolve()).replace("\\", "\\\\") in out
def test_generate_cmakelists_txt_flags_and_includes(tmp_path):
c = _make_component(tmp_path)
(tmp_path / "src").mkdir()
(tmp_path / "src" / "a.c").write_text("")
(tmp_path / "include").mkdir()
c.data = {"build": {"flags": ["-Iinclude", "-DFOO", "-Wall", "-Llibdir", "-lm"]}}
out = generate_cmakelists_txt(c)
assert "zephyr_include_directories(" in out
assert str((tmp_path / "include").resolve()).replace("\\", "\\\\") in out
assert "zephyr_library_compile_options(" in out
assert "-DFOO" in out
assert "-Wall" in out
assert "zephyr_link_libraries(" in out
assert "-Llibdir" in out
assert "-lm" in out
def test_generate_zephyr_modules_collects_all_dirs_and_writes(tmp_path, monkeypatch):
# Two converted libraries: one top-level, one transitive dependency. The
# converter calls backend.emit for both; generate_zephyr_modules must return
# *all* module dirs (not just top-level) so every module is discoverable.
top = _make_component(tmp_path / "top", "top")
(top.path / "src").mkdir(parents=True)
(top.path / "src" / "t.c").write_text("")
dep = _make_component(tmp_path / "dep", "dep")
(dep.path / "src").mkdir(parents=True)
(dep.path / "src" / "d.c").write_text("")
captured = {}
def fake_convert(libraries, backend):
captured["platform"] = backend.platform
captured["framework"] = backend.framework
backend.emit(top)
backend.emit(dep)
return [top]
monkeypatch.setattr(zlib, "convert_libraries", fake_convert)
dirs = generate_zephyr_modules([Library("top", "1.0", None)])
assert dirs == [top.path, dep.path]
# Platform check disabled for Zephyr; framework declared as zephyr.
assert captured["platform"] is None
assert captured["framework"] == "zephyr"
for comp in (top, dep):
assert (comp.path / "zephyr" / "module.yml").is_file()
assert (comp.path / "zephyr" / "CMakeLists.txt").is_file()
def test_generate_zephyr_modules_errors_on_duplicate_module_name(tmp_path, monkeypatch):
# The same library referenced under inconsistent specs (e.g. bare vs
# owner-qualified, or git vs registry) resolves to two components with the
# same Zephyr module name, which would collide in zephyr_library_named().
a = _make_component(tmp_path / "a", "esphome/noise-c")
a.path.mkdir(parents=True)
b = _make_component(tmp_path / "b", "esphome/noise-c")
b.path.mkdir(parents=True)
assert a.get_require_name() == b.get_require_name()
def fake_convert(libraries, backend):
backend.emit(a)
backend.emit(b)
return [a]
monkeypatch.setattr(zlib, "convert_libraries", fake_convert)
with pytest.raises(EsphomeError, match="same Zephyr module"):
generate_zephyr_modules([Library("esphome/noise-c", "1.0", None)])