Simplify library classification, share the manifest flag lexing, and home the build tool with the generators

This commit is contained in:
J. Nick Koston
2026-08-20 14:16:46 -05:00
parent 102973524e
commit d64592d2ea
6 changed files with 55 additions and 58 deletions
+12 -20
View File
@@ -31,11 +31,10 @@ from esphome.platformio.library import (
convert_libraries,
ensure_list,
is_lib_ignored,
join_flag_args,
lex_build_flags,
lib_ignore_set,
normalize_dependencies,
parse_library_properties,
split_flag_entry,
)
_LOGGER = logging.getLogger(__name__)
@@ -77,14 +76,7 @@ def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER))
# PlatformIO shell-lexes each build.flags entry
flag_tokens = join_flag_args(
(
token
for entry in ensure_list(build.get("flags", []))
for token in split_flag_entry(entry, f"library {name}")
),
f"library {name}",
)
flag_tokens = lex_build_flags(build.get("flags", []), f"library {name}")
lib = ArduinoLibrary(name=name)
include_flags: list[str] = []
@@ -153,20 +145,20 @@ def resolve_libraries(framework_path: Path) -> list[ArduinoLibrary]:
for library in CORE.platformio_libraries.values():
if is_lib_ignored(library.name, lib_ignore):
continue
# A version pin means a registry package ("pngle@1.1.0"), never a
# framework-bundled library.
# Only a bare name with a matching framework directory is bundled: a
# version pin means a registry package ("pngle@1.1.0"), and a bare
# name without the directory resolves from the registry at the
# latest version, matching PlatformIO (a typo fails loudly as a
# registry lookup error).
if (
library.repository
or library.version
or not library.name
or "/" in library.name
not library.repository
and not library.version
and library.name
and "/" not in library.name
and (framework_path / "libraries" / library.name).is_dir()
):
external.append(library)
elif (framework_path / "libraries" / library.name).is_dir():
bundled.append(_bundled_library(framework_path, library.name))
else:
# 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] = []
+2 -10
View File
@@ -27,8 +27,7 @@ from esphome.platformio.library import (
collect_filtered_files,
convert_libraries,
ensure_list,
join_flag_args,
split_flag_entry,
lex_build_flags,
split_list_by_condition,
)
@@ -107,14 +106,7 @@ def generate_cmakelists_txt(component: IDFComponent) -> str:
# flag and its argument (e.g. "-include cp_custom_alloc.h"); bare
# -I/-L/-l tokens re-glue to their argument ("-I foo" -> "-Ifoo") so the
# prefix classifiers below still route them.
build_flags = join_flag_args(
(
token
for entry in build_flags
for token in split_flag_entry(entry, f"library {component.name}")
),
f"library {component.name}",
)
build_flags = lex_build_flags(build_flags, f"library {component.name}")
# List all sources files
build_src_files = collect_filtered_files(
+17
View File
@@ -562,6 +562,23 @@ 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]:
"""Shell-lex a manifest ``build.flags`` list into joined tokens.
The composition every backend needs: each entry is lexed the way
PlatformIO's ParseFlags does, and bare ``-I``/``-L``/``-l``/``-D``
tokens re-glue to their argument across the whole stream.
"""
return join_flag_args(
(
token
for entry in ensure_list(entries)
for token in split_flag_entry(entry, owner)
),
owner,
)
def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]:
"""Join a bare ``-I``/``-L``/``-l``/``-D`` with its following token,
the way PlatformIO's ParseFlags lexes them."""
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino8266 import build_tool
from esphome.build_gen import build_tool
def test_ar_removes_stale_archive(tmp_path: Path) -> None:
+23 -27
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from contextlib import contextmanager
from pathlib import Path
from unittest.mock import patch
@@ -38,6 +39,25 @@ def _make_framework(tmp_path: Path) -> Path:
return framework
@contextmanager
def _emitting_converter(*converted):
"""Patch convert_libraries to emit the given components via the backend."""
def fake_convert(libraries: list, backend: LibraryBackend) -> list:
assert backend.platform == "espressif8266"
assert backend.framework == "arduino"
assert backend.cache_key == "arduino8266"
for c in converted:
backend.emit(c)
return list(converted)
with (
patch.object(component, "convert_libraries", side_effect=fake_convert),
patch.object(component, "apply_extra_script") as mock_extra,
):
yield mock_extra
def test_library_info_src_layout(tmp_path: Path) -> None:
framework = _make_framework(tmp_path)
lib = component._bundled_library(framework, "ESP8266WiFi")
@@ -176,17 +196,7 @@ def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None:
},
)
def fake_convert(libraries: list, backend: LibraryBackend) -> list:
assert backend.platform == "espressif8266"
assert backend.framework == "arduino"
assert backend.cache_key == "arduino8266"
backend.emit(converted)
return [converted]
with (
patch.object(component, "convert_libraries", side_effect=fake_convert),
patch.object(component, "apply_extra_script") as mock_extra,
):
with _emitting_converter(converted) as mock_extra:
libs = component.resolve_libraries(framework)
mock_extra.assert_called_once_with(
@@ -209,14 +219,7 @@ def test_resolve_libraries_bundled_dep_already_present(tmp_path: Path) -> None:
"some__External", lib_dir, {"dependencies": [{"name": "Wire"}]}
)
def fake_convert(libraries: list, backend: LibraryBackend) -> list:
backend.emit(converted)
return [converted]
with (
patch.object(component, "convert_libraries", side_effect=fake_convert),
patch.object(component, "apply_extra_script"),
):
with _emitting_converter(converted):
libs = component.resolve_libraries(framework)
# Wire appears once (from the explicit registration), not twice
@@ -297,14 +300,7 @@ def test_resolve_libraries_lib_ignore_covers_bundled_dependencies(
"some__External", lib_dir, {"dependencies": [{"name": "Wire"}]}
)
def fake_convert(libraries: list, backend: LibraryBackend) -> list:
backend.emit(converted)
return [converted]
with (
patch.object(component, "convert_libraries", side_effect=fake_convert),
patch.object(component, "apply_extra_script"),
):
with _emitting_converter(converted):
libs = component.resolve_libraries(framework)
assert [lib.name for lib in libs] == ["some__External"]