Pin the LOW_MEMORY lwIP fall-through, veto the stamp on a stuck stale note

LOW_MEMORY is upstream's else branch, so the default variant stands in
for it and every listed knob wins -- pinned for the ordinary SNTP
multi-server config where esp8266's HIGHER_BANDWIDTH_LOW_FLASH must
prevail. A stale warn note that cannot be removed now vetoes the stamp
(mirror of the lost-note case), and the unreadable-cached-note warning
names its cause.
This commit is contained in:
J. Nick Koston
2026-08-23 13:51:54 -05:00
parent 9766f6e911
commit 6b5b13c17e
2 changed files with 58 additions and 4 deletions
+13 -4
View File
@@ -14,7 +14,6 @@ from the build flags with the same precedence as the PlatformIO builder.
from __future__ import annotations
from contextlib import suppress
from dataclasses import dataclass
import hashlib
import logging
@@ -120,6 +119,10 @@ _LWIP_VARIANTS = {
1460, 0, 0, "lwip2-1460"
),
}
# The default is PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY's variant: upstream
# has no branch for that spelling (it is the else), so any listed knob wins
# over it -- sntp emits LOW_MEMORY while esp8266 always emits
# HIGHER_BANDWIDTH_LOW_FLASH, and the latter must win as under PlatformIO
_LWIP_DEFAULT = _LwipVariant(536, 1, 0, "lwip2-536-feat")
# Knob define -> MMU_* defines; first match wins, in insertion order (as
@@ -633,8 +636,13 @@ def generate_ld_scripts(
_LOGGER.warning("Linker-script preprocessor: %s", result.stderr.strip())
note_persisted = _write_note(stderr_note, result.stderr.strip(), warn=True)
else:
with suppress(OSError):
try:
stderr_note.unlink(missing_ok=True)
except OSError as err:
# A kept stale note would re-emit an obsolete diagnostic on
# every cache hit; skip the stamp so -E re-derives the truth
_LOGGER.debug("Could not remove %s: %s", stderr_note, err)
note_persisted = False
if "SECTIONS" not in result.stdout:
# A degenerate zero-exit run must not be stamped as a good cache
raise EsphomeError(
@@ -660,11 +668,12 @@ def generate_ld_scripts(
"Linker-script preprocessor: %s",
stderr_note.read_text(encoding="utf-8"),
)
except (OSError, UnicodeDecodeError):
except (OSError, UnicodeDecodeError) as err:
_LOGGER.warning(
"A cached linker-script preprocessor diagnostic exists at %s "
"but could not be read",
"but could not be read: %s",
stderr_note,
err,
)
if CORE.testing_mode:
@@ -219,6 +219,8 @@ def _make_framework(tmp_path: Path) -> InstalledPaths:
),
("PIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH", "lwip2-1460-feat", 1460, 1, 0),
("PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY_LOW_FLASH", "lwip2-536", 536, 0, 0),
# LOW_MEMORY has no upstream branch: it is the default (else) variant
("PIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY", "lwip2-536-feat", 536, 1, 0),
],
)
def test_build_config_lwip_variants(
@@ -233,6 +235,17 @@ def test_build_config_lwip_variants(
assert f"LWIP_IPV6={ipv6}" in config.knob_defines
def test_lwip_low_memory_loses_to_listed_knobs() -> None:
"""The ordinary SNTP multi-server config: sntp emits LOW_MEMORY, esp8266
always emits HIGHER_BANDWIDTH_LOW_FLASH, and the listed knob must win
exactly as in platformio-build.py's elif chain."""
config = _resolve(
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY",
"-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH",
)
assert config.lwip_lib == "lwip2-1460"
@pytest.mark.parametrize(
("knob", "expected"),
[
@@ -657,6 +670,38 @@ def test_generate_ld_scripts_lost_warn_note_vetoes_the_stamp(
assert caplog.text.count("Linker-script preprocessor: warning: something") == 2
def test_generate_ld_scripts_unremovable_stale_note_vetoes_the_stamp(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A stale warn note that cannot be removed skips the stamp, so the
obsolete diagnostic is not re-emitted on cache hits forever."""
paths = _make_framework(tmp_path)
_set_flags()
warn = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="warning: old")
clean = MagicMock(returncode=0, stdout=_COMMON_LD_H_OUTPUT, stderr="")
with patch.object(arduino8266.subprocess, "run", return_value=warn):
_run_generate_ld_scripts(paths)
real_unlink = Path.unlink
def fail_note_unlink(self: Path, missing_ok: bool = False) -> None:
if self.name.endswith(".stderr"):
raise OSError("locked")
real_unlink(self, missing_ok=missing_ok)
# Flags changed -> regenerate; clean stderr but the stale note is stuck
_set_flags("-DVTABLES_IN_DRAM")
with (
patch.object(arduino8266.subprocess, "run", return_value=clean),
patch.object(Path, "unlink", fail_note_unlink),
):
_run_generate_ld_scripts(paths)
# Unstamped: the next build re-runs -E instead of trusting the cache
with patch.object(arduino8266.subprocess, "run", return_value=clean) as run3:
_run_generate_ld_scripts(paths)
run3.assert_called_once()
def test_build_config_mmu_knob_with_raw_mmu_flag_raises() -> None:
"""A variant knob plus a raw MMU_* define would split the compile line
from the linker script; refuse like the no-knob case."""