mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
This commit is contained in:
+57
-106
@@ -25,22 +25,22 @@ include path.
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import functools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.core import CORE, EsphomeError, Library
|
||||
from esphome.helpers import walk_files
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_INCLUDE_DIR,
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
SRC_FILE_EXTENSIONS,
|
||||
ConvertedLibrary,
|
||||
IncompatiblePlatform,
|
||||
InvalidLibrary,
|
||||
LibraryBackend,
|
||||
check_library_data,
|
||||
collect_filtered_files,
|
||||
convert_libraries,
|
||||
dependency_is_usable,
|
||||
ensure_list,
|
||||
is_lib_ignored,
|
||||
lex_build_flags,
|
||||
@@ -48,7 +48,6 @@ from esphome.platformio.library import (
|
||||
normalize_dependencies,
|
||||
parse_library_json,
|
||||
parse_library_properties,
|
||||
request_key,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -80,10 +79,26 @@ def _is_safe_library_name(name: object) -> bool:
|
||||
and bool(name)
|
||||
and "/" not in name
|
||||
and "\\" not in name
|
||||
and ":" not in name # a Windows drive-relative name escapes the tree
|
||||
and name not in (".", "..")
|
||||
)
|
||||
|
||||
|
||||
def _warn_properties_depends(name: str, data: object) -> None:
|
||||
"""Warn when a manifest declares dependencies only as ``depends=``.
|
||||
|
||||
The dependency walk reads the JSON ``dependencies`` key; the raw
|
||||
``library.properties`` spelling would otherwise drop silently.
|
||||
"""
|
||||
if isinstance(data, dict) and not data.get("dependencies") and data.get("depends"):
|
||||
_LOGGER.warning(
|
||||
"Library %s declares dependencies via library.properties "
|
||||
"depends=, which are not resolved automatically; add them with "
|
||||
"add_library() if needed",
|
||||
name,
|
||||
)
|
||||
|
||||
|
||||
def _manifest_build(name: str, data: object) -> dict:
|
||||
"""The manifest's ``build`` section, validated by name.
|
||||
|
||||
@@ -130,8 +145,9 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
|
||||
def _parse_archive(key: str, raw: object) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
if str(raw).strip().lower() in ("true", "false"):
|
||||
return str(raw).strip().lower() == "true"
|
||||
value = str(raw).strip().lower()
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
_LOGGER.warning(
|
||||
"Library %s has an unrecognized %s value %r; assuming true",
|
||||
name,
|
||||
@@ -172,10 +188,14 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
|
||||
include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR)
|
||||
if not isinstance(include_dir, str):
|
||||
raise EsphomeError(f"Library {name} has a malformed includeDir")
|
||||
for d in [include_dir, src_dir, *include_flags]:
|
||||
for d, explicit in [
|
||||
(include_dir, "includeDir" in build),
|
||||
(src_dir, False), # the srcDir guard above already validated it
|
||||
*((flag, True) for flag in include_flags),
|
||||
]:
|
||||
if (path := (read_path / d)).is_dir():
|
||||
lib.include_dirs.append(path.resolve())
|
||||
elif d in include_flags or (d == include_dir and "includeDir" in build):
|
||||
elif explicit:
|
||||
# The includeDir/srcDir defaults are probes; an explicitly
|
||||
# declared path that does not resolve is a manifest error
|
||||
_LOGGER.warning(
|
||||
@@ -214,15 +234,14 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
|
||||
if isinstance(data, dict):
|
||||
# The dependency walk never runs for bundled libraries (a no-op for
|
||||
# the ESP8266 core, whose bundled manifests declare none); on a core
|
||||
# where one does, the skip must be visible before link errors.
|
||||
# "depends" is the library.properties spelling, which the shared
|
||||
# parser returns raw.
|
||||
if data.get("dependencies") or data.get("depends"):
|
||||
# where one does, the skip must be visible before link errors
|
||||
if data.get("dependencies"):
|
||||
_LOGGER.warning(
|
||||
"Bundled library %s declares dependencies, which are not "
|
||||
"resolved automatically; add them with add_library() if needed",
|
||||
name,
|
||||
)
|
||||
_warn_properties_depends(name, data)
|
||||
build = data.get("build")
|
||||
if isinstance(build, dict) and build.get("extraScript"):
|
||||
# apply_extra_script only runs on the converted path; a bundled
|
||||
@@ -234,9 +253,9 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
|
||||
)
|
||||
lib = _library_info(name, lib_dir, data)
|
||||
if not lib.sources and not any(
|
||||
p.suffix in (".h", ".hpp", ".hh", ".inc")
|
||||
Path(p).suffix in (".h", ".hpp", ".hh", ".inc")
|
||||
for d in lib.include_dirs
|
||||
for p in d.rglob("*")
|
||||
for p in walk_files(d)
|
||||
):
|
||||
# An empty or half-extracted bundled directory can never link; a
|
||||
# warning would scroll away and resurface as undefined symbols
|
||||
@@ -265,6 +284,16 @@ def resolve_libraries(
|
||||
# PlatformIO's lib_ignore covers framework-bundled libraries too; the
|
||||
# shared converter only filters the registry/git ones.
|
||||
lib_ignore = lib_ignore_set()
|
||||
# One memoized answer to "does the framework bundle this name?" for the
|
||||
# classification loop, the provides hook, and the dependency walk: the
|
||||
# safety guard and the dir probe must stay fused (path traversal), and
|
||||
# common names (Wire, SPI) are asked repeatedly
|
||||
_provided = functools.cache(
|
||||
lambda name: (
|
||||
_is_safe_library_name(name)
|
||||
and (framework_path / "libraries" / name).is_dir()
|
||||
)
|
||||
)
|
||||
for library in CORE.platformio_libraries.values():
|
||||
if is_lib_ignored(library.name, lib_ignore):
|
||||
continue
|
||||
@@ -273,12 +302,7 @@ def resolve_libraries(
|
||||
# name without the directory resolves from the registry at the
|
||||
# latest version, matching PlatformIO (a typo fails loudly as a
|
||||
# registry lookup error).
|
||||
if (
|
||||
not library.repository
|
||||
and not library.version
|
||||
and _is_safe_library_name(library.name)
|
||||
and (framework_path / "libraries" / library.name).is_dir()
|
||||
):
|
||||
if not library.repository and not library.version and _provided(library.name):
|
||||
# A bundled library's own manifest dependencies are not walked.
|
||||
# PlatformIO would walk them even under lib_ldf_mode=off, but no
|
||||
# library bundled with the ESP8266 core declares any, so the walk
|
||||
@@ -294,21 +318,11 @@ def resolve_libraries(
|
||||
# "skipping" warning teaches users to ignore the real one)
|
||||
external_short_names = {lib.name.split("/")[-1] for lib in external if lib.name}
|
||||
|
||||
pending_drops: list[tuple[str, str]] = []
|
||||
|
||||
def _add_bundled_dependencies(component: ConvertedLibrary) -> None:
|
||||
# A version-less bare-name dependency ("Hash" in ESPAsyncWebServer)
|
||||
# is a core-bundled library; the shared converter skips it because
|
||||
# it cannot be resolved from the registry.
|
||||
if not component.data.get("dependencies") and component.data.get("depends"):
|
||||
# A properties-only manifest spells dependencies depends=; the
|
||||
# walk below reads the JSON key, so those are not resolved
|
||||
_LOGGER.warning(
|
||||
"Library %s declares dependencies via library.properties "
|
||||
"depends=, which are not resolved automatically; add them "
|
||||
"with add_library() if needed",
|
||||
component.name,
|
||||
)
|
||||
_warn_properties_depends(component.name, component.data)
|
||||
for dep in normalize_dependencies(
|
||||
component.data.get("dependencies"), component.name
|
||||
):
|
||||
@@ -328,52 +342,23 @@ def resolve_libraries(
|
||||
or is_lib_ignored(name, lib_ignore)
|
||||
):
|
||||
continue
|
||||
bundled_dir = framework_path / "libraries" / name
|
||||
if "version" in dep and (dep.get("owner") or not bundled_dir.is_dir()):
|
||||
if "version" in dep and (dep.get("owner") or not _provided(name)):
|
||||
# The converter resolves versioned deps from the registry. An
|
||||
# owner-less versioned name that exists in the framework tree
|
||||
# ({"Wire": "*"} normalizes to version="*") falls through to
|
||||
# the bundled path below, matching PlatformIO's
|
||||
# process_dependencies preference for bundled builders.
|
||||
continue
|
||||
if dep.get("owner"):
|
||||
# Owner but no version: the converter skips it too, so this
|
||||
# is the only place the drop can be made visible
|
||||
_LOGGER.warning(
|
||||
"Dependency %s of library %s has an owner but no version "
|
||||
"to resolve; skipping",
|
||||
name,
|
||||
component.name,
|
||||
)
|
||||
if dep.get("owner") or not _provided(name):
|
||||
# The shared walk's post-emit reconciliation reports drops
|
||||
# (it alone knows the final resolution set); nothing to add
|
||||
continue
|
||||
if not bundled_dir.is_dir():
|
||||
# Deferred: the walk may still resolve this name as another
|
||||
# library's transitive registry dependency, and a false
|
||||
# "skipping" warning teaches users to ignore the real one
|
||||
pending_drops.append((name, component.name))
|
||||
continue
|
||||
try:
|
||||
check_library_data(dep, pio_platform, "arduino")
|
||||
except InvalidLibrary as err:
|
||||
# Rejecting another platform's dependency of a cross-platform
|
||||
# manifest is routine (every ESPAsyncWebServer build hits
|
||||
# it), so the platform filter stays at debug; any other
|
||||
# cause means a dropped dependency and must be visible
|
||||
if isinstance(err, IncompatiblePlatform):
|
||||
_LOGGER.debug("Skipping bundled dependency %s: %s", name, err)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Skipping bundled dependency %s of %s: %s",
|
||||
name,
|
||||
component.name,
|
||||
err,
|
||||
)
|
||||
if not dependency_is_usable(dep, pio_platform, "arduino", component.name):
|
||||
continue
|
||||
bundled_names.add(name)
|
||||
bundled.append(_bundled_library(framework_path, name))
|
||||
|
||||
def _emit(component: ConvertedLibrary) -> None:
|
||||
_manifest_build(component.get_require_name(), component.data)
|
||||
apply_extra_script(
|
||||
component, board_mcu=lambda: board_mcu, pio_platform=pio_platform
|
||||
)
|
||||
@@ -385,7 +370,10 @@ def resolve_libraries(
|
||||
_add_bundled_dependencies(component)
|
||||
|
||||
if external:
|
||||
resolved = convert_libraries(
|
||||
# Every converter drop path raises (an incompatible top-level is a
|
||||
# RuntimeError, resolution and download failures raise), so the
|
||||
# return needs no re-verification here.
|
||||
convert_libraries(
|
||||
external,
|
||||
LibraryBackend(
|
||||
platform=pio_platform,
|
||||
@@ -394,47 +382,10 @@ def resolve_libraries(
|
||||
cache_key=cache_key,
|
||||
# The graph walk must not resolve a bundled name from the
|
||||
# registry ({"Wire": "*"} in a manifest); the bundled copy
|
||||
# is added by _add_bundled_dependencies after emit
|
||||
provides=lambda name: (framework_path / "libraries" / name).is_dir(),
|
||||
# is added by _add_bundled_dependencies after emit. Unsafe
|
||||
# names are simply not provided.
|
||||
provides=_provided,
|
||||
),
|
||||
)
|
||||
if len(resolved) < len(external):
|
||||
# A requested library the converter dropped always makes the
|
||||
# firmware wrong (link errors far from the cause), so fail here
|
||||
# naming the requests. ConvertedLibrary.name is canonical (bare
|
||||
# "pngle" resolves to "bitbank2__pngle"); diff the request-side
|
||||
# node keys instead. A None node_key is a converter construction
|
||||
# path that forgot to set it: a programming error, never
|
||||
# silently substituted with the mismatched canonical name.
|
||||
if unkeyed := sorted(c.name for c in resolved if c.node_key is None):
|
||||
raise EsphomeError(
|
||||
"ConvertedLibrary without a node_key (a converter bug): "
|
||||
+ ", ".join(unkeyed)
|
||||
)
|
||||
resolved_keys = {c.node_key for c in resolved}
|
||||
dropped = sorted(
|
||||
str(lib) for lib in external if request_key(lib) not in resolved_keys
|
||||
)
|
||||
raise EsphomeError(
|
||||
f"{len(external) - len(resolved)} of {len(external)} requested "
|
||||
f"libraries were not resolved (missing: "
|
||||
f"{', '.join(dropped) or 'unknown'})"
|
||||
)
|
||||
|
||||
# The shared converter skips version-less deps too, so this is the only
|
||||
# place a genuine drop can be made visible before the missing sources
|
||||
# surface as link errors; names the walk resolved anyway stay quiet.
|
||||
# Split once from the left: strip the sanitized owner prefix only, so a
|
||||
# library whose own name carries "__" still matches
|
||||
resolved_short_names = {c.name.split("__", 1)[-1] for c in converted}
|
||||
for name, requester in pending_drops:
|
||||
if name in bundled_names or name in resolved_short_names:
|
||||
continue
|
||||
_LOGGER.warning(
|
||||
"Dependency %s of library %s is not bundled with the framework "
|
||||
"and has no version to resolve; skipping",
|
||||
name,
|
||||
requester,
|
||||
)
|
||||
|
||||
return bundled + converted
|
||||
|
||||
@@ -88,14 +88,14 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
|
||||
build_src_filter = ensure_list(
|
||||
component.data.get("build", {}).get("srcFilter", DEFAULT_BUILD_SRC_FILTER)
|
||||
)
|
||||
build_flags = ensure_list(
|
||||
component.data.get("build", {}).get("flags", DEFAULT_BUILD_FLAGS)
|
||||
)
|
||||
# PlatformIO shell-lexes each build.flags entry, so one entry can carry a
|
||||
# flag and its argument (e.g. "-include cp_custom_alloc.h"); bare
|
||||
# -I/-L/-l/-D tokens re-glue to their argument ("-I foo" -> "-Ifoo") so
|
||||
# prefix classifiers below still route them.
|
||||
build_flags = lex_build_flags(build_flags, f"library {component.name}")
|
||||
build_flags = lex_build_flags(
|
||||
component.data.get("build", {}).get("flags", DEFAULT_BUILD_FLAGS),
|
||||
f"library {component.name}",
|
||||
)
|
||||
|
||||
# List all sources files
|
||||
build_src_files = collect_filtered_files(
|
||||
|
||||
@@ -229,10 +229,6 @@ class ConvertedLibrary:
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.source = source
|
||||
# The request-side _node_key this component resolved from; the
|
||||
# canonical name can differ (bare "pngle" -> "bitbank2__pngle"), so
|
||||
# callers diff requests against this, not against name
|
||||
self.node_key: str | None = None
|
||||
self.data = {}
|
||||
self.dependencies: list[ConvertedLibrary] = []
|
||||
self._path: Path | None = None
|
||||
@@ -594,9 +590,10 @@ def split_flag_entry(entry: Any, owner: str) -> list[str]:
|
||||
def lex_build_flags(entries: str | list[str], owner: str) -> list[str]:
|
||||
"""Shell-lex a manifest ``build.flags`` list into joined tokens.
|
||||
|
||||
The composition every backend needs: each entry is lexed the way
|
||||
PlatformIO's ParseFlags does, and bare ``-I``/``-L``/``-l``/``-D``
|
||||
tokens re-glue to their argument across the whole stream.
|
||||
Each entry is lexed the way PlatformIO's ParseFlags does, and bare
|
||||
``-I``/``-L``/``-l``/``-D`` tokens re-glue to their argument across the
|
||||
whole stream. Used by the espidf and arduino backends; zephyr still
|
||||
classifies raw entries.
|
||||
"""
|
||||
# Join per entry, as SCons's ParseFlags lexes each string independently:
|
||||
# a dangling -I ending one entry must warn, not absorb the next entry's
|
||||
@@ -628,6 +625,29 @@ def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def dependency_is_usable(
|
||||
dep: dict, platform: str | None, framework: str, requester: str
|
||||
) -> bool:
|
||||
"""Whether a manifest dependency passes the compatibility filter.
|
||||
|
||||
The routine cross-platform skip logs at debug; any other
|
||||
``InvalidLibrary`` cause is a dropped dependency and warns naming the
|
||||
requester (unreachable from ``check_library_data`` today, which raises
|
||||
only for the platform filter).
|
||||
"""
|
||||
try:
|
||||
check_library_data(dep, platform, framework)
|
||||
except IncompatiblePlatform as e:
|
||||
_LOGGER.debug("Skip dependency %s of %s: %s", dep.get("name"), requester, e)
|
||||
return False
|
||||
except InvalidLibrary as e:
|
||||
_LOGGER.warning(
|
||||
"Skipping dependency %s of %s: %s", dep.get("name"), requester, e
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def normalize_dependencies(
|
||||
dependencies: Any, manifest_name: str = "manifest"
|
||||
) -> list[dict]:
|
||||
@@ -662,6 +682,16 @@ def normalize_dependencies(
|
||||
normalized = []
|
||||
for entry in dependencies:
|
||||
if isinstance(entry, dict):
|
||||
name = entry.get("name")
|
||||
if "name" in entry and (not isinstance(name, str) or not name):
|
||||
# A dependency name must be a non-empty string; every
|
||||
# consumer indexes or joins it
|
||||
_LOGGER.warning(
|
||||
"Ignoring unrecognized dependency entry %r of %s",
|
||||
entry,
|
||||
manifest_name,
|
||||
)
|
||||
continue
|
||||
normalized.append(entry)
|
||||
elif isinstance(entry, str) and entry:
|
||||
# PIO also accepts a bare list of names ("dependencies":
|
||||
@@ -804,15 +834,6 @@ def is_lib_ignored(name: str | None, lib_ignore: set[str]) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def request_key(library: Library) -> str:
|
||||
"""The node key a library spec resolves under.
|
||||
|
||||
Pairs with ``ConvertedLibrary.node_key``: diff requests against resolved
|
||||
components with these, not against the canonical ``name``.
|
||||
"""
|
||||
return _node_key(library.name, library.version, library.repository)[0]
|
||||
|
||||
|
||||
def convert_libraries(
|
||||
libraries: list[Library], backend: LibraryBackend
|
||||
) -> list[ConvertedLibrary]:
|
||||
@@ -907,6 +928,8 @@ def convert_libraries(
|
||||
components: dict[str, ConvertedLibrary] = {}
|
||||
resolved_requirements: dict[str, frozenset[str]] = {}
|
||||
top_level_keys = set(top_level)
|
||||
# (name, requester) pairs reconciled against the final resolution set
|
||||
skipped_versionless: list[tuple[Any, str]] = []
|
||||
worklist = deque(dict.fromkeys(top_level))
|
||||
while worklist:
|
||||
key = worklist.popleft()
|
||||
@@ -932,7 +955,6 @@ def convert_libraries(
|
||||
component = ConvertedLibrary(
|
||||
_owner_pkgname_to_name(owner, name), version, URLSource(url)
|
||||
)
|
||||
component.node_key = key
|
||||
component.download(salt=salt, namespace=backend.cache_key)
|
||||
|
||||
source_dir = component.source_dir
|
||||
@@ -970,6 +992,13 @@ def convert_libraries(
|
||||
f"library.properties in {source_dir}"
|
||||
)
|
||||
|
||||
if not isinstance(component.data, dict) or not isinstance(
|
||||
component.data.get("build", {}), dict
|
||||
):
|
||||
# A bare json.load imposes no shape; every backend dereferences
|
||||
# data/build, so validate once here and name the library
|
||||
raise EsphomeError(f"Library {key} has a malformed manifest")
|
||||
|
||||
try:
|
||||
check_library_data(component.data, backend.platform, backend.framework)
|
||||
except InvalidLibrary as e:
|
||||
@@ -991,59 +1020,20 @@ def convert_libraries(
|
||||
component.data.get("dependencies"), component.name
|
||||
):
|
||||
if "name" not in dependency or "version" not in dependency:
|
||||
dep_name = dependency.get("name")
|
||||
if (
|
||||
isinstance(dep_name, str)
|
||||
# _node_key raises for a malformed URL-ish name; that
|
||||
# entry belongs to the warning below, not a traceback
|
||||
and "://" not in dep_name
|
||||
and _node_key(dep_name, None, None)[0] in top_level_keys
|
||||
):
|
||||
# Already requested top-level: present in the build, not
|
||||
# a drop (a false warning teaches users to ignore the
|
||||
# real one)
|
||||
_LOGGER.debug(
|
||||
"Version-less dependency %r of %s is requested top-level",
|
||||
dep_name,
|
||||
component.name,
|
||||
)
|
||||
elif backend.provides is None:
|
||||
# No backend tree can supply it and the registry cannot
|
||||
# resolve it: a real drop, not a routine skip
|
||||
_LOGGER.warning(
|
||||
"Dependency %r of %s has no version to resolve; skipping",
|
||||
dep_name,
|
||||
component.name,
|
||||
)
|
||||
else:
|
||||
# A provides backend owns the post-emit reporting (the
|
||||
# arduino backend defers drops and suppresses names the
|
||||
# walk resolved, which this layer cannot know yet), so
|
||||
# warning here would duplicate or false-positive
|
||||
_LOGGER.debug(
|
||||
"Skip version-less dependency %r of %s",
|
||||
dep_name,
|
||||
component.name,
|
||||
)
|
||||
# Version-less deps cannot resolve from the registry.
|
||||
# Deferred: only the final resolution set can tell a real
|
||||
# drop from a name another manifest resolves later, so the
|
||||
# reconciliation after emit owns the warning
|
||||
_LOGGER.debug(
|
||||
"Skip version-less dependency %r of %s",
|
||||
dependency.get("name"),
|
||||
component.name,
|
||||
)
|
||||
skipped_versionless.append((dependency.get("name"), component.name))
|
||||
continue
|
||||
try:
|
||||
check_library_data(dependency, backend.platform, backend.framework)
|
||||
except InvalidLibrary as e:
|
||||
if isinstance(e, IncompatiblePlatform):
|
||||
# Routine cross-platform skip
|
||||
_LOGGER.debug(
|
||||
"Skip dependency %s: %s", dependency.get("name"), str(e)
|
||||
)
|
||||
else:
|
||||
# Any other cause is a dropped dependency and must be
|
||||
# visible (unreachable from check_library_data today,
|
||||
# which raises only for the platform filter)
|
||||
_LOGGER.warning(
|
||||
"Skipping dependency %s of %s: %s",
|
||||
dependency.get("name"),
|
||||
component.name,
|
||||
e,
|
||||
)
|
||||
if not dependency_is_usable(
|
||||
dependency, backend.platform, backend.framework, component.name
|
||||
):
|
||||
continue
|
||||
dep_name = _owner_pkgname_to_name(
|
||||
dependency.get("owner"), dependency.get("name")
|
||||
@@ -1129,4 +1119,29 @@ def convert_libraries(
|
||||
for component in components.values():
|
||||
backend.emit(component)
|
||||
|
||||
# A version-less dependency is satisfied when its request key resolved,
|
||||
# a resolved component's manifest name matches, or the backend provides
|
||||
# it from its own tree (e.g. the arduino bundled libraries, added by the
|
||||
# backend after emit). Anything else is a real drop that would otherwise
|
||||
# surface as link errors far from the cause.
|
||||
resolved_manifest_names = {c.data.get("name") for c in components.values()}
|
||||
warned: set[str] = set()
|
||||
for dep_name, requester in skipped_versionless:
|
||||
if not isinstance(dep_name, str) or not dep_name or dep_name in warned:
|
||||
continue
|
||||
if dep_name in components:
|
||||
# A version-less dep's request key is the name itself
|
||||
continue
|
||||
if dep_name in resolved_manifest_names:
|
||||
continue
|
||||
if backend.provides is not None and backend.provides(dep_name):
|
||||
continue
|
||||
warned.add(dep_name)
|
||||
_LOGGER.warning(
|
||||
"Dependency %s of %s has no version to resolve and nothing "
|
||||
"provides it; skipping",
|
||||
dep_name,
|
||||
requester,
|
||||
)
|
||||
|
||||
return [components[key] for key in top_level if key in components]
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -16,7 +17,7 @@ from esphome.platformio.library import ConvertedLibrary, LibraryBackend
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_libraries() -> None:
|
||||
CORE.platformio_libraries = {}
|
||||
# conftest's reset_core fixture clears platformio_libraries after each test
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP8266}
|
||||
|
||||
|
||||
@@ -58,6 +59,60 @@ def _emitting_converter(*converted):
|
||||
yield mock_extra
|
||||
|
||||
|
||||
def _converted(name: str, source_dir: Path, data: dict) -> ConvertedLibrary:
|
||||
converted = ConvertedLibrary(name, "1.0.0", source=None)
|
||||
converted.path = source_dir
|
||||
converted.data = data
|
||||
return converted
|
||||
|
||||
|
||||
def _resolve(framework: Path) -> list[component.ArduinoLibrary]:
|
||||
return component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
|
||||
|
||||
def _webserver(tmp_path: Path, data: dict) -> ConvertedLibrary:
|
||||
"""Register ESPAsyncWebServer and return its converted stand-in."""
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
return _converted("esp32async__ESPAsyncWebServer", lib_dir, data)
|
||||
|
||||
|
||||
def _local_lib(tmp_path: Path, dependencies: dict | list) -> None:
|
||||
"""Register a local file:// library declaring the given dependencies."""
|
||||
local_lib = tmp_path / "locallib"
|
||||
(local_lib / "src").mkdir(parents=True)
|
||||
(local_lib / "src" / "local.cpp").write_text("")
|
||||
(local_lib / "library.json").write_text(
|
||||
json.dumps(
|
||||
{"name": "LocalLib", "version": "1.0.0", "dependencies": dependencies}
|
||||
)
|
||||
)
|
||||
# as_uri() forms a valid file:// URL on every platform (file:///C:/...
|
||||
# on Windows; a bare f-string would embed backslashes)
|
||||
_add_library(local_lib.as_uri(), None)
|
||||
|
||||
|
||||
def _ws_tcp_pair(tmp_path: Path) -> tuple[ConvertedLibrary, ConvertedLibrary]:
|
||||
"""Build ESPAsyncWebServer (depending on ESPAsyncTCP) plus resolved TCP."""
|
||||
ws_dir = tmp_path / "converted" / "webserver"
|
||||
(ws_dir / "src").mkdir(parents=True)
|
||||
tcp_dir = tmp_path / "converted" / "tcp"
|
||||
(tcp_dir / "src").mkdir(parents=True)
|
||||
ws = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
ws_dir,
|
||||
{"build": {}, "dependencies": [{"name": "ESPAsyncTCP"}]},
|
||||
)
|
||||
tcp = _converted("esp32async__ESPAsyncTCP", tcp_dir, {"build": {}})
|
||||
return ws, tcp
|
||||
|
||||
|
||||
def test_library_info_src_layout(tmp_path: Path) -> None:
|
||||
framework = _make_framework(tmp_path)
|
||||
lib = component._bundled_library(framework, "ESP8266WiFi")
|
||||
@@ -149,41 +204,25 @@ def test_library_info_no_src_dir(tmp_path: Path) -> None:
|
||||
def test_resolve_libraries_bundled(tmp_path: Path) -> None:
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP8266WiFi", None)
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert [lib.name for lib in libs] == ["ESP8266WiFi"]
|
||||
|
||||
|
||||
def test_resolve_libraries_bare_registry_name_is_external(tmp_path: Path) -> None:
|
||||
"""A bare name that is not bundled resolves from the registry at the
|
||||
latest version, matching PlatformIO and the documented libraries: key."""
|
||||
@pytest.mark.parametrize("version", [None, "1.1.0"])
|
||||
def test_resolve_libraries_registry_name_is_external(
|
||||
tmp_path: Path, version: str | None
|
||||
) -> None:
|
||||
"""A name that is not bundled reaches the converter: bare resolves from
|
||||
the registry at the latest version (matching PlatformIO and the
|
||||
documented libraries: key) and a version pin is a registry package."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("pngle", None)
|
||||
with (
|
||||
patch.object(component, "convert_libraries", return_value=[]) as mock_convert,
|
||||
pytest.raises(EsphomeError, match="not resolved"),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_add_library("pngle", version)
|
||||
with patch.object(component, "convert_libraries", return_value=[]) as mock_convert:
|
||||
_resolve(framework)
|
||||
(libraries, _backend), _ = mock_convert.call_args
|
||||
assert [lib.name for lib in libraries] == ["pngle"]
|
||||
|
||||
|
||||
def _converted(name: str, source_dir: Path, data: dict) -> ConvertedLibrary:
|
||||
converted = ConvertedLibrary(name, "1.0.0", source=None)
|
||||
converted.path = source_dir
|
||||
converted.data = data
|
||||
return converted
|
||||
|
||||
|
||||
def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None:
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
@@ -210,12 +249,7 @@ def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
with _emitting_converter(converted) as mock_extra:
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
|
||||
mock_extra.assert_called_once()
|
||||
assert mock_extra.call_args.args == (converted,)
|
||||
@@ -240,38 +274,12 @@ def test_resolve_libraries_bundled_dep_already_present(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
with _emitting_converter(converted):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
|
||||
# Wire appears once (from the explicit registration), not twice
|
||||
assert [lib.name for lib in libs] == ["Wire", "some__External"]
|
||||
|
||||
|
||||
def test_resolve_libraries_versioned_bare_name_is_external(tmp_path: Path) -> None:
|
||||
"""A bare name with a version pin ("pngle@1.1.0") is a registry package,
|
||||
not a bundled library, and must reach the converter."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("pngle", "1.1.0")
|
||||
|
||||
with (
|
||||
patch.object(component, "convert_libraries", return_value=[]) as mock_convert,
|
||||
pytest.raises(EsphomeError, match="not resolved"),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
|
||||
(libraries, _backend), _ = mock_convert.call_args
|
||||
assert [lib.name for lib in libraries] == ["pngle"]
|
||||
|
||||
|
||||
def test_library_info_trailing_bare_flag_warns(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -316,12 +324,7 @@ def test_resolve_libraries_lib_ignore_covers_bundled(tmp_path: Path) -> None:
|
||||
_add_library("ESP8266WiFi", None)
|
||||
_add_library("Wire", None)
|
||||
CORE.platformio_options = {"lib_ignore": ["Wire"]}
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert [lib.name for lib in libs] == ["ESP8266WiFi"]
|
||||
|
||||
|
||||
@@ -339,12 +342,7 @@ def test_resolve_libraries_lib_ignore_covers_bundled_dependencies(
|
||||
)
|
||||
|
||||
with _emitting_converter(converted):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
|
||||
assert [lib.name for lib in libs] == ["some__External"]
|
||||
|
||||
@@ -389,14 +387,11 @@ def test_library_info_lib_archive_flag(tmp_path: Path) -> None:
|
||||
def test_resolve_libraries_dep_warnings(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Nameless and owner-without-version dependencies are dropped loudly."""
|
||||
"""A nameless dependency entry warns; an owner-without-version entry is
|
||||
left to the shared walk's reconciliation (no local warning)."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
converted = _webserver(
|
||||
tmp_path,
|
||||
{
|
||||
"build": {},
|
||||
"dependencies": [
|
||||
@@ -406,34 +401,9 @@ def test_resolve_libraries_dep_warnings(
|
||||
},
|
||||
)
|
||||
with _emitting_converter(converted):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_resolve(framework)
|
||||
assert "malformed dependency entry" in caplog.text
|
||||
assert "Orphan" in caplog.text
|
||||
assert "owner but no version" in caplog.text
|
||||
|
||||
|
||||
def test_resolve_libraries_raises_when_converter_drops_a_request(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""A dropped top-level request always makes the firmware wrong; fail by
|
||||
name instead of warning toward link errors far from the cause."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("pngle", "1.0.0")
|
||||
with (
|
||||
patch.object(component, "convert_libraries", return_value=[]),
|
||||
pytest.raises(EsphomeError, match="1 of 1 requested .*missing: pngle"),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Orphan" not in caplog.text
|
||||
|
||||
|
||||
def test_bundled_dependency_nonplatform_rejection_warns(
|
||||
@@ -443,29 +413,19 @@ def test_bundled_dependency_nonplatform_rejection_warns(
|
||||
from esphome.platformio.library import InvalidLibrary
|
||||
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "dependencies": [{"name": "Wire"}]},
|
||||
)
|
||||
converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]})
|
||||
import esphome.platformio.library as pio_library
|
||||
|
||||
with (
|
||||
_emitting_converter(converted),
|
||||
patch.object(
|
||||
component,
|
||||
pio_library,
|
||||
"check_library_data",
|
||||
side_effect=InvalidLibrary("manifest is corrupt"),
|
||||
),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Skipping bundled dependency Wire" in caplog.text
|
||||
_resolve(framework)
|
||||
assert "Skipping dependency Wire" in caplog.text
|
||||
assert "manifest is corrupt" in caplog.text
|
||||
|
||||
|
||||
@@ -510,21 +470,9 @@ def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> No
|
||||
to the bundled library, matching PIO's process_dependencies, instead of
|
||||
being routed to the registry."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "dependencies": {"Wire": "*"}},
|
||||
)
|
||||
converted = _webserver(tmp_path, {"build": {}, "dependencies": {"Wire": "*"}})
|
||||
with _emitting_converter(converted):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
|
||||
|
||||
@@ -536,29 +484,19 @@ def test_bundled_dependency_platform_rejection_is_debug(
|
||||
from esphome.platformio.library import IncompatiblePlatform
|
||||
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "dependencies": [{"name": "Wire"}]},
|
||||
)
|
||||
converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]})
|
||||
import esphome.platformio.library as pio_library
|
||||
|
||||
with (
|
||||
_emitting_converter(converted),
|
||||
patch.object(
|
||||
component,
|
||||
pio_library,
|
||||
"check_library_data",
|
||||
side_effect=IncompatiblePlatform("nothing about the p-word here"),
|
||||
),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Skipping bundled dependency Wire" not in caplog.text
|
||||
_resolve(framework)
|
||||
assert "Skipping dependency Wire" not in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("data", [{"build": "src"}, [], "nope"])
|
||||
@@ -570,52 +508,6 @@ def test_library_info_malformed_manifest_is_named(tmp_path: Path, data: object)
|
||||
component._library_info("x", read_path, data)
|
||||
|
||||
|
||||
def test_drop_error_maps_requests_by_node_key(tmp_path: Path) -> None:
|
||||
"""A bare request resolving to a canonical name is not falsely reported
|
||||
missing; the genuinely dropped request is the one named."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("pngle", None)
|
||||
_add_library("gone/missing", "1.0.0")
|
||||
lib_dir = tmp_path / "converted" / "pngle"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
resolved = _converted("bitbank2__pngle", lib_dir, {"build": {}})
|
||||
resolved.node_key = "pngle"
|
||||
with (
|
||||
patch.object(component, "convert_libraries", return_value=[resolved]),
|
||||
pytest.raises(EsphomeError) as excinfo,
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
missing = str(excinfo.value).split("missing:")[-1]
|
||||
assert "gone/missing" in missing
|
||||
assert "pngle@" not in missing
|
||||
|
||||
|
||||
def test_drop_error_without_node_key_is_a_converter_bug(tmp_path: Path) -> None:
|
||||
"""A resolved component missing its node_key must fail as a programming
|
||||
error, never silently substitute the mismatched canonical name."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("pngle", None)
|
||||
_add_library("gone/missing", "1.0.0")
|
||||
lib_dir = tmp_path / "converted" / "pngle"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
resolved = _converted("bitbank2__pngle", lib_dir, {"build": {}})
|
||||
with (
|
||||
patch.object(component, "convert_libraries", return_value=[resolved]),
|
||||
pytest.raises(EsphomeError, match="node_key .*bitbank2__pngle"),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
|
||||
|
||||
def test_bundled_library_with_declared_dependencies_warns(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -627,12 +519,7 @@ def test_bundled_library_with_declared_dependencies_warns(
|
||||
'{"name": "Wire", "dependencies": [{"name": "SPI"}]}'
|
||||
)
|
||||
_add_library("Wire", None)
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_resolve(framework)
|
||||
assert "Bundled library Wire declares dependencies" in caplog.text
|
||||
|
||||
|
||||
@@ -686,13 +573,8 @@ def test_bundled_library_properties_depends_warns(
|
||||
wire = framework / "libraries" / "Wire"
|
||||
(wire / "library.properties").write_text("name=Wire\nversion=1.0\ndepends=SPI\n")
|
||||
_add_library("Wire", None)
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Bundled library Wire declares dependencies" in caplog.text
|
||||
_resolve(framework)
|
||||
assert "Library Wire declares dependencies via library.properties" in caplog.text
|
||||
|
||||
|
||||
def test_bundled_library_extra_script_warns(
|
||||
@@ -706,12 +588,7 @@ def test_bundled_library_extra_script_warns(
|
||||
'{"name": "Wire", "build": {"extraScript": "extra.py"}}'
|
||||
)
|
||||
_add_library("Wire", None)
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_resolve(framework)
|
||||
assert "declares an extraScript" in caplog.text
|
||||
|
||||
|
||||
@@ -719,32 +596,19 @@ def test_dependency_requested_top_level_is_not_a_drop(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A version-less manifest dependency the config separately requests is
|
||||
already in the build; the skipping warning must not fire for it."""
|
||||
from esphome.platformio.library import request_key
|
||||
|
||||
already in the build; it is not probed as a bundled library."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
_add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
ws_dir = tmp_path / "converted" / "webserver"
|
||||
(ws_dir / "src").mkdir(parents=True)
|
||||
tcp_dir = tmp_path / "converted" / "tcp"
|
||||
(tcp_dir / "src").mkdir(parents=True)
|
||||
ws = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
ws_dir,
|
||||
{"build": {}, "dependencies": [{"name": "ESPAsyncTCP"}]},
|
||||
)
|
||||
tcp = _converted("esp32async__ESPAsyncTCP", tcp_dir, {"build": {}})
|
||||
for conv, lib in zip((ws, tcp), CORE.platformio_libraries.values(), strict=True):
|
||||
conv.node_key = request_key(lib)
|
||||
ws, tcp = _ws_tcp_pair(tmp_path)
|
||||
with _emitting_converter(ws, tcp):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "is not bundled with the framework" not in caplog.text
|
||||
libs = _resolve(framework)
|
||||
# Exactly the two converted libraries; no bundled stand-in was added
|
||||
assert [lib.name for lib in libs] == [
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
"esp32async__ESPAsyncTCP",
|
||||
]
|
||||
assert "Skipping" not in caplog.text
|
||||
|
||||
|
||||
def test_bundled_library_non_dict_manifest_skips_probes_and_raises(
|
||||
@@ -768,15 +632,7 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter(
|
||||
import esphome.platformio.library as pio_library
|
||||
|
||||
framework = _make_framework(tmp_path)
|
||||
local_lib = tmp_path / "locallib"
|
||||
(local_lib / "src").mkdir(parents=True)
|
||||
(local_lib / "src" / "local.cpp").write_text("")
|
||||
(local_lib / "library.json").write_text(
|
||||
'{"name": "LocalLib", "version": "1.0.0", "dependencies": {"Wire": "*"}}'
|
||||
)
|
||||
# as_uri() forms a valid file:// URL on every platform (file:///C:/...
|
||||
# on Windows; a bare f-string would embed backslashes)
|
||||
_add_library(local_lib.as_uri(), None)
|
||||
_local_lib(tmp_path, {"Wire": "*"})
|
||||
# Pin the component cache to tmp_path (data_dir honors an ambient
|
||||
# ESPHOME_DATA_DIR otherwise)
|
||||
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
|
||||
@@ -785,85 +641,46 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter(
|
||||
"_resolve_registry_version",
|
||||
side_effect=AssertionError("registry touched"),
|
||||
):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
names = [lib.name for lib in libs]
|
||||
assert "Wire" in names
|
||||
assert any("locallib" in n.lower() for n in names)
|
||||
|
||||
|
||||
def test_emit_validates_manifest_before_extra_script(tmp_path: Path) -> None:
|
||||
"""A malformed build section fails by name before apply_extra_script
|
||||
dereferences it."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted("esp32async__ESPAsyncWebServer", lib_dir, {"build": "src"})
|
||||
with (
|
||||
_emitting_converter(converted) as mock_extra,
|
||||
pytest.raises(EsphomeError, match="has a malformed manifest"),
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
mock_extra.assert_not_called()
|
||||
|
||||
|
||||
def test_converted_properties_depends_warns(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A converted library shipping only the properties depends= spelling
|
||||
is visible, not a silent bundled-dependency drop."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "depends": "Wire,SPI"},
|
||||
)
|
||||
converted = _webserver(tmp_path, {"build": {}, "depends": "Wire,SPI"})
|
||||
with _emitting_converter(converted):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_resolve(framework)
|
||||
assert "declares dependencies via library.properties" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_name", [1, "../escape", "a/b", ".."])
|
||||
@pytest.mark.parametrize(
|
||||
("bad_name", "message"),
|
||||
[
|
||||
# A non-string name never leaves the shared normalizer
|
||||
(1, "Ignoring unrecognized dependency entry"),
|
||||
("../escape", "Ignoring malformed dependency entry"),
|
||||
("a/b", "Ignoring malformed dependency entry"),
|
||||
("..", "Ignoring malformed dependency entry"),
|
||||
],
|
||||
)
|
||||
def test_bundled_dependency_bad_name_is_malformed(
|
||||
tmp_path: Path, bad_name: object, caplog: pytest.LogCaptureFixture
|
||||
tmp_path: Path, bad_name: object, message: str, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A dependency name becomes a path component; a traversal or a
|
||||
non-string is a malformed entry, never joined."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "dependencies": [{"name": bad_name}]},
|
||||
converted = _webserver(
|
||||
tmp_path, {"build": {}, "dependencies": [{"name": bad_name}]}
|
||||
)
|
||||
with _emitting_converter(converted):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Ignoring malformed dependency entry" in caplog.text
|
||||
_resolve(framework)
|
||||
assert message in caplog.text
|
||||
|
||||
|
||||
def test_bundled_dependency_string_list_form(
|
||||
@@ -872,21 +689,9 @@ def test_bundled_dependency_string_list_form(
|
||||
"""The bare string-list dependency form (PIO-legal) resolves to the
|
||||
bundled library instead of vanishing in normalization."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
lib_dir = tmp_path / "converted" / "webserver"
|
||||
(lib_dir / "src").mkdir(parents=True)
|
||||
converted = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
lib_dir,
|
||||
{"build": {}, "dependencies": ["Wire"]},
|
||||
)
|
||||
converted = _webserver(tmp_path, {"build": {}, "dependencies": ["Wire"]})
|
||||
with _emitting_converter(converted):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
|
||||
|
||||
@@ -900,25 +705,14 @@ def test_pinned_bundled_dependency_substitution_warns(
|
||||
import esphome.platformio.library as pio_library
|
||||
|
||||
framework = _make_framework(tmp_path)
|
||||
local_lib = tmp_path / "locallib"
|
||||
(local_lib / "src").mkdir(parents=True)
|
||||
(local_lib / "src" / "local.cpp").write_text("")
|
||||
(local_lib / "library.json").write_text(
|
||||
'{"name": "LocalLib", "version": "1.0.0", "dependencies": {"Wire": "^2.0.0"}}'
|
||||
)
|
||||
_add_library(local_lib.as_uri(), None)
|
||||
_local_lib(tmp_path, {"Wire": "^2.0.0"})
|
||||
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
|
||||
with patch.object(
|
||||
pio_library,
|
||||
"_resolve_registry_version",
|
||||
side_effect=AssertionError("registry touched"),
|
||||
):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
assert "pins version ^2.0.0; using the library bundled" in caplog.text
|
||||
|
||||
@@ -931,24 +725,14 @@ def test_transitively_resolved_dependency_does_not_warn(
|
||||
stay quiet for it."""
|
||||
framework = _make_framework(tmp_path)
|
||||
_add_library("ESP32Async/ESPAsyncWebServer", "3.9.6")
|
||||
ws_dir = tmp_path / "converted" / "webserver"
|
||||
(ws_dir / "src").mkdir(parents=True)
|
||||
tcp_dir = tmp_path / "converted" / "tcp"
|
||||
(tcp_dir / "src").mkdir(parents=True)
|
||||
ws = _converted(
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
ws_dir,
|
||||
{"build": {}, "dependencies": [{"name": "ESPAsyncTCP"}]},
|
||||
)
|
||||
tcp = _converted("esp32async__ESPAsyncTCP", tcp_dir, {"build": {}})
|
||||
ws, tcp = _ws_tcp_pair(tmp_path)
|
||||
with _emitting_converter(ws, tcp):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "is not bundled with the framework" not in caplog.text
|
||||
libs = _resolve(framework)
|
||||
assert [lib.name for lib in libs] == [
|
||||
"esp32async__ESPAsyncWebServer",
|
||||
"esp32async__ESPAsyncTCP",
|
||||
]
|
||||
assert "Skipping" not in caplog.text
|
||||
|
||||
|
||||
def test_empty_bundled_library_warns(
|
||||
@@ -962,12 +746,7 @@ def test_empty_bundled_library_warns(
|
||||
with pytest.raises(
|
||||
EsphomeError, match="Bundled library Empty has no sources or headers"
|
||||
):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
_resolve(framework)
|
||||
|
||||
|
||||
def test_versionless_dependency_with_provider_stays_quiet(
|
||||
@@ -980,24 +759,13 @@ def test_versionless_dependency_with_provider_stays_quiet(
|
||||
import esphome.platformio.library as pio_library
|
||||
|
||||
framework = _make_framework(tmp_path)
|
||||
local_lib = tmp_path / "locallib"
|
||||
(local_lib / "src").mkdir(parents=True)
|
||||
(local_lib / "src" / "local.cpp").write_text("")
|
||||
(local_lib / "library.json").write_text(
|
||||
'{"name": "LocalLib", "version": "1.0.0", "dependencies": [{"name": "Wire"}]}'
|
||||
)
|
||||
_add_library(local_lib.as_uri(), None)
|
||||
_local_lib(tmp_path, [{"name": "Wire"}])
|
||||
monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome"))
|
||||
with patch.object(
|
||||
pio_library,
|
||||
"_resolve_registry_version",
|
||||
side_effect=AssertionError("registry touched"),
|
||||
):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
libs = _resolve(framework)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
assert "has no version to resolve" not in caplog.text
|
||||
|
||||
@@ -609,6 +609,19 @@ def test_normalize_dependencies_forms(caplog) -> None:
|
||||
assert normalize_dependencies("Wire") == [{"name": "Wire"}]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"manifest", [["not", "a", "manifest"], {"name": "A", "build": "src"}]
|
||||
)
|
||||
def test_convert_libraries_malformed_manifest_raises(
|
||||
tmp_path, monkeypatch, manifest
|
||||
) -> None:
|
||||
"""A manifest without the expected dict shape fails by library name
|
||||
before any backend dereferences data/build."""
|
||||
_patch_download_with_manifests(monkeypatch, tmp_path, {"esphome/A": manifest})
|
||||
with pytest.raises(EsphomeError, match="has a malformed manifest"):
|
||||
convert_libraries([Library("esphome/A", None, None)], _backend())
|
||||
|
||||
|
||||
def test_versionless_dependency_without_provider_warns(
|
||||
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
@@ -620,7 +633,10 @@ def test_versionless_dependency_without_provider_warns(
|
||||
{"esphome/A": {"name": "A", "dependencies": [{"name": "Hash"}]}},
|
||||
)
|
||||
convert_libraries([Library("esphome/A", None, None)], _backend())
|
||||
assert "'Hash' of esphome/A has no version to resolve" in caplog.text
|
||||
assert (
|
||||
"Hash of esphome/A has no version to resolve and nothing provides it"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
def test_walk_warns_for_nonplatform_invalid_library(
|
||||
@@ -678,4 +694,31 @@ def test_versionless_url_ish_dependency_name_warns_cleanly(
|
||||
{"esphome/A": {"name": "A", "dependencies": [{"name": "file://"}]}},
|
||||
)
|
||||
convert_libraries([Library("esphome/A", None, None)], _backend())
|
||||
assert "'file://' of esphome/A has no version to resolve" in caplog.text
|
||||
assert (
|
||||
"file:// of esphome/A has no version to resolve and nothing provides it"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
|
||||
def test_versionless_dependency_matching_resolved_manifest_name_stays_quiet(
|
||||
tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A bare dependency name satisfied by a component requested under an
|
||||
owner-qualified spec (manifest names match) is not a drop; a nameless
|
||||
entry is skipped without a reconciliation warning."""
|
||||
_patch_download_with_manifests(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
{
|
||||
"esphome/A": {
|
||||
"name": "A",
|
||||
"dependencies": [{"name": "B"}, {"version": "1.0"}],
|
||||
},
|
||||
"esphome/B": {"name": "B"},
|
||||
},
|
||||
)
|
||||
convert_libraries(
|
||||
[Library("esphome/A", None, None), Library("esphome/B", None, None)],
|
||||
_backend(),
|
||||
)
|
||||
assert "has no version to resolve" not in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user