Pin the decoy anchor, guard unselected segments, fingerprint the module source

This commit is contained in:
J. Nick Koston
2026-08-24 18:46:47 -05:00
committed by J. Nick Koston
parent b2ac49d8a8
commit 4d92741e69
3 changed files with 50 additions and 37 deletions
+18 -19
View File
@@ -361,31 +361,30 @@ BOARDS = {
},
}
"""
ESP8266_BOARD_BUILD generate with (v4.2.1 is the platform version the
native toolchain mirrors; regenerate against the tag when bumping it):
git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
python3 - <<'EOF'
import json, glob, os
for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
b = json.load(open(f))["build"]
extra = b["extra_flags"]
extra = extra.split() if isinstance(extra, str) else extra
defines = [
e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
]
entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
board = os.path.splitext(os.path.basename(f))[0]
print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
EOF
"""
# Per-board Arduino core build metadata for the native (PlatformIO-free)
# toolchain: the variant directory (supplies pins_arduino.h) and the
# board-identity defines the PlatformIO builder passes via build.extra_flags.
# -DESP8266 and -DARDUINO_ARCH_ESP8266 are shared by every board and added by
# the generator; only the per-board defines are listed here.
#
# ESP8266_BOARD_BUILD generate with (v4.2.1 is the platform version the
# native toolchain mirrors; regenerate against the tag when bumping it):
#
# git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
# python3 - <<'EOF'
# import json, glob, os
# for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
# b = json.load(open(f))["build"]
# extra = b["extra_flags"]
# extra = extra.split() if isinstance(extra, str) else extra
# defines = [
# e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
# ]
# entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
# board = os.path.splitext(os.path.basename(f))[0]
# print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
# EOF
ESP8266_BOARD_BUILD = {
"agruminolemon": {
"variant": "agruminolemonv4",
+16 -9
View File
@@ -82,6 +82,14 @@ def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str
raises, since a silently kept real memory limit would fail grouped
builds far from the cause.
"""
for segment in _TESTING_SEGMENT_SIZES:
if segment not in segments and _segment_line_re(segment).search(content):
# A known segment left unpatched would keep its real memory limit
# and silently under-provision the testing build
raise RuntimeError(
f"Testing-mode segment {segment} is present in the linker "
"script but was not selected for patching"
)
for segment in segments:
if segment not in _TESTING_SEGMENT_SIZES:
raise RuntimeError(f"Unknown testing-mode segment {segment!r}")
@@ -103,15 +111,14 @@ def segment_length(content: str, segment_name: str) -> int | None:
def surgery_fingerprint() -> str:
"""Fingerprint of every behavioral input to the surgeries.
"""Fingerprint of this module's source, covering every behavioral input.
Linker-script caches include it so an edit here invalidates them.
Linker-script caches include it so an edit here invalidates them; hashing
the source over-invalidates on comment edits, which is the safe direction.
Native-toolchain-only, like ``segment_length``; no script twin.
"""
parts = (
RATETABLE_RULE,
_RATETABLE_COMMENT,
_RATETABLE_ANCHOR.pattern,
repr(sorted(_TESTING_SEGMENT_SIZES.items())),
)
return hashlib.sha256("|".join(parts).encode()).hexdigest()
import inspect
import sys
source = inspect.getsource(sys.modules[__name__])
return hashlib.sha256(source.encode()).hexdigest()
@@ -49,7 +49,8 @@ def test_relocate_ratetable_inserts_after_data_start() -> None:
patched = relocate_ratetable(_COMMON_LD_SNIPPET)
assert RATETABLE_RULE in patched
# Inserted after the .data section's anchor, not the .dport0.data one
assert patched.index("_data_start = ABSOLUTE(.);") < patched.index(RATETABLE_RULE)
# (whose closing brace bounds the decoy block)
assert RATETABLE_RULE not in patched[: patched.index("} >dport0_0_seg")]
assert patched.index(RATETABLE_RULE) < patched.index("*(.data)")
# Idempotent on an already-patched script
assert relocate_ratetable(patched) == patched
@@ -106,14 +107,20 @@ def test_board_build_covers_every_board() -> None:
assert set(BOARDS) <= set(ESP8266_BOARD_BUILD)
def test_surgery_fingerprint_tracks_inputs() -> None:
"""The fingerprint changes with any behavioral input, so linker-script
caches stamped with it self-invalidate on surgery edits."""
from unittest.mock import patch
def test_surgery_fingerprint_covers_module_source() -> None:
"""The fingerprint hashes the module source, so any surgery edit
invalidates linker-script caches stamped with it."""
import hashlib
import inspect
from esphome.components.esp8266 import build_surgery
base = build_surgery.surgery_fingerprint()
assert base == build_surgery.surgery_fingerprint()
with patch.object(build_surgery, "_TESTING_SEGMENT_SIZES", {"iram1_0_seg": "0x1"}):
assert build_surgery.surgery_fingerprint() != base
expected = hashlib.sha256(inspect.getsource(build_surgery).encode()).hexdigest()
assert build_surgery.surgery_fingerprint() == expected
def test_testing_memory_patches_present_but_unselected_raises() -> None:
"""A known segment left off the caller's list must fail, not silently
keep its real memory limit."""
with pytest.raises(RuntimeError, match="not selected"):
apply_testing_memory_patches(_FLASH_LD_SNIPPET, ("dram0_0_seg",))