Merge branch 'esp8266-native-toolchain-plumbing' into esp8266-native-framework-installer

This commit is contained in:
J. Nick Koston
2026-08-20 15:55:01 -05:00
6 changed files with 138 additions and 59 deletions
+61 -12
View File
@@ -278,30 +278,39 @@ def test_parse_entry_strips_launcher_prefix() -> None:
)
assert cxx_path == "/tools/xtensa-lx106-elf-g++"
assert defines == ["USE_ESP8266"]
# Without a configured launcher nothing is stripped, even a token that
# happens to be named ccache -- but the surprise is warned about (once
# per path, however many entries the compile DB has)
idedata._warn_not_a_compiler.cache_clear()
cxx_path, _, _, _ = idedata.parse_entry(entry)
assert cxx_path == "/opt/homebrew/bin/ccache"
def test_parse_entry_warns_when_first_token_is_not_a_compiler(
def test_parse_entry_recovers_from_unconfigured_launcher(
caplog: pytest.LogCaptureFixture,
) -> None:
idedata._warn_not_a_compiler.cache_clear()
"""A stale compile DB built with a launcher this run no longer configures
still yields the real compiler (the next token), not the launcher."""
entry = _entry(
f"{ABS}build",
f"{ABS}build/src/esphome/core/application.cpp",
"/opt/homebrew/bin/ccache /tools/xtensa-lx106-elf-g++ -c a.cpp -o a.o",
)
idedata.parse_entry(entry)
assert "does not start with a compiler" in caplog.text
caplog.clear()
idedata.parse_entry(entry, launcher="/opt/homebrew/bin/ccache")
cxx_path, _, _, _ = idedata.parse_entry(entry)
assert cxx_path == "/tools/xtensa-lx106-elf-g++"
assert "does not start with a compiler" not in caplog.text
def test_parse_entry_warns_when_first_token_is_not_a_compiler(
caplog: pytest.LogCaptureFixture,
) -> None:
"""An unrecoverable non-compiler leading token is warned about, once per
path however many entries the compile DB has."""
idedata._warned_not_a_compiler.clear()
entry = _entry(
f"{ABS}build",
f"{ABS}build/src/esphome/core/application.cpp",
"/usr/bin/python3 wrapper.py -c a.cpp -o a.o",
)
idedata.parse_entry(entry)
idedata.parse_entry(entry)
assert caplog.text.count("does not start with a compiler") == 1
def _write_compile_commands(tmp_path: Path) -> Path:
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(
@@ -402,3 +411,43 @@ def test_parse_entry_accepts_versioned_compilers() -> None:
assert idedata._COMPILER_STEM.search(stem)
assert not idedata._COMPILER_STEM.search("ccache")
assert not idedata._COMPILER_STEM.search("distcc")
def test_load_or_build_idedata_corrupted_cache_is_logged(
tmp_path: Path, caplog: pytest.LogCaptureFixture
) -> None:
"""A truncated cache is diagnosable, not a silent slow-build cause."""
compile_commands = _write_compile_commands(tmp_path)
cache = tmp_path / "c.json"
cache.write_text('{"cc_path": trunc')
os.utime(cache, (compile_commands.stat().st_mtime + 5,) * 2)
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
data = idedata.load_or_build_idedata(
compile_commands, tmp_path / "f.elf", cache
)
assert data["cxx_path"] == "/tools/g++"
assert "Discarding unreadable idedata cache" in caplog.text
def test_load_or_build_idedata_never_caches_bad_compiler(tmp_path: Path) -> None:
"""Idedata whose compiler path failed the sanity check is served for this
run but not persisted, so the next build re-parses."""
compile_commands = tmp_path / "compile_commands.json"
compile_commands.write_text(
json.dumps(
[
_entry(
f"{ABS}build",
f"{ABS}build/src/esphome/core/application.cpp",
"/usr/bin/python3 wrapper.py -c app.cpp -o app.cpp.o",
)
]
)
)
cache = tmp_path / "c.json"
with patch.object(idedata, "get_toolchain_includes", return_value=[]):
data = idedata.load_or_build_idedata(
compile_commands, tmp_path / "f.elf", cache
)
assert data["cxx_path"] == "/usr/bin/python3"
assert not cache.exists()
+5 -4
View File
@@ -1069,10 +1069,11 @@ def test_idf_component_download_passes_salt() -> None:
assert c.path == Path("/converted/owner/name")
def test_apply_extra_script_wrapper_wires_esp32_target(tmp_path, monkeypatch):
"""The espidf wrapper resolves the esp32 variant into the shared helper."""
def test_emit_idf_component_wires_esp32_target(tmp_path, monkeypatch):
"""Emitting a component resolves the esp32 variant into the shared
extraScript helper."""
from esphome.components import esp32 as esp32_module
from esphome.espidf.component import _apply_extra_script
from esphome.espidf.component import _emit_idf_component
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
(tmp_path / "src").mkdir()
@@ -1081,5 +1082,5 @@ def test_apply_extra_script_wrapper_wires_esp32_target(tmp_path, monkeypatch):
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py"}}
_apply_extra_script(c)
_emit_idf_component(c)
assert c.data["build"]["flags"] == ["-lesp32"]
@@ -117,7 +117,7 @@ def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
c.path = library_dir
c.data = {"build": {"extraScript": "../evil.py"}}
apply_extra_script(c, board_mcu="esp32", pio_platform="espressif32")
apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32")
# Nothing was folded into flags: the traversal was rejected before
# the script could run.
@@ -135,14 +135,14 @@ def test_apply_extra_script_merges_into_existing_flags(tmp_path):
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}}
apply_extra_script(c, board_mcu="esp32", pio_platform="espressif32")
apply_extra_script(c, board_mcu=lambda: "esp32", pio_platform="espressif32")
assert "-DEXISTING" in c.data["build"]["flags"]
assert "-lalgobsec" in c.data["build"]["flags"]
def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None:
"""The shared helper resolves a callable board_mcu lazily and normalizes
"""The shared helper resolves the board_mcu callable lazily and normalizes
a string ``build.flags`` value into a list before extending it."""
from esphome.platformio.extra_script import apply_extra_script
@@ -176,7 +176,7 @@ def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None:
script = tmp_path / "noop.py"
script.write_text("pass\n")
c.data = {"build": {"extraScript": "noop.py"}}
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
assert "flags" not in c.data["build"]
@@ -191,7 +191,7 @@ def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None:
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py"}}
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
assert c.data["build"]["flags"] == ["-lsingle"]
@@ -204,7 +204,7 @@ def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None:
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py"}}
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
assert "flags" not in c.data["build"]
assert "skipping" in caplog.text
@@ -218,7 +218,7 @@ def test_apply_extra_script_pio_platform(tmp_path) -> None:
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "extra.py"}}
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
assert c.data["build"]["flags"] == ["-lespressif8266"]
@@ -230,5 +230,5 @@ def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None:
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
c.path = tmp_path
c.data = {"build": {"extraScript": "nope.py"}}
apply_extra_script(c, board_mcu="esp8266", pio_platform="espressif8266")
apply_extra_script(c, board_mcu=lambda: "esp8266", pio_platform="espressif8266")
assert "not found" in caplog.text