Merge branch 'esp8266-native-library-backend' into esp8266-native-build-spec

This commit is contained in:
J. Nick Koston
2026-08-25 22:04:05 -05:00
4 changed files with 64 additions and 7 deletions
+14 -1
View File
@@ -264,6 +264,10 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
data = parse_library_json(manifest_json)
else:
manifest = lib_dir / "library.properties"
if not manifest.is_file():
# Defaults still build core libraries correctly, but a missing
# manifest can also mean a torn framework extraction
_LOGGER.debug("Bundled library %s has no manifest; using defaults", name)
data = parse_library_properties(manifest) if manifest.is_file() else {}
if isinstance(data, dict):
# Bundled manifest deps are never walked; make the skip visible
@@ -384,6 +388,9 @@ def resolve_libraries(
converted: list[ArduinoLibrary] = []
bundled_names = {lib.name for lib in bundled}
converted_manifest_names: set[str] = set()
# Bundled candidates skipped on purpose (platform filter); the
# provides() reconciliation must count them as satisfied
knowingly_skipped: 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
@@ -454,6 +461,9 @@ def resolve_libraries(
# which fails if the walk stops evaluating these deps.
check_library_data(dep, pio_platform, None)
except InvalidLibrary as err:
# A knowing skip (platform filter), not a broken promise;
# the reconciliation must accept it as satisfied
knowingly_skipped.add(name)
_LOGGER.debug("Skip bundled candidate %s: %s", name, err)
continue
# Deferred: a later-emitted library's manifest name may satisfy
@@ -513,7 +523,10 @@ def resolve_libraries(
_check_unfulfilled_provides(
backend.provided_requests,
bundled_names | converted_manifest_names | external_short_names,
bundled_names
| converted_manifest_names
| external_short_names
| knowingly_skipped,
)
return bundled + converted
+9 -2
View File
@@ -945,6 +945,7 @@ def _warn_unsatisfied_versionless(
# 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
warned.add(dep_name)
_LOGGER.warning(
"Version-less dependency %s of %s assumed satisfied by a "
"resolved library's manifest name only",
@@ -959,10 +960,16 @@ def _warn_unsatisfied_versionless(
):
# provides() only satisfies owner-less names: the walk's
# backend-provided skip has the same owner guard, so an
# owner-qualified version-less dep was added by nobody
# owner-qualified version-less dep was added by nobody.
# Recorded so the backend's post-emit reconciliation covers
# this path like the versioned one
backend.provided_requests.append(dep_name)
continue
warned.add(dep_name)
_LOGGER.warning(
# A backend with no framework tree can never supply a bundled
# name, so for it this is unactionable noise
log = _LOGGER.warning if backend.provides is not None else _LOGGER.debug
log(
"Dependency %s of %s has no version to resolve and nothing "
"provides it; skipping",
dep_name,
+24
View File
@@ -918,6 +918,30 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter(
# the bundled copy, so the reconciliation passed without raising
def test_versionless_provides_skip_is_reconciled_through_real_converter(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A truly version-less bare-name dependency the walk skips on the
backend's promise is recorded and fulfilled by the bundled copy."""
framework = _make_framework(tmp_path)
_local_lib(tmp_path, ["Wire"])
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
libs = _resolve(framework)
assert "Wire" in [lib.name for lib in libs]
def test_platform_filtered_bundled_candidate_does_not_break_reconciliation(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A bundled candidate the backend knowingly skips (platform filter)
counts as satisfied; the promise reconciliation must not raise."""
framework = _make_framework(tmp_path)
_local_lib(tmp_path, [{"name": "Wire", "platforms": ["espressif32"]}])
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
libs = _resolve(framework)
assert "Wire" not in [lib.name for lib in libs]
@pytest.mark.parametrize(
("bad_name", "message"),
[
+17 -4
View File
@@ -998,8 +998,8 @@ def test_versionless_ignored_dependency_stays_quiet(
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."""
"""A backend whose tree could supply the name warns on the drop; one
without provides() can never act on it, so it stays at debug."""
_patch_download_with_manifests(
monkeypatch,
tmp_path,
@@ -1011,13 +1011,24 @@ def test_versionless_dependency_without_provider_warns(
}
},
)
convert_libraries([Library("esphome/A", None, None)], _backend())
convert_libraries(
[Library("esphome/A", None, None)], _backend(provides=lambda name: False)
)
assert (
caplog.text.count(
"Hash of esphome/A has no version to resolve and nothing provides it"
)
== 1
)
caplog.clear()
with caplog.at_level(logging.DEBUG):
convert_libraries([Library("esphome/A", None, None)], _backend())
records = [
r
for r in caplog.records
if "has no version to resolve and nothing provides it" in r.message
]
assert records and all(r.levelno == logging.DEBUG for r in records)
def test_url_version_dependency_is_not_substituted_by_provides(
@@ -1117,7 +1128,9 @@ def test_versionless_url_ish_dependency_name_warns_cleanly(
tmp_path,
{"esphome/A": {"name": "A", "dependencies": [{"name": "file://"}]}},
)
convert_libraries([Library("esphome/A", None, None)], _backend())
convert_libraries(
[Library("esphome/A", None, None)], _backend(provides=lambda name: False)
)
assert (
"file:// of esphome/A has no version to resolve and nothing provides it"
in caplog.text