Merge branch 'esp8266-native-ninja-emission' into esp8266-arduino-toolchain

This commit is contained in:
J. Nick Koston
2026-08-20 14:30:15 -05:00
2 changed files with 49 additions and 32 deletions
+23 -13
View File
@@ -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
+26 -19
View File
@@ -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: