Expand ar response files in the shim and honor bundled library.json manifests

This commit is contained in:
J. Nick Koston
2026-08-20 14:46:09 -05:00
parent 976a5ed988
commit 8bbaa09891
5 changed files with 61 additions and 8 deletions
+13 -3
View File
@@ -26,6 +26,7 @@ from esphome.platformio.library import (
ConvertedLibrary,
InvalidLibrary,
LibraryBackend,
_parse_library_json,
check_library_data,
collect_filtered_files,
convert_libraries,
@@ -128,10 +129,19 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
"""A library bundled with the Arduino core, read from the framework tree."""
"""A library bundled with the Arduino core, read from the framework tree.
``library.json`` wins over ``library.properties`` when both exist, as in
PlatformIO's LibBuilderFactory; only the JSON manifest can carry a
``build`` section (srcDir, srcFilter, flags).
"""
lib_dir = framework_path / "libraries" / name
manifest = lib_dir / "library.properties"
data = parse_library_properties(manifest) if manifest.is_file() else {}
manifest_json = lib_dir / "library.json"
if manifest_json.is_file():
data = _parse_library_json(manifest_json)
else:
manifest = lib_dir / "library.properties"
data = parse_library_properties(manifest) if manifest.is_file() else {}
return _library_info(name, lib_dir, {"name": name, **data})
+5 -3
View File
@@ -21,9 +21,11 @@ def main() -> int:
# Remove first: ``ar rc`` replaces members but never drops ones whose
# source was removed from the build, which would leak stale objects.
Path(archive).unlink(missing_ok=True)
return subprocess.run(
[ar, "rc", archive, f"@{rspfile}"], check=False
).returncode
# 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").
objects = Path(rspfile).read_text(encoding="utf-8").split()
return subprocess.run([ar, "rc", archive, *objects], check=False).returncode
if mode == "copy":
src, dst = sys.argv[2:4]
shutil.copyfile(src, dst)
+1 -1
View File
@@ -562,7 +562,7 @@ def split_flag_entry(entry: str, owner: str) -> list[str]:
raise EsphomeError(f"Malformed build flag {entry!r} in {owner}: {err}") from err
def lex_build_flags(entries, 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
+27 -1
View File
@@ -27,7 +27,8 @@ def test_ar_removes_stale_archive(tmp_path: Path) -> None:
):
assert build_tool.main() == 0
assert not archive.exists()
assert mock_run.call_args[0][0] == ["ar-bin", "rc", str(archive), f"@{rsp}"]
# The rspfile is expanded by the shim (GNU ar would escape backslashes)
assert mock_run.call_args[0][0] == ["ar-bin", "rc", str(archive), "a.o"]
def test_copy(tmp_path: Path) -> None:
@@ -61,3 +62,28 @@ def test_runs_as_script(tmp_path: Path) -> None:
)
assert result.returncode == 0
assert dst.read_text() == "x"
def test_ar_expands_rspfile_without_escaping(tmp_path) -> None:
"""Backslash paths survive: the shim expands the rspfile itself instead
of letting GNU ar treat backslashes as escapes."""
rsp = tmp_path / "objs.rsp"
rsp.write_text("obj/a.o\nsub\\b.o\n")
with (
patch.object(
build_tool.sys,
"argv",
["build_tool", "ar", "ar-bin", str(tmp_path / "lib.a"), str(rsp)],
),
patch.object(
build_tool.subprocess, "run", return_value=MagicMock(returncode=0)
) as mock_run,
):
assert build_tool.main() == 0
assert mock_run.call_args[0][0] == [
"ar-bin",
"rc",
str(tmp_path / "lib.a"),
"obj/a.o",
"sub\\b.o",
]
@@ -304,3 +304,18 @@ def test_resolve_libraries_lib_ignore_covers_bundled_dependencies(
libs = component.resolve_libraries(framework)
assert [lib.name for lib in libs] == ["some__External"]
def test_bundled_library_prefers_library_json(tmp_path: Path) -> None:
"""A bundled library.json wins over library.properties (PIO semantics);
its build section is honored."""
framework = _make_framework(tmp_path)
lib_dir = framework / "libraries" / "GDBStub"
(lib_dir / "custom").mkdir(parents=True)
(lib_dir / "custom" / "gdb.cpp").write_text("")
(lib_dir / "library.properties").write_text("name=GDBStub\n")
(lib_dir / "library.json").write_text(
'{"name": "GDBStub", "build": {"srcDir": "custom"}}'
)
lib = component._bundled_library(framework, "GDBStub")
assert [s.name for s in lib.sources] == ["gdb.cpp"]