From 18e3eac419808d4d176e7d6b5840b081fca4c354 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 14:14:32 -0500 Subject: [PATCH] Export a surgery fingerprint for linker-script cache stamps --- esphome/components/esp8266/build_surgery.py | 21 ++++++++++++++++++- .../components/esp8266/test_build_surgery.py | 13 ++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp8266/build_surgery.py b/esphome/components/esp8266/build_surgery.py index 9aeee79216..c0034f7bad 100644 --- a/esphome/components/esp8266/build_surgery.py +++ b/esphome/components/esp8266/build_surgery.py @@ -11,11 +11,15 @@ as plain functions. Keep both in sync when changing either. from __future__ import annotations from collections.abc import Collection +import hashlib import re # Move the NONOS SDK wifi rate tables from flash to DRAM; see # relocate_ratetable.py.script for the full background (NONOS SDK issue 320). RATETABLE_RULE = "*libnet80211.a:ieee80211_phy.o(.irom.text .irom.text.*)" +_RATETABLE_COMMENT = ( + "/* ESPHome: wifi rate tables must live in DRAM, see NONOS SDK issue 320 */" +) # Match the whole line: "_data_start" is also a substring of the # "_dport0_data_start" line in the earlier .dport0.data section _RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE) @@ -40,7 +44,7 @@ def relocate_ratetable(content: str) -> str: insert_pos = match.end() return ( content[:insert_pos] - + "\n /* ESPHome: wifi rate tables must live in DRAM, see NONOS SDK issue 320 */" + + f"\n {_RATETABLE_COMMENT}" + f"\n {RATETABLE_RULE}" + content[insert_pos:] ) @@ -84,3 +88,18 @@ def segment_length(content: str, segment_name: str) -> int | None: """Read a memory segment's length from linker script content.""" match = _segment_line_re(segment_name).search(content) return int(match.group(2), 16) if match else None + + +def surgery_fingerprint() -> str: + """Fingerprint of every behavioral input to the surgeries. + + Linker-script caches include it so an edit here invalidates them. + 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() diff --git a/tests/unit_tests/components/esp8266/test_build_surgery.py b/tests/unit_tests/components/esp8266/test_build_surgery.py index 0e5a26294a..64e36f1591 100644 --- a/tests/unit_tests/components/esp8266/test_build_surgery.py +++ b/tests/unit_tests/components/esp8266/test_build_surgery.py @@ -77,3 +77,16 @@ 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) + + +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 + + 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