From 3fa4d478e5a0605c939d113b412d98c3af0c0aba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 22:52:39 -0500 Subject: [PATCH] Fail by name on a corrupt bundled manifest; name the recording side effect A truncated bundled library.json now raises the module's named error with the clean-all hint instead of a raw JSONDecodeError. The renamed _reconcile_versionless_skips advertises that it records into backend.provided_requests. Two real-converter tests gain the registry-guard patch their siblings use so a regression fails offline. --- esphome/arduino/library.py | 8 +++++++- esphome/platformio/library.py | 10 ++++++---- tests/unit_tests/test_arduino_library.py | 23 +++++++++++++++++++++-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index affa053741..e2fa0a705e 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -257,7 +257,13 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary: lib_dir = framework_path / "libraries" / name manifest_json = lib_dir / "library.json" if manifest_json.is_file(): - data = parse_library_json(manifest_json) + try: + data = parse_library_json(manifest_json) + except ValueError as err: # JSONDecodeError + raise EsphomeError( + f"Bundled library {name} has a corrupt library.json ({err}); " + "the framework install may be incomplete (run 'esphome clean-all')" + ) from err elif (manifest := lib_dir / "library.properties").is_file(): data = parse_library_properties(manifest) else: diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 64785c105a..20211a9fc0 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -924,13 +924,15 @@ def is_lib_ignored(name: str | None, lib_ignore: set[str]) -> bool: ) -def _warn_unsatisfied_versionless( +def _reconcile_versionless_skips( skipped_versionless: list[tuple[Any, Any, str]], components: dict[str, ConvertedLibrary], backend: LibraryBackend, ) -> None: - """Warn for version-less deps nothing satisfied; a silent drop - surfaces as link errors far from the cause.""" + """Warn for version-less deps nothing satisfied, and record the + backend-provided ones in ``backend.provided_requests`` for its + post-emit reconciliation; a silent drop surfaces as link errors far + from the cause.""" resolved_manifest_names = {c.data.get("name") for c in components.values()} # A treeless backend can never supply a bundled name; noise for it log = _LOGGER.warning if backend.provides is not None else _LOGGER.debug @@ -1355,6 +1357,6 @@ def convert_libraries( for component in components.values(): backend.emit(component) - _warn_unsatisfied_versionless(skipped_versionless, components, backend) + _reconcile_versionless_skips(skipped_versionless, components, backend) return [components[key] for key in top_level if key in components] diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 6969eebed4..dcadb47317 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -900,6 +900,15 @@ def test_bundled_library_non_dict_manifest_skips_probes_and_raises( component._bundled_library(framework, "Wire") +def test_bundled_corrupt_library_json_fails_by_name(tmp_path: Path) -> None: + """A truncated bundled library.json fails with the library name and the + clean-all hint, not a raw JSONDecodeError.""" + framework = _make_framework(tmp_path) + (framework / "libraries" / "Wire" / "library.json").write_text("{truncated") + with pytest.raises(EsphomeError, match="Wire has a corrupt library.json"): + component._bundled_library(framework, "Wire") + + def test_dict_shorthand_dependency_skips_registry_through_real_converter( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -931,7 +940,12 @@ def test_versionless_provides_skip_is_reconciled_through_real_converter( framework = _make_framework(tmp_path) _local_lib(tmp_path, ["Wire"]) monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) - libs = _resolve(framework) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) assert "Wire" in [lib.name for lib in libs] @@ -943,7 +957,12 @@ def test_platform_filtered_bundled_candidate_does_not_break_reconciliation( 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) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) assert "Wire" not in [lib.name for lib in libs]