mirror of
https://github.com/esphome/esphome.git
synced 2026-08-27 08:28:30 +00:00
Merge branch 'esp8266-native-library-backend' into esp8266-native-build-spec
This commit is contained in:
+36
-29
@@ -66,6 +66,11 @@ class ArduinoLibrary:
|
||||
link_flags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Source-like suffixes the case-sensitive suffix map rejects
|
||||
_UNMAPPED_SOURCE_SUFFIXES = frozenset(
|
||||
{s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"}
|
||||
)
|
||||
|
||||
# Filename-plain names: an allowlist excludes separators, drive colons,
|
||||
# and dot-only names by shape
|
||||
_SAFE_LIBRARY_NAME_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_. +-]*\Z")
|
||||
@@ -195,30 +200,32 @@ def _collect_lib_sources(
|
||||
src_dir: str,
|
||||
src_filter: list[str],
|
||||
) -> None:
|
||||
matched = collect_filtered_files(read_path / src_dir, src_filter)
|
||||
lib.sources = sorted(
|
||||
path.resolve()
|
||||
for f in matched
|
||||
if (path := Path(f)).suffix in SRC_FILE_EXTENSIONS
|
||||
)
|
||||
# A source-like suffix the case-sensitive map rejects (.CPP, .ino)
|
||||
# is a dropped compilation unit; headers fall through silently
|
||||
source_like = {s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"}
|
||||
if dropped := [
|
||||
Path(f).name
|
||||
for f in matched
|
||||
if Path(f).suffix not in SRC_FILE_EXTENSIONS
|
||||
and Path(f).suffix.lower() in source_like
|
||||
]:
|
||||
root = read_path / src_dir
|
||||
resolved_root = root.resolve()
|
||||
sources: list[Path] = []
|
||||
dropped: list[str] = []
|
||||
saw_header = False
|
||||
for f in collect_filtered_files(root, src_filter):
|
||||
path = Path(f)
|
||||
suffix = path.suffix
|
||||
if suffix in SRC_FILE_EXTENSIONS:
|
||||
# Re-root on the resolved dir instead of a realpath() per file
|
||||
sources.append(resolved_root / path.relative_to(root))
|
||||
elif suffix.lower() in _UNMAPPED_SOURCE_SUFFIXES:
|
||||
# A source-like suffix the case-sensitive map rejects (.CPP,
|
||||
# .ino) is a dropped compilation unit; headers fall through
|
||||
dropped.append(path.name)
|
||||
elif suffix.lower() in LIBRARY_HEADER_SUFFIXES:
|
||||
saw_header = True
|
||||
lib.sources = sorted(sources)
|
||||
if dropped:
|
||||
_LOGGER.warning(
|
||||
"Library %s: %d file(s) with unmapped source suffixes are not compiled: %s",
|
||||
name,
|
||||
len(dropped),
|
||||
", ".join(sorted(dropped)),
|
||||
)
|
||||
if not lib.sources and not any(
|
||||
Path(f).suffix.lower() in LIBRARY_HEADER_SUFFIXES for f in matched
|
||||
):
|
||||
if not lib.sources and not saw_header:
|
||||
# Matched headers mean header-only; a filter matching nothing is
|
||||
# a manifest/tree problem (a truly empty tree raises elsewhere)
|
||||
_LOGGER.warning("Library %s: no source files matched", name)
|
||||
@@ -253,12 +260,12 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
|
||||
manifest_json = lib_dir / "library.json"
|
||||
if manifest_json.is_file():
|
||||
data = parse_library_json(manifest_json)
|
||||
elif (manifest := lib_dir / "library.properties").is_file():
|
||||
data = parse_library_properties(manifest)
|
||||
else:
|
||||
manifest = lib_dir / "library.properties"
|
||||
if not manifest.is_file():
|
||||
# Defaults build core libraries; can also mean a torn extraction
|
||||
_LOGGER.debug("Bundled library %s has no manifest; using defaults", name)
|
||||
data = parse_library_properties(manifest) if manifest.is_file() else {}
|
||||
# Defaults build core libraries; can also mean a torn extraction
|
||||
_LOGGER.debug("Bundled library %s has no manifest; using defaults", name)
|
||||
data = {}
|
||||
if isinstance(data, dict):
|
||||
# Bundled manifest deps are never walked; make the skip visible
|
||||
if data.get("dependencies"):
|
||||
@@ -312,7 +319,7 @@ def _external_short_name(name: str) -> str:
|
||||
|
||||
|
||||
def _check_unfulfilled_provides(
|
||||
provided_requests: list[str], satisfied: set[str], still_requested: set[str]
|
||||
provided_requests: set[str], satisfied: set[str], still_requested: set[str]
|
||||
) -> None:
|
||||
"""Fail by name when a walk-skipped dependency was never added.
|
||||
|
||||
@@ -320,7 +327,7 @@ def _check_unfulfilled_provides(
|
||||
at link. The walk records across re-resolutions, so a name no final
|
||||
manifest still requests is stale state, never a failure.
|
||||
"""
|
||||
if missing := sorted((set(provided_requests) & still_requested) - satisfied):
|
||||
if missing := sorted((provided_requests & still_requested) - satisfied):
|
||||
raise EsphomeError(
|
||||
"provides() skipped these dependencies but nothing added them: "
|
||||
f"{', '.join(missing)}; the build is missing libraries"
|
||||
@@ -395,10 +402,10 @@ def resolve_libraries(
|
||||
for dep in normalize_dependencies(
|
||||
component.data.get("dependencies"), component.name
|
||||
):
|
||||
name = dep.get("name")
|
||||
if isinstance(name, str):
|
||||
final_dep_names.add(name)
|
||||
if isinstance(name, str) and "/" in name:
|
||||
# normalize_dependencies guarantees a non-empty str name
|
||||
name = dep["name"]
|
||||
final_dep_names.add(name)
|
||||
if "/" in name:
|
||||
owner, _, pkg = name.partition("/")
|
||||
if _is_safe_library_name(owner) and _is_safe_library_name(pkg):
|
||||
# Owner-qualified; the converter resolves it from the registry
|
||||
|
||||
@@ -337,7 +337,7 @@ class LibraryBackend:
|
||||
# the backend supplies them itself (e.g. core-bundled libraries) and
|
||||
# reconciles provided_requests after resolving
|
||||
provides: Callable[[str], bool] | None = None
|
||||
provided_requests: list[str] = field(default_factory=list)
|
||||
provided_requests: set[str] = field(default_factory=set)
|
||||
|
||||
|
||||
def ensure_list[T](obj: T | list[T]) -> list[T]:
|
||||
@@ -932,6 +932,8 @@ def _warn_unsatisfied_versionless(
|
||||
"""Warn for version-less deps nothing satisfied; 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
|
||||
warned: set[str] = set()
|
||||
for dep_name, dep_owner, requester in skipped_versionless:
|
||||
if not isinstance(dep_name, str) or not dep_name or dep_name in warned:
|
||||
@@ -943,7 +945,6 @@ def _warn_unsatisfied_versionless(
|
||||
# Name-only evidence: a coincidental collision must stay
|
||||
# visible where the user could pin it
|
||||
warned.add(dep_name)
|
||||
log = _LOGGER.warning if backend.provides is not None else _LOGGER.debug
|
||||
log(
|
||||
"Version-less dependency %s of %s assumed satisfied by a "
|
||||
"resolved library's manifest name only",
|
||||
@@ -958,11 +959,9 @@ def _warn_unsatisfied_versionless(
|
||||
):
|
||||
# provides() only satisfies owner-less names (same guard as
|
||||
# the walk's skip); record for the post-emit reconciliation
|
||||
backend.provided_requests.append(dep_name)
|
||||
backend.provided_requests.add(dep_name)
|
||||
continue
|
||||
warned.add(dep_name)
|
||||
# A treeless backend can never supply a bundled name; noise for it
|
||||
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",
|
||||
@@ -1252,23 +1251,20 @@ def convert_libraries(
|
||||
if "version" not in dependency:
|
||||
# Cannot resolve from the registry; the post-emit
|
||||
# reconciliation owns the drop warning
|
||||
dep_name = dependency.get("name")
|
||||
_LOGGER.debug(
|
||||
"Skip version-less dependency %r of %s",
|
||||
dependency.get("name"),
|
||||
dep_name,
|
||||
component.name,
|
||||
)
|
||||
if not is_lib_ignored(
|
||||
dependency.get("name"), lib_ignore
|
||||
dep_name, lib_ignore
|
||||
) and dependency_is_usable(
|
||||
dependency, backend.platform, backend.framework, component.name
|
||||
):
|
||||
# Filtered or ignored deps are deliberately absent
|
||||
skipped_versionless.append(
|
||||
(
|
||||
dependency.get("name"),
|
||||
dependency.get("owner"),
|
||||
component.name,
|
||||
)
|
||||
(dep_name, dependency.get("owner"), component.name)
|
||||
)
|
||||
continue
|
||||
if not dependency_is_usable(
|
||||
@@ -1304,7 +1300,7 @@ def convert_libraries(
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Skip backend-provided dependency %s", dep_name)
|
||||
backend.provided_requests.append(dep_name)
|
||||
backend.provided_requests.add(dep_name)
|
||||
continue
|
||||
dep_key = add_spec(dep_name, dep_version, dep_url)
|
||||
node.edges.add(dep_key)
|
||||
|
||||
@@ -718,18 +718,18 @@ def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> No
|
||||
|
||||
def test_unfulfilled_provides_promise_raises(tmp_path: Path) -> None:
|
||||
"""A provides()-skipped dependency nothing added can only surface as
|
||||
undefined symbols at link, so it fails here by name, deduplicated;
|
||||
satisfied ones pass silently."""
|
||||
undefined symbols at link, so it fails here by name; satisfied ones
|
||||
pass silently."""
|
||||
with pytest.raises(EsphomeError, match="Wire") as err:
|
||||
component._check_unfulfilled_provides(
|
||||
["Wire", "Wire", "Hash"], {"Hash"}, {"Wire", "Hash"}
|
||||
{"Wire", "Hash"}, {"Hash"}, {"Wire", "Hash"}
|
||||
)
|
||||
assert str(err.value).count("Wire") == 1
|
||||
assert "Hash" not in str(err.value)
|
||||
component._check_unfulfilled_provides(["Hash"], {"Hash"}, {"Hash"})
|
||||
component._check_unfulfilled_provides({"Hash"}, {"Hash"}, {"Hash"})
|
||||
# A recording for a since-re-resolved manifest is stale walk state,
|
||||
# never a failure: no final manifest still requests Wire
|
||||
component._check_unfulfilled_provides(["Wire"], set(), set())
|
||||
component._check_unfulfilled_provides({"Wire"}, set(), set())
|
||||
|
||||
|
||||
def test_extra_script_link_flags_reach_the_library(tmp_path: Path) -> None:
|
||||
@@ -988,9 +988,7 @@ def test_owner_qualified_dependency_is_silent(
|
||||
assert "malformed" not in caplog.text
|
||||
|
||||
|
||||
def test_bundled_dependency_string_list_form(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
def test_bundled_dependency_string_list_form(tmp_path: Path) -> None:
|
||||
"""The bare string-list dependency form (PIO-legal) resolves to the
|
||||
bundled library instead of vanishing in normalization."""
|
||||
framework = _make_framework(tmp_path)
|
||||
|
||||
Reference in New Issue
Block a user