diff --git a/esphome/arduino8266/component.py b/esphome/arduino8266/component.py index a39f3f9a8b..2e0de9a57d 100644 --- a/esphome/arduino8266/component.py +++ b/esphome/arduino8266/component.py @@ -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}) diff --git a/esphome/build_gen/build_tool.py b/esphome/build_gen/build_tool.py index bfcf4128eb..19275a8eaf 100644 --- a/esphome/build_gen/build_tool.py +++ b/esphome/build_gen/build_tool.py @@ -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) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 6283a566e4..719df2eeec 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -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 diff --git a/tests/unit_tests/build_gen/test_build_tool.py b/tests/unit_tests/build_gen/test_build_tool.py index 82ca90a8c7..0c8eb00f9d 100644 --- a/tests/unit_tests/build_gen/test_build_tool.py +++ b/tests/unit_tests/build_gen/test_build_tool.py @@ -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", + ] diff --git a/tests/unit_tests/test_arduino8266_component.py b/tests/unit_tests/test_arduino8266_component.py index 222a881f85..edc81be6cb 100644 --- a/tests/unit_tests/test_arduino8266_component.py +++ b/tests/unit_tests/test_arduino8266_component.py @@ -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"]