From 6dbe414b774abdd5953714b9ce14ea20764d95e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 23 Aug 2026 18:59:45 -0500 Subject: [PATCH] Assemble plain .s without the preprocessor, allow unflagging operand-less linker flags --- esphome/build_gen/arduino8266.py | 15 ++++++++--- .../unit_tests/build_gen/test_arduino8266.py | 26 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 9b3ecb007a..0a47cdf178 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -631,6 +631,10 @@ _PLAIN_LINKER_FLAGS = ( "-nostdlib", "-rdynamic", ) +# The subset whose next token is an operand; unflagging the bare flag +# would strand the operand. Operand-less members of the list above filter +# whole-token from both the compile and link lines, as PlatformIO allows. +_PLAIN_LINKER_OPERAND_FLAGS = ("-u", "-e") _PLAIN_LINKER_PREFIXES = ("-T", "-Xlinker") # Driver options, not ld options: -Wl, has no equivalent for these _PLAIN_DRIVER_LINK_PREFIXES = ("-fuse-ld=", "--specs=", "-specs=") @@ -993,7 +997,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: if plain := sorted( u for u in unflags - if u in _PLAIN_LINKER_FLAGS or u.startswith(_PLAIN_LINKER_PREFIXES) + if u in _PLAIN_LINKER_OPERAND_FLAGS or u.startswith(_PLAIN_LINKER_PREFIXES) ): raise EsphomeError( f"build_unflags cannot remove plain linker flag(s) " @@ -1040,7 +1044,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: f"buildtool = {_q(build_tool)}", f"ccache = {_q(ccache) if ccache else ''}", "", - # Rule names match SOURCE_KIND_FOR_SUFFIX values (c, cxx, asm) + # Rule names match SOURCE_KIND_FOR_SUFFIX values (c, cxx, asm, aspp) "rule c", " command = $ccache $cc -MMD -MF $out.d $cflags $flags -c $in -o $out", " depfile = $out.d", @@ -1051,11 +1055,16 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool: " depfile = $out.d", " deps = gcc", " description = CXX $out", - "rule asm", + "rule aspp", " command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $asflags $flags -c $in -o $out", " depfile = $out.d", " deps = gcc", " description = AS $out", + # Plain assembler, as SCons's ASCOM: no preprocessor, so no + # depfile and no $flags (defines/includes) either + "rule asm", + " command = $ccache $cc -x assembler $asflags -c $in -o $out", + " description = AS $out", "rule ar", f" command = $python $buildtool ar {_q(toolchain_tool(paths.toolchain, 'ar'))} $out $out.rsp", " rspfile = $out.rsp", diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 2ea5dca8aa..947778e400 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -346,7 +346,7 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None: assert "core_esp8266_waveform_phase.cpp" not in content assert "core_esp8266_main.cpp.o" in content # Assembly and C sources compile through their own rules - assert "cont.S.o: asm" in content + assert "cont.S.o: aspp" in content assert "abi.c.o: c" in content # throw_stubs is force-included for ESPHome sources only, via one shared # srcflags variable rather than a copy of the flags line per edge @@ -688,6 +688,30 @@ def test_build_config_nonosdk_precedence() -> None: assert _resolve_build_config(_defines()).nonosdk == "NONOSDK221" +def test_write_project_plain_asm_rule_skips_preprocessor(tmp_path: Path) -> None: + """A lowercase .s source assembles plain (SCons AS), never through the + preprocessor rule that a .S source gets.""" + paths = _make_framework(tmp_path) + core_dir = paths.framework / "cores" / "esp8266" + (core_dir / "lowlevel.s").write_text("nop\n") + _set_flags() + content = _write_ninja(paths) + assert "lowlevel.s.o: asm " in content + assert "rule asm\n command = $ccache $cc -x assembler $asflags -c $in -o $out" in ( + content + ) + + +def test_write_project_unflags_operandless_linker_flag(tmp_path: Path) -> None: + """build_unflags: -nostdlib filters whole-token from both lines, as + PlatformIO allows; only operand-taking flags hard-error.""" + paths = _make_framework(tmp_path) + _set_flags() + CORE.build_unflags = {"-nostdlib"} + content = _write_ninja(paths) + assert "-nostdlib" not in content + + def test_write_project_unflagged_symbol_takes_its_dash_u(tmp_path: Path) -> None: """Unflagging a -u symbol drops the -u that carried it; a dangling -u would consume the next token and hand ld a symbol as an input file."""