mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 10:09:13 +00:00
[nrf52] Build PlatformIO libraries as Zephyr modules (sdk-nrf) (#17250)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)])
|
||||
Reference in New Issue
Block a user