mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 11:07:33 +00:00
[nrf52] add support for native builds (#16898)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.framework_helpers import get_project_compile_flags, get_project_link_flags
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||
@@ -84,12 +85,7 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
||||
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
||||
# third-party components we don't author.
|
||||
project_compile_opts = [
|
||||
flag
|
||||
for flag in sorted(CORE.build_flags)
|
||||
if flag.startswith("-D")
|
||||
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
|
||||
]
|
||||
project_compile_opts = get_project_compile_flags()
|
||||
extra_compile_options = "\n".join(
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
||||
for flag in project_compile_opts
|
||||
@@ -188,8 +184,8 @@ def get_component_cmakelists() -> str:
|
||||
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
||||
# emitted project-wide via idf_build_set_property in
|
||||
# get_project_cmakelists so they reach every component, not just src/.
|
||||
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
|
||||
link_opts = get_project_link_flags()
|
||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||
|
||||
return f"""\
|
||||
# Auto-generated by ESPHome
|
||||
|
||||
@@ -52,6 +52,11 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
import esphome.final_validate as fv
|
||||
from esphome.framework_helpers import (
|
||||
get_project_compile_flags,
|
||||
get_project_link_flags,
|
||||
run_command_ok,
|
||||
)
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
@@ -63,7 +68,7 @@ from .const import (
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
||||
)
|
||||
from .framework import check_and_install
|
||||
from .framework import check_and_install, get_build_env, get_build_paths
|
||||
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import nrf52_pin_to_code # noqa: F401
|
||||
@@ -99,9 +104,6 @@ FAKE_BOARD_MANIFEST = """
|
||||
|
||||
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
|
||||
zephyr_set_core_data(config)
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||
@@ -112,6 +114,12 @@ def set_core_data(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _resolve_toolchain(config: ConfigType) -> ConfigType:
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO)
|
||||
return config
|
||||
|
||||
|
||||
def set_framework(config: ConfigType) -> ConfigType:
|
||||
if CONF_VERSION not in config[CONF_FRAMEWORK]:
|
||||
default_version = "2.6.1-b" if CORE.using_toolchain_platformio else "2.9.2"
|
||||
@@ -147,6 +155,12 @@ BOOTLOADERS = [
|
||||
]
|
||||
|
||||
|
||||
def _validate_toolchain(value) -> Toolchain:
|
||||
return Toolchain(
|
||||
cv.one_of(Toolchain.PLATFORMIO, Toolchain.SDK_NRF, lower=True)(value)
|
||||
)
|
||||
|
||||
|
||||
def _detect_bootloader(config: ConfigType) -> ConfigType:
|
||||
"""Detect the bootloader for the given board."""
|
||||
config = config.copy()
|
||||
@@ -233,9 +247,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_TOOLCHAIN): _validate_toolchain,
|
||||
cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm),
|
||||
}
|
||||
),
|
||||
_resolve_toolchain,
|
||||
set_framework,
|
||||
)
|
||||
|
||||
@@ -565,6 +581,47 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
||||
return False
|
||||
|
||||
|
||||
def _generate_cmake_lists() -> None:
|
||||
compile_flags = get_project_compile_flags()
|
||||
link_flags = get_project_link_flags()
|
||||
|
||||
lines = [
|
||||
"cmake_minimum_required(VERSION 3.20.0)",
|
||||
"",
|
||||
'set(Zephyr_DIR "$ENV{ZEPHYR_BASE}/share/zephyr-package/cmake/")',
|
||||
"",
|
||||
"find_package(Zephyr REQUIRED)",
|
||||
"",
|
||||
f"project({CORE.name})",
|
||||
"",
|
||||
'file(GLOB_RECURSE APP_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/../src/*.cpp" "${CMAKE_CURRENT_LIST_DIR}/../src/*.c")',
|
||||
"",
|
||||
"target_sources(app PRIVATE ${APP_SOURCES})",
|
||||
'target_include_directories(app PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../src")',
|
||||
]
|
||||
|
||||
if compile_flags:
|
||||
lines += [
|
||||
"",
|
||||
"target_compile_options(app PRIVATE",
|
||||
*[f' "{flag}"' for flag in compile_flags],
|
||||
")",
|
||||
]
|
||||
|
||||
if link_flags:
|
||||
lines += [
|
||||
"",
|
||||
"zephyr_ld_options(",
|
||||
*[f' "{flag}"' for flag in link_flags],
|
||||
")",
|
||||
]
|
||||
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("zephyr", "CMakeLists.txt"),
|
||||
"\n".join(lines) + "\n",
|
||||
)
|
||||
|
||||
|
||||
def run_compile(args, config: ConfigType) -> bool:
|
||||
if CORE.using_toolchain_platformio:
|
||||
return False
|
||||
@@ -574,4 +631,35 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
"Supported toolchains are 'platformio' and 'sdk-nrf'."
|
||||
)
|
||||
check_and_install()
|
||||
raise EsphomeError("Native build for nRF52 is not implemented yet")
|
||||
|
||||
paths = get_build_paths()
|
||||
env = get_build_env()
|
||||
|
||||
_generate_cmake_lists()
|
||||
|
||||
board = zephyr_data()[KEY_BOARD]
|
||||
build_dir = CORE.relative_pioenvs_path(CORE.name)
|
||||
source_dir = CORE.relative_build_path("zephyr")
|
||||
|
||||
west_cmd = [
|
||||
str(paths["python_executable"]),
|
||||
"-m",
|
||||
"west",
|
||||
"build",
|
||||
"--pristine=auto",
|
||||
"-b",
|
||||
board,
|
||||
"-d",
|
||||
str(build_dir),
|
||||
str(source_dir),
|
||||
]
|
||||
|
||||
if not run_command_ok(
|
||||
west_cmd,
|
||||
env=env,
|
||||
stream_output=True,
|
||||
cwd=str(paths["framework_path"]),
|
||||
):
|
||||
raise EsphomeError("nRF52 native build failed")
|
||||
|
||||
return True
|
||||
|
||||
@@ -18,7 +18,7 @@ from esphome.framework_helpers import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_WEST_VERSION = "1.5.0"
|
||||
_REQUIREMENTS = Path(__file__).parent / "requirements.txt"
|
||||
_TOOLCHAIN_VERSION = "0.17.4"
|
||||
|
||||
SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
||||
@@ -28,6 +28,15 @@ SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
||||
)
|
||||
)
|
||||
|
||||
# Minimal SDK provides cmake discovery files (Zephyr-sdkConfig.cmake) and
|
||||
# host tools (dtc etc.) required by the Zephyr cmake build system.
|
||||
SDK_NG_MINIMAL_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get(
|
||||
"ESPHOME_SDK_NG_MINIMAL_MIRRORS",
|
||||
"https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{VERSION}/zephyr-sdk-{VERSION}_{sysname}-{machine}_minimal.{extension}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _get_tools_path() -> Path:
|
||||
return CORE.data_dir / "sdk-nrf"
|
||||
@@ -38,11 +47,11 @@ def _get_python_env_path(version: str) -> Path:
|
||||
|
||||
|
||||
def _get_framework_path(version: str) -> Path:
|
||||
return _get_tools_path() / "frameworks" / f"{version}"
|
||||
return _get_tools_path() / "frameworks" / version
|
||||
|
||||
|
||||
def _get_toolchain_path(version: str) -> Path:
|
||||
return _get_tools_path() / "toolchains" / f"{version}"
|
||||
return _get_tools_path() / "toolchains" / version
|
||||
|
||||
|
||||
# onexc/dir_fd were added to shutil.rmtree in 3.12; the 3.11 branch uses onerror.
|
||||
@@ -95,29 +104,68 @@ def _get_toolchain_platform_info() -> tuple[str, str, str]:
|
||||
return sysname, machine, extension
|
||||
|
||||
|
||||
def check_and_install() -> None:
|
||||
def _get_version_str() -> str:
|
||||
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
version = f"v{framework_ver.major}.{framework_ver.minor}.{framework_ver.patch}"
|
||||
return f"v{framework_ver.major}.{framework_ver.minor}.{framework_ver.patch}"
|
||||
|
||||
|
||||
def get_build_paths() -> dict:
|
||||
version = _get_version_str()
|
||||
return {
|
||||
"python_executable": get_python_env_executable_path(
|
||||
_get_python_env_path(version), "python"
|
||||
),
|
||||
"framework_path": _get_framework_path(version),
|
||||
}
|
||||
|
||||
|
||||
def get_build_env() -> dict:
|
||||
version = _get_version_str()
|
||||
venv_bin_dir = get_python_env_executable_path(
|
||||
_get_python_env_path(version), "python"
|
||||
).parent
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = str(venv_bin_dir) + os.pathsep + env.get("PATH", "")
|
||||
env["ZEPHYR_BASE"] = str(_get_framework_path(version) / "zephyr")
|
||||
env["Zephyr-sdk_DIR"] = str(_get_toolchain_path(_TOOLCHAIN_VERSION) / "cmake")
|
||||
return env
|
||||
|
||||
|
||||
def check_and_install() -> None:
|
||||
version = _get_version_str()
|
||||
python_env_path = _get_python_env_path(version)
|
||||
env_python_path = get_python_env_executable_path(python_env_path, "python")
|
||||
sentinel = python_env_path / ".ready"
|
||||
install_venv = not sentinel.exists()
|
||||
install_venv = (
|
||||
not sentinel.exists()
|
||||
or _REQUIREMENTS.stat().st_mtime > sentinel.stat().st_mtime
|
||||
)
|
||||
if install_venv:
|
||||
rmdir(python_env_path, msg=f"Clean up {version} Python environment")
|
||||
|
||||
create_venv(python_env_path, msg=f"{version}")
|
||||
create_venv(python_env_path, msg=version)
|
||||
|
||||
_install_sitecustomize(python_env_path)
|
||||
|
||||
_LOGGER.info("Installing west %s ...", _WEST_VERSION)
|
||||
cmd = [str(env_python_path), "-m", "pip", "install", f"west=={_WEST_VERSION}"]
|
||||
_LOGGER.info("Installing requirements ...")
|
||||
cmd = [
|
||||
str(env_python_path),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
str(_REQUIREMENTS),
|
||||
]
|
||||
if not run_command_ok(cmd):
|
||||
raise EsphomeError(f"Install west for {version} Python environment failure")
|
||||
raise EsphomeError(
|
||||
f"Install requirements for {version} Python environment failure"
|
||||
)
|
||||
sentinel.touch()
|
||||
|
||||
framework_path = _get_framework_path(version)
|
||||
sentinel = framework_path / ".ready"
|
||||
if install_venv or not sentinel.exists():
|
||||
zephyr_reqs = framework_path / "zephyr" / "scripts" / "requirements.txt"
|
||||
if not sentinel.exists() or not zephyr_reqs.exists():
|
||||
rmdir(framework_path, msg=f"Clean up {version} framework environment")
|
||||
_LOGGER.info("Initializing nRF Connect SDK %s ...", version)
|
||||
cmd = [
|
||||
@@ -128,7 +176,7 @@ def check_and_install() -> None:
|
||||
"-m",
|
||||
"https://github.com/nrfconnect/sdk-nrf",
|
||||
"--mr",
|
||||
f"{version}",
|
||||
version,
|
||||
str(framework_path),
|
||||
]
|
||||
if not run_command_ok(cmd):
|
||||
@@ -146,17 +194,47 @@ def check_and_install() -> None:
|
||||
raise EsphomeError(f"Can't update nRF Connect SDK {version}")
|
||||
sentinel.touch()
|
||||
|
||||
zephyr_sentinel = python_env_path / ".zephyr_reqs_ready"
|
||||
if (
|
||||
install_venv
|
||||
or not zephyr_sentinel.exists()
|
||||
or zephyr_reqs.stat().st_mtime > zephyr_sentinel.stat().st_mtime
|
||||
):
|
||||
_LOGGER.info("Installing Zephyr requirements ...")
|
||||
cmd = [
|
||||
str(env_python_path),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"-r",
|
||||
str(zephyr_reqs),
|
||||
]
|
||||
if not run_command_ok(cmd):
|
||||
raise EsphomeError(f"Install Zephyr requirements for {version} failure")
|
||||
zephyr_sentinel.touch()
|
||||
|
||||
toolchains_dir = _get_toolchain_path(_TOOLCHAIN_VERSION)
|
||||
sentinel = toolchains_dir / ".ready"
|
||||
if not sentinel.exists():
|
||||
rmdir(
|
||||
toolchains_dir, msg=f"Clean up {_TOOLCHAIN_VERSION} toolchain environment"
|
||||
)
|
||||
sysname, machine, extension = _get_toolchain_platform_info()
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading Zephyr SDK %s minimal ...", _TOOLCHAIN_VERSION)
|
||||
download_from_mirrors(
|
||||
SDK_NG_MINIMAL_MIRRORS,
|
||||
{
|
||||
"VERSION": _TOOLCHAIN_VERSION,
|
||||
"sysname": sysname,
|
||||
"machine": machine,
|
||||
"extension": extension,
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting")
|
||||
with tempfile.NamedTemporaryFile() as tmp:
|
||||
_LOGGER.info("Downloading %s toolchain ...", _TOOLCHAIN_VERSION)
|
||||
|
||||
sysname, machine, extension = _get_toolchain_platform_info()
|
||||
|
||||
download_from_mirrors(
|
||||
SDK_NG_TOOLCHAIN_MIRRORS,
|
||||
{
|
||||
@@ -167,5 +245,9 @@ def check_and_install() -> None:
|
||||
},
|
||||
tmp.file,
|
||||
)
|
||||
archive_extract_all(tmp.file, toolchains_dir, progress_header="Extracting")
|
||||
archive_extract_all(
|
||||
tmp.file,
|
||||
toolchains_dir / "arm-zephyr-eabi",
|
||||
progress_header="Extracting",
|
||||
)
|
||||
sentinel.touch()
|
||||
|
||||
3
esphome/components/nrf52/requirements.txt
Normal file
3
esphome/components/nrf52/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
west==1.5.0
|
||||
ninja==1.13.0
|
||||
cmake==4.3.2
|
||||
@@ -20,6 +20,25 @@ PathType = str | os.PathLike
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_project_link_flags() -> list[str]:
|
||||
"""Return the sorted -Wl, linker flags from the current build."""
|
||||
from esphome.core import CORE # local import to avoid circular dependency
|
||||
|
||||
return sorted(flag for flag in CORE.build_flags if flag.startswith("-Wl,"))
|
||||
|
||||
|
||||
def get_project_compile_flags() -> list[str]:
|
||||
"""Return the sorted -D and -W (non-linker) flags from the current build."""
|
||||
from esphome.core import CORE # local import to avoid circular dependency
|
||||
|
||||
return [
|
||||
flag
|
||||
for flag in sorted(CORE.build_flags)
|
||||
if flag.startswith("-D")
|
||||
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
|
||||
]
|
||||
|
||||
|
||||
def str_to_lst_of_str(a: str | list[str]) -> list[str]:
|
||||
"""
|
||||
Convert a string to a list of string
|
||||
|
||||
@@ -136,6 +136,54 @@ def test_get_project_cmakelists_full_emits_builtin_components_property(
|
||||
assert "JPEGDEC APPEND" not in content
|
||||
|
||||
|
||||
def test_get_component_cmakelists_no_link_flags() -> None:
|
||||
"""With no -Wl, flags the target_link_options block is emitted with an empty body."""
|
||||
CORE.build_flags = set()
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
assert "target_link_options(${COMPONENT_LIB} PUBLIC\n \n)" in content
|
||||
|
||||
|
||||
def test_get_component_cmakelists_single_link_flag() -> None:
|
||||
"""A single -Wl, flag appears indented inside target_link_options."""
|
||||
CORE.build_flags = {"-Wl,--gc-sections"}
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
assert (
|
||||
"target_link_options(${COMPONENT_LIB} PUBLIC\n -Wl,--gc-sections\n)"
|
||||
in content
|
||||
)
|
||||
|
||||
|
||||
def test_get_component_cmakelists_multiple_link_flags_sorted() -> None:
|
||||
"""Multiple -Wl, flags are sorted and joined with the four-space indent."""
|
||||
CORE.build_flags = {"-Wl,-z,noexecstack", "-Wl,--gc-sections", "-Wl,-Map=out.map"}
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
expected = (
|
||||
"target_link_options(${COMPONENT_LIB} PUBLIC\n"
|
||||
" -Wl,--gc-sections\n"
|
||||
" -Wl,-Map=out.map\n"
|
||||
" -Wl,-z,noexecstack\n"
|
||||
")"
|
||||
)
|
||||
assert expected in content
|
||||
|
||||
|
||||
def test_get_component_cmakelists_compile_flags_excluded_from_link_opts() -> None:
|
||||
"""-D and -W (non-linker) flags must not appear in target_link_options."""
|
||||
CORE.build_flags = {"-DFOO", "-Wall", "-Wl,--gc-sections"}
|
||||
from esphome.build_gen.espidf import get_component_cmakelists
|
||||
|
||||
content = get_component_cmakelists()
|
||||
assert "-DFOO" not in content.split("target_link_options")[1]
|
||||
assert "-Wall" not in content.split("target_link_options")[1]
|
||||
assert "-Wl,--gc-sections" in content
|
||||
|
||||
|
||||
def test_get_project_cmakelists_emits_managed_components_property(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -25,6 +25,8 @@ from esphome.framework_helpers import (
|
||||
archive_extract_all,
|
||||
create_venv,
|
||||
download_from_mirrors,
|
||||
get_project_compile_flags,
|
||||
get_project_link_flags,
|
||||
get_python_env_executable_path,
|
||||
get_system_python_path,
|
||||
rmdir,
|
||||
@@ -952,3 +954,84 @@ class TestSevenZipExtractAll:
|
||||
out.mkdir()
|
||||
archive_extract_all(archive, out)
|
||||
assert (out / "hello.txt").exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_project_compile_flags / get_project_link_flags
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_core(flags: set[str]):
|
||||
core = MagicMock()
|
||||
core.build_flags = flags
|
||||
return core
|
||||
|
||||
|
||||
class TestGetProjectCompileFlags:
|
||||
def test_returns_define_flags(self) -> None:
|
||||
with patch("esphome.core.CORE", _make_core({"-DFOO", "-DBAR=1"})):
|
||||
assert get_project_compile_flags() == ["-DBAR=1", "-DFOO"]
|
||||
|
||||
def test_returns_warning_flags(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-Wno-error", "-Wall"}),
|
||||
):
|
||||
assert get_project_compile_flags() == ["-Wall", "-Wno-error"]
|
||||
|
||||
def test_excludes_linker_flags(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-DFOO", "-Wl,--gc-sections", "-Wl,-Map=output.map"}),
|
||||
):
|
||||
assert get_project_compile_flags() == ["-DFOO"]
|
||||
|
||||
def test_excludes_other_flags(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-O2", "-std=gnu++20", "-DFOO"}),
|
||||
):
|
||||
assert get_project_compile_flags() == ["-DFOO"]
|
||||
|
||||
def test_empty_build_flags(self) -> None:
|
||||
with patch("esphome.core.CORE", _make_core(set())):
|
||||
assert get_project_compile_flags() == []
|
||||
|
||||
def test_result_is_sorted(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-DZFLAG", "-DAFLAG", "-Wno-unused"}),
|
||||
):
|
||||
result = get_project_compile_flags()
|
||||
assert result == sorted(result)
|
||||
|
||||
|
||||
class TestGetProjectLinkFlags:
|
||||
def test_returns_linker_flags(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-Wl,--gc-sections", "-Wl,-Map=output.map"}),
|
||||
):
|
||||
assert get_project_link_flags() == [
|
||||
"-Wl,--gc-sections",
|
||||
"-Wl,-Map=output.map",
|
||||
]
|
||||
|
||||
def test_excludes_compile_flags(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-DFOO", "-Wall", "-Wl,--gc-sections"}),
|
||||
):
|
||||
assert get_project_link_flags() == ["-Wl,--gc-sections"]
|
||||
|
||||
def test_empty_build_flags(self) -> None:
|
||||
with patch("esphome.core.CORE", _make_core(set())):
|
||||
assert get_project_link_flags() == []
|
||||
|
||||
def test_result_is_sorted(self) -> None:
|
||||
with patch(
|
||||
"esphome.core.CORE",
|
||||
_make_core({"-Wl,-z", "-Wl,-a", "-Wl,-m"}),
|
||||
):
|
||||
result = get_project_link_flags()
|
||||
assert result == sorted(result)
|
||||
|
||||
@@ -58,6 +58,9 @@ def nrf52_dirs(setup_core: Path) -> SimpleNamespace:
|
||||
toolchain_dir = tools / "toolchains" / _TOOLCHAIN_VERSION
|
||||
for d in (python_env, framework, toolchain_dir):
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
zephyr_scripts = framework / "zephyr" / "scripts"
|
||||
zephyr_scripts.mkdir(parents=True, exist_ok=True)
|
||||
(zephyr_scripts / "requirements.txt").touch()
|
||||
return SimpleNamespace(
|
||||
python_env=python_env,
|
||||
framework=framework,
|
||||
@@ -102,6 +105,7 @@ class TestCheckAndInstall:
|
||||
) -> None:
|
||||
"""All three sentinels present → nothing downloaded or compiled."""
|
||||
(nrf52_dirs.python_env / ".ready").touch()
|
||||
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
|
||||
(nrf52_dirs.framework / ".ready").touch()
|
||||
(nrf52_dirs.toolchain / ".ready").touch()
|
||||
|
||||
@@ -121,11 +125,13 @@ class TestCheckAndInstall:
|
||||
check_and_install()
|
||||
|
||||
mock_nrf52_ops.create_venv.assert_called_once()
|
||||
# pip install west, west init, west update
|
||||
assert mock_nrf52_ops.run_command_ok.call_count == 3
|
||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
||||
mock_nrf52_ops.archive_extract_all.assert_called_once()
|
||||
# pip install requirements, west init, west update, pip install zephyr reqs
|
||||
assert mock_nrf52_ops.run_command_ok.call_count == 4
|
||||
# minimal SDK + per-arch toolchain
|
||||
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
|
||||
assert mock_nrf52_ops.archive_extract_all.call_count == 2
|
||||
assert (nrf52_dirs.python_env / ".ready").exists()
|
||||
assert (nrf52_dirs.python_env / ".zephyr_reqs_ready").exists()
|
||||
assert (nrf52_dirs.framework / ".ready").exists()
|
||||
assert (nrf52_dirs.toolchain / ".ready").exists()
|
||||
|
||||
@@ -140,9 +146,10 @@ class TestCheckAndInstall:
|
||||
check_and_install()
|
||||
|
||||
mock_nrf52_ops.create_venv.assert_not_called()
|
||||
# west init + west update only (no pip install)
|
||||
assert mock_nrf52_ops.run_command_ok.call_count == 2
|
||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
||||
# west init, west update, pip install zephyr reqs
|
||||
assert mock_nrf52_ops.run_command_ok.call_count == 3
|
||||
# minimal SDK + per-arch toolchain
|
||||
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
|
||||
|
||||
def test_toolchain_only_missing(
|
||||
self,
|
||||
@@ -151,24 +158,26 @@ class TestCheckAndInstall:
|
||||
) -> None:
|
||||
"""Venv and framework ready → only toolchain downloaded and extracted."""
|
||||
(nrf52_dirs.python_env / ".ready").touch()
|
||||
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
|
||||
(nrf52_dirs.framework / ".ready").touch()
|
||||
|
||||
check_and_install()
|
||||
|
||||
mock_nrf52_ops.create_venv.assert_not_called()
|
||||
mock_nrf52_ops.run_command_ok.assert_not_called()
|
||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
||||
mock_nrf52_ops.archive_extract_all.assert_called_once()
|
||||
# minimal SDK + per-arch toolchain
|
||||
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
|
||||
assert mock_nrf52_ops.archive_extract_all.call_count == 2
|
||||
|
||||
def test_west_install_failure_raises(
|
||||
def test_requirements_install_failure_raises(
|
||||
self,
|
||||
nrf52_dirs: SimpleNamespace,
|
||||
mock_nrf52_ops: SimpleNamespace,
|
||||
) -> None:
|
||||
"""Failing pip install west raises EsphomeError."""
|
||||
"""Failing pip install -r requirements.txt raises EsphomeError."""
|
||||
mock_nrf52_ops.run_command_ok.return_value = False
|
||||
|
||||
with pytest.raises(EsphomeError, match="Install west"):
|
||||
with pytest.raises(EsphomeError, match="Install requirements"):
|
||||
check_and_install()
|
||||
|
||||
def test_framework_init_failure_raises(
|
||||
|
||||
Reference in New Issue
Block a user