diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 645df0131c..c6a6ca7855 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -131,6 +131,9 @@ _MMU_VARIANTS = ( ) _MMU_DEFAULT = ("MMU_IRAM_SIZE=0x8000", "MMU_ICACHE_SIZE=0x8000") +# Upstream's CXXFLAGS (-fno-rtti, the -std level, -f(no-)exceptions) and the +# trailing stdc++/m/c/gcc system libs are composed at emission +# (write_project) from CORE.cpp_standard and _BuildConfig.exceptions. _ASFLAGS = ["-mlongcalls", "-mtext-section-literals"] _CFLAGS = [ "-std=gnu17", @@ -208,7 +211,10 @@ class _BuildConfig: def _flag_defines() -> dict[str, str]: """Map define name -> full ``NAME[=VALUE]`` for every -D build flag.""" defines: dict[str, str] = {} - for flag in CORE.build_flags: + # Sorted so duplicate defines resolve the same way every run: the + # winner feeds the linker-script preprocessor line, which is also the + # cache stamp + for flag in sorted(CORE.build_flags): # Shell-lex every entry the way PlatformIO's ParseFlags does, so a # knob in "-DKNOB -DOTHER", a spaced "-D KNOB", and quoted bodies all # read identically to _project_flags (and the compile line). @@ -414,9 +420,15 @@ def generate_ld_scripts( and stamp.is_file() and stamp.read_text(encoding="utf-8") == stamp_content ): - result = subprocess.run( - cmd, capture_output=True, text=True, check=False, close_fds=False - ) + try: + result = subprocess.run( + cmd, capture_output=True, text=True, check=False, close_fds=False + ) + 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 if result.returncode != 0: raise EsphomeError(f"Generating the linker script failed:\n{result.stderr}") content = build_surgery.relocate_ratetable(result.stdout) diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 244f00cbda..8d9894e3ce 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -727,3 +727,48 @@ 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()["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()["MMU_IRAM_SIZE"] == "MMU_IRAM_SIZE=0xC000" + + +def test_flag_tables_match_platformio_builder() -> None: + """The transliterated flag lists pinned verbatim, like the define set: + a drift lands as a test failure, not a binary-size regression.""" + assert arduino8266._ASFLAGS == ["-mlongcalls", "-mtext-section-literals"] + assert arduino8266._CFLAGS == [ + "-std=gnu17", + "-Wpointer-arith", + "-Wno-implicit-function-declaration", + "-Wl,-EL", + "-fno-inline-functions", + "-nostdlib", + ] + assert arduino8266._CCFLAGS[:6] == [ + "-Os", + "-mlongcalls", + "-mtext-section-literals", + "-falign-functions=4", + "-U__STRICT_ANSI__", + "-ffunction-sections", + ] + assert arduino8266._LINKFLAGS[:5] == [ + "-Os", + "-nostdlib", + "-Wl,--no-check-sections", + "-Wl,-static", + "-Wl,--gc-sections", + ] + + +def test_generate_ld_scripts_missing_compiler_is_clean(tmp_path: Path) -> None: + """A half-deleted toolchain cache fails with an ESPHome error naming the + binary, not a FileNotFoundError traceback.""" + paths = _make_framework(tmp_path) + _set_flags() + with pytest.raises(EsphomeError, match="Could not run"): + _run_generate_ld_scripts(paths)