From 923be0736bec953ae4a4d5f96ab543b264a6e020 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 12:07:38 -0500 Subject: [PATCH] Match glob archive specs in the fragment belt and quote the list expansion --- esphome/build_gen/espidf.py | 2 +- esphome/espidf/toolchain.py | 19 +++++++++++++++++-- tests/unit_tests/build_gen/test_espidf.py | 2 ++ tests/unit_tests/test_espidf_toolchain.py | 19 +++++++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 87aa1e0690..7b1b7a4990 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -56,7 +56,7 @@ if(COMMAND __ldgen_get_lib_deps_of_target) "nothing; app edits will regenerate sections.ld.") endif() endif() - set(${out_list_var} ${${out_list_var}} PARENT_SCOPE) + set(${out_list_var} "${${out_list_var}}" PARENT_SCOPE) endfunction() else() message(@MISSING@ "ESPHome ldgen override target not found; " diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 3906c469bf..6de4010612 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -1,6 +1,7 @@ """ESP-IDF direct build API for ESPHome.""" from dataclasses import dataclass, field +import fnmatch import hashlib import json import logging @@ -480,7 +481,21 @@ def _patch_memory_segments(): _LDGEN_FRAGMENTS_RE = re.compile(r'--fragments-list\s+"([^"]+)"') -_APP_ARCHIVE_MAPPED_RE = re.compile(r"^\s*archive:\s*libsrc\.a\b", re.MULTILINE) +_LDGEN_ARCHIVE_RE = re.compile(r"^\s*archive:\s*(\S+)", re.MULTILINE) + + +def _fragment_maps_app_archive(text: str) -> bool: + """True when an archive: spec selects libsrc.a, the archive of the src + component excluded as idf::src/__idf_src in build_gen/espidf.py. + + The bare * is IDF's stock catch-all; its archive-level entries resolve + in the linker against all link inputs, so it stays safe when the + archive is excluded from ldgen's own inputs. + """ + return any( + value != "*" and fnmatch.fnmatch("libsrc.a", value) + for value in _LDGEN_ARCHIVE_RE.findall(text) + ) def _ldgen_check_skip(msg: str, strict: bool) -> None: @@ -512,7 +527,7 @@ def _warn_if_app_archive_mapped() -> None: except OSError as e: _ldgen_check_skip(f"could not read {fragment}: {e}", strict) continue - if _APP_ARCHIVE_MAPPED_RE.search(text): + if _fragment_maps_app_archive(text): msg = ( f"Linker fragment {fragment} maps the app archive; its " "entries may be skipped. Set ESPHOME_LDGEN_FULL_DEPS=1 " diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 4adb3d7884..47ce8fea04 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -173,6 +173,8 @@ def test_get_project_cmakelists_emits_ldgen_override( monkeypatch.delenv("ESPHOME_LDGEN_STRICT", raising=False) content = _render(minimal=minimal) assert "REMOVE_ITEM ${out_list_var} idf::src __idf_src" in content + # Quoted so spaced elements survive and an empty list stays defined + assert 'set(${out_list_var} "${${out_list_var}}" PARENT_SCOPE)' in content assert 'message(WARNING "ESPHome ldgen app archive exclusion' in content assert 'message(STATUS "ESPHome ldgen override target not found' in content assert 'message(WARNING "ESPHome ldgen override never filtered' in content diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 66bcf43450..d1b8d23083 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -690,13 +690,28 @@ def test_warn_if_app_archive_mapped_strict_raises( toolchain._warn_if_app_archive_mapped() +def test_warn_if_app_archive_mapped_glob( + setup_core: Path, tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A glob archive spec that selects the app archive is also flagged.""" + _setup_build(setup_core) + frag = tmp_path / "linker.lf" + frag.write_text("[mapping:evil]\narchive: lib*\n") + _write_fragments_build_ninja(tmp_path, [frag]) + toolchain._warn_if_app_archive_mapped() + assert "maps the app archive" in caplog.text + + def test_warn_if_app_archive_mapped_clean( setup_core: Path, tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: - """Normal fragments produce no warning.""" + """Normal fragments, including IDF's stock archive: * catch-all, + produce no warning.""" _setup_build(setup_core) frag = tmp_path / "linker.lf" - frag.write_text("[mapping:freertos]\narchive: libfreertos.a\n") + frag.write_text( + "[mapping:freertos]\narchive: libfreertos.a\n[mapping:default]\narchive: *\n" + ) _write_fragments_build_ninja(tmp_path, [frag]) toolchain._warn_if_app_archive_mapped() assert "maps the app archive" not in caplog.text