Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-22 00:48:41 -05:00
4 changed files with 113 additions and 19 deletions
+34 -10
View File
@@ -221,7 +221,20 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
"run for bundled libraries",
name,
)
return _library_info(name, lib_dir, data)
lib = _library_info(name, lib_dir, data)
if not lib.sources and not any(
p.suffix in (".h", ".hpp", ".hh", ".inc")
for d in lib.include_dirs
for p in d.rglob("*")
):
# An empty or half-extracted bundled directory would otherwise
# become a silent no-op that surfaces as undefined symbols at link
_LOGGER.warning(
"Bundled library %s has no sources or headers; the framework "
"install may be incomplete (run 'esphome clean-all')",
name,
)
return lib
def resolve_libraries(
@@ -272,6 +285,8 @@ 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
@@ -327,15 +342,10 @@ def resolve_libraries(
)
continue
if not bundled_dir.is_dir():
# The shared converter skips version-less deps too, so this
# is the only place the drop can be made visible before the
# missing sources surface as link errors.
_LOGGER.warning(
"Dependency %s of library %s is not bundled with the "
"framework and has no version to resolve; skipping",
name,
component.name,
)
# 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")
@@ -406,4 +416,18 @@ def resolve_libraries(
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.
resolved_short_names = {c.name.split("__")[-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
+24 -5
View File
@@ -628,15 +628,21 @@ def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]:
return out
def normalize_dependencies(dependencies: Any) -> list[dict]:
def normalize_dependencies(dependencies: Any, owner: str = "manifest") -> list[dict]:
"""Normalize a library manifest's ``dependencies`` to a list of dicts.
PIO's library.json accepts both the list-of-dicts form and the shorthand
dict form (``{"owner/Name": "version_spec"}``); normalize the latter so
callers see a uniform list.
PIO's library.json accepts the list-of-dicts form, the shorthand dict
form (``{"owner/Name": "version_spec"}``), bare name strings inside the
list, and a plain (possibly comma-separated) string; normalize them all
so callers see a uniform list. ``owner`` names the manifest in the
warning for entries that cannot be normalized.
"""
if not dependencies:
return []
if isinstance(dependencies, str):
# A plain string is one or more comma-separated names; iterating it
# as a list would shred it into one-character "libraries"
return [{"name": n.strip()} for n in dependencies.split(",") if n.strip()]
if isinstance(dependencies, dict):
normalized = []
for raw_name, spec in dependencies.items():
@@ -660,6 +666,10 @@ def normalize_dependencies(dependencies: Any) -> list[dict]:
# ["Wire"]); dropping them here would hide a real dependency
# from every caller's visibility warning
normalized.append({"name": entry})
else:
_LOGGER.warning(
"Ignoring unrecognized dependency entry %r of %s", entry, owner
)
return normalized
@@ -973,8 +983,17 @@ def convert_libraries(
# Requirements changed (we got past the short-circuit above), so
# (re)walk this component's dependencies.
node.edges = set()
for dependency in normalize_dependencies(component.data.get("dependencies")):
for dependency in normalize_dependencies(
component.data.get("dependencies"), component.name
):
if "name" not in dependency or "version" not in dependency:
# Version-less deps cannot resolve from the registry; the
# arduino backend picks bundled ones up after emit
_LOGGER.debug(
"Skip version-less dependency %r of %s",
dependency.get("name"),
component.name,
)
continue
try:
check_library_data(dependency, backend.platform, backend.framework)
+45
View File
@@ -920,3 +920,48 @@ def test_pinned_bundled_dependency_substitution_warns(
)
assert "Wire" in [lib.name for lib in libs]
assert "pins version ^2.0.0; using the library bundled" in caplog.text
def test_transitively_resolved_dependency_does_not_warn(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A version-less dependency the walk resolved as another library's
registry dependency is present in the build; the skipping warning must
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": {}})
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
def test_empty_bundled_library_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A bundled directory with no sources or headers is a broken install,
not a silent no-op archive."""
framework = _make_framework(tmp_path)
(framework / "libraries" / "Empty").mkdir()
_add_library("Empty", None)
component.resolve_libraries(
framework,
pio_platform="espressif8266",
board_mcu="esp8266",
cache_key="arduino8266",
)
assert "Bundled library Empty has no sources or headers" in caplog.text
+10 -4
View File
@@ -592,12 +592,18 @@ def test_source_kind_map_shape() -> None:
assert SOURCE_KIND_FOR_SUFFIX[".cpp"] == "cxx"
def test_normalize_dependencies_string_entries() -> None:
def test_normalize_dependencies_forms(caplog) -> None:
"""Every PIO-legal spelling normalizes; unrecognizable entries warn."""
from esphome.platformio.library import normalize_dependencies
"""PIO's bare string-list form coerces to name dicts; other non-dict
entries still drop."""
assert normalize_dependencies(["Wire", {"name": "SPI"}, 5, ""]) == [
assert normalize_dependencies(["Wire", {"name": "SPI"}, 5, ""], "libx") == [
{"name": "Wire"},
{"name": "SPI"},
]
assert caplog.text.count("unrecognized dependency entry") == 2
# A plain string is names, never iterated into characters
assert normalize_dependencies("Wire, SPI") == [
{"name": "Wire"},
{"name": "SPI"},
]
assert normalize_dependencies("Wire") == [{"name": "Wire"}]