Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission

# Conflicts:
#	esphome/build_gen/arduino8266.py
This commit is contained in:
J. Nick Koston
2026-08-22 15:06:30 -05:00
2 changed files with 64 additions and 9 deletions
+29 -9
View File
@@ -48,7 +48,7 @@ from esphome.framework_helpers import (
get_project_cxx_compile_flags, get_project_cxx_compile_flags,
strip_win_long_path_prefix, strip_win_long_path_prefix,
) )
from esphome.helpers import mkdir_p, write_file, write_file_if_changed from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -274,6 +274,10 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
# SDK header's #error # SDK header's #error
if unknown := [k for k in vtables_knobs if k not in known_vtables]: if unknown := [k for k in vtables_knobs if k not in known_vtables]:
raise EsphomeError(f"Unknown VTABLES_IN_* define(s): {', '.join(unknown)}") raise EsphomeError(f"Unknown VTABLES_IN_* define(s): {', '.join(unknown)}")
# A body (e.g. VTABLES_IN_FLASH=0) would split the compile line from the
# linker script, which always defines the bare name
if valued := [defines[k] for k in vtables_knobs if defines[k] not in (k, f"{k}=1")]:
raise EsphomeError(f"VTABLES_IN_* defines take no value: {', '.join(valued)}")
if len(vtables_knobs) > 1: if len(vtables_knobs) > 1:
raise EsphomeError( raise EsphomeError(
f"Conflicting VTABLES_IN_* defines: {', '.join(vtables_knobs)}" f"Conflicting VTABLES_IN_* defines: {', '.join(vtables_knobs)}"
@@ -377,6 +381,10 @@ def _defines_flags(
defines embed ``\"``), so they must be emitted unquoted; wrapping defines embed ``\"``), so they must be emitted unquoted; wrapping
them in ``_shell_token`` would deliver literal backslashes to gcc. them in ``_shell_token`` would deliver literal backslashes to gcc.
""" """
if not re.fullmatch(r"[\w.-]+", board):
# The name lands unquoted in two -D bodies; reject it by name
# instead of corrupting the compile line
raise EsphomeError(f"Invalid board name {board!r}")
# Every supported board ships 80 MHz; board_build.f_cpu overrides # Every supported board ships 80 MHz; board_build.f_cpu overrides
f_cpu = _pio_option("board_build.f_cpu", "80000000L") f_cpu = _pio_option("board_build.f_cpu", "80000000L")
if not re.fullmatch(r"\d+L?", f_cpu): if not re.fullmatch(r"\d+L?", f_cpu):
@@ -496,6 +504,21 @@ def _stat_sig(path: Path) -> str:
return f"unreadable:{os.urandom(8).hex()}" return f"unreadable:{os.urandom(8).hex()}"
def _write_generated(path: Path, content: str) -> None:
"""write_file_if_changed, replacing an unreadable existing copy.
The recovery is scoped to the comparison read: a damaged cached file is
logged and overwritten, while a genuine write failure still raises.
"""
try:
if path.is_file():
path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as err:
_LOGGER.warning("Replacing damaged generated file %s: %s", path, err)
path.unlink(missing_ok=True)
write_file_if_changed(path, content)
def generate_ld_scripts( def generate_ld_scripts(
paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str
) -> None: ) -> None:
@@ -505,6 +528,9 @@ def generate_ld_scripts(
``eagle.app.v6.common.ld.h``, then applies ESPHome's surgeries: the wifi ``eagle.app.v6.common.ld.h``, then applies ESPHome's surgeries: the wifi
rate-table DRAM relocation, and enlarged memory segments in testing mode. rate-table DRAM relocation, and enlarged memory segments in testing mode.
""" """
if not re.fullmatch(r"[\w.-]+\.ld", flash_ld_name):
# Joined under the SDK and build ld dirs; never a path or traversal
raise EsphomeError(f"Invalid flash linker script name {flash_ld_name!r}")
framework = paths.framework framework = paths.framework
gcc = toolchain_tool(paths.toolchain, "gcc") gcc = toolchain_tool(paths.toolchain, "gcc")
ld_dir = CORE.relative_pioenvs_path(CORE.name, "ld") ld_dir = CORE.relative_pioenvs_path(CORE.name, "ld")
@@ -591,13 +617,7 @@ def generate_ld_scripts(
except RuntimeError as err: except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable # Same changed-linker-script failure class as the ratetable
raise EsphomeError(str(err)) from err raise EsphomeError(str(err)) from err
try: _write_generated(output, content)
write_file_if_changed(output, content)
except EsphomeError:
# A non-UTF-8/unreadable cached script fails the comparison
# read; regeneration must overwrite it, not abort
output.unlink(missing_ok=True)
write_file(output, content)
stamp.write_text( stamp.write_text(
f"{stamp_content} content={hashlib.sha256(content.encode('utf-8')).hexdigest()}", f"{stamp_content} content={hashlib.sha256(content.encode('utf-8')).hexdigest()}",
encoding="utf-8", encoding="utf-8",
@@ -635,7 +655,7 @@ def generate_ld_scripts(
except RuntimeError as err: except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable # Same changed-linker-script failure class as the ratetable
raise EsphomeError(str(err)) from err raise EsphomeError(str(err)) from err
write_file_if_changed(ld_dir / f"testing_{flash_ld_name}", patched_flash_ld) _write_generated(ld_dir / f"testing_{flash_ld_name}", patched_flash_ld)
def _ninja_compile_edges( def _ninja_compile_edges(
@@ -1071,6 +1071,41 @@ def test_generate_ld_scripts_unreadable_stamp_regenerates(tmp_path: Path) -> Non
mock_run.assert_called_once() mock_run.assert_called_once()
def test_vtables_valued_define_raises() -> None:
"""A VTABLES_IN_* body would split the compile line from the linker
script, which always defines the bare name."""
with pytest.raises(EsphomeError, match="take no value.*VTABLES_IN_FLASH=0"):
_resolve("-DVTABLES_IN_FLASH=0")
def test_defines_flags_invalid_board_raises() -> None:
"""The board name lands unquoted in two -D bodies; reject it by name."""
with pytest.raises(EsphomeError, match="Invalid board name"):
_defines_flags(_resolve(), "dout", "evil board", ())
def test_generate_ld_scripts_invalid_flash_ld_name_raises(tmp_path: Path) -> None:
"""The script name joins under the SDK and build ld dirs; a traversal
or path is rejected by name."""
paths = _make_framework(tmp_path)
_set_flags()
config = _resolve()
with pytest.raises(EsphomeError, match="Invalid flash linker script name"):
arduino8266.generate_ld_scripts(paths, config, "../evil.ld")
def test_write_generated_replaces_damaged_file(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A non-UTF-8 existing copy is logged and overwritten; the write path
still raises for real failures."""
target = tmp_path / "gen.ld"
target.write_bytes(b"\xff\xfe")
arduino8266._write_generated(target, "SECTIONS { }")
assert target.read_text(encoding="utf-8") == "SECTIONS { }"
assert "Replacing damaged generated file" in caplog.text
def test_generate_ld_scripts_edited_output_regenerates(tmp_path: Path) -> None: def test_generate_ld_scripts_edited_output_regenerates(tmp_path: Path) -> None:
"""The stamp records the content hash, so an externally edited cached """The stamp records the content hash, so an externally edited cached
script regenerates instead of linking untrusted content.""" script regenerates instead of linking untrusted content."""