From ad4c59873b2540d4c484063dbba5bf3d16c42221 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 16:42:25 -0500 Subject: [PATCH] Read flash mode from its real producer, share the mode set, store the scanf decision flash_mode comes from the board_build.flash_mode option the component already pins (validated by cv.one_of against the same BUILD_FLASH_MODES, now living in const.py so the two sides cannot drift), retiring KEY_FLASH_MODE from the generator. The scanf-float decision is stored in CORE.data where it is computed instead of being a discarded local, so KEY_SCANF_FLOAT has a producer in this PR. A trailing bare -include in build_src_flags fails by name instead of degrading into the source dir. --- esphome/build_gen/arduino8266.py | 16 +++++++++---- esphome/components/esp8266/__init__.py | 6 ++++- esphome/components/esp8266/const.py | 1 + .../unit_tests/build_gen/test_arduino8266.py | 23 +++++++++++-------- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 47230d2f39..cbba929897 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -37,9 +37,9 @@ from esphome.components.esp8266.boards import ( board_ld_script, ) from esphome.components.esp8266.const import ( + BUILD_FLASH_MODES, KEY_BOARD, KEY_ESP8266, - KEY_FLASH_MODE, KEY_FLASH_SIZE, KEY_SCANF_FLOAT, ) @@ -107,8 +107,8 @@ def _apply_surgery(fn, *args: object) -> str: # Every supported board's f_flash is 40 MHz; re-check on a platform bump -# board_flash_mode's closed set (cv.one_of in esp8266/__init__.py) -_FLASH_MODES = frozenset({"qio", "qout", "dio", "dout"}) +# board_flash_mode's closed set, shared with cv.one_of's validation +_FLASH_MODES = frozenset(BUILD_FLASH_MODES) _FLASH_FREQ_MHZ = 40 # From platformio-build.py. Knob suffix -> SDK define; the first entry is @@ -851,7 +851,8 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: if board not in ESP8266_BOARD_BUILD: raise EsphomeError(f"Board '{board}' is not supported by the native toolchain") board_build = ESP8266_BOARD_BUILD[board] - flash_mode = esp8266_data[KEY_FLASH_MODE] + # From the same producer the PlatformIO path reads (one source) + flash_mode = _pio_option("board_build.flash_mode", "dout") if flash_mode not in _FLASH_MODES: # Lands unquoted in the elf2bin command and a -D body; validation # (cv.one_of on board_flash_mode) already gates it, defense-in-depth @@ -1102,7 +1103,12 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: ) for tok in src_it: if tok == "-include": - src_parts.append(f"-include {_q(src_dir / next(src_it, ''))}") + header = next(src_it, "") + if not header: + raise EsphomeError( + "build_src_flags has a trailing '-include' with no header" + ) + src_parts.append(f"-include {_q(src_dir / header)}") else: src_parts.append(_shell_token(tok)) src_extra = " ".join(src_parts) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 32dcbd1c42..3897c53555 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -37,6 +37,7 @@ from esphome.types import ConfigType from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script from .const import ( + BUILD_FLASH_MODES, CONF_EARLY_PIN_INIT, CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL1, @@ -46,6 +47,7 @@ from .const import ( KEY_FLASH_SIZE, KEY_LDSCRIPT, KEY_PIN_INITIAL_STATES, + KEY_SCANF_FLOAT, KEY_SERIAL1_REQUIRED, KEY_SERIAL_REQUIRED, KEY_WAVEFORM_REQUIRED, @@ -237,7 +239,6 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( ) -BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"] CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -312,6 +313,9 @@ async def to_code(config: ConfigType) -> None: "enabling scanf float support (~8KB flash)" ) + # The native generator reads the same decision (KEY_SCANF_FLOAT) + CORE.data[KEY_ESP8266][KEY_SCANF_FLOAT] = bool(enable_scanf_float) + extra_scripts = [ "pre:ccache.py", "pre:testing_mode.py", diff --git a/esphome/components/esp8266/const.py b/esphome/components/esp8266/const.py index c6f550c594..a724c88ec8 100644 --- a/esphome/components/esp8266/const.py +++ b/esphome/components/esp8266/const.py @@ -74,3 +74,4 @@ def enable_serial1() -> None: KEY_LDSCRIPT = "ldscript" +BUILD_FLASH_MODES = ("qio", "qout", "dio", "dout") diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 0935c65d7f..879fe3c958 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -28,12 +28,7 @@ from esphome.build_gen.arduino8266 import ( ) from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import RATETABLE_RULE -from esphome.components.esp8266.const import ( - KEY_BOARD, - KEY_ESP8266, - KEY_FLASH_MODE, - KEY_SCANF_FLOAT, -) +from esphome.components.esp8266.const import KEY_BOARD, KEY_ESP8266, KEY_SCANF_FLOAT import esphome.config_validation as cv from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, EsphomeError @@ -48,12 +43,12 @@ def _setup_core(tmp_path: Path) -> Generator[None]: CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 1, 2)} CORE.data[KEY_ESP8266] = { KEY_BOARD: "nodemcuv2", - KEY_FLASH_MODE: "dout", KEY_SCANF_FLOAT: False, } - # The producer esp8266/__init__ pins unconditionally + # The producers esp8266/__init__ pins unconditionally CORE.platformio_options = { - "build_src_flags": "-include esphome/components/esp8266/throw_stubs.h" + "board_build.flash_mode": "dout", + "build_src_flags": "-include esphome/components/esp8266/throw_stubs.h", } yield # CORE.reset() (the suite-wide autouse fixture) does not clear this flag @@ -245,11 +240,19 @@ def test_write_project_rejects_bad_flash_mode(tmp_path: Path) -> None: """A flash mode outside the closed set fails by name before landing unquoted in the elf2bin command.""" paths = _make_framework(tmp_path) - CORE.data[KEY_ESP8266][KEY_FLASH_MODE] = "dout; rm -rf /" + CORE.platformio_options["board_build.flash_mode"] = "dout; rm -rf /" with pytest.raises(EsphomeError, match="Invalid flash mode"): _write_ninja(paths) +def test_write_project_trailing_include_raises(tmp_path: Path) -> None: + """A dangling -include must fail by name, not become -include .""" + paths = _make_framework(tmp_path) + CORE.platformio_options["build_src_flags"] = "-include" + with pytest.raises(EsphomeError, match="trailing '-include'"): + _write_ninja(paths) + + def test_write_project_passes_other_src_flags_through(tmp_path: Path) -> None: """Non-include build_src_flags tokens are requoted onto the src edges.""" paths = _make_framework(tmp_path)