Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-22 15:06:32 -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,
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
if TYPE_CHECKING:
@@ -274,6 +274,10 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
# SDK header's #error
if unknown := [k for k in vtables_knobs if k not in known_vtables]:
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:
raise EsphomeError(
f"Conflicting VTABLES_IN_* defines: {', '.join(vtables_knobs)}"
@@ -377,6 +381,10 @@ def _defines_flags(
defines embed ``\"``), so they must be emitted unquoted; wrapping
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
f_cpu = _pio_option("board_build.f_cpu", "80000000L")
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()}"
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(
paths: InstalledPaths, config: _BuildConfig, flash_ld_name: str
) -> None:
@@ -505,6 +528,9 @@ def generate_ld_scripts(
``eagle.app.v6.common.ld.h``, then applies ESPHome's surgeries: the wifi
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
gcc = toolchain_tool(paths.toolchain, "gcc")
ld_dir = CORE.relative_pioenvs_path(CORE.name, "ld")
@@ -591,13 +617,7 @@ def generate_ld_scripts(
except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable
raise EsphomeError(str(err)) from err
try:
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)
_write_generated(output, content)
stamp.write_text(
f"{stamp_content} content={hashlib.sha256(content.encode('utf-8')).hexdigest()}",
encoding="utf-8",
@@ -635,7 +655,7 @@ def generate_ld_scripts(
except RuntimeError as err:
# Same changed-linker-script failure class as the ratetable
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(
@@ -1071,6 +1071,41 @@ def test_generate_ld_scripts_unreadable_stamp_regenerates(tmp_path: Path) -> Non
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:
"""The stamp records the content hash, so an externally edited cached
script regenerates instead of linking untrusted content."""