From a0d56d02d052586358ceeb04f243c5c7065a61a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 20 Aug 2026 14:29:54 -0500 Subject: [PATCH] Tighten the emission half: shared srcflags, single escapes, one testing-name derivation --- esphome/build_gen/arduino8266.py | 36 +++++++++------ .../unit_tests/build_gen/test_arduino8266.py | 45 +++++++++++-------- 2 files changed, 49 insertions(+), 32 deletions(-) diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index 63fb8a07b9..f3de978f76 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -277,6 +277,16 @@ 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 + + def _flash_ld_name(board: str) -> str: return ESP8266_LD_SCRIPTS[BOARDS[board][KEY_FLASH_SIZE]][1] @@ -358,7 +368,7 @@ def _unflag_tokens() -> set[str]: def _project_flags( - unflags: set[str] | None = None, + unflags: set[str], ) -> tuple[list[str], list[str], list[Path], list[str]]: """Split the ESPHome build flags into compile, linker, -L, and -l lists. @@ -367,8 +377,6 @@ def _project_flags( ``build_unflags`` matches individual tokens (``-Os`` inside ``-Os -g3``). Lexed tokens are re-quoted at emission via ``_shell_token``. """ - if unflags is None: - unflags = _unflag_tokens() compile_flags: list[str] = [] link_flags: list[str] = [] lib_dirs: list[Path] = [] @@ -477,12 +485,13 @@ def _ninja_compile_edges( for src in sources: rel = src.relative_to(root).as_posix() obj = f"obj/{group}/{rel}.o" - lines.append(f"build {_e(obj)}: {_RULE_FOR_SUFFIX[src.suffix]} {_e(src)}") + escaped_obj = _e(obj) + lines.append(f"build {escaped_obj}: {_RULE_FOR_SUFFIX[src.suffix]} {_e(src)}") if flags: lines.append(f" flags = {flags}") # Escaped once here: the returned paths only ever appear in build # statements (archive/link inputs), which use ninja escaping. - objects.append(_e(obj)) + objects.append(escaped_obj) return objects @@ -534,8 +543,7 @@ def write_project(paths: dict[str, Path]) -> bool: for required in include_dirs: if not required.is_dir(): raise EsphomeError( - f"Arduino toolchain install is incomplete: missing {required}; " - "run 'esphome clean-all' and retry" + f"{_INCOMPLETE_INSTALL}: missing {required}; {_CLEAN_HINT}" ) for lib in libraries: include_dirs += lib.include_dirs @@ -575,7 +583,7 @@ def write_project(paths: dict[str, Path]) -> bool: link_flags += ["-u", "_scanf_float"] link_flags += project_link_flags link_flags += [flag for lib in libraries for flag in lib.link_flags] - flash_ld = f"testing_{flash_ld_name}" if CORE.testing_mode else flash_ld_name + flash_ld = _active_flash_ld_name(flash_ld_name) link_flags += ["-T", flash_ld] lib_dirs = [Path("ld"), sdk / "lib", sdk / "ld", sdk / "lib" / config.nonosdk] @@ -666,8 +674,7 @@ def write_project(paths: dict[str, Path]) -> bool: # An empty archive would link into a wall of undefined references # (app_entry, the exception vectors) far from the cause raise EsphomeError( - f"Arduino toolchain install is incomplete: no core sources in " - f"{core_dir}; run 'esphome clean-all' and retry" + f"{_INCOMPLETE_INSTALL}: no core sources in {core_dir}; {_CLEAN_HINT}" ) lines.append(f"build libFrameworkArduino.a: ar {' '.join(core_objs)}") archives.append("libFrameworkArduino.a") @@ -694,8 +701,11 @@ def write_project(paths: dict[str, Path]) -> bool: archives.append(archive) src_extra = f"-include {_q(src_dir / 'esphome' / 'components' / 'esp8266' / 'throw_stubs.h')}" + # One shared variable instead of repeating the flags line on every src + # edge (hundreds of edges in a real project) + lines.append(f"srcflags = {src_extra}") src_objs = _ninja_compile_edges( - lines, _collect_sources(src_dir), src_dir, "src", flags=src_extra + lines, _collect_sources(src_dir), src_dir, "src", flags="$srcflags" ) ld_deps = ["ld/local.eagle.app.v6.common.ld"] @@ -722,9 +732,9 @@ def get_flash_ld_path(build_dir: Path) -> Path: get_framework_path, ) - name = _flash_ld_name(CORE.data[KEY_ESP8266][KEY_BOARD]) + name = _active_flash_ld_name(_flash_ld_name(CORE.data[KEY_ESP8266][KEY_BOARD])) if CORE.testing_mode: - return build_dir / "ld" / f"testing_{name}" + return build_dir / "ld" / name version = framework_package_version(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]) return get_framework_path(version) / "tools" / "sdk" / "ld" / name diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index 6eab144b91..4b0c2e8af7 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -283,12 +283,17 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None: # Assembly and C sources compile through their own rules assert "cont.S.o: asm" in content assert "abi.c.o: cc" in content - # throw_stubs is force-included for ESPHome sources only + # throw_stubs is force-included for ESPHome sources only, via one shared + # srcflags variable rather than a copy of the flags line per edge src_lines = [line for line in content.splitlines() if "obj/src/" in line] assert any("main.cpp.o: cxx" in line for line in src_lines) - assert content.count("throw_stubs.h") == len( - [line for line in content.splitlines() if line.startswith(" flags = ")] - ) + assert content.count("throw_stubs.h") == 1 + assert "srcflags = -include" in content + flags_lines = [ + line for line in content.splitlines() if line.startswith(" flags = ") + ] + assert flags_lines + assert all(line == " flags = $srcflags" for line in flags_lines) def test_write_project_scanf_float_and_waveform_kept(tmp_path: Path) -> None: @@ -579,7 +584,9 @@ 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() + compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert "Ignoring trailing '-l'" in caplog.text assert not libs assert not lib_dirs @@ -589,7 +596,9 @@ 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() + compile_flags, link_flags, lib_dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert lib_dirs == [Path("/d")] assert link_flags == ["-Wl,-Map=m"] assert "stray" in compile_flags @@ -609,7 +618,9 @@ 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() + compile_flags, _link, _dirs, libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert libs == ["bar"] assert "-DFOO=1" in compile_flags @@ -618,7 +629,9 @@ 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() + compile_flags, _link, _dirs, _libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) assert "-g3" in compile_flags assert "-Os" not in compile_flags @@ -626,7 +639,9 @@ 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() + compile_flags, _link, _dirs, _libs = arduino8266._project_flags( + arduino8266._unflag_tokens() + ) # 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 == ['"-DGREETING=hello world"'] @@ -652,16 +667,8 @@ def test_write_project_empty_core_raises(tmp_path: Path) -> None: for f in core.iterdir(): f.unlink() _set_flags() - src = CORE.relative_src_path() - (src / "esphome" / "components" / "esp8266").mkdir(parents=True, exist_ok=True) - (src / "main.cpp").write_text("") - with ( - patch.object(arduino8266, "generate_ld_scripts"), - patch("esphome.arduino8266.component.resolve_libraries", return_value=[]), - patch("esphome.arduino8266.framework.ccache_path", return_value=None), - pytest.raises(EsphomeError, match="no core sources"), - ): - arduino8266.write_project(paths) + with pytest.raises(EsphomeError, match="no core sources"): + _write_ninja(paths) def test_flag_defines_joins_spaced_define() -> None: