[nrf52] add support for native builds (#16898)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
tomaszduda23
2026-06-18 03:17:07 +02:00
committed by GitHub
parent c9095841ae
commit e3f164fff2
8 changed files with 369 additions and 41 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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(