From 8e6af6aa8c3171ecbafc0f6611187772d7f520f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 09:20:57 -0500 Subject: [PATCH] Name the manifest in every dependency diagnostic, warn where no backend can recover normalize_dependencies' parameter is manifest_name (the dict branch already binds owner to a package owner), and the arduino call site passes the library's name so its unrecognized-entry warning stops saying 'of manifest'. A version-less dependency warns when the backend declares no provides tree (espidf/zephyr/nrf52 have no post-emit pickup), and the shared walk mirrors the typed IncompatiblePlatform branch: routine platform skips stay at debug, any other InvalidLibrary cause warns naming the component. The two real-converter tests pin ESPHOME_DATA_DIR to tmp_path so an ambient data dir cannot leak in. --- esphome/arduino/library.py | 4 +- esphome/platformio/library.py | 49 ++++++++++++++++----- tests/unit_tests/test_arduino_library.py | 12 ++--- tests/unit_tests/test_platformio_library.py | 38 ++++++++++++++++ 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index e360d763bc..a70938ef22 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -300,7 +300,9 @@ def resolve_libraries( "with add_library() if needed", component.name, ) - for dep in normalize_dependencies(component.data.get("dependencies")): + for dep in normalize_dependencies( + component.data.get("dependencies"), component.name + ): name = dep.get("name") if ( not name diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 92bec64ec2..90c1eb27ae 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -628,13 +628,15 @@ def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]: return out -def normalize_dependencies(dependencies: Any, owner: str = "manifest") -> list[dict]: +def normalize_dependencies( + dependencies: Any, manifest_name: str = "manifest" +) -> list[dict]: """Normalize a library manifest's ``dependencies`` to a list of dicts. PIO's library.json accepts the list-of-dicts form, the shorthand dict form (``{"owner/Name": "version_spec"}``), bare name strings inside the list, and a plain (possibly comma-separated) string; normalize them all - so callers see a uniform list. ``owner`` names the manifest in the + so callers see a uniform list. ``manifest_name`` names the manifest in the warning for entries that cannot be normalized. """ if not dependencies: @@ -668,7 +670,9 @@ def normalize_dependencies(dependencies: Any, owner: str = "manifest") -> list[d normalized.append({"name": entry}) else: _LOGGER.warning( - "Ignoring unrecognized dependency entry %r of %s", entry, owner + "Ignoring unrecognized dependency entry %r of %s", + entry, + manifest_name, ) return normalized @@ -987,18 +991,41 @@ def convert_libraries( component.data.get("dependencies"), component.name ): if "name" not in dependency or "version" not in dependency: - # Version-less deps cannot resolve from the registry; the - # arduino backend picks bundled ones up after emit - _LOGGER.debug( - "Skip version-less dependency %r of %s", - dependency.get("name"), - component.name, - ) + if backend.provides is None: + # No backend tree can supply it and the registry cannot + # resolve it: a real drop, not a routine skip + _LOGGER.warning( + "Dependency %r of %s has no version to resolve; skipping", + dependency.get("name"), + component.name, + ) + else: + # The backend (e.g. the arduino bundled tree) picks + # these up after emit + _LOGGER.debug( + "Skip version-less dependency %r of %s", + dependency.get("name"), + component.name, + ) continue try: check_library_data(dependency, backend.platform, backend.framework) except InvalidLibrary as e: - _LOGGER.debug("Skip dependency %s: %s", dependency.get("name"), str(e)) + if isinstance(e, IncompatiblePlatform): + # Routine cross-platform skip + _LOGGER.debug( + "Skip dependency %s: %s", dependency.get("name"), str(e) + ) + else: + # Any other cause is a dropped dependency and must be + # visible (unreachable from check_library_data today, + # which raises only for the platform filter) + _LOGGER.warning( + "Skipping dependency %s of %s: %s", + dependency.get("name"), + component.name, + e, + ) continue dep_name = _owner_pkgname_to_name( dependency.get("owner"), dependency.get("name") diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 2351143622..96696ac2a0 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -760,7 +760,7 @@ def test_bundled_library_non_dict_manifest_skips_probes_and_raises( def test_dict_shorthand_dependency_skips_registry_through_real_converter( - tmp_path: Path, + tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """{"Wire": "*"} in a real manifest must never reach the registry: the graph walk skips backend-provided names and the bundled copy is added @@ -777,9 +777,9 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter( # as_uri() forms a valid file:// URL on every platform (file:///C:/... # on Windows; a bare f-string would embed backslashes) _add_library(local_lib.as_uri(), None) - # The real converter writes its component cache under the config dir - CORE.config_path = tmp_path / "test.yaml" - CORE.config_path.write_text("") + # Pin the component cache to tmp_path (data_dir honors an ambient + # ESPHOME_DATA_DIR otherwise) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) with patch.object( pio_library, "_resolve_registry_version", @@ -891,7 +891,9 @@ def test_bundled_dependency_string_list_form( def test_pinned_bundled_dependency_substitution_warns( - tmp_path: Path, caplog: pytest.LogCaptureFixture + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, ) -> None: """A non-* version pin on a backend-provided dependency is discarded for the bundled copy; the substitution must be visible.""" diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index f3ca4d0170..38d7942394 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -607,3 +607,41 @@ def test_normalize_dependencies_forms(caplog) -> None: {"name": "SPI"}, ] assert normalize_dependencies("Wire") == [{"name": "Wire"}] + + +def test_versionless_dependency_without_provider_warns( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """When no backend tree can supply a version-less dependency, the drop + is a warning, not a debug line.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + {"esphome/A": {"name": "A", "dependencies": [{"name": "Hash"}]}}, + ) + convert_libraries([Library("esphome/A", None, None)], _backend()) + assert "'Hash' of esphome/A has no version to resolve" in caplog.text + + +def test_walk_warns_for_nonplatform_invalid_library( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A dependency dropped for any cause other than the routine platform + filter is visible in every backend.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + {"esphome/A": {"name": "A", "dependencies": [{"name": "B", "version": "1.0"}]}}, + ) + calls = {"n": 0} + real = lib.check_library_data + + def flaky(data, platform, framework): + calls["n"] += 1 + if calls["n"] > 1: + raise InvalidLibrary("manifest is corrupt") + return real(data, platform, framework) + + monkeypatch.setattr(lib, "check_library_data", flaky) + convert_libraries([Library("esphome/A", None, None)], _backend()) + assert "Skipping dependency B of esphome/A: manifest is corrupt" in caplog.text