From 69a3ab6fb17a44bc81f2ceadefef9be7a8364316 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2026 18:51:10 -0500 Subject: [PATCH] Error instead of substituting a state-destroying layout on legacy cores, extract the ld choice, regroup KEY_LDSCRIPT, document segment_length's None --- esphome/components/esp8266/__init__.py | 48 ++++++++++--------- esphome/components/esp8266/build_surgery.py | 7 ++- esphome/components/esp8266/const.py | 5 +- .../components/esp8266/test_boards.py | 19 ++++++++ 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 593e80056f..75483c5293 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -277,6 +277,31 @@ def check_rosetta() -> None: ) +def _choose_ld_script(board: str, ver: cv.Version) -> str | None: + """The flash ld to pin for this board and core, or None for cores + without ld-script support.""" + board_data = BOARDS[board] + ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]] + if ver <= cv.Version(2, 3, 0): + # No ld script support + return None + if ver <= cv.Version(2, 4, 2): + # Old ld script path; the modern per-board override names do not + # exist in this core's SDK, so the override cannot be honored. + # Substituting the size default would move _FS_end and the + # preferences sector, wiping flash-backed state on flash. + if KEY_LDSCRIPT in board_data: + raise EsphomeError( + f"Board {board} requires its {board_data[KEY_LDSCRIPT]} " + f"flash layout, which Arduino core {ver} cannot honor; " + "use a core newer than 2.4.2" + ) + return ld_scripts[0] + # A per-board override preserves a layout the board shipped with + # (see d1_wroom_02 in boards.py) + return board_ld_script(board_data) + + @coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config: ConfigType) -> None: cg.add(esp8266_ns.setup_preferences()) @@ -398,28 +423,7 @@ async def to_code(config: ConfigType) -> None: ) if config[CONF_BOARD] in BOARDS: - board_data = BOARDS[config[CONF_BOARD]] - ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]] - - if ver <= cv.Version(2, 3, 0): - # No ld script support - ld_script = None - elif ver <= cv.Version(2, 4, 2): - # Old ld script path; the modern per-board override names do - # not exist in this core's SDK, so it cannot be honored - if KEY_LDSCRIPT in board_data: - _LOGGER.warning( - "Board %s pins %s, which Arduino core %s cannot honor; " - "using the default flash layout", - config[CONF_BOARD], - board_data[KEY_LDSCRIPT], - ver, - ) - ld_script = ld_scripts[0] - else: - # A per-board override preserves a layout the board shipped - # with (see d1_wroom_02 in boards.py) - ld_script = board_ld_script(board_data) + ld_script = _choose_ld_script(config[CONF_BOARD], ver) if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) diff --git a/esphome/components/esp8266/build_surgery.py b/esphome/components/esp8266/build_surgery.py index 2df1d5dbb8..eb6ed1b91b 100644 --- a/esphome/components/esp8266/build_surgery.py +++ b/esphome/components/esp8266/build_surgery.py @@ -103,7 +103,12 @@ def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str def segment_length(content: str, segment_name: str) -> int | None: - """Read a memory segment's length from linker script content.""" + """Read a memory segment's length from linker script content. + + Returns None for an absent segment OR an unparsable line; callers must + treat None as "no usable budget" and warn (as the Flash summary does), + never as "no limit". + """ match = _segment_line_re(segment_name).search(content) return int(match.group(2), 16) if match else None diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index c6f550c594..50f103ed2d 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -18,6 +18,8 @@ KEY_SERIAL1_REQUIRED = "serial1_required" # Set for the native (non-PlatformIO) toolchain's build generator KEY_FLASH_MODE = "flash_mode" KEY_SCANF_FLOAT = "scanf_float" +# Per-board flash-layout override consumed by board_ld_script() +KEY_LDSCRIPT = "ldscript" # esp8266 namespace is already defined by arduino, manually prefix esphome esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266") @@ -71,6 +73,3 @@ def enable_serial1() -> None: enable_serial1() """ CORE.data.setdefault(KEY_ESP8266, {})[KEY_SERIAL1_REQUIRED] = True - - -KEY_LDSCRIPT = "ldscript" diff --git a/tests/unit_tests/components/esp8266/test_boards.py b/tests/unit_tests/components/esp8266/test_boards.py index 288e0a1eb9..975e34186b 100644 --- a/tests/unit_tests/components/esp8266/test_boards.py +++ b/tests/unit_tests/components/esp8266/test_boards.py @@ -13,3 +13,22 @@ def test_d1_wroom_02_keeps_its_shipped_layout() -> None: def test_default_boards_use_the_flash_size_layout() -> None: assert board_ld_script(BOARDS["d1_mini"]) == "eagle.flash.4m.ld" assert board_ld_script(BOARDS["esp01_1m"]) == "eagle.flash.1m.ld" + + +def test_choose_ld_script_paths() -> None: + """Old cores get the size default, overriding boards hard-error there + (a substituted layout would wipe flash-backed state), modern cores + honor the override.""" + import pytest + + from esphome.components.esp8266 import _choose_ld_script + import esphome.config_validation as cv + from esphome.core import EsphomeError + + assert _choose_ld_script("nodemcuv2", cv.Version(2, 3, 0)) is None + assert _choose_ld_script("nodemcuv2", cv.Version(2, 4, 2)) == "eagle.flash.4m.ld" + assert _choose_ld_script("d1_wroom_02", cv.Version(2, 7, 4)) == ( + "eagle.flash.2m64.ld" + ) + with pytest.raises(EsphomeError, match="cannot honor"): + _choose_ld_script("d1_wroom_02", cv.Version(2, 4, 2))