Reconcile the provides() promise after resolving; name the double-warning invariant's pinning test

This commit is contained in:
J. Nick Koston
2026-08-22 22:54:56 -05:00
parent 9adba2ac06
commit 4b317deea3
3 changed files with 64 additions and 15 deletions
+36 -14
View File
@@ -317,6 +317,22 @@ def _external_short_name(name: str) -> str:
return short.partition("#")[0].removesuffix(".git")
def _warn_unfulfilled_provides(
provided_requests: list[str], satisfied: 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 would surface only as undefined symbols at link."""
for name in provided_requests:
if name not in satisfied:
_LOGGER.warning(
"provides() skipped dependency %s but nothing added it; "
"the build is missing a library",
name,
)
def resolve_libraries(
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
) -> list[ArduinoLibrary]:
@@ -430,8 +446,11 @@ def resolve_libraries(
continue
try:
# framework=None: the walk already ran dependency_is_usable
# on this entry (and warned for any non-platform cause);
# re-checking with a framework would warn twice
# on this entry and warned for any non-platform cause, so
# debug here is what keeps one manifest fault from warning
# twice. That invariant is pinned by
# test_nonplatform_rejection_warns_once_through_real_converter,
# which fails if the walk stops evaluating these deps.
check_library_data(dep, pio_platform, None)
except InvalidLibrary as err:
_LOGGER.debug("Skip bundled candidate %s: %s", name, err)
@@ -458,19 +477,17 @@ def resolve_libraries(
)
_add_bundled_dependencies(component)
backend = LibraryBackend(
platform=pio_platform,
framework="arduino",
emit=_emit,
cache_key=cache_key,
# The walk must not resolve bundled names from the registry;
# _add_bundled_dependencies adds them after emit
provides=_provided,
)
if external:
convert_libraries(
external,
LibraryBackend(
platform=pio_platform,
framework="arduino",
emit=_emit,
cache_key=cache_key,
# The walk must not resolve bundled names from the registry;
# _add_bundled_dependencies adds them after emit
provides=_provided,
),
)
convert_libraries(external, backend)
for name in pending_bundled:
if name in converted_manifest_names:
# Manifest-name evidence: the converted library is this library,
@@ -486,4 +503,9 @@ def resolve_libraries(
bundled_names.add(name)
bundled.append(_bundled_library(framework_path, name))
_warn_unfulfilled_provides(
backend.provided_requests,
bundled_names | converted_manifest_names | external_short_names,
)
return bundled + converted
+4 -1
View File
@@ -351,8 +351,10 @@ class LibraryBackend:
cache_key: str
# Owner-less dependency names this returns True for are skipped by the
# walk; the backend supplies them outside the registry (e.g. core-bundled
# libraries).
# libraries). The walk records every skipped name in provided_requests
# so the backend can reconcile its promise after resolving.
provides: Callable[[str], bool] | None = None
provided_requests: list[str] = field(default_factory=list)
def ensure_list[T](obj: T | list[T]) -> list[T]:
@@ -1303,6 +1305,7 @@ def convert_libraries(
)
else:
_LOGGER.debug("Skip backend-provided dependency %s", dep_name)
backend.provided_requests.append(dep_name)
continue
dep_key = add_spec(dep_name, dep_version, dep_url)
node.edges.add(dep_key)
+24
View File
@@ -716,6 +716,30 @@ def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> No
assert "Wire" in [lib.name for lib in libs]
def test_unfulfilled_provides_promise_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""The walk records every provides()-skipped dependency; one nothing
added must warn instead of surfacing as link errors, while satisfied
ones stay silent."""
component._warn_unfulfilled_provides(["Wire", "Hash"], {"Hash"})
assert "provides() skipped dependency Wire but nothing added it" in caplog.text
assert "Hash" not in caplog.text
def test_fulfilled_provides_promise_is_silent_end_to_end(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""The normal path: the walk records the skip and the backend adds the
bundled copy, so the reconciliation stays quiet."""
framework = _make_framework(tmp_path)
converted = _webserver(tmp_path, {"build": {}, "dependencies": {"Wire": "*"}})
with _emitting_converter(converted):
libs = _resolve(framework)
assert "Wire" in [lib.name for lib in libs]
assert "provides() skipped dependency" not in caplog.text
def test_bundled_dependency_platform_rejection_is_debug(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None: