diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index f699d79704..45a89784e5 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -35,6 +35,7 @@ from esphome.platformio.library import ( IncompatiblePlatform, InvalidLibrary, LibraryBackend, + _node_key, check_library_data, collect_filtered_files, convert_libraries, @@ -71,7 +72,11 @@ class ArduinoLibrary: def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: """Resolve one library's sources, include dirs, and flags (PIO semantics).""" - build = data.get("build", {}) + build = data.get("build", {}) if isinstance(data, dict) else None + if not isinstance(build, dict): + # A bare json.load imposes no shape; name the library instead of an + # AttributeError deep in a traceback + raise EsphomeError(f"Library {name} has a malformed manifest") # PIO's source-dir resolution: manifest srcDir, else src/Src, else the root if "srcDir" in build: @@ -177,6 +182,15 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary: else: manifest = lib_dir / "library.properties" data = parse_library_properties(manifest) if manifest.is_file() else {} + if isinstance(data, dict) and data.get("dependencies"): + # The dependency walk never runs for bundled libraries (a no-op for + # the ESP8266 core, whose bundled manifests declare none); on a core + # where one does, the skip must be visible before link errors + _LOGGER.warning( + "Bundled library %s declares dependencies, which are not " + "resolved automatically; add them with add_library() if needed", + name, + ) return _library_info(name, lib_dir, data) @@ -309,8 +323,15 @@ def resolve_libraries( # A requested library the converter dropped would otherwise # surface only as link errors far from the cause; name the # requests that went missing, not just the survivors - resolved_names = {c.name for c in resolved} - dropped = [str(lib) for lib in external if lib.name not in resolved_names] + # ConvertedLibrary.name is canonical (bare "pngle" resolves to + # "bitbank2__pngle"); diff the request-side node keys instead + resolved_keys = {c.node_key or c.name for c in resolved} + dropped = [ + str(lib) + for lib in external + if _node_key(lib.name, lib.version, lib.repository)[0] + not in resolved_keys + ] _LOGGER.warning( "%d of %d requested libraries were not resolved (missing: %s)", len(external) - len(resolved), diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 34c7a272cc..643a020df8 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -229,6 +229,10 @@ class ConvertedLibrary: self.name = name self.version = version self.source = source + # The request-side _node_key this component resolved from; the + # canonical name can differ (bare "pngle" -> "bitbank2__pngle"), so + # callers diff requests against this, not against name + self.node_key: str | None = None self.data = {} self.dependencies: list[ConvertedLibrary] = [] self._path: Path | None = None @@ -891,6 +895,7 @@ def convert_libraries( component = ConvertedLibrary( _owner_pkgname_to_name(owner, name), version, URLSource(url) ) + component.node_key = key component.download(salt=salt, namespace=backend.cache_key) source_dir = component.source_dir diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 59a1f9fa45..ebc03da497 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -552,3 +552,57 @@ def test_bundled_dependency_platform_rejection_is_debug( cache_key="arduino8266", ) assert "Skipping bundled dependency Wire" not in caplog.text + + +@pytest.mark.parametrize("data", [{"build": "src"}, [], "nope"]) +def test_library_info_malformed_manifest_is_named(tmp_path: Path, data: object) -> None: + """A malformed manifest names the library, never an AttributeError.""" + read_path = tmp_path / "lib" + read_path.mkdir() + with pytest.raises(EsphomeError, match="Library x has a malformed manifest"): + component._library_info("x", read_path, data) + + +def test_drop_warning_maps_requests_by_node_key( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A bare request resolving to a canonical name is not falsely reported + missing; the genuinely dropped request is the one named.""" + framework = _make_framework(tmp_path) + _add_library("pngle", None) + _add_library("gone/missing", "1.0.0") + lib_dir = tmp_path / "converted" / "pngle" + (lib_dir / "src").mkdir(parents=True) + resolved = _converted("bitbank2__pngle", lib_dir, {"build": {}}) + resolved.node_key = "pngle" + with patch.object(component, "convert_libraries", return_value=[resolved]): + component.resolve_libraries( + framework, + pio_platform="espressif8266", + board_mcu="esp8266", + cache_key="arduino8266", + ) + assert "1 of 2 requested libraries were not resolved" in caplog.text + assert "missing" in caplog.text + assert "pngle" not in caplog.text.split("missing:")[-1] + assert "gone/missing" in caplog.text.split("missing:")[-1] + + +def test_bundled_library_with_declared_dependencies_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A bundled manifest that declares dependencies is visible, not + silently skipped (a no-op for the ESP8266 core, not for every core).""" + framework = _make_framework(tmp_path) + wire = framework / "libraries" / "Wire" + (wire / "library.json").write_text( + '{"name": "Wire", "dependencies": [{"name": "SPI"}]}' + ) + _add_library("Wire", None) + component.resolve_libraries( + framework, + pio_platform="espressif8266", + board_mcu="esp8266", + cache_key="arduino8266", + ) + assert "Bundled library Wire declares dependencies" in caplog.text