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

This commit is contained in:
J. Nick Koston
2026-08-22 12:26:39 -05:00
6 changed files with 118 additions and 25 deletions
+15 -10
View File
@@ -180,19 +180,24 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
for f in matched for f in matched
if (path := Path(f)).suffix in SRC_FILE_EXTENSIONS if (path := Path(f)).suffix in SRC_FILE_EXTENSIONS
) )
if skipped := [f for f in matched if Path(f).suffix not in SRC_FILE_EXTENSIONS]: # A source-like suffix the case-sensitive map rejects (.CPP, .ino) is a
_LOGGER.debug( # dropped compilation unit that surfaces as undefined symbols at link;
"Library %s: %d matched files are not sources", name, len(skipped) # headers and metadata files fall through silently (header-only
) # libraries are routine)
if not lib.sources: source_like = {s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"}
if matched: if dropped := [
# Every matched file fell through the suffix map: an empty Path(f).name
# archive would fail far away at link for f in matched
if Path(f).suffix not in SRC_FILE_EXTENSIONS
and Path(f).suffix.lower() in source_like
]:
_LOGGER.warning( _LOGGER.warning(
"Library %s: no matched file has a recognized source suffix", "Library %s: %d file(s) with unmapped source suffixes are not compiled: %s",
name, name,
len(dropped),
", ".join(sorted(dropped)),
) )
elif "srcFilter" in build or "srcDir" in build: if not lib.sources and not matched and ("srcFilter" in build or "srcDir" in build):
# A default probe finding nothing is a header-only library; a # A default probe finding nothing is a header-only library; a
# declared filter matching nothing is a manifest/tree problem. # declared filter matching nothing is a manifest/tree problem.
_LOGGER.warning( _LOGGER.warning(
+16 -2
View File
@@ -40,9 +40,23 @@ def main() -> int:
# An empty archive would "succeed" here and fail far away at link # 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) print(f"ar: no objects listed in {rspfile} for {archive}", file=sys.stderr)
return 1 return 1
return subprocess.run( # Batch by argv length: expanding the rspfile gives back the Windows
[ar, "rc", archive, *objects], check=False, close_fds=False # 32767-char command-line limit it existed to avoid. "rc" creates,
# "q" appends the remainder.
op = "rc"
while objects:
batch = [objects.pop(0)]
batch_len = len(batch[0])
while objects and batch_len + len(objects[0]) < 25000:
batch_len += len(objects[0]) + 1
batch.append(objects.pop(0))
rc = subprocess.run(
[ar, op, archive, *batch], check=False, close_fds=False
).returncode ).returncode
if rc != 0:
return rc
op = "q"
return 0
if mode == "copy": if mode == "copy":
src, dst = sys.argv[2:4] src, dst = sys.argv[2:4]
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
+7
View File
@@ -699,6 +699,13 @@ def normalize_dependencies(
continue continue
normalized.append(entry) normalized.append(entry)
return normalized return normalized
if not isinstance(dependencies, (list, tuple)):
_LOGGER.warning(
"Ignoring unrecognized dependencies %r of %s",
dependencies,
manifest_name,
)
return []
normalized = [] normalized = []
for entry in dependencies: for entry in dependencies:
if isinstance(entry, dict): if isinstance(entry, dict):
@@ -124,3 +124,48 @@ def test_ar_empty_object_list_fails(
rc = build_tool.main() rc = build_tool.main()
assert rc == 1 assert rc == 1
assert "no objects listed" in capsys.readouterr().err assert "no objects listed" in capsys.readouterr().err
def test_ar_batches_long_object_lists(tmp_path: Path) -> None:
"""The expanded argv must stay under the Windows 32767-char limit: a
long object list creates with rc, then appends with q."""
archive = tmp_path / "lib.a"
rsp = tmp_path / "lib.a.rsp"
objects = [f"dir/{'x' * 120}_{i}.o" for i in range(400)]
rsp.write_text("\n".join(objects) + "\n")
with (
patch.object(
build_tool.sys,
"argv",
["build_tool", "ar", "ar-bin", str(archive), str(rsp)],
),
patch.object(
build_tool.subprocess, "run", return_value=MagicMock(returncode=0)
) as mock_run,
):
assert build_tool.main() == 0
calls = [c[0][0] for c in mock_run.call_args_list]
assert len(calls) > 1
assert calls[0][1] == "rc"
assert all(c[1] == "q" for c in calls[1:])
assert [o for c in calls for o in c[3:]] == objects
assert all(sum(len(a) + 1 for a in c) < 32000 for c in calls)
def test_ar_batch_failure_stops(tmp_path: Path) -> None:
"""A failing batch propagates its exit code without running the rest."""
archive = tmp_path / "lib.a"
rsp = tmp_path / "lib.a.rsp"
rsp.write_text("\n".join(f"{'y' * 200}_{i}.o" for i in range(300)) + "\n")
with (
patch.object(
build_tool.sys,
"argv",
["build_tool", "ar", "ar-bin", str(archive), str(rsp)],
),
patch.object(
build_tool.subprocess, "run", return_value=MagicMock(returncode=3)
) as mock_run,
):
assert build_tool.main() == 3
assert mock_run.call_count == 1
+23 -4
View File
@@ -477,13 +477,32 @@ def test_library_info_dropped_link_fields_warn(
def test_library_info_unmapped_sources_warn( def test_library_info_unmapped_sources_warn(
tmp_path: Path, caplog: pytest.LogCaptureFixture tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None: ) -> None:
"""Matched files that all fall through the suffix map are visible; an """Source-like files the case-sensitive suffix map rejects are named,
empty archive would fail far away at link.""" even when other sources compiled (a partial drop links with undefined
symbols far from the cause)."""
read_path = tmp_path / "lib" read_path = tmp_path / "lib"
(read_path / "src").mkdir(parents=True) (read_path / "src").mkdir(parents=True)
(read_path / "src" / "impl.CPP").write_text("") (read_path / "src" / "impl.CPP").write_text("")
component._library_info("x", read_path, {"build": {}}) (read_path / "src" / "sketch.ino").write_text("")
assert "no matched file has a recognized source suffix" in caplog.text (read_path / "src" / "ok.cpp").write_text("")
lib = component._library_info("x", read_path, {"build": {}})
assert [s.name for s in lib.sources] == ["ok.cpp"]
assert "not compiled: impl.CPP, sketch.ino" in caplog.text
def test_library_info_header_only_src_stays_quiet(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A header-only library (real headers in src/) is routine, not a
warning (the default +<*> filter matches the headers too)."""
read_path = tmp_path / "lib"
(read_path / "src").mkdir(parents=True)
(read_path / "src" / "ArduinoJson.h").write_text("")
(read_path / "keywords.txt").write_text("")
lib = component._library_info("x", read_path, {"build": {}})
assert lib.sources == []
assert "not compiled" not in caplog.text
assert "srcFilter" not in caplog.text
def test_library_info_lib_archive_malformed_raises(tmp_path: Path) -> None: def test_library_info_lib_archive_malformed_raises(tmp_path: Path) -> None:
@@ -645,6 +645,9 @@ def test_normalize_dependencies_forms(caplog) -> None:
{"name": "SPI"}, {"name": "SPI"},
] ]
assert normalize_dependencies("Wire") == [{"name": "Wire"}] assert normalize_dependencies("Wire") == [{"name": "Wire"}]
# A non-iterable value fails by manifest name, never a bare TypeError
assert normalize_dependencies(5, "libx") == []
assert "Ignoring unrecognized dependencies 5 of libx" in caplog.text
# The dict-shorthand form validates names like the list form: an empty # The dict-shorthand form validates names like the list form: an empty
# key and a spec overriding name with a non-string both warn and drop # key and a spec overriding name with a non-string both warn and drop
assert normalize_dependencies( assert normalize_dependencies(