mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
Surface every dependency-drop path the walk can take
The converted path warns for a properties-only depends= spelling (the walk reads the JSON key). A dependency name is validated before becoming a path component: non-strings and separator names are malformed entries, never joined (apply_extra_script already guards the same shape). normalize_dependencies coerces PIO's bare string-list form to name dicts instead of dropping it before every visibility warning. A non-* version pin discarded for a backend-provided bundled copy warns naming the substitution. The -I global-include promotion and the start-group link-order contract are documented.
This commit is contained in:
@@ -13,7 +13,9 @@ libraries get the recursive default source filter rather than PlatformIO's
|
||||
root-only Arduino-1.0 filter (no bundled library is affected), and the
|
||||
Arduino ``dot_a_linkage`` property is honored even though PlatformIO
|
||||
ignores it. Bundled libraries never run a manifest ``extraScript`` (a
|
||||
warning names the library if one declares it).
|
||||
warning names the library if one declares it). Manifest ``-I`` build
|
||||
flags join the global include path rather than staying private to the
|
||||
library's own sources as under PlatformIO.
|
||||
|
||||
Mirrors PlatformIO's ``lib_ldf_mode=off`` behavior: each library builds into
|
||||
its own static archive and every library's include dir joins one global
|
||||
@@ -230,6 +232,10 @@ def resolve_libraries(
|
||||
``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would
|
||||
for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the
|
||||
shared converter's download cache.
|
||||
|
||||
The returned order is unordered with respect to link dependencies
|
||||
(bundled dependencies precede their dependents); the caller must link
|
||||
the archives inside one ``--start-group``/``--end-group`` pair.
|
||||
"""
|
||||
bundled: list[ArduinoLibrary] = []
|
||||
external: list[Library] = []
|
||||
@@ -270,9 +276,26 @@ def resolve_libraries(
|
||||
# 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,
|
||||
)
|
||||
for dep in normalize_dependencies(component.data.get("dependencies")):
|
||||
name = dep.get("name")
|
||||
if not name:
|
||||
if (
|
||||
not name
|
||||
or not isinstance(name, str)
|
||||
or "/" in name
|
||||
or "\\" in name
|
||||
or name in (".", "..")
|
||||
):
|
||||
# The name becomes a path component under the framework
|
||||
# tree; never join a traversal or a non-string
|
||||
_LOGGER.warning(
|
||||
"Ignoring malformed dependency entry %r of library %s",
|
||||
dep,
|
||||
|
||||
@@ -651,7 +651,16 @@ def normalize_dependencies(dependencies: Any) -> list[dict]:
|
||||
entry["version"] = spec
|
||||
normalized.append(entry)
|
||||
return normalized
|
||||
return [d for d in dependencies if isinstance(d, dict)]
|
||||
normalized = []
|
||||
for entry in dependencies:
|
||||
if isinstance(entry, dict):
|
||||
normalized.append(entry)
|
||||
elif isinstance(entry, str) and entry:
|
||||
# PIO also accepts a bare list of names ("dependencies":
|
||||
# ["Wire"]); dropping them here would hide a real dependency
|
||||
# from every caller's visibility warning
|
||||
normalized.append({"name": entry})
|
||||
return normalized
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -985,6 +994,17 @@ def convert_libraries(
|
||||
):
|
||||
# The backend adds it from its own tree; resolving it here
|
||||
# would fetch a same-named registry package instead
|
||||
if (pin := dependency.get("version")) and pin != "*":
|
||||
# The declared constraint is discarded for the bundled
|
||||
# copy; a too-old bundled library must not surface as
|
||||
# link errors with no stated cause
|
||||
_LOGGER.warning(
|
||||
"Dependency %s pins version %s; using the library "
|
||||
"bundled with the framework instead",
|
||||
dep_name,
|
||||
pin,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Skip backend-provided dependency %s", dep_name)
|
||||
continue
|
||||
# The version field may actually be a URL (git/archive dependency).
|
||||
|
||||
@@ -815,3 +815,108 @@ def test_emit_validates_manifest_before_extra_script(tmp_path: Path) -> None:
|
||||
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"},
|
||||
)
|
||||
with _emitting_converter(converted):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "declares dependencies via library.properties" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_name", [1, "../escape", "a/b", ".."])
|
||||
def test_bundled_dependency_bad_name_is_malformed(
|
||||
tmp_path: Path, bad_name: object, 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}]},
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
def test_bundled_dependency_string_list_form(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""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"]},
|
||||
)
|
||||
with _emitting_converter(converted):
|
||||
libs = component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
|
||||
|
||||
def test_pinned_bundled_dependency_substitution_warns(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A non-* version pin on a backend-provided dependency is discarded
|
||||
for the bundled copy; the substitution must be visible."""
|
||||
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)
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
CORE.config_path.write_text("")
|
||||
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",
|
||||
)
|
||||
assert "Wire" in [lib.name for lib in libs]
|
||||
assert "pins version ^2.0.0; using the library bundled" in caplog.text
|
||||
|
||||
@@ -590,3 +590,14 @@ def test_source_kind_map_shape() -> None:
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".S"] == "asm"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".c"] == "c"
|
||||
assert SOURCE_KIND_FOR_SUFFIX[".cpp"] == "cxx"
|
||||
|
||||
|
||||
def test_normalize_dependencies_string_entries() -> None:
|
||||
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, ""]) == [
|
||||
{"name": "Wire"},
|
||||
{"name": "SPI"},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user