Patch exactly the named segments and share the segment-line pattern

This commit is contained in:
J. Nick Koston
2026-08-20 13:44:12 -05:00
parent cea86989c9
commit e3936f500f
2 changed files with 45 additions and 59 deletions
+25 -31
View File
@@ -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: ``<seg> : 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
@@ -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)