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

This commit is contained in:
J. Nick Koston
2026-08-20 15:49:05 -05:00
16 changed files with 247 additions and 207 deletions
+10 -16
View File
@@ -17,6 +17,7 @@ from unittest.mock import MagicMock, patch
import pytest
from esphome.arduino8266.framework import InstalledPaths
from esphome.build_gen import arduino8266
from esphome.build_gen.arduino8266 import (
_defines_flags,
@@ -181,15 +182,11 @@ def _make_framework(tmp_path: Path) -> dict[str, Path]:
toolchain = tmp_path / "toolchain"
(toolchain / "bin").mkdir(parents=True)
(toolchain / "include").mkdir()
return {
"framework_path": framework,
"toolchain_path": toolchain,
"ninja_path": Path("ninja"),
}
return InstalledPaths(framework=framework, toolchain=toolchain, ninja=Path("ninja"))
def _write_ninja(
paths: dict[str, Path],
paths: InstalledPaths,
libraries: list | None = None,
ccache: str | None = None,
) -> str:
@@ -390,7 +387,7 @@ SECTIONS
"""
def _run_generate_ld_scripts(paths: dict[str, Path]) -> Path:
def _run_generate_ld_scripts(paths: InstalledPaths) -> Path:
config = _resolve_build_config(_flag_defines())
arduino8266.generate_ld_scripts(paths, config, "eagle.flash.4m.ld")
@@ -442,7 +439,7 @@ def test_generate_ld_scripts_failure(tmp_path: Path) -> None:
def test_generate_ld_scripts_testing_mode(tmp_path: Path) -> None:
paths = _make_framework(tmp_path)
(paths["framework_path"] / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld").write_text(
(paths.framework / "tools" / "sdk" / "ld" / "eagle.flash.4m.ld").write_text(
"MEMORY\n{\n"
" dram0_0_seg : org = 0x3FFE8000, len = 0x14000\n"
" iram1_0_seg : org = 0x40100000, len = 0x8000\n"
@@ -463,7 +460,7 @@ def test_write_project_libraries_and_variant(
from esphome.arduino.library import ArduinoLibrary
paths = _make_framework(tmp_path)
variant_src = paths["framework_path"] / "variants" / "nodemcu" / "variant.cpp"
variant_src = paths.framework / "variants" / "nodemcu" / "variant.cpp"
variant_src.write_text("")
lib_dir = tmp_path / "libsrc"
@@ -529,11 +526,8 @@ def test_get_flash_ld_path(tmp_path: Path) -> None:
def test_flash_size_str() -> None:
assert _flash_size_str("eagle.flash.4m.ld") == "4M"
assert _flash_size_str("eagle.flash.512k.ld") == "512K"
with pytest.raises(EsphomeError, match="Cannot parse flash size"):
_flash_size_str("bogus.ld")
assert _flash_size_str(4 * 1024 * 1024) == "4M"
assert _flash_size_str(512 * 1024) == "512K"
def test_write_project_testing_mode(tmp_path: Path) -> None:
@@ -550,7 +544,7 @@ def test_write_project_missing_framework_dir_raises(tmp_path: Path) -> None:
import shutil
paths = _make_framework(tmp_path)
shutil.rmtree(paths["framework_path"] / "tools" / "sdk" / "lwip2")
shutil.rmtree(paths.framework / "tools" / "sdk" / "lwip2")
_set_flags()
with pytest.raises(EsphomeError, match="incomplete.*lwip2"):
_write_ninja(paths)
@@ -663,7 +657,7 @@ def test_shell_token_escaping() -> None:
def test_write_project_empty_core_raises(tmp_path: Path) -> None:
"""A framework tree with no core sources fails at generation, not link."""
paths = _make_framework(tmp_path)
core = paths["framework_path"] / "cores" / "esp8266"
core = paths.framework / "cores" / "esp8266"
for f in core.iterdir():
f.unlink()
_set_flags()
@@ -0,0 +1,50 @@
"""Tests for esphome.build_helpers.ninja."""
from __future__ import annotations
import os
from pathlib import Path
import sys
from unittest.mock import MagicMock, patch
import pytest
from esphome.build_helpers import ninja as ninja_helper
from esphome.core import EsphomeError
def test_find_ninja_prefers_path(tmp_path: Path) -> None:
with patch("shutil.which", return_value=str(tmp_path / "ninja")):
assert ninja_helper.find_ninja() == tmp_path / "ninja"
def test_find_ninja_falls_back_to_wheel(tmp_path: Path) -> None:
"""Without a PATH entry, the ninja PyPI wheel's binary is used."""
binary_name = "ninja.exe" if os.name == "nt" else "ninja"
(tmp_path / binary_name).touch()
wheel = MagicMock(BIN_DIR=str(tmp_path))
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": wheel}),
):
assert ninja_helper.find_ninja() == tmp_path / binary_name
def test_find_ninja_package_not_installed() -> None:
"""A missing ninja package raises the actionable message, not ImportError."""
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": None}),
pytest.raises(EsphomeError, match="ninja not found"),
):
ninja_helper.find_ninja()
def test_find_ninja_missing_everywhere(tmp_path: Path) -> None:
wheel = MagicMock(BIN_DIR=str(tmp_path))
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": wheel}),
pytest.raises(EsphomeError, match="ninja not found"),
):
ninja_helper.find_ninja()
+5 -45
View File
@@ -5,8 +5,7 @@ from __future__ import annotations
import os
from pathlib import Path
import subprocess
import sys
from unittest.mock import MagicMock, patch
from unittest.mock import patch
import pytest
@@ -36,55 +35,16 @@ def test_tools_path_default_and_prefix(tmp_path: Path) -> None:
assert path != Path.cwd()
def test_find_ninja_prefers_path(tmp_path: Path) -> None:
with patch("shutil.which", return_value=str(tmp_path / "ninja")):
assert framework._find_ninja() == tmp_path / "ninja"
def test_find_ninja_falls_back_to_wheel(tmp_path: Path) -> None:
"""Without a PATH entry, the ninja PyPI wheel's binary is used."""
binary_name = "ninja.exe" if os.name == "nt" else "ninja"
(tmp_path / binary_name).touch()
wheel = MagicMock(BIN_DIR=str(tmp_path))
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": wheel}),
):
assert framework._find_ninja() == tmp_path / binary_name
def test_find_ninja_package_not_installed() -> None:
"""A missing ninja package raises the actionable message, not ImportError."""
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": None}),
pytest.raises(EsphomeError, match="ninja not found"),
):
framework._find_ninja()
def test_find_ninja_missing_everywhere(tmp_path: Path) -> None:
wheel = MagicMock(BIN_DIR=str(tmp_path))
with (
patch("shutil.which", return_value=None),
patch.dict(sys.modules, {"ninja": wheel}),
pytest.raises(EsphomeError, match="ninja not found"),
):
framework._find_ninja()
def test_check_and_install_returns_paths(tmp_path: Path) -> None:
with (
patch.dict(os.environ, {"ESPHOME_ARDUINO8266_PREFIX": str(tmp_path)}),
patch.object(framework, "install_package") as mock_install,
patch.object(framework, "_find_ninja", return_value=tmp_path / "ninja"),
patch.object(framework, "find_ninja", return_value=tmp_path / "ninja"),
):
paths = framework.check_and_install(cv.Version(3, 1, 2))
assert paths["framework_path"] == tmp_path / "frameworks" / "3.30102.0"
assert (
paths["toolchain_path"] == tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION
)
assert paths["ninja_path"] == tmp_path / "ninja"
assert paths.framework == tmp_path / "frameworks" / "3.30102.0"
assert paths.toolchain == tmp_path / "toolchains" / framework.TOOLCHAIN_VERSION
assert paths.ninja == tmp_path / "ninja"
assert mock_install.call_count == 2
# The layout checks cover the directories write_project needs, including
# the bundled libraries/ tree
+5 -3
View File
@@ -214,9 +214,11 @@ def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None:
cache_key="arduino8266",
)
mock_extra.assert_called_once_with(
converted, board_mcu="esp8266", pio_platform="espressif8266"
)
mock_extra.assert_called_once()
assert mock_extra.call_args.args == (converted,)
assert mock_extra.call_args.kwargs["pio_platform"] == "espressif8266"
# board_mcu is passed lazily, as the shared helper requires
assert mock_extra.call_args.kwargs["board_mcu"]() == "esp8266"
assert [lib.name for lib in libs] == [
"Wire",
"esp32async__ESPAsyncWebServer",
+2 -1
View File
@@ -1400,8 +1400,9 @@ def _ccache_patches(tmp_path: Path, which: str | None, build_path: Path | None):
"esphome.espidf.framework.get_idf_tools_path",
return_value=tmp_path / "tools",
),
# ccache_defaults_env (framework_helpers) reads CORE at call time
patch(
"esphome.espidf.framework.CORE",
"esphome.core.CORE",
SimpleNamespace(build_path=build_path),
),
)