Strip the python path for cmd.exe, name rules by kind, take the resolved install

The build.ninja python variable goes through strip_win_long_path_prefix
like every other emitted binary path (the NSIS launcher's extended
prefix cannot be spawned by cmd.exe). Compile rules are named by the
shared source kinds directly, retiring the identity translation table
and its drift test. write_project takes the caller's resolved ccache,
get_flash_ld_path reads the InstalledPaths the ninja file linked
against instead of re-resolving the version, the board backstop checks
the one table it indexes with an honest comment, and cc/cxx/ar paths go
through the shared toolchain_tool accessor (gaining the Windows
suffix).
This commit is contained in:
J. Nick Koston
2026-08-21 20:49:30 -05:00
parent d628bc3aae
commit 0973044214
2 changed files with 49 additions and 57 deletions
+33 -29
View File
@@ -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:
+14 -26
View File
@@ -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,23 +539,19 @@ 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) == (
# 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"
)
@@ -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: