mirror of
https://github.com/esphome/esphome.git
synced 2026-08-26 08:06:21 +00:00
Keep the spec half honest: fingerprint-stamped ld cache, MMU table, emission helpers deferred
This commit is contained in:
@@ -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/<name>/``. 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()}"
|
||||
|
||||
@@ -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"'
|
||||
|
||||
Reference in New Issue
Block a user