From e3936f500fb1fa3d4e14faab8db7bbbd40153001 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 13:44:12 -0500 Subject: [PATCH] Patch exactly the named segments and share the segment-line pattern --- esphome/components/esp8266/build_surgery.py | 56 +++++++++---------- .../components/esp8266/test_build_surgery.py | 48 +++++++--------- 2 files changed, 45 insertions(+), 59 deletions(-) diff --git a/esphome/components/esp8266/build_surgery.py b/esphome/components/esp8266/build_surgery.py index 039ecaf0c6..9aeee79216 100644 --- a/esphome/components/esp8266/build_surgery.py +++ b/esphome/components/esp8266/build_surgery.py @@ -5,6 +5,7 @@ These mirror the PlatformIO extra scripts in this directory inside SCons and must stay self-contained. The native build generator applies the same patches to the linker scripts it generates, so the logic lives here as plain functions. Keep both in sync when changing either. +``segment_length`` is native-toolchain-only and has no script twin. """ from __future__ import annotations @@ -45,48 +46,41 @@ def relocate_ratetable(content: str) -> str: ) -_TESTING_SEGMENT_SIZES = ( - ("iram1_0_seg", TESTING_IRAM_SIZE), - ("dram0_0_seg", TESTING_DRAM_SIZE), - ("irom0_0_seg", TESTING_FLASH_SIZE), -) +_TESTING_SEGMENT_SIZES = { + "iram1_0_seg": TESTING_IRAM_SIZE, + "dram0_0_seg": TESTING_DRAM_SIZE, + "irom0_0_seg": TESTING_FLASH_SIZE, +} -def _patch_segment_size(content: str, segment_name: str, new_size: str) -> str: - pattern = ( +def _segment_line_re(segment_name: str) -> re.Pattern[str]: + """The MEMORY line for one segment: `` : org = 0x..., len = 0x...``.""" + return re.compile( rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)" - r"0x[0-9a-fA-F]+" + r"(0x[0-9a-fA-F]+)" ) - return re.sub(pattern, rf"\g<1>{new_size}", content) -def apply_testing_memory_patches(content: str, require: Collection[str]) -> str: - """Enlarge IRAM/DRAM/flash segments so grouped CI test builds can link. +def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str: + """Enlarge the named memory segments so grouped CI test builds can link. - ``require`` names the segments this file must define; a silently - unpatched segment would keep the real memory limits and fail grouped - builds far from the cause. The segments are split across the two linker - scripts (iram1_0_seg in the generated common one, dram0_0_seg and - irom0_0_seg in the flash one), so each caller requires only its own. + Each caller passes the segments its linker script defines; a segment + that fails to match raises, since a silently kept real memory limit + would fail grouped builds far from the cause. """ - missing = set(require) - for segment, size in _TESTING_SEGMENT_SIZES: - patched = _patch_segment_size(content, segment, size) - if patched != content: - missing.discard(segment) - content = patched - if missing: - raise RuntimeError( - f"Testing-mode memory patch failed: segment(s) {', '.join(sorted(missing))} " - "not found (has the Arduino core linker script changed?)" + for segment in segments: + content, count = _segment_line_re(segment).subn( + rf"\g<1>{_TESTING_SEGMENT_SIZES[segment]}", content ) + if count == 0: + raise RuntimeError( + f"Testing-mode memory patch failed: segment {segment} " + "not found (has the Arduino core linker script changed?)" + ) return content def segment_length(content: str, segment_name: str) -> int | None: """Read a memory segment's length from linker script content.""" - match = re.search( - rf"{segment_name}\s*:.+len\s*=\s*(0x[\da-fA-F]+)", - content, - ) - return int(match.group(1), 16) if match else None + match = _segment_line_re(segment_name).search(content) + return int(match.group(2), 16) if match else None diff --git a/tests/unit_tests/components/esp8266/test_build_surgery.py b/tests/unit_tests/components/esp8266/test_build_surgery.py index 200954494b..0e5a26294a 100644 --- a/tests/unit_tests/components/esp8266/test_build_surgery.py +++ b/tests/unit_tests/components/esp8266/test_build_surgery.py @@ -4,10 +4,12 @@ from __future__ import annotations import pytest +from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import ( RATETABLE_RULE, apply_testing_memory_patches, relocate_ratetable, + segment_length, ) _COMMON_LD_SNIPPET = """\ @@ -49,39 +51,29 @@ def test_relocate_ratetable_requires_anchor() -> None: def test_testing_memory_patches_enlarge_segments() -> None: - patched = apply_testing_memory_patches(_FLASH_LD_SNIPPET, require=()) - assert ( - "iram1_0_seg : org = 0x40100000, len = 0x200000" - in patched - ) - assert ( - "dram0_0_seg : org = 0x3FFE8000, len = 0x200000" - in patched - ) - assert ( - "irom0_0_seg : org = 0x40201010, len = 0x2000000" - in patched + patched = apply_testing_memory_patches( + _FLASH_LD_SNIPPET, ("iram1_0_seg", "dram0_0_seg", "irom0_0_seg") ) + assert segment_length(patched, "iram1_0_seg") == 0x200000 + assert segment_length(patched, "dram0_0_seg") == 0x200000 + assert segment_length(patched, "irom0_0_seg") == 0x2000000 + # Untouched segments keep their sizes + assert segment_length(patched, "dport0_0_seg") == 0x10 def test_segment_length() -> None: - from esphome.components.esp8266.build_surgery import segment_length - assert segment_length(_FLASH_LD_SNIPPET, "irom0_0_seg") == 0xFEFF0 assert segment_length(_FLASH_LD_SNIPPET, "missing_seg") is None -def test_testing_memory_patches_require() -> None: - """A required segment the patch could not find raises instead of - silently keeping the real memory limits.""" - patched = apply_testing_memory_patches( - _FLASH_LD_SNIPPET, require=("dram0_0_seg", "irom0_0_seg") - ) - assert "0x2000000" in patched - with pytest.raises(RuntimeError, match="dram0_0_seg, irom0_0_seg"): - apply_testing_memory_patches( - "MEMORY { }", require=("dram0_0_seg", "irom0_0_seg") - ) - # Segments a file does not require are patched opportunistically only - # With nothing required, unmatched content passes through unchanged - assert apply_testing_memory_patches("MEMORY { }", require=()) == "MEMORY { }" +def test_testing_memory_patches_missing_segment_raises() -> None: + """A named segment the patch could not find raises instead of silently + keeping the real memory limits.""" + with pytest.raises(RuntimeError, match="dram0_0_seg"): + apply_testing_memory_patches("MEMORY { }", ("dram0_0_seg",)) + + +def test_board_build_covers_every_board() -> None: + """Every supported board has native build metadata (the table may carry + extras that BOARDS does not expose).""" + assert set(BOARDS) <= set(ESP8266_BOARD_BUILD)