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

This commit is contained in:
J. Nick Koston
2026-08-22 16:38:32 -05:00
4 changed files with 78 additions and 23 deletions
+19 -19
View File
@@ -99,14 +99,18 @@ def _resolve_src_dir(name: str, read_path: Path, build: dict) -> str:
def _reject_unsupported_link_fields(name: str, data: dict) -> None: def _reject_unsupported_link_fields(name: str, data: dict) -> None:
for dropped_key in ("precompiled", "ldflags"): # PIO's Arduino lib builder honors these; building without them would
if data.get(dropped_key): # fail at link with no stated cause. library.properties values are
# PIO's Arduino lib builder honors these; building without them # strings, so "false" (the spec's explicit opt-out) is not a declaration.
# would fail at link with no stated cause precompiled = data.get("precompiled")
raise EsphomeError( if precompiled and str(precompiled).strip().lower() != "false":
f"Library {name} declares {dropped_key}, which this backend " raise EsphomeError(
"does not support" f"Library {name} declares precompiled, which this backend does not support"
) )
if data.get("ldflags"):
raise EsphomeError(
f"Library {name} declares ldflags, which this backend does not support"
)
def _resolve_lib_archive(name: str, data: dict, build: dict) -> bool: def _resolve_lib_archive(name: str, data: dict, build: dict) -> bool:
@@ -218,18 +222,14 @@ def _collect_lib_sources(
len(dropped), len(dropped),
", ".join(sorted(dropped)), ", ".join(sorted(dropped)),
) )
if ( if not lib.sources and not any(
not lib.sources Path(f).suffix.lower() in LIBRARY_HEADER_SUFFIXES for f in matched
and ("srcFilter" in build or "srcDir" in build)
and not any(Path(f).suffix.lower() in LIBRARY_HEADER_SUFFIXES for f in matched)
): ):
# Matched headers mean a header-only library; a declared filter # Matched headers mean a header-only library; a filter matching
# matching nothing (or only inert files) is a manifest/tree problem. # nothing (or only inert files) is a manifest/tree problem whether
# The truly empty tree raises via _assert_tree_has_code. # or not it was declared. The truly empty tree raises via
_LOGGER.warning( # _assert_tree_has_code.
"Library %s declares srcFilter/srcDir but no source files matched", _LOGGER.warning("Library %s: no source files matched", name)
name,
)
def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
+5 -3
View File
@@ -67,9 +67,11 @@ def _run_ar(ar: str, archive: str, rspfile: str) -> int:
def _run_copy(src: str, dst: str) -> int: def _run_copy(src: str, dst: str) -> int:
try: try:
shutil.copyfile(src, dst) shutil.copyfile(src, dst)
except OSError: except OSError as err:
# Never leave a partially written output (e.g. a firmware image) # Never leave a partially written output (e.g. a firmware image);
Path(dst).unlink(missing_ok=True) # SameFileError means dst IS src, where unlinking destroys the input
if not isinstance(err, shutil.SameFileError):
Path(dst).unlink(missing_ok=True)
raise raise
return 0 return 0
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import shutil
import subprocess import subprocess
import sys import sys
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@@ -188,6 +189,20 @@ def test_surplus_arguments_error(capsys: pytest.CaptureFixture[str]) -> None:
assert "expected 2 arguments, got 3" in capsys.readouterr().err assert "expected 2 arguments, got 3" in capsys.readouterr().err
def test_copy_same_file_keeps_the_input(tmp_path: Path) -> None:
"""A same-file copy (dst IS src) must not unlink the input."""
src = tmp_path / "firmware.bin"
src.write_bytes(b"image")
with (
patch.object(
build_tool.sys, "argv", ["build_tool", "copy", str(src), str(src)]
),
pytest.raises(shutil.SameFileError),
):
build_tool.main()
assert src.read_bytes() == b"image"
def test_copy_failure_leaves_no_partial_output(tmp_path: Path) -> None: def test_copy_failure_leaves_no_partial_output(tmp_path: Path) -> None:
"""A failed copy unlinks the destination; a partial firmware image must """A failed copy unlinks the destination; a partial firmware image must
never be left on disk.""" never be left on disk."""
+39 -1
View File
@@ -189,7 +189,7 @@ def test_library_info_declared_filter_matches_nothing_warns(
data = {"build": {"srcFilter": ["+<nothing/*>"]}} data = {"build": {"srcFilter": ["+<nothing/*>"]}}
lib = component._library_info("x", read_path, data) lib = component._library_info("x", read_path, data)
assert not lib.sources assert not lib.sources
assert "declares srcFilter/srcDir but no source files matched" in caplog.text assert "no source files matched" in caplog.text
def test_empty_converted_tree_raises_at_emit(tmp_path: Path) -> None: def test_empty_converted_tree_raises_at_emit(tmp_path: Path) -> None:
@@ -606,6 +606,44 @@ def test_library_info_unsupported_link_fields_raise(tmp_path: Path) -> None:
component._library_info("x", read_path, {"ldflags": "-lfoo", "build": {}}) component._library_info("x", read_path, {"ldflags": "-lfoo", "build": {}})
@pytest.mark.parametrize("value", ["false", "False", " false ", "", False, None])
def test_library_info_precompiled_opt_out_accepted(
tmp_path: Path, value: object
) -> None:
"""Manifest values are strings; precompiled=false is the spec's
explicit opt-out, not a declaration."""
read_path = tmp_path / "lib"
(read_path / "src").mkdir(parents=True)
(read_path / "src" / "stub.cpp").write_text("")
data = {"build": {}}
if value is not None:
data["precompiled"] = value
component._library_info("x", read_path, data)
@pytest.mark.parametrize("value", ["full", True, "weird"])
def test_library_info_precompiled_set_raises(tmp_path: Path, value: object) -> None:
"""Both full (Arduino's other legal value) and unknown spellings fail safe."""
read_path = tmp_path / "lib"
(read_path / "src").mkdir(parents=True)
(read_path / "src" / "stub.cpp").write_text("")
with pytest.raises(EsphomeError, match="declares precompiled"):
component._library_info("x", read_path, {"precompiled": value, "build": {}})
def test_library_info_default_filter_matching_nothing_warns(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""The empty-match warning is not gated on a declared srcFilter/srcDir;
a default-filter src/ holding only inert files warns too."""
read_path = tmp_path / "lib"
(read_path / "src").mkdir(parents=True)
(read_path / "src" / "keywords.txt").write_text("")
lib = component._library_info("x", read_path, {"build": {}})
assert not lib.sources
assert "no source files matched" in caplog.text
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: