diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index 30c5549e69..fbf84eb2e8 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -324,14 +324,16 @@ def _external_short_name(name: str) -> str: def _check_unfulfilled_provides( - provided_requests: list[str], satisfied: set[str] + provided_requests: list[str], satisfied: set[str], still_requested: set[str] ) -> None: """Reconcile the provides() promise: every dependency the walk skipped on the backend's word must have been added from the framework tree (or knowingly satisfied by a converted/external library). An unfulfilled promise can only surface as undefined symbols at link, so it fails - here by name like the other can-never-link checks in this module.""" - if missing := sorted(set(provided_requests) - satisfied): + here by name like the other can-never-link checks in this module. The + walk records across re-resolutions, so a name no final manifest still + requests is stale state, never a failure.""" + if missing := sorted((set(provided_requests) & still_requested) - satisfied): raise EsphomeError( "provides() skipped these dependencies but nothing added them: " f"{', '.join(missing)}; the build is missing libraries" @@ -391,6 +393,9 @@ def resolve_libraries( # Bundled candidates skipped on purpose (platform filter); the # provides() reconciliation must count them as satisfied knowingly_skipped: set[str] = set() + # Dependency names of the manifests actually emitted; a walk recording + # for a since-re-resolved manifest must not fail the reconciliation + final_dep_names: set[str] = set() # Ordered set of bundled dependency names to add once conversion is done pending_bundled: dict[str, None] = {} # Deps matching a separately-requested external are already in the build @@ -407,6 +412,8 @@ def resolve_libraries( component.data.get("dependencies"), component.name ): name = dep.get("name") + if isinstance(name, str): + final_dep_names.add(name) if isinstance(name, str) and "/" in name: owner, _, pkg = name.partition("/") if _is_safe_library_name(owner) and _is_safe_library_name(pkg): @@ -527,6 +534,7 @@ def resolve_libraries( | converted_manifest_names | external_short_names | knowingly_skipped, + final_dep_names, ) return bundled + converted diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 80e3fa160a..3b0f5d6c76 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -944,9 +944,11 @@ def _warn_unsatisfied_versionless( if dep_name in resolved_manifest_names: # Name-only evidence: any resolved component with this manifest # name counts, not just ones the requester can reach, so a - # coincidental name collision must stay visible + # coincidental name collision must stay visible where the user + # could pin it; treeless backends cannot act on it warned.add(dep_name) - _LOGGER.warning( + log = _LOGGER.warning if backend.provides is not None else _LOGGER.debug + log( "Version-less dependency %s of %s assumed satisfied by a " "resolved library's manifest name only", dep_name, diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 24e78ea7da..eed86db5a7 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -721,10 +721,15 @@ def test_unfulfilled_provides_promise_raises(tmp_path: Path) -> None: undefined symbols at link, so it fails here by name, deduplicated; satisfied ones pass silently.""" with pytest.raises(EsphomeError, match="Wire") as err: - component._check_unfulfilled_provides(["Wire", "Wire", "Hash"], {"Hash"}) + component._check_unfulfilled_provides( + ["Wire", "Wire", "Hash"], {"Hash"}, {"Wire", "Hash"} + ) assert str(err.value).count("Wire") == 1 assert "Hash" not in str(err.value) - component._check_unfulfilled_provides(["Hash"], {"Hash"}) + component._check_unfulfilled_provides(["Hash"], {"Hash"}, {"Hash"}) + # A recording for a since-re-resolved manifest is stale walk state, + # never a failure: no final manifest still requests Wire + component._check_unfulfilled_provides(["Wire"], set(), set()) def test_extra_script_link_flags_reach_the_library(tmp_path: Path) -> None: