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

This commit is contained in:
J. Nick Koston
2026-08-21 20:49:32 -05:00
5 changed files with 121 additions and 104 deletions
+37 -65
View File
@@ -69,19 +69,15 @@ 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)
return _resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
def test_build_config_defaults() -> None:
_set_flags()
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve()
assert config.nonosdk == "NONOSDK22x_190703"
assert config.lwip_lib == "lwip2-536-feat"
assert not config.exceptions
@@ -99,10 +95,7 @@ def test_build_config_esphome_lwip_knob() -> None:
"""The lwIP variant ESPHome selects maps to the same defines and library
as the PlatformIO builder."""
_set_flags("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve("-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH")
assert config.lwip_lib == "lwip2-1460"
assert "TCP_MSS=1460" in config.knob_defines
assert "LWIP_FEATURES=0" in config.knob_defines
@@ -128,9 +121,8 @@ def test_build_config_knobs() -> None:
def test_build_config_mmu_custom_requires_sizes() -> None:
_set_flags("-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM")
with pytest.raises(EsphomeError, match="MMU_IRAM_SIZE"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM")
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM",
@@ -222,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()
@@ -300,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]
@@ -345,10 +336,7 @@ def test_build_config_lwip_variants(
) -> None:
"""Every lwIP knob maps to the same defines and library as the PIO builder."""
_set_flags(f"-D{knob}")
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve(f"-D{knob}")
assert config.lwip_lib == lib
assert f"TCP_MSS={mss}" in config.knob_defines
assert f"LWIP_FEATURES={features}" in config.knob_defines
@@ -394,10 +382,7 @@ def test_build_config_mmu_variants(knob: str, expected: list[str]) -> None:
def test_build_config_waveform_locked_phase() -> None:
_set_flags("-DPIO_FRAMEWORK_ARDUINO_WAVEFORM_LOCKED_PHASE", "-DFP_IN_IROM")
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve("-DPIO_FRAMEWORK_ARDUINO_WAVEFORM_LOCKED_PHASE", "-DFP_IN_IROM")
assert "WAVEFORM_LOCKED_PHASE=1" in config.knob_defines
assert config.fp_in_irom
@@ -554,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:
@@ -763,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:
@@ -781,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:
@@ -796,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:
@@ -804,9 +785,8 @@ def test_build_config_custom_mmu_without_knob_raises() -> None:
layout the linker script does not implement; refuse instead of warning
(PlatformIO warns, but its defaults win the compile line; ours would
not)."""
_set_flags("-DMMU_IRAM_SIZE=0xC000")
with pytest.raises(EsphomeError, match="PIO_FRAMEWORK_ARDUINO_MMU_CUSTOM"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DMMU_IRAM_SIZE=0xC000")
def test_flag_defines_lexes_quoted_single_tokens() -> None:
@@ -972,15 +952,13 @@ def test_flag_defines_respects_unflags() -> None:
def test_vtables_unknown_raises() -> None:
"""A typo'd knob would win the sorted pick and die in the SDK header's
#error; fail by name at generation instead."""
_set_flags("-DVTABLES_IN_BANANA")
with pytest.raises(EsphomeError, match="Unknown VTABLES_IN_.*BANANA"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DVTABLES_IN_BANANA")
def test_vtables_conflicting_raises() -> None:
_set_flags("-DVTABLES_IN_DRAM", "-DVTABLES_IN_IRAM")
with pytest.raises(EsphomeError, match="Conflicting VTABLES_IN_"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DVTABLES_IN_DRAM", "-DVTABLES_IN_IRAM")
def test_project_flags_empty_lib_flags_warn(
@@ -1023,26 +1001,21 @@ def test_generate_ld_scripts_surfaces_preprocessor_warnings(
def test_build_config_mmu_knob_with_raw_mmu_flag_raises() -> None:
"""A variant knob plus a raw MMU_* define would split the compile line
from the linker script; refuse like the no-knob case."""
_set_flags("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48", "-DMMU_IRAM_SIZE=0x4000")
with pytest.raises(EsphomeError, match="MMU_IRAM_SIZE conflict with .*CACHE16"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48", "-DMMU_IRAM_SIZE=0x4000")
def test_build_config_raw_lwip_define_raises() -> None:
"""TCP_MSS/LWIP_* belong to the lwIP knobs: a raw value would win the
compile line while the prebuilt library stays the knob's."""
_set_flags("-DTCP_MSS=1024")
with pytest.raises(EsphomeError, match="TCP_MSS are set by the .*LWIP2"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve("-DTCP_MSS=1024")
def test_build_config_mmu_defines_do_not_alias_the_table() -> None:
"""The resolved list must be a copy; mutating it must not corrupt the
module table for later builds in the same process."""
_set_flags("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48")
config = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
)
config = _resolve("-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48")
config.mmu_defines.append("MMU_BOGUS")
again = _resolve_build_config(
_flag_defines(set(), arduino8266._lexed_build_flags())
@@ -1155,14 +1128,13 @@ def test_write_project_lexes_build_flags_once(
def test_build_config_mmu_conflict_names_the_variant_knob_with_custom() -> None:
"""With MMU_CUSTOM also set, the actionable fix is dropping the variant
knob, not setting the knob the user already set."""
_set_flags(
"-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48",
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM",
"-DMMU_IRAM_SIZE=0xC000",
"-DMMU_ICACHE_SIZE=0x4000",
)
with pytest.raises(EsphomeError, match="drop PIO_FRAMEWORK_ARDUINO_MMU_CACHE16"):
_resolve_build_config(_flag_defines(set(), arduino8266._lexed_build_flags()))
_resolve(
"-DPIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48",
"-DPIO_FRAMEWORK_ARDUINO_MMU_CUSTOM",
"-DMMU_IRAM_SIZE=0xC000",
"-DMMU_ICACHE_SIZE=0x4000",
)
def test_generate_ld_scripts_testing_surgery_failure_is_named(
@@ -152,3 +152,21 @@ def test_get_build_env_without_path_has_no_empty_entry(tmp_path: Path) -> None:
):
env = framework.get_build_env(tmp_path)
assert env["PATH"].split(os.pathsep) == [str(tmp_path / "bin"), "/usr/bin", "/bin"]
def test_ccache_env_accepts_a_preresolved_path() -> None:
"""A caller that already resolved ccache threads it through; the probe
must not run again (None means resolved-and-disabled)."""
with patch.object(framework, "ccache_path") as mock_resolve:
assert framework.ccache_env(None) == {}
env = framework.ccache_env("/usr/bin/ccache")
mock_resolve.assert_not_called()
assert env["CCACHE_DIR"].endswith("ccache")
def test_toolchain_tool_layout(tmp_path: Path) -> None:
"""One owner for the bin/xtensa-lx106-elf-<name> layout."""
tool = framework.toolchain_tool(tmp_path, "addr2line")
assert tool.parent == tmp_path / "bin"
assert tool.name.startswith("xtensa-lx106-elf-addr2line")
assert (tool.suffix == ".exe") is (os.name == "nt")
+3 -1
View File
@@ -774,7 +774,9 @@ def test_dict_shorthand_dependency_skips_registry_through_real_converter(
(local_lib / "library.json").write_text(
'{"name": "LocalLib", "version": "1.0.0", "dependencies": {"Wire": "*"}}'
)
_add_library(f"file://{local_lib}", None)
# as_uri() forms a valid file:// URL on every platform (file:///C:/...
# on Windows; a bare f-string would embed backslashes)
_add_library(local_lib.as_uri(), None)
# The real converter writes its component cache under the config dir
CORE.config_path = tmp_path / "test.yaml"
CORE.config_path.write_text("")