mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
Merge branch 'esp8266-native-build-surgery' into esp8266-native-toolchain-plumbing
This commit is contained in:
@@ -361,31 +361,30 @@ BOARDS = {
|
||||
},
|
||||
}
|
||||
|
||||
"""
|
||||
ESP8266_BOARD_BUILD generate with (v4.2.1 is the platform version the
|
||||
native toolchain mirrors; regenerate against the tag when bumping it):
|
||||
|
||||
git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
|
||||
python3 - <<'EOF'
|
||||
import json, glob, os
|
||||
for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
|
||||
b = json.load(open(f))["build"]
|
||||
extra = b["extra_flags"]
|
||||
extra = extra.split() if isinstance(extra, str) else extra
|
||||
defines = [
|
||||
e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
|
||||
]
|
||||
entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
|
||||
board = os.path.splitext(os.path.basename(f))[0]
|
||||
print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
|
||||
EOF
|
||||
"""
|
||||
|
||||
# Per-board Arduino core build metadata for the native (PlatformIO-free)
|
||||
# toolchain: the variant directory (supplies pins_arduino.h) and the
|
||||
# board-identity defines the PlatformIO builder passes via build.extra_flags.
|
||||
# -DESP8266 and -DARDUINO_ARCH_ESP8266 are shared by every board and added by
|
||||
# the generator; only the per-board defines are listed here.
|
||||
#
|
||||
# ESP8266_BOARD_BUILD generate with (v4.2.1 is the platform version the
|
||||
# native toolchain mirrors; regenerate against the tag when bumping it):
|
||||
#
|
||||
# git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
|
||||
# python3 - <<'EOF'
|
||||
# import json, glob, os
|
||||
# for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
|
||||
# b = json.load(open(f))["build"]
|
||||
# extra = b["extra_flags"]
|
||||
# extra = extra.split() if isinstance(extra, str) else extra
|
||||
# defines = [
|
||||
# e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
|
||||
# ]
|
||||
# entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
|
||||
# board = os.path.splitext(os.path.basename(f))[0]
|
||||
# print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
|
||||
# EOF
|
||||
ESP8266_BOARD_BUILD = {
|
||||
"agruminolemon": {
|
||||
"variant": "agruminolemonv4",
|
||||
|
||||
@@ -82,6 +82,14 @@ def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str
|
||||
raises, since a silently kept real memory limit would fail grouped
|
||||
builds far from the cause.
|
||||
"""
|
||||
for segment in _TESTING_SEGMENT_SIZES:
|
||||
if segment not in segments and _segment_line_re(segment).search(content):
|
||||
# A known segment left unpatched would keep its real memory limit
|
||||
# and silently under-provision the testing build
|
||||
raise RuntimeError(
|
||||
f"Testing-mode segment {segment} is present in the linker "
|
||||
"script but was not selected for patching"
|
||||
)
|
||||
for segment in segments:
|
||||
if segment not in _TESTING_SEGMENT_SIZES:
|
||||
raise RuntimeError(f"Unknown testing-mode segment {segment!r}")
|
||||
@@ -103,15 +111,14 @@ def segment_length(content: str, segment_name: str) -> int | None:
|
||||
|
||||
|
||||
def surgery_fingerprint() -> str:
|
||||
"""Fingerprint of every behavioral input to the surgeries.
|
||||
"""Fingerprint of this module's source, covering every behavioral input.
|
||||
|
||||
Linker-script caches include it so an edit here invalidates them.
|
||||
Linker-script caches include it so an edit here invalidates them; hashing
|
||||
the source over-invalidates on comment edits, which is the safe direction.
|
||||
Native-toolchain-only, like ``segment_length``; no script twin.
|
||||
"""
|
||||
parts = (
|
||||
RATETABLE_RULE,
|
||||
_RATETABLE_COMMENT,
|
||||
_RATETABLE_ANCHOR.pattern,
|
||||
repr(sorted(_TESTING_SEGMENT_SIZES.items())),
|
||||
)
|
||||
return hashlib.sha256("|".join(parts).encode()).hexdigest()
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
source = inspect.getsource(sys.modules[__name__])
|
||||
return hashlib.sha256(source.encode()).hexdigest()
|
||||
|
||||
@@ -35,7 +35,7 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.platformio.library import ensure_list
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio.library import ConvertedLibrary
|
||||
@@ -89,9 +89,17 @@ def apply_extra_script(
|
||||
extra_flags = captured_as_build_flags(result, library_dir=source_path)
|
||||
if not extra_flags:
|
||||
return
|
||||
flags = ensure_list(component.data.setdefault("build", {}).setdefault("flags", []))
|
||||
flags.extend(extra_flags)
|
||||
component.data["build"]["flags"] = flags
|
||||
flags = component.data.setdefault("build", {}).setdefault("flags", [])
|
||||
if isinstance(flags, str):
|
||||
flags = [flags]
|
||||
elif not isinstance(flags, list):
|
||||
# A null/dict value coerced through a list wrapper would inject a
|
||||
# non-string into the compiler command line; fail naming the library
|
||||
raise EsphomeError(
|
||||
f"Library {component.name} has a malformed build.flags "
|
||||
f"({type(flags).__name__}); expected a string or list"
|
||||
)
|
||||
component.data["build"]["flags"] = [*flags, *extra_flags]
|
||||
|
||||
|
||||
# Keys we know how to translate back into ESPHome's build-flag pipeline.
|
||||
|
||||
@@ -451,3 +451,17 @@ def test_load_or_build_idedata_never_caches_bad_compiler(tmp_path: Path) -> None
|
||||
)
|
||||
assert data["cxx_path"] == "/usr/bin/python3"
|
||||
assert not cache.exists()
|
||||
|
||||
|
||||
def test_load_or_build_idedata_cache_hit_skips_rebuild(tmp_path: Path) -> None:
|
||||
"""A valid cache newer than the compile DB is served without re-parsing."""
|
||||
compile_commands = _write_compile_commands(tmp_path)
|
||||
cache = tmp_path / "c.json"
|
||||
cache.write_text(json.dumps({"cc_path": "/tools/gcc", "cached": True}))
|
||||
os.utime(cache, (compile_commands.stat().st_mtime + 5,) * 2)
|
||||
with patch.object(idedata, "idedata_from_build") as mock_build:
|
||||
data = idedata.load_or_build_idedata(
|
||||
compile_commands, tmp_path / "f.elf", cache
|
||||
)
|
||||
mock_build.assert_not_called()
|
||||
assert data["cached"] is True
|
||||
|
||||
@@ -49,7 +49,8 @@ def test_relocate_ratetable_inserts_after_data_start() -> None:
|
||||
patched = relocate_ratetable(_COMMON_LD_SNIPPET)
|
||||
assert RATETABLE_RULE in patched
|
||||
# Inserted after the .data section's anchor, not the .dport0.data one
|
||||
assert patched.index("_data_start = ABSOLUTE(.);") < patched.index(RATETABLE_RULE)
|
||||
# (whose closing brace bounds the decoy block)
|
||||
assert RATETABLE_RULE not in patched[: patched.index("} >dport0_0_seg")]
|
||||
assert patched.index(RATETABLE_RULE) < patched.index("*(.data)")
|
||||
# Idempotent on an already-patched script
|
||||
assert relocate_ratetable(patched) == patched
|
||||
@@ -106,14 +107,20 @@ def test_board_build_covers_every_board() -> None:
|
||||
assert set(BOARDS) <= set(ESP8266_BOARD_BUILD)
|
||||
|
||||
|
||||
def test_surgery_fingerprint_tracks_inputs() -> None:
|
||||
"""The fingerprint changes with any behavioral input, so linker-script
|
||||
caches stamped with it self-invalidate on surgery edits."""
|
||||
from unittest.mock import patch
|
||||
def test_surgery_fingerprint_covers_module_source() -> None:
|
||||
"""The fingerprint hashes the module source, so any surgery edit
|
||||
invalidates linker-script caches stamped with it."""
|
||||
import hashlib
|
||||
import inspect
|
||||
|
||||
from esphome.components.esp8266 import build_surgery
|
||||
|
||||
base = build_surgery.surgery_fingerprint()
|
||||
assert base == build_surgery.surgery_fingerprint()
|
||||
with patch.object(build_surgery, "_TESTING_SEGMENT_SIZES", {"iram1_0_seg": "0x1"}):
|
||||
assert build_surgery.surgery_fingerprint() != base
|
||||
expected = hashlib.sha256(inspect.getsource(build_surgery).encode()).hexdigest()
|
||||
assert build_surgery.surgery_fingerprint() == expected
|
||||
|
||||
|
||||
def test_testing_memory_patches_present_but_unselected_raises() -> None:
|
||||
"""A known segment left off the caller's list must fail, not silently
|
||||
keep its real memory limit."""
|
||||
with pytest.raises(RuntimeError, match="not selected"):
|
||||
apply_testing_memory_patches(_FLASH_LD_SNIPPET, ("dram0_0_seg",))
|
||||
|
||||
Reference in New Issue
Block a user