mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
# Conflicts: # esphome/build_gen/arduino8266.py
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user