diff --git a/esphome/arduino8266/component.py b/esphome/arduino8266/component.py index 09cd4a5ea4..8fee3d05f8 100644 --- a/esphome/arduino8266/component.py +++ b/esphome/arduino8266/component.py @@ -18,7 +18,7 @@ import logging from pathlib import Path import shlex -from esphome.core import CORE, EsphomeError, Library +from esphome.core import CORE, Library from esphome.espidf.extra_script import apply_extra_script from esphome.platformio.library import ( DEFAULT_BUILD_INCLUDE_DIR, @@ -140,12 +140,9 @@ def resolve_libraries(framework_path: Path) -> list[ArduinoLibrary]: elif (framework_path / "libraries" / library.name).is_dir(): bundled.append(_bundled_library(framework_path, library.name)) else: - # PlatformIO fails on an unresolvable lib_deps entry too; building - # without it would only surface as unrelated include/link errors. - raise EsphomeError( - f"Library {library.name} is not bundled with the Arduino " - "framework and has no version or repository to download it from" - ) + # A bare registry name; resolved at the latest version, matching + # PlatformIO (a typo fails loudly as a registry lookup error). + external.append(library) converted: list[ArduinoLibrary] = [] bundled_names = {lib.name for lib in bundled} diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index 32689ccc1a..b1295f9469 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -36,6 +36,7 @@ from esphome.framework_helpers import ( str_to_lst_of_str, ) from esphome.helpers import get_bool_env, get_str_env +from esphome.platformio.library import ensure_list _LOGGER = logging.getLogger(__name__) @@ -135,11 +136,9 @@ def _registry_download( if ver.get("name") != version: continue for file in ver.get("files", []): - systems = file.get("system") or "*" - # A bare string would make ``in`` a substring test - if isinstance(systems, str) and systems != "*": - systems = [systems] - if systems == "*" or system in systems: + # ensure_list: a bare string would make ``in`` a substring test + systems = ensure_list(file.get("system") or "*") + if "*" in systems or system in systems: return ( file["download_url"], (file.get("checksum") or {}).get("sha256"), diff --git a/esphome/arduino8266/toolchain.py b/esphome/arduino8266/toolchain.py index dbd24277ab..7cfd4262d3 100644 --- a/esphome/arduino8266/toolchain.py +++ b/esphome/arduino8266/toolchain.py @@ -144,13 +144,11 @@ def _print_size_summary(build_dir: Path, toolchain_path: Path) -> None: try: sections[parts[0]] = int(parts[1]) except ValueError: - # A confident total built on a dropped section would feed a - # wrong number to CI's memory-impact metric; skip the summary. - _LOGGER.warning( - "Unparsable size output for section %s; skipping the size summary", - parts[0], - ) - return + _LOGGER.warning("Unparsable size output for section %s", parts[0]) + if parts[0] in _RAM_SECTIONS or parts[0] in _FLASH_SECTIONS: + # A confident total built on a dropped section would feed + # a wrong number to CI's memory-impact metric + return ram = sum(sections.get(s, 0) for s in _RAM_SECTIONS) flash = sum(sections.get(s, 0) for s in _FLASH_SECTIONS) print(f"RAM: {format_bar(ram, _MAX_RAM_SIZE)}") diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 8c0b5495e7..4a18c029a6 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -69,7 +69,9 @@ _CORE_EXCLUDE_WAVEFORM = { "core_esp8266_waveform_phase.cpp", } -# From platformio-build.py, in its order of precedence (first is the default). +# From platformio-build.py. The first entry is the default; with multiple SDK +# knobs set (a pathological config) ties break by table order, since +# upstream's tie-break depends on define order and is not reproducible here. _NONOSDK_VERSIONS = ( ("SDK22x_190703", "NONOSDK22x_190703"), ("SDK221", "NONOSDK221"), diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 1e743ae031..d7bfab1569 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -252,11 +252,10 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None: # -L/-l from esphome build_flags reach the link line, not the compiles assert '-L"/opt/blobs"' in content assert "-luser_blob" in content - assert "cflags" not in [ - line.split(" = ")[0].strip() - for line in content.splitlines() - if "user_blob" in line - ] + for line in content.splitlines(): + if line.split(" = ")[0] in ("cflags", "cxxflags", "asflags"): + assert "user_blob" not in line + assert "/opt/blobs" not in line # System libraries with the selected lwIP variant, in the builder's order assert ( "-lhal -lphy -lpp -lnet80211 -llwip2-1460 -lwpa -lcrypto -lmain -lwps " @@ -524,7 +523,8 @@ def test_write_project_missing_framework_dir_raises(tmp_path: Path) -> None: def test_build_config_nonosdk_precedence() -> None: - """With two SDK knobs set, the first table entry wins (documented order).""" + """With two SDK knobs set (a pathological config), ties break + deterministically by table order.""" _set_flags( "-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305", "-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221", diff --git a/tests/unit_tests/test_arduino8266_component.py b/tests/unit_tests/test_arduino8266_component.py index f654ee57da..d27da30847 100644 --- a/tests/unit_tests/test_arduino8266_component.py +++ b/tests/unit_tests/test_arduino8266_component.py @@ -100,14 +100,15 @@ def test_resolve_libraries_bundled(tmp_path: Path) -> None: assert [lib.name for lib in libs] == ["ESP8266WiFi"] -def test_resolve_libraries_unknown_bare_name_raises(tmp_path: Path) -> None: - """An unresolvable library fails the build by name, as PlatformIO does.""" - from esphome.core import EsphomeError - +def test_resolve_libraries_bare_registry_name_is_external(tmp_path: Path) -> None: + """A bare name that is not bundled resolves from the registry at the + latest version, matching PlatformIO and the documented libraries: key.""" framework = _make_framework(tmp_path) - _add_library("Typoo", None) - with pytest.raises(EsphomeError, match="Typoo"): + _add_library("pngle", None) + with patch.object(component, "convert_libraries", return_value=[]) as mock_convert: component.resolve_libraries(framework) + (libraries, _backend), _ = mock_convert.call_args + assert [lib.name for lib in libraries] == ["pngle"] def _converted(name: str, source_dir: Path, data: dict) -> ConvertedLibrary: diff --git a/tests/unit_tests/test_arduino8266_toolchain.py b/tests/unit_tests/test_arduino8266_toolchain.py index 84ff8f7436..28311823b0 100644 --- a/tests/unit_tests/test_arduino8266_toolchain.py +++ b/tests/unit_tests/test_arduino8266_toolchain.py @@ -223,13 +223,14 @@ def test_run_compile_skips_compdb_when_ninja_unchanged(tmp_path: Path) -> None: run(regenerate_expected=False) -def test_print_size_summary_unparsable_section_skips_summary( +def test_print_size_summary_unparsable_section( tmp_path: Path, capsys: pytest.CaptureFixture[str], caplog: pytest.LogCaptureFixture, ) -> None: - """A section that fails to parse must not produce a confident wrong total.""" - bad = _SIZE_OUTPUT + ".broken abc 0\n" + """A totals-relevant section that fails to parse must not produce a + confident wrong number; an irrelevant one only warns.""" + bad = _SIZE_OUTPUT.replace(".bss 26504", ".bss abc") with patch.object( toolchain.subprocess, "run", @@ -238,3 +239,17 @@ def test_print_size_summary_unparsable_section_skips_summary( toolchain._print_size_summary(tmp_path, tmp_path / "toolchain") assert capsys.readouterr().out == "" assert "Unparsable size output" in caplog.text + + caplog.clear() + harmless = _SIZE_OUTPUT + ".broken abc 0\n" + with ( + patch.object( + toolchain.subprocess, + "run", + return_value=MagicMock(returncode=0, stdout=harmless), + ), + patch.object(toolchain, "_parse_app_size", return_value=1044464), + ): + toolchain._print_size_summary(tmp_path, tmp_path / "toolchain") + assert "RAM:" in capsys.readouterr().out + assert "Unparsable size output" in caplog.text