mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
Carry lib_archive, warn on unresolvable deps, unquote rsp lines, fail empty archives, note deviations
This commit is contained in:
@@ -8,6 +8,11 @@ resolution/download pipeline in ``esphome.platformio.library``. Nothing here
|
||||
is core-specific: the caller names the PlatformIO platform, MCU, and cache
|
||||
key of the Arduino core it builds.
|
||||
|
||||
Known deviation: flat-layout (``library.properties``, no ``src/``) libraries
|
||||
get the recursive default source filter rather than PlatformIO's root-only
|
||||
Arduino-1.0 filter; no bundled library is affected, only user-supplied ones
|
||||
carrying sources in unusual subdirectories.
|
||||
|
||||
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
|
||||
include path.
|
||||
@@ -52,6 +57,9 @@ class ArduinoLibrary:
|
||||
include_dirs: list[Path] = field(default_factory=list)
|
||||
# Extra compile flags private to this library's own sources
|
||||
flags: list[str] = field(default_factory=list)
|
||||
# PlatformIO's build.libArchive / Arduino's dot_a_linkage: when False the
|
||||
# objects go to the linker directly (symbols nothing references survive)
|
||||
lib_archive: bool = True
|
||||
# Link inputs the library contributes (-L dirs / -l libs, e.g. from
|
||||
# precompiled vendor blobs) and -Wl, options for the firmware link
|
||||
link_dirs: list[Path] = field(default_factory=list)
|
||||
@@ -79,7 +87,15 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
|
||||
# PlatformIO shell-lexes each build.flags entry
|
||||
flag_tokens = lex_build_flags(build.get("flags", []), f"library {name}")
|
||||
|
||||
lib = ArduinoLibrary(name=name)
|
||||
# PIO precedence: build.libArchive, else the Arduino-format
|
||||
# dot_a_linkage property, else archive (PlatformIO's default)
|
||||
if "libArchive" in build:
|
||||
lib_archive = bool(build["libArchive"])
|
||||
elif "dot_a_linkage" in data:
|
||||
lib_archive = str(data["dot_a_linkage"]).lower() == "true"
|
||||
else:
|
||||
lib_archive = True
|
||||
lib = ArduinoLibrary(name=name, lib_archive=lib_archive)
|
||||
include_flags: list[str] = []
|
||||
for tok in flag_tokens:
|
||||
if tok.startswith("-I"):
|
||||
@@ -190,13 +206,30 @@ def resolve_libraries(
|
||||
# it cannot be resolved from the registry.
|
||||
for dep in normalize_dependencies(component.data.get("dependencies")):
|
||||
name = dep.get("name")
|
||||
if (
|
||||
not name
|
||||
or dep.get("owner")
|
||||
or "version" in dep
|
||||
or name in bundled_names
|
||||
or is_lib_ignored(name, lib_ignore)
|
||||
):
|
||||
if not name:
|
||||
_LOGGER.warning(
|
||||
"Ignoring malformed dependency entry %r of library %s",
|
||||
dep,
|
||||
component.name,
|
||||
)
|
||||
continue
|
||||
if name in bundled_names or is_lib_ignored(name, lib_ignore):
|
||||
continue
|
||||
if "version" in dep:
|
||||
# The converter resolves versioned deps from the registry.
|
||||
# Note: the dict shorthand {"Wire": "*"} normalizes to
|
||||
# version="*" and takes this path even for a bundled name;
|
||||
# use the list form for bundled dependencies.
|
||||
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,
|
||||
)
|
||||
continue
|
||||
if not (framework_path / "libraries" / name).is_dir():
|
||||
# The shared converter skips version-less deps too, so this
|
||||
|
||||
@@ -24,13 +24,24 @@ def main() -> int:
|
||||
# Expand the response file here instead of passing @rspfile: GNU ar
|
||||
# treats backslashes in response files as escapes, corrupting Windows
|
||||
# paths ("sub\a.o" -> "suba.o").
|
||||
# One path per line (rspfile_content = $in_newline, written without
|
||||
# escaping), so a path containing a space survives. Expanding into
|
||||
# argv trades away the OS command-line length limit rspfiles dodge;
|
||||
# the relative object paths used here stay far below it.
|
||||
objects = Path(rspfile).read_text(encoding="utf-8").splitlines()
|
||||
# One path per line (rspfile_content = $in_newline). ninja shell-quotes
|
||||
# a path containing specials, so undo a simple surrounding quote per
|
||||
# line. Expanding into argv trades away the OS command-line length
|
||||
# limit rspfiles dodge; the relative object paths here stay far
|
||||
# below it.
|
||||
objects = [
|
||||
line[1:-1]
|
||||
if len(line) >= 2 and line[0] == line[-1] and line[0] in "'\""
|
||||
else line
|
||||
for line in Path(rspfile).read_text(encoding="utf-8").splitlines()
|
||||
if line
|
||||
]
|
||||
if not objects:
|
||||
# An empty archive would "succeed" here and fail far away at link
|
||||
print(f"ar: no objects listed in {rspfile} for {archive}", file=sys.stderr)
|
||||
return 1
|
||||
return subprocess.run(
|
||||
[ar, "rc", archive, *filter(None, objects)], check=False, close_fds=False
|
||||
[ar, "rc", archive, *objects], check=False, close_fds=False
|
||||
).returncode
|
||||
if mode == "copy":
|
||||
src, dst = sys.argv[2:4]
|
||||
|
||||
@@ -87,3 +87,40 @@ def test_ar_expands_rspfile_without_escaping(tmp_path) -> None:
|
||||
"obj/a.o",
|
||||
"sub\\b.o",
|
||||
]
|
||||
|
||||
|
||||
def test_ar_unquotes_ninja_escaped_paths(tmp_path: Path) -> None:
|
||||
"""The shim strips a simple surrounding quote, since ninja shell-
|
||||
quotes special rsp paths, so ar sees the real filename."""
|
||||
rsp = tmp_path / "t.rsp"
|
||||
rsp.write_text("'obj/a b.o'\nobj/c.o\n")
|
||||
with (
|
||||
patch.object(
|
||||
build_tool.sys, "argv", ["bt", "ar", "/usr/bin/ar", "lib.a", str(rsp)]
|
||||
),
|
||||
patch.object(build_tool.subprocess, "run") as mock_run,
|
||||
):
|
||||
mock_run.return_value.returncode = 0
|
||||
rc = build_tool.main()
|
||||
assert rc == 0
|
||||
assert mock_run.call_args.args[0] == [
|
||||
"/usr/bin/ar",
|
||||
"rc",
|
||||
"lib.a",
|
||||
"obj/a b.o",
|
||||
"obj/c.o",
|
||||
]
|
||||
|
||||
|
||||
def test_ar_empty_object_list_fails(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
"""A lost object list is an error here, not undefined symbols at link."""
|
||||
rsp = tmp_path / "t.rsp"
|
||||
rsp.write_text("\n\n")
|
||||
with patch.object(
|
||||
build_tool.sys, "argv", ["bt", "ar", "/usr/bin/ar", "lib.a", str(rsp)]
|
||||
):
|
||||
rc = build_tool.main()
|
||||
assert rc == 1
|
||||
assert "no objects listed" in capsys.readouterr().err
|
||||
|
||||
@@ -356,3 +356,56 @@ def test_bundled_library_prefers_library_json(tmp_path: Path) -> None:
|
||||
)
|
||||
lib = component._bundled_library(framework, "GDBStub")
|
||||
assert [s.name for s in lib.sources] == ["gdb.cpp"]
|
||||
|
||||
|
||||
def test_library_info_lib_archive_flag(tmp_path: Path) -> None:
|
||||
"""Both libArchive (library.json) and dot_a_linkage (properties) reach
|
||||
the generator's contract; default is archive."""
|
||||
read_path = tmp_path / "lib"
|
||||
(read_path / "src").mkdir(parents=True)
|
||||
assert component._library_info("x", read_path, {}).lib_archive is True
|
||||
assert (
|
||||
component._library_info(
|
||||
"x", read_path, {"build": {"libArchive": False}}
|
||||
).lib_archive
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
component._library_info("x", read_path, {"dot_a_linkage": "false"}).lib_archive
|
||||
is False
|
||||
)
|
||||
assert (
|
||||
component._library_info("x", read_path, {"dot_a_linkage": "true"}).lib_archive
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_resolve_libraries_dep_warnings(
|
||||
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Nameless and owner-without-version dependencies are dropped loudly."""
|
||||
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": [
|
||||
{"owner": "someone"},
|
||||
{"name": "Orphan", "owner": "someone"},
|
||||
],
|
||||
},
|
||||
)
|
||||
with _emitting_converter(converted):
|
||||
component.resolve_libraries(
|
||||
framework,
|
||||
pio_platform="espressif8266",
|
||||
board_mcu="esp8266",
|
||||
cache_key="arduino8266",
|
||||
)
|
||||
assert "malformed dependency entry" in caplog.text
|
||||
assert "Orphan" in caplog.text
|
||||
assert "owner but no version" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user