From 557ba9953d0b57abb43dad98be5741f84162ec72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 22 Aug 2026 14:21:08 -0500 Subject: [PATCH] Fail by name on a missing libraries dir; single frameworks warning; fix a vacuous test A framework tree without libraries/ now raises naming the path instead of silently rerouting every bundled name to the registry, matching the empty-bundled-dir error. check_library_data accepts framework=None to skip the framework check (mirroring platform=None), so the backend-side platform re-check no longer re-fires the frameworks-mismatch warning the walk already emitted. The platform-rejection-is-debug test patched the wrong module and asserted nothing; it now patches the backend's import and asserts the dependency is absent from the resolved set. --- esphome/arduino/library.py | 17 +++++++++++------ esphome/platformio/library.py | 7 ++++--- tests/unit_tests/test_arduino_library.py | 15 +++++++++++++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py index 625ac865d7..869a939f7c 100644 --- a/esphome/arduino/library.py +++ b/esphome/arduino/library.py @@ -331,11 +331,14 @@ def resolve_libraries( # macOS/Windows and build the bundled Wire twice); the safety guard # stays fused with the lookup (path traversal) libraries_dir = framework_path / "libraries" - bundled_dir_names = ( - frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir()) - if libraries_dir.is_dir() - else frozenset() - ) + if not libraries_dir.is_dir(): + # Falling back to the registry would fail later with a misleading + # package-not-found error for every bundled name + raise EsphomeError( + f"{libraries_dir} is missing; the framework install may be " + "incomplete (run 'esphome clean-all')" + ) + bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir()) def _provided(name: object) -> bool: return _is_safe_library_name(name) and name in bundled_dir_names @@ -404,7 +407,9 @@ def resolve_libraries( # via the converter, and the walk reports any real drops continue try: - check_library_data(dep, pio_platform, "arduino") + # framework=None: the walk already warned for a frameworks + # mismatch; re-checking would warn twice + check_library_data(dep, pio_platform, None) except InvalidLibrary as err: # The shared walk already reported any non-platform cause; # warning again here would read as two distinct failures diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 36de4313fc..d9c7cf4c1e 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -429,7 +429,7 @@ def split_list_by_condition( return matched, non_matched -def check_library_data(data: dict, platform: str | None, framework: str): +def check_library_data(data: dict, platform: str | None, framework: str | None): """ Check whether a library manifest is compatible with the target toolchain. @@ -446,7 +446,8 @@ def check_library_data(data: dict, platform: str | None, framework: str): for targets (e.g. Zephyr) where PIO manifests rarely declare the platform yet portable libraries still build. framework: The active framework name (e.g. ``espidf``, ``arduino``, - ``zephyr``) the manifest is expected to declare. + ``zephyr``) the manifest is expected to declare. ``None`` skips + the framework check (and its warning), mirroring ``platform``. Raises: InvalidLibrary: If the library does not support the target platform. @@ -472,7 +473,7 @@ def check_library_data(data: dict, platform: str | None, framework: str): # under the target framework, and there's no way to opt out of the check at # this layer. Warn instead of failing so the user isn't forced to fork the # library to fix the manifest. - valid_framework = "*" in frameworks or framework in frameworks + valid_framework = framework is None or "*" in frameworks or framework in frameworks if not valid_framework: _LOGGER.warning( diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py index 349e0cd7f1..87dbd9b088 100644 --- a/tests/unit_tests/test_arduino_library.py +++ b/tests/unit_tests/test_arduino_library.py @@ -458,6 +458,16 @@ def test_nonplatform_rejection_warns_once_through_real_converter( assert caplog.text.count("manifest is corrupt") == 1 +def test_missing_libraries_dir_is_a_broken_install(tmp_path: Path) -> None: + """A framework tree without libraries/ must fail by name, not silently + reroute every bundled name to the registry.""" + framework = tmp_path / "framework" + framework.mkdir() + _add_library("Wire", None) + with pytest.raises(EsphomeError, match="framework install may be incomplete"): + _resolve(framework) + + def test_provided_is_case_sensitive(tmp_path: Path) -> None: """Membership uses the exact on-disk names, so a case-insensitive filesystem cannot add the same bundled library twice.""" @@ -595,12 +605,13 @@ def test_bundled_dependency_platform_rejection_is_debug( with ( _emitting_converter(converted), patch.object( - pio_library, + component, "check_library_data", side_effect=IncompatiblePlatform("nothing about the p-word here"), ), ): - _resolve(framework) + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] assert "Skipping dependency Wire" not in caplog.text