mirror of
https://github.com/esphome/esphome.git
synced 2026-10-02 09:20:22 +00:00
Merge branch 'esp8266-native-shared-helpers' into esp8266-arduino-toolchain
This commit is contained in:
@@ -17,8 +17,8 @@ from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
from esphome.core import CORE, EsphomeError, Library
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_INCLUDE_DIR,
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
|
||||
@@ -163,7 +163,7 @@ def _print_size_summary(build_dir: Path) -> None:
|
||||
The exact shape (including the bar) is parsed by
|
||||
``script/ci_memory_impact_extract.py``; ``format_bar`` matches it.
|
||||
"""
|
||||
from esphome.espidf.size_summary import format_bar
|
||||
from esphome.build_helpers.size_summary import format_bar
|
||||
|
||||
size_tool = _toolchain_tool("size")
|
||||
result = subprocess.run(
|
||||
@@ -213,7 +213,7 @@ def get_idedata() -> dict | None:
|
||||
Same contract as ``espidf.toolchain.get_idedata``: the fields IDE
|
||||
integrations, clang-tidy, and the memory analyzer expect.
|
||||
"""
|
||||
from esphome.espidf.idedata import load_or_build_idedata
|
||||
from esphome.build_helpers.idedata import load_or_build_idedata
|
||||
|
||||
ccache = framework.ccache_path()
|
||||
return load_or_build_idedata(
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
|
||||
@@ -0,0 +1,15 @@
|
||||
"""The PlatformIO-format size bar shared by the native toolchains."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def format_bar(used: int, total: int) -> str:
|
||||
"""Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly."""
|
||||
pct_raw = used / total if total else 0
|
||||
blocks = 10
|
||||
filled = min(int(round(blocks * pct_raw)), blocks)
|
||||
progress = "=" * filled
|
||||
return (
|
||||
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
|
||||
f"(used {used:d} bytes from {total:d} bytes)"
|
||||
)
|
||||
@@ -415,7 +415,7 @@ def _idedata_from_tidy_project(compile_commands: Path) -> dict:
|
||||
"""
|
||||
import json
|
||||
|
||||
from esphome.espidf.idedata import _get_toolchain_includes, _parse_entry
|
||||
from esphome.build_helpers.idedata import _get_toolchain_includes, _parse_entry
|
||||
|
||||
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
|
||||
entry = next((e for e in entries if e["file"].endswith("tidy.cpp")), None)
|
||||
|
||||
@@ -43,8 +43,8 @@ def _idf_framework() -> str:
|
||||
|
||||
|
||||
def _apply_extra_script(component: IDFComponent) -> None:
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
apply_extra_script(component, lambda: variant_to_idf_target(get_esp32_variant()))
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.build_helpers.size_summary import format_bar
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_SIZE_SUFFIXES = {"K": 1024, "M": 1024 * 1024}
|
||||
|
||||
@@ -67,18 +69,6 @@ def _find_app_partition_size(partitions_csv: Path) -> int:
|
||||
raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}")
|
||||
|
||||
|
||||
def format_bar(used: int, total: int) -> str:
|
||||
"""Match PlatformIO's ``_format_availale_bytes`` (pioupload.py) exactly."""
|
||||
pct_raw = used / total if total else 0
|
||||
blocks = 10
|
||||
filled = min(int(round(blocks * pct_raw)), blocks)
|
||||
progress = "=" * filled
|
||||
return (
|
||||
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
|
||||
f"(used {used:d} bytes from {total:d} bytes)"
|
||||
)
|
||||
|
||||
|
||||
def print_summary(size_json: Path, partitions_csv: Path | None) -> None:
|
||||
"""Print PlatformIO-shaped RAM and Flash one-liners.
|
||||
|
||||
|
||||
@@ -526,7 +526,7 @@ def get_idedata() -> dict | None:
|
||||
idedata fields IDE integrations and clang-tidy expect, cached alongside the
|
||||
PlatformIO idedata path. Returns None if the compile DB doesn't exist yet.
|
||||
"""
|
||||
from esphome.espidf.idedata import load_or_build_idedata
|
||||
from esphome.build_helpers.idedata import load_or_build_idedata
|
||||
|
||||
# No launcher: CMake excludes CMAKE_<LANG>_COMPILER_LAUNCHER (ccache)
|
||||
# from the exported compile database, unlike ninja's compdb dump.
|
||||
|
||||
@@ -542,7 +542,7 @@ def _esp32_platformio_path_or_file_trigger(files: list[str]) -> bool:
|
||||
# components, so the component matrix wouldn't otherwise force any esp32
|
||||
# compile. When they change we fold the `esp32` component into the matrix so
|
||||
# the default native-IDF build path is still compiled on an infra-only PR.
|
||||
ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/",)
|
||||
ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/", "esphome/build_helpers/")
|
||||
ESP_IDF_INFRA_TRIGGER_FILES = frozenset({"esphome/build_gen/espidf.py"})
|
||||
|
||||
|
||||
@@ -637,7 +637,10 @@ ESP8266_NATIVE_TEST_COMPONENTS = frozenset(
|
||||
# Infrastructure whose changes always trigger the native ESP8266 compile
|
||||
# test. esphome/espidf/ is included because the backend shares its idedata,
|
||||
# extra-script, and size-summary helpers.
|
||||
ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = ("esphome/arduino8266/", "esphome/espidf/")
|
||||
ESP8266_NATIVE_TRIGGER_PATH_PREFIXES = (
|
||||
"esphome/arduino8266/",
|
||||
"esphome/build_helpers/",
|
||||
)
|
||||
ESP8266_NATIVE_TRIGGER_FILES = frozenset(
|
||||
{
|
||||
"esphome/build_gen/arduino8266.py",
|
||||
|
||||
@@ -3081,7 +3081,7 @@ def test_esp8266_native_components_full_list_on_infra_change() -> None:
|
||||
["esphome/build_gen/arduino8266.py"],
|
||||
["esphome/components/esp8266/build_surgery.py"],
|
||||
# Shared modules the native build depends on
|
||||
["esphome/espidf/idedata.py"],
|
||||
["esphome/build_helpers/idedata.py"],
|
||||
["esphome/platformio/library.py"],
|
||||
# Top-level esphome/*.py modules the backend imports directly
|
||||
["esphome/framework_helpers.py"],
|
||||
@@ -3116,9 +3116,10 @@ def test_esp8266_native_components_full_list_on_infra_change() -> None:
|
||||
["wifi", "network"],
|
||||
[],
|
||||
),
|
||||
# The espidf build generator is not an esp8266-native trigger
|
||||
# (only the shared esphome/espidf/ package is).
|
||||
# espidf infrastructure is not an esp8266-native trigger; the
|
||||
# native backend depends on esphome/build_helpers/ instead.
|
||||
(["esphome/build_gen/espidf.py"], [], []),
|
||||
(["esphome/espidf/toolchain.py"], [], []),
|
||||
(["README.md"], [], []),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from esphome.analyze_memory.toolchain import (
|
||||
find_idedata_path,
|
||||
idedata_candidates,
|
||||
)
|
||||
from esphome.espidf.idedata import _cc_path_from_cxx
|
||||
from esphome.build_helpers.idedata import _cc_path_from_cxx
|
||||
from esphome.platformio.toolchain import IDEData
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"""Tests for the shared extraScript machinery (build_helpers.extra_script)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.platformio.library import ConvertedLibrary as IDFComponent, URLSource
|
||||
|
||||
|
||||
def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
|
||||
from esphome.build_helpers.extra_script import (
|
||||
captured_as_build_flags,
|
||||
run_extra_script,
|
||||
)
|
||||
|
||||
(tmp_path / "src" / "esp32").mkdir(parents=True)
|
||||
script = tmp_path / "extra_script.py"
|
||||
script.write_text(
|
||||
"Import('env')\n"
|
||||
"mcu = env.get('BOARD_MCU')\n"
|
||||
"env.Append(\n"
|
||||
" LIBPATH=[join('src', mcu)],\n"
|
||||
" LIBS=['algobsec'],\n"
|
||||
" CPPDEFINES=['FOO', ('BAR', '1')],\n"
|
||||
" LINKFLAGS=['-Wl,--gc-sections'],\n"
|
||||
")\n"
|
||||
)
|
||||
# The script uses bare ``join`` (PIO's extra-scripts run inside SCons
|
||||
# where this is in scope). Inject it via the script header so the
|
||||
# shim's exec namespace can resolve it.
|
||||
script.write_text("from os.path import join\n" + script.read_text())
|
||||
|
||||
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
|
||||
|
||||
assert result.libpath == [str(Path("src") / "esp32")]
|
||||
assert result.libs == ["algobsec"]
|
||||
assert ("BAR", "1") in result.cppdefines
|
||||
assert "FOO" in result.cppdefines
|
||||
assert result.linkflags == ["-Wl,--gc-sections"]
|
||||
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
sep = os.sep
|
||||
assert f"-Lsrc{sep}esp32" in flags
|
||||
assert "-lalgobsec" in flags
|
||||
assert "-DFOO" in flags
|
||||
assert "-DBAR=1" in flags
|
||||
assert "-Wl,--gc-sections" in flags
|
||||
|
||||
|
||||
def test_extra_script_libpath_relative_resolves_against_library_dir(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Relative LIBPATH entries must resolve against ``library_dir``, not the
|
||||
caller's CWD (the shim restores CWD before ``captured_as_build_flags``
|
||||
runs)."""
|
||||
from esphome.build_helpers.extra_script import (
|
||||
ExtraScriptResult,
|
||||
captured_as_build_flags,
|
||||
)
|
||||
|
||||
(tmp_path / "lib" / "esp32").mkdir(parents=True)
|
||||
elsewhere = tmp_path.parent / "not_the_library_dir"
|
||||
elsewhere.mkdir(exist_ok=True)
|
||||
monkeypatch.chdir(elsewhere)
|
||||
|
||||
result = ExtraScriptResult(libpath=["lib/esp32"])
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
|
||||
sep = os.sep
|
||||
assert flags == [f"-Llib{sep}esp32"]
|
||||
|
||||
|
||||
def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
|
||||
from esphome.build_helpers.extra_script import (
|
||||
ExtraScriptResult,
|
||||
captured_as_build_flags,
|
||||
)
|
||||
|
||||
outside = tmp_path.parent / "system_lib"
|
||||
outside.mkdir(exist_ok=True)
|
||||
result = ExtraScriptResult(libpath=[str(outside)])
|
||||
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
assert flags == [f"-L{outside.resolve()}"]
|
||||
|
||||
|
||||
def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
|
||||
from esphome.build_helpers.extra_script import run_extra_script
|
||||
|
||||
script = tmp_path / "broken.py"
|
||||
script.write_text("raise RuntimeError('boom')\n")
|
||||
|
||||
with caplog.at_level("WARNING"):
|
||||
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
|
||||
|
||||
assert result.libpath == []
|
||||
assert result.libs == []
|
||||
assert "broken.py" in caplog.text
|
||||
|
||||
|
||||
def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
|
||||
library_dir = tmp_path / "lib"
|
||||
library_dir.mkdir()
|
||||
outside = tmp_path / "evil.py"
|
||||
outside.write_text("env.Append(LIBS=['pwned'])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = library_dir
|
||||
c.data = {"build": {"extraScript": "../evil.py"}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
|
||||
# Nothing was folded into flags: the traversal was rejected before
|
||||
# the script could run.
|
||||
assert "flags" not in c.data["build"]
|
||||
|
||||
|
||||
def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch):
|
||||
from esphome.components import esp32 as esp32_module
|
||||
|
||||
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
|
||||
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=['algobsec'])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
|
||||
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 idf_target lazily and normalizes
|
||||
a string ``build.flags`` value into a list before extending it."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=[env.get('BOARD_MCU')])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": "-DBASE=1"}}
|
||||
|
||||
apply_extra_script(c, lambda: "esp8266")
|
||||
|
||||
assert c.data["build"]["flags"] == ["-DBASE=1", "-lesp8266"]
|
||||
|
||||
|
||||
def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None:
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
# No extraScript declared: nothing happens, the target is never resolved
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {}}
|
||||
apply_extra_script(c, lambda: pytest.fail("target resolved without a script"))
|
||||
|
||||
# A script that captures nothing leaves the flags untouched
|
||||
script = tmp_path / "noop.py"
|
||||
script.write_text("pass\n")
|
||||
c.data = {"build": {"extraScript": "noop.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "flags" not in c.data["build"]
|
||||
|
||||
|
||||
def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None:
|
||||
"""Un-captured env vars and unsupported env methods are silent no-ops."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text(
|
||||
"env.Replace(CC='clang')\nenv.Append(UNCAPTURED=['x'], LIBS='single')\n"
|
||||
)
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert c.data["build"]["flags"] == ["-lsingle"]
|
||||
|
||||
|
||||
def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None:
|
||||
"""A raising extra-script is best-effort: logged and skipped."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("raise RuntimeError('boom')\n")
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "flags" not in c.data["build"]
|
||||
assert "skipping" in caplog.text
|
||||
|
||||
|
||||
def test_apply_extra_script_pio_platform(tmp_path) -> None:
|
||||
"""The backend's platform token is exposed to the script as PIOPLATFORM."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n")
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266", pio_platform="espressif8266")
|
||||
assert c.data["build"]["flags"] == ["-lespressif8266"]
|
||||
|
||||
|
||||
def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None:
|
||||
"""A declared but absent extraScript is skipped with a visible warning:
|
||||
its captured link flags are lost."""
|
||||
from esphome.build_helpers.extra_script import apply_extra_script
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "nope.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "not found" in caplog.text
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Tests for esphome.espidf.idedata (compile_commands.json -> idedata)."""
|
||||
"""Tests for esphome.build_helpers.idedata (compile_commands.json -> idedata)."""
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.espidf import idedata
|
||||
from esphome.build_helpers import idedata
|
||||
|
||||
# An absolute, forward-slash (shlex-safe) path prefix valid on the host OS, so
|
||||
# tests exercise the same is-absolute / normalize behavior as a real compile DB
|
||||
@@ -203,7 +203,7 @@ def test_print_size_summary_size_tool_failure(
|
||||
def test_get_idedata_delegates(tmp_path: Path) -> None:
|
||||
with (
|
||||
patch(
|
||||
"esphome.espidf.idedata.load_or_build_idedata",
|
||||
"esphome.build_helpers.idedata.load_or_build_idedata",
|
||||
return_value={"cc_path": "x"},
|
||||
) as mock_load,
|
||||
patch.object(framework, "ccache_path", return_value=Path("/cc/ccache")),
|
||||
@@ -220,7 +220,7 @@ def test_get_idedata_delegates(tmp_path: Path) -> None:
|
||||
def test_get_idedata_no_ccache(tmp_path: Path) -> None:
|
||||
with (
|
||||
patch(
|
||||
"esphome.espidf.idedata.load_or_build_idedata", return_value={}
|
||||
"esphome.build_helpers.idedata.load_or_build_idedata", return_value={}
|
||||
) as mock_load,
|
||||
patch.object(framework, "ccache_path", return_value=None),
|
||||
):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
@@ -369,128 +368,6 @@ def test_generate_idf_component_yml_missing_path_raises(tmp_component):
|
||||
generate_idf_component_yml(tmp_component)
|
||||
|
||||
|
||||
def test_extra_script_captures_libpath_libs_and_defines(tmp_path):
|
||||
from esphome.espidf.extra_script import captured_as_build_flags, run_extra_script
|
||||
|
||||
(tmp_path / "src" / "esp32").mkdir(parents=True)
|
||||
script = tmp_path / "extra_script.py"
|
||||
script.write_text(
|
||||
"Import('env')\n"
|
||||
"mcu = env.get('BOARD_MCU')\n"
|
||||
"env.Append(\n"
|
||||
" LIBPATH=[join('src', mcu)],\n"
|
||||
" LIBS=['algobsec'],\n"
|
||||
" CPPDEFINES=['FOO', ('BAR', '1')],\n"
|
||||
" LINKFLAGS=['-Wl,--gc-sections'],\n"
|
||||
")\n"
|
||||
)
|
||||
# The script uses bare ``join`` (PIO's extra-scripts run inside SCons
|
||||
# where this is in scope). Inject it via the script header so the
|
||||
# shim's exec namespace can resolve it.
|
||||
script.write_text("from os.path import join\n" + script.read_text())
|
||||
|
||||
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
|
||||
|
||||
assert result.libpath == [str(Path("src") / "esp32")]
|
||||
assert result.libs == ["algobsec"]
|
||||
assert ("BAR", "1") in result.cppdefines
|
||||
assert "FOO" in result.cppdefines
|
||||
assert result.linkflags == ["-Wl,--gc-sections"]
|
||||
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
sep = os.sep
|
||||
assert f"-Lsrc{sep}esp32" in flags
|
||||
assert "-lalgobsec" in flags
|
||||
assert "-DFOO" in flags
|
||||
assert "-DBAR=1" in flags
|
||||
assert "-Wl,--gc-sections" in flags
|
||||
|
||||
|
||||
def test_extra_script_libpath_relative_resolves_against_library_dir(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Relative LIBPATH entries must resolve against ``library_dir``, not the
|
||||
caller's CWD (the shim restores CWD before ``captured_as_build_flags``
|
||||
runs)."""
|
||||
from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags
|
||||
|
||||
(tmp_path / "lib" / "esp32").mkdir(parents=True)
|
||||
elsewhere = tmp_path.parent / "not_the_library_dir"
|
||||
elsewhere.mkdir(exist_ok=True)
|
||||
monkeypatch.chdir(elsewhere)
|
||||
|
||||
result = ExtraScriptResult(libpath=["lib/esp32"])
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
|
||||
sep = os.sep
|
||||
assert flags == [f"-Llib{sep}esp32"]
|
||||
|
||||
|
||||
def test_extra_script_libpath_absolute_outside_library_dir(tmp_path):
|
||||
from esphome.espidf.extra_script import ExtraScriptResult, captured_as_build_flags
|
||||
|
||||
outside = tmp_path.parent / "system_lib"
|
||||
outside.mkdir(exist_ok=True)
|
||||
result = ExtraScriptResult(libpath=[str(outside)])
|
||||
|
||||
flags = captured_as_build_flags(result, library_dir=tmp_path)
|
||||
assert flags == [f"-L{outside.resolve()}"]
|
||||
|
||||
|
||||
def test_extra_script_failure_returns_empty_result(tmp_path, caplog):
|
||||
from esphome.espidf.extra_script import run_extra_script
|
||||
|
||||
script = tmp_path / "broken.py"
|
||||
script.write_text("raise RuntimeError('boom')\n")
|
||||
|
||||
with caplog.at_level("WARNING"):
|
||||
result = run_extra_script(script, library_dir=tmp_path, idf_target="esp32")
|
||||
|
||||
assert result.libpath == []
|
||||
assert result.libs == []
|
||||
assert "broken.py" in caplog.text
|
||||
|
||||
|
||||
def test_apply_extra_script_path_traversal_is_rejected(tmp_path):
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
|
||||
library_dir = tmp_path / "lib"
|
||||
library_dir.mkdir()
|
||||
outside = tmp_path / "evil.py"
|
||||
outside.write_text("env.Append(LIBS=['pwned'])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = library_dir
|
||||
c.data = {"build": {"extraScript": "../evil.py"}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
|
||||
# Nothing was folded into flags: the traversal was rejected before
|
||||
# the script could run.
|
||||
assert "flags" not in c.data["build"]
|
||||
|
||||
|
||||
def test_apply_extra_script_merges_into_existing_flags(tmp_path, monkeypatch):
|
||||
from esphome.components import esp32 as esp32_module
|
||||
|
||||
monkeypatch.setattr(esp32_module, "get_esp32_variant", lambda: "ESP32")
|
||||
|
||||
from esphome.espidf.component import _apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=['algobsec'])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": ["-DEXISTING"]}}
|
||||
|
||||
_apply_extra_script(c)
|
||||
|
||||
assert "-DEXISTING" in c.data["build"]["flags"]
|
||||
assert "-lalgobsec" in c.data["build"]["flags"]
|
||||
|
||||
|
||||
def test_parse_library_json(tmp_path):
|
||||
f = tmp_path / "library.json"
|
||||
f.write_text(json.dumps({"name": "test"}))
|
||||
@@ -1190,92 +1067,3 @@ def test_idf_component_download_passes_salt() -> None:
|
||||
"owner/name", force=True, salt="abcd1234", namespace="idf"
|
||||
)
|
||||
assert c.path == Path("/converted/owner/name")
|
||||
|
||||
|
||||
def test_apply_extra_script_callable_target_and_str_flags(tmp_path) -> None:
|
||||
"""The shared helper resolves a callable idf_target lazily and normalizes
|
||||
a string ``build.flags`` value into a list before extending it."""
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
(tmp_path / "src").mkdir()
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=[env.get('BOARD_MCU')])\n")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py", "flags": "-DBASE=1"}}
|
||||
|
||||
apply_extra_script(c, lambda: "esp8266")
|
||||
|
||||
assert c.data["build"]["flags"] == ["-DBASE=1", "-lesp8266"]
|
||||
|
||||
|
||||
def test_apply_extra_script_no_script_and_no_flags(tmp_path) -> None:
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
# No extraScript declared: nothing happens, the target is never resolved
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {}}
|
||||
apply_extra_script(c, lambda: pytest.fail("target resolved without a script"))
|
||||
|
||||
# A script that captures nothing leaves the flags untouched
|
||||
script = tmp_path / "noop.py"
|
||||
script.write_text("pass\n")
|
||||
c.data = {"build": {"extraScript": "noop.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "flags" not in c.data["build"]
|
||||
|
||||
|
||||
def test_apply_extra_script_ignores_uncaptured_env_calls(tmp_path) -> None:
|
||||
"""Un-captured env vars and unsupported env methods are silent no-ops."""
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text(
|
||||
"env.Replace(CC='clang')\nenv.Append(UNCAPTURED=['x'], LIBS='single')\n"
|
||||
)
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert c.data["build"]["flags"] == ["-lsingle"]
|
||||
|
||||
|
||||
def test_apply_extra_script_swallows_script_errors(tmp_path, caplog) -> None:
|
||||
"""A raising extra-script is best-effort: logged and skipped."""
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("raise RuntimeError('boom')\n")
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "flags" not in c.data["build"]
|
||||
assert "skipping" in caplog.text
|
||||
|
||||
|
||||
def test_apply_extra_script_pio_platform(tmp_path) -> None:
|
||||
"""The backend's platform token is exposed to the script as PIOPLATFORM."""
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
script = tmp_path / "extra.py"
|
||||
script.write_text("env.Append(LIBS=[env.get('PIOPLATFORM')])\n")
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "extra.py"}}
|
||||
apply_extra_script(c, "esp8266", pio_platform="espressif8266")
|
||||
assert c.data["build"]["flags"] == ["-lespressif8266"]
|
||||
|
||||
|
||||
def test_apply_extra_script_missing_script_logged(tmp_path, caplog) -> None:
|
||||
"""A declared but absent extraScript is skipped with a visible warning:
|
||||
its captured link flags are lost."""
|
||||
from esphome.espidf.extra_script import apply_extra_script
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=URLSource("http://dummy"))
|
||||
c.path = tmp_path
|
||||
c.data = {"build": {"extraScript": "nope.py"}}
|
||||
apply_extra_script(c, "esp8266")
|
||||
assert "not found" in caplog.text
|
||||
|
||||
@@ -140,7 +140,7 @@ def test_get_idedata_generates_and_caches(setup_core: Path) -> None:
|
||||
compile_commands.write_text("[]")
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "g++"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
@@ -161,7 +161,7 @@ def test_get_idedata_uses_cache_when_valid(setup_core: Path) -> None:
|
||||
cc_mtime = compile_commands.stat().st_mtime
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch("esphome.espidf.idedata.idedata_from_build") as mock_transform:
|
||||
with patch("esphome.build_helpers.idedata.idedata_from_build") as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
mock_transform.assert_not_called()
|
||||
@@ -183,7 +183,7 @@ def test_get_idedata_regenerates_cache_without_cc_path(setup_core: Path) -> None
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cc_path": "gcc", "cxx_path": "g++"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
@@ -203,7 +203,7 @@ def test_get_idedata_regenerates_when_compile_commands_newer(setup_core: Path) -
|
||||
os.utime(compile_commands, (cache_mtime + 1, cache_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "fresh"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
@@ -230,7 +230,7 @@ def test_get_idedata_regenerates_on_non_dict_cache(
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cc_path": "gcc", "cxx_path": "g++"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
@@ -250,7 +250,7 @@ def test_get_idedata_regenerates_on_corrupted_cache(setup_core: Path) -> None:
|
||||
os.utime(cache, (cc_mtime + 1, cc_mtime + 1))
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "regen"},
|
||||
) as mock_transform:
|
||||
result = toolchain.get_idedata()
|
||||
@@ -267,7 +267,7 @@ def test_get_idedata_prog_path_points_at_firmware_elf(setup_core: Path) -> None:
|
||||
compile_commands.write_text("[]")
|
||||
|
||||
with patch(
|
||||
"esphome.espidf.idedata.idedata_from_build",
|
||||
"esphome.build_helpers.idedata.idedata_from_build",
|
||||
return_value={"cxx_path": "g++"},
|
||||
):
|
||||
result = toolchain.get_idedata()
|
||||
|
||||
@@ -544,3 +544,12 @@ def test_split_flag_entry_unbalanced_quote_is_clean() -> None:
|
||||
assert join_flag_args(["-D", "FOO=1", "-Os"], "x") == ["-DFOO=1", "-Os"]
|
||||
with pytest.raises(EsphomeError, match=r"Malformed build flag.*library x"):
|
||||
split_flag_entry('-DX="unclosed', "library x")
|
||||
|
||||
|
||||
def test_join_flag_args_trailing_bare_flag_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
from esphome.platformio.library import join_flag_args
|
||||
|
||||
assert join_flag_args(["-Os", "-l"], "library x") == ["-Os"]
|
||||
assert "Ignoring trailing '-l'" in caplog.text
|
||||
|
||||
@@ -130,7 +130,7 @@ def test_print_summary_handles_no_memory_types(
|
||||
|
||||
def test_format_bar_zero_total() -> None:
|
||||
"""A zero total must not divide by zero."""
|
||||
from esphome.espidf.size_summary import format_bar
|
||||
from esphome.build_helpers.size_summary import format_bar
|
||||
|
||||
assert format_bar(0, 0) == "[ ] 0.0% (used 0 bytes from 0 bytes)"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user