diff --git a/esphome/build_gen/arduino8266.py b/esphome/build_gen/arduino8266.py index ace39a847b..7fe68d46bd 100644 --- a/esphome/build_gen/arduino8266.py +++ b/esphome/build_gen/arduino8266.py @@ -42,9 +42,11 @@ from esphome.components.esp8266.const import ( KEY_FLASH_SIZE, KEY_SCANF_FLOAT, ) -from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE, EsphomeError -from esphome.framework_helpers import get_project_cxx_compile_flags +from esphome.framework_helpers import ( + get_project_cxx_compile_flags, + strip_win_long_path_prefix, +) from esphome.helpers import mkdir_p, write_file_if_changed from esphome.platformio.library import SOURCE_KIND_FOR_SUFFIX, lex_build_flags @@ -55,10 +57,8 @@ _LOGGER = logging.getLogger(__name__) # Compile rule per source suffix, derived from the shared suffix -> kind map # so every source extension a library manifest can select has a rule. -_RULE_FOR_KIND = {"c": "cc", "cxx": "cxx", "asm": "asm"} -_RULE_FOR_SUFFIX = { - suffix: _RULE_FOR_KIND[kind] for suffix, kind in SOURCE_KIND_FOR_SUFFIX.items() -} +# Compile rule names are exactly the shared suffix -> kind values (c, cxx, +# asm), so every source extension a library manifest can select has a rule. # Always excluded from the core build: ESPHome uses its own native OTA # backend, so the Arduino Updater (and its 228-byte global) never links. @@ -476,7 +476,7 @@ def _collect_sources(root: Path, exclude: set[str] = frozenset()) -> list[Path]: return sorted( p for p in root.rglob("*") - if p.suffix in _RULE_FOR_SUFFIX and p.name not in exclude + if p.suffix in SOURCE_KIND_FOR_SUFFIX and p.name not in exclude ) @@ -645,7 +645,9 @@ def _ninja_compile_edges( rel = src.relative_to(root).as_posix() obj = f"obj/{group}/{rel}.o" escaped_obj = _e(obj) - lines.append(f"build {escaped_obj}: {_RULE_FOR_SUFFIX[src.suffix]} {_e(src)}") + lines.append( + f"build {escaped_obj}: {SOURCE_KIND_FOR_SUFFIX[src.suffix]} {_e(src)}" + ) if flags: lines.append(f" flags = {flags}") # Escaped once here: the returned paths only ever appear in build @@ -658,14 +660,15 @@ def _common_parent(paths: list[Path]) -> Path: return Path(os.path.commonpath([str(p.parent) for p in paths])) -def write_project(paths: InstalledPaths) -> bool: +def write_project(paths: InstalledPaths, ccache: str | None) -> bool: """Write the ninja build for the current configuration. - Returns True when ``build.ninja`` changed, so the caller can skip work - derived purely from it (the compile database) on unchanged builds. + ``ccache`` is the caller's already-resolved binary (None when disabled) + so one build never pays the runnability probe per consumer. Returns + True when ``build.ninja`` changed, so the caller can skip work derived + purely from it (the compile database) on unchanged builds. """ from esphome.arduino.library import resolve_libraries - from esphome.arduino8266.framework import ccache_path framework = paths.framework toolchain_bin = paths.toolchain / "bin" @@ -679,9 +682,10 @@ def write_project(paths: InstalledPaths) -> bool: config = _resolve_build_config(flag_defines) esp8266_data = CORE.data[KEY_ESP8266] board = esp8266_data[KEY_BOARD] - # Config validation accepts the board as a bare string, so this is the - # first place an unknown board can fail by name instead of a KeyError - if board not in BOARDS or board not in ESP8266_BOARD_BUILD: + # _validate_native_toolchain rejects unknown boards at config time and a + # test pins the two board tables equal; this backstop covers callers + # that bypassed validation + if 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) @@ -801,7 +805,6 @@ def write_project(paths: InstalledPaths) -> bool: ) build_tool = Path(__file__).parent / "build_tool.py" - ccache = ccache_path() # $in/$out stay unquoted in the rule commands: ninja shell-escapes its # built-in path variables itself when expanding a command (POSIX and @@ -810,13 +813,16 @@ def write_project(paths: InstalledPaths) -> bool: lines = [ "# Auto-generated by ESPHome", "ninja_required_version = 1.5", - f"cc = {_q(toolchain_bin / 'xtensa-lx106-elf-gcc')}", - f"cxx = {_q(toolchain_bin / 'xtensa-lx106-elf-g++')}", - f"python = {_q(sys.executable)}", + f"cc = {_q(toolchain_tool(paths.toolchain, 'gcc'))}", + f"cxx = {_q(toolchain_tool(paths.toolchain, 'g++'))}", + # The NSIS launcher starts Python with a \\?\ extended-length path + # that cmd.exe cannot spawn; same strip every other emitted binary + # path gets + f"python = {_q(strip_win_long_path_prefix(sys.executable))}", f"buildtool = {_q(build_tool)}", f"ccache = {_q(ccache) if ccache else ''}", "", - "rule cc", + "rule c", " command = $ccache $cc -MMD -MF $out.d $cflags $flags -c $in -o $out", " depfile = $out.d", " deps = gcc", @@ -832,7 +838,7 @@ def write_project(paths: InstalledPaths) -> bool: " deps = gcc", " description = AS $out", "rule ar", - f" command = $python $buildtool ar {_q(toolchain_bin / 'xtensa-lx106-elf-ar')} $out $out.rsp", + f" command = $python $buildtool ar {_q(toolchain_tool(paths.toolchain, 'ar'))} $out $out.rsp", " rspfile = $out.rsp", " rspfile_content = $in_newline", " description = AR $out", @@ -938,18 +944,16 @@ def write_project(paths: InstalledPaths) -> bool: return write_file_if_changed(build_dir / "build.ninja", "\n".join(lines)) -def get_flash_ld_path(build_dir: Path) -> Path: - """The flash linker script the link actually uses (for size reporting).""" - from esphome.arduino8266.framework import ( - framework_package_version, - get_framework_path, - ) +def get_flash_ld_path(build_dir: Path, paths: InstalledPaths) -> Path: + """The flash linker script the link actually uses (for size reporting). + Reads the same install the ninja file linked against instead of + re-resolving the framework version. + """ name = _active_flash_ld_name(_flash_ld_name(CORE.data[KEY_ESP8266][KEY_BOARD])) if CORE.testing_mode: 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 + return paths.framework / "tools" / "sdk" / "ld" / name def _flash_size_str(flash_size: int) -> str: diff --git a/tests/unit_tests/build_gen/test_arduino8266.py b/tests/unit_tests/build_gen/test_arduino8266.py index a6712345ac..6f099214f4 100644 --- a/tests/unit_tests/build_gen/test_arduino8266.py +++ b/tests/unit_tests/build_gen/test_arduino8266.py @@ -69,13 +69,6 @@ def _shq(tok: str) -> str: return f'"{tok}"' if os.name == "nt" else f"'{tok}'" -def test_rule_map_covers_all_source_suffixes() -> None: - """Every suffix a library manifest can select must map to a ninja rule.""" - from esphome.platformio.library import SRC_FILE_EXTENSIONS - - assert set(arduino8266._RULE_FOR_SUFFIX) == set(SRC_FILE_EXTENSIONS) - - def _resolve(*flags: str): """Set the build flags and resolve the knob config in one step.""" _set_flags(*flags) @@ -221,9 +214,8 @@ def _write_ninja( "esphome.arduino.library.resolve_libraries", return_value=libraries or [], ), - patch("esphome.arduino8266.framework.ccache_path", return_value=ccache), ): - arduino8266.write_project(paths) + arduino8266.write_project(paths, ccache) return (CORE.relative_pioenvs_path(CORE.name) / "build.ninja").read_text() @@ -299,7 +291,7 @@ def test_write_project_link_line_and_exclusions(tmp_path: Path) -> None: 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 "abi.c.o: cc" 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 src_lines = [line for line in content.splitlines() if "obj/src/" in line] @@ -547,25 +539,21 @@ def test_write_project_libraries_and_variant( def test_get_flash_ld_path(tmp_path: Path) -> None: + paths = InstalledPaths( + framework=tmp_path / "framework", + toolchain=tmp_path / "toolchain", + ninja=Path("ninja"), + ) CORE.testing_mode = True - assert get_flash_ld_path(tmp_path) == ( + assert get_flash_ld_path(tmp_path, paths) == ( tmp_path / "ld" / "testing_eagle.flash.4m.ld" ) CORE.testing_mode = False - with ( - patch( - "esphome.arduino8266.framework.get_framework_path", - return_value=tmp_path / "framework", - ), - patch( - "esphome.arduino8266.framework.framework_package_version", - return_value="3.30102.0", - ), - ): - assert get_flash_ld_path(tmp_path) == ( - tmp_path / "framework" / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld" - ) + # Reads the same install the ninja file linked against; no re-resolve + assert get_flash_ld_path(tmp_path, paths) == ( + tmp_path / "framework" / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld" + ) def test_flash_size_str() -> None: @@ -756,8 +744,8 @@ def test_write_project_returns_changed(tmp_path: Path) -> None: patch("esphome.arduino.library.resolve_libraries", return_value=[]), patch("esphome.arduino8266.framework.ccache_path", return_value=None), ): - assert arduino8266.write_project(paths) is True - assert arduino8266.write_project(paths) is False + assert arduino8266.write_project(paths, None) is True + assert arduino8266.write_project(paths, None) is False def test_write_project_missing_elf2bin_raises(tmp_path: Path) -> None: @@ -774,7 +762,7 @@ def test_write_project_missing_elf2bin_raises(tmp_path: Path) -> None: patch("esphome.arduino.library.resolve_libraries", return_value=[]), pytest.raises(EsphomeError, match="elf2bin"), ): - arduino8266.write_project(paths) + arduino8266.write_project(paths, None) def test_write_project_missing_src_dir_raises(tmp_path: Path) -> None: @@ -789,7 +777,7 @@ def test_write_project_missing_src_dir_raises(tmp_path: Path) -> None: ), pytest.raises(EsphomeError, match="source directory"), ): - arduino8266.write_project(paths) + arduino8266.write_project(paths, None) def test_build_config_custom_mmu_without_knob_raises() -> None: