mirror of
https://github.com/esphome/esphome.git
synced 2026-08-29 01:03:29 +00:00
Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain
This commit is contained in:
@@ -50,9 +50,9 @@ from esphome.framework_helpers import (
|
||||
)
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
from esphome.platformio.library import (
|
||||
BARE_ARG_FLAGS,
|
||||
SOURCE_KIND_FOR_SUFFIX,
|
||||
lex_build_flags,
|
||||
raise_on_empty_arg_flags,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -72,7 +72,12 @@ _CORE_EXCLUDE_WAVEFORM = {
|
||||
|
||||
# Values that land unquoted on generated command lines are shape-checked
|
||||
# against these before use
|
||||
_MMU_VALUE_RE = re.compile(r"(?:0[xX][0-9a-fA-F]+|\d+)[uUlL]*")
|
||||
_MMU_HEX_VALUE_RE = re.compile(r"0[xX][0-9a-fA-F]+[uUlL]*")
|
||||
# Only these land in the preprocessed script's ``len =`` fields, which
|
||||
# build_surgery's segment parser reads back as hex; the other MMU_* macros
|
||||
# (MMU_EXTERNAL_HEAP=128) are consumed by mmu_iram.h and may be decimal
|
||||
_MMU_SEGMENT_SIZE_NAMES = ("MMU_IRAM_SIZE", "MMU_ICACHE_SIZE")
|
||||
_BOARD_NAME_RE = re.compile(r"[\w.-]+")
|
||||
_F_CPU_RE = re.compile(r"\d+L?")
|
||||
_FLASH_LD_NAME_RE = re.compile(r"[\w.-]+\.ld")
|
||||
@@ -84,6 +89,26 @@ _DEFAULT_F_CPU = "80000000L"
|
||||
# against; the cache stamp and stderr sidecars derive from the output name
|
||||
_COMMON_LD_HEADER = "eagle.app.v6.common.ld.h"
|
||||
_COMMON_LD_NAME = "local.eagle.app.v6.common.ld"
|
||||
# Testing mode shadows the SDK flash ld with a patched copy under this name
|
||||
_TESTING_LD_PREFIX = "testing_"
|
||||
|
||||
# The recovery hint for a half-extracted or damaged framework cache
|
||||
_CLEAN_HINT = "run 'esphome clean-all' and retry"
|
||||
|
||||
|
||||
def _sdk_ld_dir(framework: Path) -> Path:
|
||||
return framework / "tools" / "sdk" / "ld"
|
||||
|
||||
|
||||
def _apply_surgery(fn, *args: object) -> str:
|
||||
"""Run one build_surgery edit, naming a failed anchor instead of a
|
||||
traceback (the surgery module raises bare RuntimeError so its
|
||||
``.py.script`` twins stay importable without esphome)."""
|
||||
try:
|
||||
return fn(*args)
|
||||
except RuntimeError as err:
|
||||
raise EsphomeError(str(err)) from err
|
||||
|
||||
|
||||
# Every supported board's f_flash is 40 MHz; re-check on a platform bump
|
||||
_FLASH_FREQ_MHZ = 40
|
||||
@@ -251,13 +276,8 @@ def _lexed_build_flags() -> list[str]:
|
||||
Lex once per build; consumers share the tokens.
|
||||
"""
|
||||
tokens = lex_build_flags(sorted(CORE.build_flags), "esphome")
|
||||
# The lexer glues '-D ""' to a bare "-D"; gcc would eat the next flag
|
||||
# as its argument (or add the CWD for -L). Always a typo, so raise for
|
||||
# every consumer of the shared token list.
|
||||
if empty := sorted({tok for tok in tokens if tok in BARE_ARG_FLAGS}):
|
||||
raise EsphomeError(
|
||||
f"build_flags contain empty-argument flag(s): {', '.join(empty)}"
|
||||
)
|
||||
# Raises for every consumer of the shared token list
|
||||
raise_on_empty_arg_flags(tokens, "build_flags")
|
||||
return tokens
|
||||
|
||||
|
||||
@@ -280,11 +300,14 @@ def _flag_defines(unflags: set[str], tokens: list[str]) -> dict[str, str]:
|
||||
|
||||
|
||||
def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
|
||||
nonosdk = next(iter(_NONOSDK_VERSIONS.values()))
|
||||
for name, define in _NONOSDK_VERSIONS.items():
|
||||
if f"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_{name}" in defines:
|
||||
nonosdk = define
|
||||
break
|
||||
nonosdk = next(
|
||||
(
|
||||
define
|
||||
for name, define in _NONOSDK_VERSIONS.items()
|
||||
if f"PIO_FRAMEWORK_ARDUINO_ESPRESSIF_{name}" in defines
|
||||
),
|
||||
next(iter(_NONOSDK_VERSIONS.values())),
|
||||
)
|
||||
# Same compile-line/linked-artifact split as the lwIP knobs below: a
|
||||
# raw NONOSDK* would define a second SDK macro while the link still
|
||||
# resolves against the knob's libraries
|
||||
@@ -295,11 +318,10 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
|
||||
"build flags"
|
||||
)
|
||||
|
||||
tcp_mss, features, ipv6, lwip_lib = _LWIP_DEFAULT
|
||||
for knob, variant in _LWIP_VARIANTS.items():
|
||||
if knob in defines:
|
||||
tcp_mss, features, ipv6, lwip_lib = variant
|
||||
break
|
||||
tcp_mss, features, ipv6, lwip_lib = next(
|
||||
(variant for knob, variant in _LWIP_VARIANTS.items() if knob in defines),
|
||||
_LWIP_DEFAULT,
|
||||
)
|
||||
|
||||
# The lwIP triple selects a prebuilt library; a raw override would win
|
||||
# the compile line (user tokens come last here) while the link still
|
||||
@@ -361,14 +383,21 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
|
||||
# Valueless flags (MMU_IRAM_HEAP) are legitimate switches
|
||||
continue
|
||||
# Every valued MMU_* reaches the linker-script preprocessor; a
|
||||
# bare or non-numeric value would corrupt the segment lengths
|
||||
# and fail far away in ld. Hex only: build_surgery's segment
|
||||
# parser (and upstream's spellings) cannot read decimal.
|
||||
# bare or non-numeric value would corrupt it and fail far away
|
||||
# in ld. The two segment sizes must additionally be hex:
|
||||
# build_surgery's segment parser cannot read decimal back.
|
||||
value = body.partition("=")[2]
|
||||
if not _MMU_HEX_VALUE_RE.fullmatch(value):
|
||||
rule = (
|
||||
_MMU_HEX_VALUE_RE if name in _MMU_SEGMENT_SIZE_NAMES else _MMU_VALUE_RE
|
||||
)
|
||||
if not rule.fullmatch(value):
|
||||
shape = (
|
||||
"a hex literal (e.g. 0x8000)"
|
||||
if name in _MMU_SEGMENT_SIZE_NAMES
|
||||
else "a numeric literal"
|
||||
)
|
||||
raise EsphomeError(
|
||||
f"{name} must be a hex literal (e.g. 0x8000), got "
|
||||
f"{value or '(no value)'}"
|
||||
f"{name} must be {shape}, got {value or '(no value)'}"
|
||||
)
|
||||
# Sorted so build.ninja and the linker-script stamp stay
|
||||
# byte-stable across runs (the flag set has no deterministic
|
||||
@@ -397,13 +426,14 @@ def _resolve_build_config(defines: dict[str, str]) -> _BuildConfig:
|
||||
|
||||
|
||||
_INCOMPLETE_INSTALL = "Arduino toolchain install is incomplete"
|
||||
_CLEAN_HINT = "run 'esphome clean-all' and retry"
|
||||
|
||||
|
||||
def _active_flash_ld_name(flash_ld_name: str) -> str:
|
||||
"""The flash linker-script filename the link uses (testing mode renames
|
||||
the surgically patched copy)."""
|
||||
return f"testing_{flash_ld_name}" if CORE.testing_mode else flash_ld_name
|
||||
return (
|
||||
f"{_TESTING_LD_PREFIX}{flash_ld_name}" if CORE.testing_mode else flash_ld_name
|
||||
)
|
||||
|
||||
|
||||
def _flash_ld_name(board: str) -> str:
|
||||
@@ -569,21 +599,6 @@ def _write_note(path: Path, text: str, *, warn: bool = False) -> None:
|
||||
log("Could not write %s: %s", path, err)
|
||||
|
||||
|
||||
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:
|
||||
@@ -605,7 +620,7 @@ def generate_ld_scripts(
|
||||
cmd += [f"-D{d}" for d in config.mmu_defines]
|
||||
if config.fp_in_irom:
|
||||
cmd.append("-DFP_IN_IROM")
|
||||
header = framework / "tools" / "sdk" / "ld" / _COMMON_LD_HEADER
|
||||
header = _sdk_ld_dir(framework) / _COMMON_LD_HEADER
|
||||
cmd += [str(header), "-o", "-"]
|
||||
|
||||
# The inputs are the command line (defines + framework version, which is
|
||||
@@ -657,9 +672,7 @@ def generate_ld_scripts(
|
||||
)
|
||||
except OSError as err:
|
||||
# A half-extracted or half-deleted toolchain cache reaches here
|
||||
raise EsphomeError(
|
||||
f"Could not run {gcc}: {err}; run 'esphome clean-all' and retry"
|
||||
) from err
|
||||
raise EsphomeError(f"Could not run {gcc}: {err}; {_CLEAN_HINT}") from err
|
||||
if result.returncode != 0:
|
||||
raise EsphomeError(f"Generating the linker script failed:\n{result.stderr}")
|
||||
if result.stderr.strip():
|
||||
@@ -672,24 +685,14 @@ def generate_ld_scripts(
|
||||
if "SECTIONS" not in result.stdout:
|
||||
# A degenerate zero-exit run must not be stamped as a good cache
|
||||
raise EsphomeError(
|
||||
"Generated linker script is missing its SECTIONS block; "
|
||||
"run 'esphome clean-all' and retry"
|
||||
f"Generated linker script is missing its SECTIONS block; {_CLEAN_HINT}"
|
||||
)
|
||||
try:
|
||||
content = build_surgery.relocate_ratetable(result.stdout)
|
||||
except RuntimeError as err:
|
||||
# The anchor moved in a new core release: a named error, not a
|
||||
# traceback, and never a silently unrelocated rate table
|
||||
raise EsphomeError(str(err)) from err
|
||||
content = _apply_surgery(build_surgery.relocate_ratetable, result.stdout)
|
||||
if CORE.testing_mode:
|
||||
try:
|
||||
content = build_surgery.apply_testing_memory_patches(
|
||||
content, ("iram1_0_seg",)
|
||||
)
|
||||
except RuntimeError as err:
|
||||
# Same changed-linker-script failure class as the ratetable
|
||||
raise EsphomeError(str(err)) from err
|
||||
_write_generated(output, content)
|
||||
content = _apply_surgery(
|
||||
build_surgery.apply_testing_memory_patches, content, ("iram1_0_seg",)
|
||||
)
|
||||
write_file_if_changed(output, content)
|
||||
_write_note(
|
||||
stamp,
|
||||
f"{stamp_content} content={hashlib.sha256(content.encode('utf-8')).hexdigest()}",
|
||||
@@ -709,25 +712,28 @@ def generate_ld_scripts(
|
||||
)
|
||||
|
||||
if CORE.testing_mode:
|
||||
# A patched copy of the flash ld in the build dir; resolved through
|
||||
# the same -L path as the SDK original it shadows.
|
||||
flash_ld = framework / "tools" / "sdk" / "ld" / flash_ld_name
|
||||
try:
|
||||
flash_ld_text = flash_ld.read_text(encoding="utf-8")
|
||||
except OSError as err:
|
||||
# Same half-extracted-cache hazard as the gcc spawn above
|
||||
raise EsphomeError(
|
||||
f"Could not read {flash_ld}: {err}; run 'esphome clean-all' and retry"
|
||||
) from err
|
||||
try:
|
||||
patched_flash_ld = build_surgery.apply_testing_memory_patches(
|
||||
flash_ld_text,
|
||||
("dram0_0_seg", "irom0_0_seg"),
|
||||
)
|
||||
except RuntimeError as err:
|
||||
# Same changed-linker-script failure class as the ratetable
|
||||
raise EsphomeError(str(err)) from err
|
||||
_write_generated(ld_dir / f"testing_{flash_ld_name}", patched_flash_ld)
|
||||
_generate_testing_flash_ld(framework, ld_dir, flash_ld_name)
|
||||
|
||||
|
||||
def _generate_testing_flash_ld(
|
||||
framework: Path, ld_dir: Path, flash_ld_name: str
|
||||
) -> None:
|
||||
"""A patched copy of the flash ld in the build dir; resolved through the
|
||||
same -L path as the SDK original it shadows."""
|
||||
flash_ld = _sdk_ld_dir(framework) / flash_ld_name
|
||||
try:
|
||||
flash_ld_text = flash_ld.read_text(encoding="utf-8")
|
||||
except OSError as err:
|
||||
# Same half-extracted-cache hazard as the preprocessor spawn
|
||||
raise EsphomeError(f"Could not read {flash_ld}: {err}; {_CLEAN_HINT}") from err
|
||||
patched_flash_ld = _apply_surgery(
|
||||
build_surgery.apply_testing_memory_patches,
|
||||
flash_ld_text,
|
||||
("dram0_0_seg", "irom0_0_seg"),
|
||||
)
|
||||
write_file_if_changed(
|
||||
ld_dir / f"{_TESTING_LD_PREFIX}{flash_ld_name}", patched_flash_ld
|
||||
)
|
||||
|
||||
|
||||
def _ninja_compile_edges(
|
||||
|
||||
+9
-1
@@ -556,9 +556,17 @@ def write_file_if_changed(path: Path, text: str) -> bool:
|
||||
|
||||
Returns true if the file was changed.
|
||||
"""
|
||||
from esphome.core import EsphomeError
|
||||
|
||||
src_content = None
|
||||
if path.is_file():
|
||||
src_content = read_file(path)
|
||||
try:
|
||||
src_content = read_file(path)
|
||||
except (EsphomeError, UnicodeDecodeError) as err:
|
||||
# A damaged existing file (unreadable, non-UTF-8) must be
|
||||
# replaced, not abort the regeneration that would fix it
|
||||
_LOGGER.warning("Replacing damaged file %s: %s", path, err)
|
||||
path.unlink(missing_ok=True)
|
||||
if src_content == text:
|
||||
return False
|
||||
write_file(path, text)
|
||||
|
||||
@@ -655,6 +655,21 @@ def lex_build_flags(entries: str | list[str], owner: str) -> list[str]:
|
||||
BARE_ARG_FLAGS = frozenset({"-I", "-L", "-l", "-D"})
|
||||
|
||||
|
||||
def raise_on_empty_arg_flags(tokens: list[str], owner: str) -> None:
|
||||
"""Reject bare ``-I``/``-D``/``-L``/``-l`` tokens left by an empty glued
|
||||
argument (``-D ""``).
|
||||
|
||||
Lives next to ``join_flag_args`` because the bare token is its
|
||||
postcondition: a trailing bare flag is warned and dropped there, so a
|
||||
surviving one always means an empty argument. gcc would eat the next
|
||||
flag as the argument (or add the CWD for ``-L``); always a typo.
|
||||
"""
|
||||
if empty := sorted({tok for tok in tokens if tok in BARE_ARG_FLAGS}):
|
||||
raise EsphomeError(
|
||||
f"{owner} contain empty-argument flag(s): {', '.join(empty)}"
|
||||
)
|
||||
|
||||
|
||||
def join_flag_args(tokens: Iterable[str], owner: str) -> list[str]:
|
||||
"""Join a bare ``-I``/``-L``/``-l``/``-D`` with its following token,
|
||||
the way PlatformIO's ParseFlags lexes them."""
|
||||
|
||||
@@ -68,7 +68,33 @@ def _shq(tok: str) -> str:
|
||||
def _resolve(*flags: str):
|
||||
"""Set the build flags and resolve the knob config in one step."""
|
||||
_set_flags(*flags)
|
||||
return _resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
|
||||
return _resolve_current()
|
||||
|
||||
|
||||
def _defines():
|
||||
"""The -D map for the current build flags."""
|
||||
return _flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
|
||||
|
||||
def _resolve_current():
|
||||
"""Resolve whatever flags are already set (must not clear them)."""
|
||||
return _resolve_build_config(_defines())
|
||||
|
||||
|
||||
def _split_flags():
|
||||
"""Classify the current build flags the way write_project does."""
|
||||
return arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
|
||||
|
||||
def _ok_result(stdout=None, stderr=""):
|
||||
"""A successful preprocessor spawn (defaults to the common ld output)."""
|
||||
return MagicMock(
|
||||
returncode=0,
|
||||
stdout=_COMMON_LD_H_OUTPUT if stdout is None else stdout,
|
||||
stderr=stderr,
|
||||
)
|
||||
|
||||
|
||||
def test_build_config_defaults() -> None:
|
||||
@@ -106,9 +132,7 @@ def test_build_config_knobs() -> None:
|
||||
"-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48",
|
||||
"-DVTABLES_IN_DRAM",
|
||||
)
|
||||
config = _resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
)
|
||||
config = _resolve_current()
|
||||
assert config.nonosdk == "NONOSDK305"
|
||||
assert config.exceptions
|
||||
assert config.vtables == "VTABLES_IN_DRAM"
|
||||
@@ -125,9 +149,7 @@ def test_build_config_mmu_custom_requires_sizes() -> None:
|
||||
"-DMMU_IRAM_SIZE=0xC000",
|
||||
"-DMMU_ICACHE_SIZE=0x4000",
|
||||
)
|
||||
config = _resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
)
|
||||
config = _resolve_current()
|
||||
# Emitted pre-sorted so build.ninja stays byte-stable across runs
|
||||
assert config.mmu_defines == [
|
||||
"MMU_ICACHE_SIZE=0x4000",
|
||||
@@ -140,7 +162,7 @@ def test_defines_match_platformio_builder() -> None:
|
||||
|
||||
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
assert _defines_flags(
|
||||
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags())),
|
||||
_resolve_current(),
|
||||
"dout",
|
||||
"nodemcuv2",
|
||||
ESP8266_BOARD_BUILD["nodemcuv2"]["defines"],
|
||||
@@ -366,12 +388,7 @@ def test_build_config_lwip_variants(
|
||||
def test_build_config_mmu_variants(knob: str, expected: list[str]) -> None:
|
||||
|
||||
_set_flags(f"-D{knob}")
|
||||
assert (
|
||||
_resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
).mmu_defines
|
||||
== expected
|
||||
)
|
||||
assert _resolve_build_config(_defines()).mmu_defines == expected
|
||||
|
||||
|
||||
def test_build_config_waveform_locked_phase() -> None:
|
||||
@@ -398,9 +415,7 @@ SECTIONS
|
||||
|
||||
def _run_generate_ld_scripts(paths: InstalledPaths) -> Path:
|
||||
|
||||
config = _resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
)
|
||||
config = _resolve_current()
|
||||
arduino8266.generate_ld_scripts(paths, config, "eagle.flash.4m.ld")
|
||||
return CORE.relative_pioenvs_path(CORE.name, "ld")
|
||||
|
||||
@@ -409,7 +424,7 @@ def test_generate_ld_scripts(tmp_path: Path) -> None:
|
||||
|
||||
paths = _make_framework(tmp_path)
|
||||
_set_flags("-DFP_IN_IROM")
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with (
|
||||
patch.object(arduino8266.subprocess, "run", return_value=result) as mock_run,
|
||||
patch.object(arduino8266._LOGGER, "warning") as mock_warn,
|
||||
@@ -444,7 +459,7 @@ def test_generate_ld_scripts(tmp_path: Path) -> None:
|
||||
def test_generate_ld_scripts_corrupt_cache_regenerates(tmp_path: Path) -> None:
|
||||
"""A truncated cached linker script regenerates even with a fresh stamp."""
|
||||
paths = _make_framework(tmp_path)
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
output = ld_dir / "local.eagle.app.v6.common.ld"
|
||||
@@ -476,7 +491,7 @@ def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
|
||||
"}\n"
|
||||
)
|
||||
CORE.testing_mode = True
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
patched = (ld_dir / "testing_eagle.flash.4m.ld").read_text()
|
||||
@@ -579,7 +594,7 @@ def test_generate_ld_scripts_testing_mode_missing_flash_ld_raises(
|
||||
"""A missing flash ld in testing mode names the file and the fix."""
|
||||
paths = _make_framework(tmp_path)
|
||||
CORE.testing_mode = True
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with (
|
||||
patch.object(arduino8266.subprocess, "run", return_value=result),
|
||||
pytest.raises(EsphomeError, match="Could not read .*clean-all"),
|
||||
@@ -594,12 +609,7 @@ def test_build_config_nonosdk_precedence() -> None:
|
||||
"-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK305",
|
||||
"-DPIO_FRAMEWORK_ARDUINO_ESPRESSIF_SDK221",
|
||||
)
|
||||
assert (
|
||||
_resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
).nonosdk
|
||||
== "NONOSDK221"
|
||||
)
|
||||
assert _resolve_build_config(_defines()).nonosdk == "NONOSDK221"
|
||||
|
||||
|
||||
def test_write_project_build_unflags_apply_to_framework_flags(tmp_path: Path) -> None:
|
||||
@@ -620,9 +630,7 @@ def test_project_flags_trailing_bare_linker_flag_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
_set_flags("-l")
|
||||
compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
compile_flags, link_flags, lib_dirs, libs = _split_flags()
|
||||
assert "Ignoring trailing '-l'" in caplog.text
|
||||
assert not libs
|
||||
assert not lib_dirs
|
||||
@@ -632,9 +640,7 @@ def test_project_flags_trailing_bare_linker_flag_warns(
|
||||
|
||||
def test_project_flags_lexed_entry_scatters_non_linker_tokens() -> None:
|
||||
_set_flags("-L /d -Wl,-Map=m stray")
|
||||
compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
compile_flags, link_flags, lib_dirs, libs = _split_flags()
|
||||
assert lib_dirs == [Path("/d")]
|
||||
assert link_flags == ["-Wl,-Map=m"]
|
||||
assert "stray" in compile_flags
|
||||
@@ -644,7 +650,7 @@ def test_project_flags_lexed_entry_scatters_non_linker_tokens() -> None:
|
||||
def test_flag_defines_lexes_multi_token_entries() -> None:
|
||||
"""A knob inside a multi-token entry is detected like PlatformIO does."""
|
||||
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH -DFOO=1 -Os")
|
||||
defines = _flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
defines = _defines()
|
||||
assert "PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH" in defines
|
||||
assert defines["FOO"] == "FOO=1"
|
||||
config = _resolve_build_config(defines)
|
||||
@@ -654,9 +660,7 @@ def test_flag_defines_lexes_multi_token_entries() -> None:
|
||||
def test_project_flags_lexes_every_entry() -> None:
|
||||
"""A linker flag anywhere in an entry reaches the link line (PIO parity)."""
|
||||
_set_flags("-DFOO=1 -lbar")
|
||||
compile_flags, _link, _dirs, libs = arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
compile_flags, _link, _dirs, libs = _split_flags()
|
||||
assert libs == ["bar"]
|
||||
assert "-DFOO=1" in compile_flags
|
||||
|
||||
@@ -665,9 +669,7 @@ def test_project_flags_unflags_match_tokens() -> None:
|
||||
"""build_unflags removes a token embedded in a multi-token entry."""
|
||||
_set_flags("-Os -g3")
|
||||
CORE.build_unflags = {"-Os"}
|
||||
compile_flags, _link, _dirs, _libs = arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
compile_flags, _link, _dirs, _libs = _split_flags()
|
||||
assert "-g3" in compile_flags
|
||||
assert "-Os" not in compile_flags
|
||||
|
||||
@@ -675,9 +677,7 @@ def test_project_flags_unflags_match_tokens() -> None:
|
||||
def test_project_flags_requotes_lexed_defines() -> None:
|
||||
"""A quoted spaced value stays one compiler argument after lex/emit."""
|
||||
_set_flags('-DGREETING="hello world"')
|
||||
compile_flags, _link, _dirs, _libs = arduino8266._project_flags(
|
||||
arduino8266._unflag_tokens(), arduino8266._lexed_build_flags()
|
||||
)
|
||||
compile_flags, _link, _dirs, _libs = _split_flags()
|
||||
# shlex folds the quotes (as PIO's ParseFlags does); _shell_token
|
||||
# re-quotes the spaced token so the shell passes one argv element
|
||||
assert compile_flags == [_shq("-DGREETING=hello world")]
|
||||
@@ -697,7 +697,7 @@ def test_write_project_empty_core_raises(tmp_path: Path) -> None:
|
||||
def test_flag_defines_joins_spaced_define() -> None:
|
||||
"""A spaced "-D KNOB" entry is detected exactly as PlatformIO detects it."""
|
||||
_set_flags("-D PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
|
||||
defines = _flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
defines = _defines()
|
||||
assert "PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH" in defines
|
||||
assert "" not in defines
|
||||
|
||||
@@ -780,20 +780,14 @@ def test_build_config_custom_mmu_without_knob_raises() -> None:
|
||||
def test_flag_defines_lexes_quoted_single_tokens() -> None:
|
||||
"""A quoted single-token define reads the same as on the compile line."""
|
||||
_set_flags('-DMMU_SEC_HEAP="0x40108000"')
|
||||
assert (
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())["MMU_SEC_HEAP"]
|
||||
== "MMU_SEC_HEAP=0x40108000"
|
||||
)
|
||||
assert _defines()["MMU_SEC_HEAP"] == "MMU_SEC_HEAP=0x40108000"
|
||||
|
||||
|
||||
def test_flag_defines_duplicate_defines_resolve_deterministically() -> None:
|
||||
"""Duplicate conflicting defines pick the same winner every run (sorted
|
||||
iteration, last writer wins), independent of the set's hash seed."""
|
||||
_set_flags("-DMMU_IRAM_SIZE=0x8000", "-DMMU_IRAM_SIZE=0xC000")
|
||||
assert (
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())["MMU_IRAM_SIZE"]
|
||||
== "MMU_IRAM_SIZE=0xC000"
|
||||
)
|
||||
assert _defines()["MMU_IRAM_SIZE"] == "MMU_IRAM_SIZE=0xC000"
|
||||
|
||||
|
||||
def test_flag_tables_match_platformio_builder() -> None:
|
||||
@@ -995,9 +989,7 @@ def test_build_config_mmu_defines_do_not_alias_the_table() -> None:
|
||||
module table for later builds in the same process."""
|
||||
config = _resolve("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48")
|
||||
config.mmu_defines.append("MMU_BOGUS")
|
||||
again = _resolve_build_config(
|
||||
_flag_defines(set(), arduino8266._lexed_build_flags())
|
||||
)
|
||||
again = _resolve_current()
|
||||
assert "MMU_BOGUS" not in again.mmu_defines
|
||||
assert all(isinstance(v, tuple) for v in arduino8266._MMU_VARIANTS.values())
|
||||
|
||||
@@ -1043,7 +1035,7 @@ def test_generate_ld_scripts_header_change_invalidates_stamp(
|
||||
paths = _make_framework(tmp_path)
|
||||
header = paths.framework / "tools" / "sdk" / "ld" / "eagle.app.v6.common.ld.h"
|
||||
header.write_text("v1")
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
_run_generate_ld_scripts(paths)
|
||||
header.write_text("v2 (longer)")
|
||||
@@ -1055,7 +1047,7 @@ def test_generate_ld_scripts_header_change_invalidates_stamp(
|
||||
def test_generate_ld_scripts_unreadable_stamp_regenerates(tmp_path: Path) -> None:
|
||||
"""A non-UTF-8 stamp is a damaged cache: regenerate, never abort."""
|
||||
paths = _make_framework(tmp_path)
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
(ld_dir / ".local.eagle.app.v6.common.ld.stamp").write_bytes(b"\xff\xfe")
|
||||
@@ -1091,19 +1083,21 @@ 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."""
|
||||
still raises for real failures (now the shared helper's contract)."""
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
target = tmp_path / "gen.ld"
|
||||
target.write_bytes(b"\xff\xfe")
|
||||
arduino8266._write_generated(target, "SECTIONS { }")
|
||||
write_file_if_changed(target, "SECTIONS { }")
|
||||
assert target.read_text(encoding="utf-8") == "SECTIONS { }"
|
||||
assert "Replacing damaged generated file" in caplog.text
|
||||
assert "Replacing damaged 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."""
|
||||
paths = _make_framework(tmp_path)
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
output = ld_dir / "local.eagle.app.v6.common.ld"
|
||||
@@ -1118,7 +1112,7 @@ def test_generate_ld_scripts_corrupt_output_is_overwritten(tmp_path: Path) -> No
|
||||
"""A non-UTF-8 cached script must be overwritten by the regeneration,
|
||||
not abort it (write_file_if_changed reads the old content)."""
|
||||
paths = _make_framework(tmp_path)
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
output = ld_dir / "local.eagle.app.v6.common.ld"
|
||||
@@ -1133,7 +1127,7 @@ def test_generate_ld_scripts_unreadable_note_still_warns(
|
||||
) -> None:
|
||||
"""A cached diagnostic that cannot be read must not vanish silently."""
|
||||
paths = _make_framework(tmp_path)
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warn!")
|
||||
result = _ok_result(stderr="warn!")
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
ld_dir = _run_generate_ld_scripts(paths)
|
||||
(ld_dir / ".local.eagle.app.v6.common.ld.stderr").write_bytes(b"\xff\xfe")
|
||||
@@ -1182,7 +1176,7 @@ def test_mmu_custom_valueless_switch_accepted_and_others_validated() -> None:
|
||||
"-DMMU_IRAM_HEAP",
|
||||
)
|
||||
assert "MMU_IRAM_HEAP" in config.mmu_defines
|
||||
with pytest.raises(EsphomeError, match="MMU_SEC_HEAP_SIZE must be a hex"):
|
||||
with pytest.raises(EsphomeError, match="MMU_SEC_HEAP_SIZE must be a numeric"):
|
||||
_resolve(
|
||||
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM",
|
||||
"-DMMU_IRAM_SIZE=0x8000",
|
||||
@@ -1191,6 +1185,19 @@ def test_mmu_custom_valueless_switch_accepted_and_others_validated() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_mmu_custom_accepts_decimal_non_segment_values() -> None:
|
||||
"""MMU_EXTERNAL_HEAP=128 (the module's own EXTERNAL_128K shape) is a
|
||||
mmu_iram.h count, not a segment length; decimal is legal there while
|
||||
the two segment sizes stay hex-only for the surgery parser."""
|
||||
config = _resolve(
|
||||
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM",
|
||||
"-DMMU_IRAM_SIZE=0x8000",
|
||||
"-DMMU_ICACHE_SIZE=0x8000",
|
||||
"-DMMU_EXTERNAL_HEAP=128",
|
||||
)
|
||||
assert "MMU_EXTERNAL_HEAP=128" in config.mmu_defines
|
||||
|
||||
|
||||
def test_mmu_no_knob_rejects_any_raw_mmu_flag() -> None:
|
||||
"""The no-knob branch refuses every raw MMU_*, like the knob branch; a
|
||||
lone switch would win the compile line but not the linker script."""
|
||||
@@ -1301,7 +1308,7 @@ def test_generate_ld_scripts_testing_surgery_failure_is_named(
|
||||
named error, like the ratetable surgery."""
|
||||
paths = _make_framework(tmp_path)
|
||||
CORE.testing_mode = True
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with (
|
||||
patch.object(arduino8266.subprocess, "run", return_value=result),
|
||||
patch.object(
|
||||
@@ -1323,7 +1330,7 @@ def test_generate_ld_scripts_testing_flash_ld_surgery_failure_is_named(
|
||||
"MEMORY { }"
|
||||
)
|
||||
CORE.testing_mode = True
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with (
|
||||
patch.object(arduino8266.subprocess, "run", return_value=result),
|
||||
patch.object(
|
||||
@@ -1369,7 +1376,7 @@ def test_generate_ld_scripts_unreadable_header_forces_regeneration(
|
||||
raise PermissionError(13, "denied")
|
||||
return real_stat(self, **kwargs)
|
||||
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(Path, "stat", fake_stat):
|
||||
with patch.object(
|
||||
arduino8266.subprocess, "run", return_value=result
|
||||
@@ -1402,7 +1409,7 @@ def test_generate_ld_scripts_gcc_change_invalidates_stamp(tmp_path: Path) -> Non
|
||||
paths = _make_framework(tmp_path)
|
||||
gcc = toolchain_tool(paths.toolchain, "gcc")
|
||||
gcc.write_text("v1")
|
||||
result = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
|
||||
result = _ok_result()
|
||||
with patch.object(arduino8266.subprocess, "run", return_value=result):
|
||||
_run_generate_ld_scripts(paths)
|
||||
gcc.write_text("v2 (longer)")
|
||||
|
||||
@@ -690,6 +690,13 @@ def test_prefetch_wave_unknown_size_falls_back_to_sequential(
|
||||
assert "No Content-Length for https://x/b.tar.gz" in caplog.text
|
||||
|
||||
|
||||
def test_raise_on_empty_arg_flags() -> None:
|
||||
"""A surviving bare flag means an empty glued argument; reject by name."""
|
||||
with pytest.raises(EsphomeError, match=r"build_flags contain empty-argument"):
|
||||
lib.raise_on_empty_arg_flags(["-DFOO", "-D", "-l"], "build_flags")
|
||||
lib.raise_on_empty_arg_flags(["-DFOO", "-Iinc"], "build_flags")
|
||||
|
||||
|
||||
def test_content_lengths_head_requests(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Sizes come from HEAD Content-Length; a failing HEAD reads as 0 so
|
||||
the combined bar is skipped rather than wrong."""
|
||||
|
||||
Reference in New Issue
Block a user