diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index c6a6ca7855..deb48f6d54 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -495,8 +495,11 @@ def write_project(paths: InstalledPaths) -> bool: flag_defines = _flag_defines() config = _resolve_build_config(flag_defines) esp8266_data = CORE.data[KEY_ESP8266] - # Board support was validated at config time (_validate_native_toolchain). board = esp8266_data[KEY_BOARD] + # Config-time validation rejects unsupported boards before this runs; + # guard anyway so a bypassing caller fails by name, not KeyError + if board not in BOARDS or board not in ESP8266_BOARD_BUILD: + raise EsphomeError(f"Board '{board}' is not supported by the native toolchain") board_build = ESP8266_BOARD_BUILD[board] flash_ld_name = _flash_ld_name(board) @@ -557,12 +560,13 @@ def write_project(paths: InstalledPaths) -> bool: + get_project_cxx_compile_flags() ) # PlatformIO's ASPPCOM carries defines and includes but not CCFLAGS, - # so only -D/-I user flags reach assembly there; match it. + # so only -D/-I user flags reach assembly there; match it. The tokens + # are already shell-quoted, so test past a leading quote too. asflags = ( _ASFLAGS + defines + includes - + [f for f in project_compile_flags if f.startswith(("-D", "-I"))] + + [f for f in project_compile_flags if f.lstrip('"').startswith(("-D", "-I"))] ) # build_unflags applies to the framework flag sets too (compile and link), @@ -654,6 +658,7 @@ def write_project(paths: InstalledPaths) -> bool: core_exclude |= _CORE_EXCLUDE_WAVEFORM archives = [] + direct_objs: list[str] = [] # variant_dir existence was already enforced with the include dirs variant_sources = _collect_sources(variant_dir) if variant_sources: @@ -690,6 +695,12 @@ def write_project(paths: InstalledPaths) -> bool: f"lib/{lib.name}", flags=" ".join(_shell_token(f) for f in lib.flags), ) + if not lib.lib_archive: + # libArchive: false / dot_a_linkage=false: hand the objects to + # the linker directly so unreferenced-but-required symbols + # (exception handlers, weak overrides) survive + direct_objs.extend(objs) + continue archive = f"lib{lib.name}.a" lines.append(f"build {_e(archive)}: ar {' '.join(objs)}") archives.append(archive) @@ -706,10 +717,10 @@ def write_project(paths: InstalledPaths) -> bool: if CORE.testing_mode: ld_deps.append(f"ld/{flash_ld}") lines.append( - f"build firmware.elf: link {' '.join(src_objs)} | " + f"build firmware.elf: link {' '.join(src_objs + direct_objs)} | " f"{' '.join(_e(a) for a in archives)} {' '.join(_e(d) for d in ld_deps)}" ) - lines.append(f" archives = {' '.join(archives)}") + lines.append(f" archives = {' '.join(_shell_token(a) for a in archives)}") lines.append("build firmware.bin: elf2bin firmware.elf") lines.append("build firmware.factory.bin: copy firmware.bin") lines.append("build firmware.ota.bin: copy firmware.bin") diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 8d9894e3ce..87093ce3e7 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -772,3 +772,47 @@ def test_generate_ld_scripts_missing_compiler_is_clean(tmp_path: Path) -> None: _set_flags() with pytest.raises(EsphomeError, match="Could not run"): _run_generate_ld_scripts(paths) + + +def test_write_project_asm_keeps_quoted_defines(tmp_path: Path) -> None: + """A spaced -D/-I user flag arrives shell-quoted; assembly must still + receive it.""" + paths = _make_framework(tmp_path) + _set_flags('-DGREETING="hello world"', "-Wno-volatile") + content = _write_ninja(paths) + asflags = next(line for line in content.splitlines() if line.startswith("asflags")) + assert '"-DGREETING=hello world"' in asflags + assert "-Wno-volatile" not in asflags + + +def test_write_project_unarchived_library_links_objects(tmp_path: Path) -> None: + """A libArchive:false library's objects reach the link directly.""" + from esphome.arduino.library import ArduinoLibrary + + paths = _make_framework(tmp_path) + lib_src = tmp_path / "gdb" / "src" + lib_src.mkdir(parents=True) + (lib_src / "GDBStub.cpp").write_text("") + _set_flags() + lib = ArduinoLibrary( + name="GDBStub", + sources=[lib_src / "GDBStub.cpp"], + include_dirs=[lib_src], + lib_archive=False, + ) + content = _write_ninja(paths, libraries=[lib]) + assert "libGDBStub.a" not in content + link_line = next( + line for line in content.splitlines() if line.startswith("build firmware.elf") + ) + assert "GDBStub.cpp.o" in link_line + + +def test_write_project_unknown_board_fails_by_name(tmp_path: Path) -> None: + """A caller bypassing config validation gets the board named, not a + KeyError.""" + paths = _make_framework(tmp_path) + _set_flags() + CORE.data[KEY_ESP8266][KEY_BOARD] = "not_a_board" + with pytest.raises(EsphomeError, match="'not_a_board' is not supported"): + _write_ninja(paths)