diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 9dad8c8c41..1ed1404db3 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -456,7 +456,12 @@ def _flash_ld_name(board: str) -> str: """ override = _pio_option("board_build.ldscript", "") if not override: - return ESP8266_LD_SCRIPTS[BOARDS[board][KEY_FLASH_SIZE]][1] + # The same per-board override the PlatformIO path pins (layout + # preservation, see boards.py) + board_data = BOARDS[board] + return board_data.get( + "ldscript", ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]][1] + ) if Path(override).name != override: raise EsphomeError( f"board_build.ldscript must be a bare script name, got {override!r}" @@ -893,6 +898,18 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: _LOGGER.warning( "build_unflags entries matched no build flag: %s", ", ".join(unmatched) ) + # _LINKFLAGS stores -u and its operand as two tokens; unflagging the + # bare -u would strip all seven and leave the operands as ld "input + # files" with an error pointing nowhere near build_unflags + if plain := sorted( + u + for u in unflags + if u in _PLAIN_LINKER_FLAGS or u.startswith(_PLAIN_LINKER_PREFIXES) + ): + raise EsphomeError( + f"build_unflags cannot remove plain linker flag(s) " + f"{', '.join(plain)}; unflag the full -Wl, form or the symbol" + ) cflags, cxxflags, asflags, link_flags = ( [f for f in flags if f not in unflags] for flags in (cflags, cxxflags, asflags, _LINKFLAGS) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 5c8bef7fca..0c13625a64 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -409,8 +409,8 @@ async def to_code(config: ConfigType) -> None: ) if config[CONF_BOARD] in BOARDS: - flash_size = BOARDS[config[CONF_BOARD]][KEY_FLASH_SIZE] - ld_scripts = ESP8266_LD_SCRIPTS[flash_size] + 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 @@ -419,7 +419,9 @@ async def to_code(config: ConfigType) -> None: # Old ld script path ld_script = ld_scripts[0] else: - ld_script = ld_scripts[1] + # A per-board override preserves a layout the board shipped + # with (see d1_wroom_02 in boards.py) + ld_script = board_data.get("ldscript", ld_scripts[1]) if ld_script is not None: cg.add_platformio_option("board_build.ldscript", ld_script) diff --git a/esphome/components/esp8266/boards.py b/esphome/components/esp8266/boards.py index 88fe74f59d..397de60e24 100644 --- a/esphome/components/esp8266/boards.py +++ b/esphome/components/esp8266/boards.py @@ -202,6 +202,11 @@ BOARDS = { "d1_wroom_02": { "name": "WeMos D1 ESP-WROOM-02", "flash_size": FLASH_SIZE_2_MB, + # This board joined BOARDS after shipping with the manifest default + # (64 KB filesystem region); the flash-size default (2m.ld) would + # move _FS_end and with it the preferences sector, wiping existing + # devices' flash-backed state on update. + "ldscript": "eagle.flash.2m64.ld", }, "d1": { "name": "WEMOS D1 R1", diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 94a0cb50db..358202e613 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -1447,12 +1447,29 @@ def test_flash_ld_name_honors_ldscript_override(tmp_path: Path) -> None: arduino8266._flash_ld_name("nodemcuv2") -def test_write_project_quotes_spaced_ldscript_override(tmp_path: Path) -> None: - """An overridden script name re-quotes on the link line like every other - user token (a space would otherwise split into two argv elements).""" +def test_unflagging_a_plain_linker_flag_raises(tmp_path: Path) -> None: + """build_unflags: -u would strip all seven -u tokens and leave the + operands as ld input files; refuse by name instead.""" + paths = _make_framework(tmp_path) + _set_flags() + CORE.build_unflags = {"-u _printf_float"} + with pytest.raises(EsphomeError, match="cannot remove plain linker"): + _write_ninja(paths) + + +def test_d1_wroom_02_keeps_its_shipped_flash_layout() -> None: + """The board joined BOARDS late; the flash-size default (2m.ld) would + move _FS_end and the preferences sector on existing devices.""" + assert arduino8266._flash_ld_name("d1_wroom_02") == "eagle.flash.2m64.ld" + assert arduino8266._flash_ld_name("nodemcuv2") == "eagle.flash.4m.ld" + + +def test_write_project_rejects_spaced_ldscript_override(tmp_path: Path) -> None: + """A spaced override never reaches the link line: generate_ld_scripts + rejects the name first (the -T _shell_token quoting behind it is + defence-in-depth).""" CORE.platformio_options = {"board_build.ldscript": "my script.ld"} paths = _make_framework(tmp_path) _set_flags() - content = _write_ninja(paths) - assert "'my script.ld'" in content or '"my script.ld"' in content - assert "-T my script.ld" not in content + with pytest.raises(EsphomeError, match="Invalid flash linker script name"): + arduino8266.write_project(paths, None)