From cd0c3bb0694dcf9e842b3ae9747e280c0db7ca49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 14:19:53 -0500 Subject: [PATCH] Keep the spec half honest: fingerprint-stamped ld cache, MMU table, emission helpers deferred --- esphome/build_gen/arduino8266.py | 134 +++++++----------- .../unit_tests/build_gen/test_arduino8266.py | 53 ++----- 2 files changed, 61 insertions(+), 126 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 433c0b5d7d..e33512681d 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -1,11 +1,11 @@ -"""Native ninja build generator for the ESP8266 Arduino core. +"""Build specification for the native ESP8266 Arduino toolchain. Transliterates the PlatformIO build spec for the Arduino ESP8266 framework (``framework-arduinoespressif8266/tools/platformio-build.py`` plus -``platform-espressif8266/builder/main.py``) into a ``build.ninja`` under -``.pioenvs//``. The flag sets, defines, link line, linker-script -generation, and ``elf2bin`` invocation deliberately match what PlatformIO -produces so the binaries stay near-identical between the two toolchains. +``platform-espressif8266/builder/main.py``): the flag sets, defines, and +linker-script generation deliberately match what PlatformIO produces so the +binaries stay near-identical between the two toolchains. The ninja emission +(``write_project``) builds on these pieces. The ``PIO_FRAMEWORK_ARDUINO_*`` knob defines (lwIP variant, NONOS SDK version, MMU layout, exceptions, waveform phase) keep working: they are read @@ -21,9 +21,6 @@ import re import subprocess from esphome.components.esp8266 import build_surgery -from esphome.components.esp8266.boards import BOARDS, ESP8266_LD_SCRIPTS -from esphome.components.esp8266.const import KEY_BOARD, KEY_ESP8266, KEY_FLASH_SIZE -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, EsphomeError from esphome.helpers import mkdir_p, write_file_if_changed from esphome.platformio.library import join_flag_args, split_flag_entry @@ -59,6 +56,36 @@ _LWIP_VARIANTS = ( ) _LWIP_DEFAULT = (536, 1, 0, "lwip2-536-feat") +# knob define -> MMU_* defines, first match wins (as in platformio-build.py) +_MMU_VARIANTS = ( + ( + "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48", + ["MMU_IRAM_SIZE=0xC000", "MMU_ICACHE_SIZE=0x4000"], + ), + ( + "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED", + ["MMU_IRAM_SIZE=0xC000", "MMU_ICACHE_SIZE=0x4000", "MMU_IRAM_HEAP"], + ), + ( + "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM32_SECHEAP_NOTSHARED", + [ + "MMU_IRAM_SIZE=0x8000", + "MMU_ICACHE_SIZE=0x4000", + "MMU_SEC_HEAP_SIZE=0x4000", + "MMU_SEC_HEAP=0x40108000", + ], + ), + ( + "PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_128K", + ["MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000", "MMU_EXTERNAL_HEAP=128"], + ), + ( + "PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_1024K", + ["MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000", "MMU_EXTERNAL_HEAP=256"], + ), +) +_MMU_DEFAULT = ("MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000") + _ASFLAGS = ["-mlongcalls", "-mtext-section-literals"] _CFLAGS = [ "-std=gnu17", @@ -179,40 +206,22 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig: "VTABLES_IN_FLASH", ) - if "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48" in defines: - mmu = ["MMU_IRAM_SIZE=0xC000", "MMU_ICACHE_SIZE=0x4000"] - elif "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED" in defines: - mmu = ["MMU_IRAM_SIZE=0xC000", "MMU_ICACHE_SIZE=0x4000", "MMU_IRAM_HEAP"] - elif "PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM32_SECHEAP_NOTSHARED" in defines: - mmu = [ - "MMU_IRAM_SIZE=0x8000", - "MMU_ICACHE_SIZE=0x4000", - "MMU_SEC_HEAP_SIZE=0x4000", - "MMU_SEC_HEAP=0x40108000", - ] - elif "PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_128K" in defines: - mmu = [ - "MMU_IRAM_SIZE=0x8000", - "MMU_ICACHE_SIZE=0x8000", - "MMU_EXTERNAL_HEAP=128", - ] - elif "PIO_FRAMEWORK_ARDUINO_MMU_EXTERNAL_1024K" in defines: - mmu = [ - "MMU_IRAM_SIZE=0x8000", - "MMU_ICACHE_SIZE=0x8000", - "MMU_EXTERNAL_HEAP=256", - ] - elif "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM" in defines: - if "MMU_IRAM_SIZE" not in defines or "MMU_ICACHE_SIZE" not in defines: - raise EsphomeError( - "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM requires MMU_IRAM_SIZE and " - "MMU_ICACHE_SIZE build flags" + mmu = next((variant for knob, variant in _MMU_VARIANTS if knob in defines), None) + if mmu is None: + if "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM" in defines: + if "MMU_IRAM_SIZE" not in defines or "MMU_ICACHE_SIZE" not in defines: + raise EsphomeError( + "PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM requires MMU_IRAM_SIZE and " + "MMU_ICACHE_SIZE build flags" + ) + # Sorted so build.ninja and the linker-script stamp stay + # byte-stable across runs (the flag set has no deterministic + # iteration order). + mmu = sorted( + body for name, body in defines.items() if name.startswith("MMU_") ) - # Sorted so build.ninja and the linker-script stamp stay byte-stable - # across runs (the flag set has no deterministic iteration order). - mmu = sorted(body for name, body in defines.items() if name.startswith("MMU_")) - else: - mmu = ["MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000"] + else: + mmu = list(_MMU_DEFAULT) return _BuildConfig( nonosdk=nonosdk, @@ -225,15 +234,6 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig: ) -def _flash_ld_name(board: str) -> str: - return ESP8266_LD_SCRIPTS[BOARDS[board][KEY_FLASH_SIZE]][1] - - -def _e(value) -> str: - """Escape a path or token for a ninja file.""" - return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ") - - def _quote_arg(tok: str) -> str: """Wrap a token in double quotes with the Windows argv rule. @@ -247,11 +247,6 @@ def _quote_arg(tok: str) -> str: return f'"{quoted}"' -def _q(value) -> str: - """Force-quote a path for the ninja command line (shell/CreateProcess).""" - return _quote_arg(str(value).replace("$", "$$")) - - _NEEDS_QUOTE = re.compile(r'[\s"\']') @@ -370,10 +365,9 @@ def generate_ld_scripts( stamp_content = ( " ".join(cmd) + f" testing={CORE.testing_mode}" - + f" {build_surgery.RATETABLE_RULE}" - + f" {build_surgery.TESTING_IRAM_SIZE}" - + f" {build_surgery.TESTING_DRAM_SIZE}" - + f" {build_surgery.TESTING_FLASH_SIZE}" + # One fingerprint instead of enumerating surgery internals here, so + # any behavioral edit in build_surgery self-invalidates the cache + + f" {build_surgery.surgery_fingerprint()}" ) if not ( output.is_file() @@ -404,25 +398,3 @@ def generate_ld_scripts( ("dram0_0_seg", "irom0_0_seg"), ), ) - - -def get_flash_ld_path(build_dir: Path) -> Path: - """The flash linker script the link actually uses (for size reporting).""" - from esphome.arduino8266.framework import ( - framework_package_version, - get_framework_path, - ) - - name = _flash_ld_name(CORE.data[KEY_ESP8266][KEY_BOARD]) - if CORE.testing_mode: - return build_dir / "ld" / f"testing_{name}" - version = framework_package_version(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]) - return get_framework_path(version) / "tools" / "sdk" / "ld" / name - - -def _flash_size_str(flash_ld_name: str) -> str: - """Flash size for elf2bin, derived from the ld script name (PIO logic).""" - match = re.search(r"\.flash\.(\d+)([mk])", flash_ld_name) - if not match: - raise EsphomeError(f"Cannot parse flash size from {flash_ld_name}") - return f"{match.group(1)}{match.group(2).upper()}" diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 9bba720da6..d758058549 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -1,11 +1,11 @@ -"""Drift tests for the native ESP8266 Arduino build generator. +"""Drift tests for the native ESP8266 Arduino build spec. These pin the build spec transliterated from the PlatformIO builder (framework-arduinoespressif8266/tools/platformio-build.py and platform-espressif8266/builder/main.py) so a change on either side of the toolchain seam is caught: the knob-define precedence, the define/flag sets, -the link line, and the core source exclusions must keep matching what the -PlatformIO toolchain produces for the same configuration. +and the linker-script generation must keep matching what the PlatformIO +toolchain produces for the same configuration. """ from __future__ import annotations @@ -20,9 +20,7 @@ from esphome.build_gen import arduino8266 from esphome.build_gen.arduino8266 import ( _defines_flags, _flag_defines, - _flash_size_str, _resolve_build_config, - get_flash_ld_path, ) from esphome.components.esp8266.boards import ESP8266_BOARD_BUILD from esphome.components.esp8266.build_surgery import RATETABLE_RULE @@ -289,10 +287,12 @@ def test_generate_ld_scripts(tmp_path: Path) -> None: _run_generate_ld_scripts(paths) mock_run.assert_not_called() - # An edit to the surgery constants invalidates the stamp (a stale linker - # script would otherwise persist until an esphome clean) + # A surgery edit invalidates the stamp via the fingerprint (a stale + # linker script would otherwise persist until an esphome clean) with ( - patch.object(arduino8266.build_surgery, "TESTING_FLASH_SIZE", "0x3000000"), + patch.object( + arduino8266.build_surgery, "surgery_fingerprint", return_value="changed" + ), patch.object(arduino8266.subprocess, "run", return_value=result) as mock_run, ): _run_generate_ld_scripts(paths) @@ -328,37 +328,6 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None: assert "len = 0x2000000" in patched -def test_get_flash_ld_path(tmp_path: Path) -> None: - - CORE.testing_mode = True - assert get_flash_ld_path(tmp_path) == ( - tmp_path / "ld" / "testing_eagle.flash.4m.ld" - ) - - CORE.testing_mode = False - with ( - patch( - "esphome.arduino8266.framework.get_framework_path", - return_value=tmp_path / "framework", - ), - patch( - "esphome.arduino8266.framework.framework_package_version", - return_value="3.30102.0", - ), - ): - assert get_flash_ld_path(tmp_path) == ( - tmp_path / "framework" / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld" - ) - - -def test_flash_size_str() -> None: - - assert _flash_size_str("eagle.flash.4m.ld") == "4M" - assert _flash_size_str("eagle.flash.512k.ld") == "512K" - with pytest.raises(EsphomeError, match="Cannot parse flash size"): - _flash_size_str("bogus.ld") - - def test_build_config_nonosdk_precedence() -> None: """With two SDK knobs set (a pathological config), ties break deterministically by table order.""" @@ -447,9 +416,3 @@ def test_flag_defines_joins_spaced_define() -> None: defines = _flag_defines() assert "PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH" in defines assert "" not in defines - - -def test_ninja_path_escaping() -> None: - """Build-statement paths and command-line paths escape differently.""" - assert arduino8266._e("a b:$c") == "a$ b$:$$c" - assert arduino8266._q("/a b/$x") == '"/a b/$$x"'