mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 12:33:10 +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
|
from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.core import CORE
|
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
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
|
|
||||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
# 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
|
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
||||||
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
||||||
# third-party components we don't author.
|
# third-party components we don't author.
|
||||||
project_compile_opts = [
|
project_compile_opts = get_project_compile_flags()
|
||||||
flag
|
|
||||||
for flag in sorted(CORE.build_flags)
|
|
||||||
if flag.startswith("-D")
|
|
||||||
or (flag.startswith("-W") and not flag.startswith("-Wl,"))
|
|
||||||
]
|
|
||||||
extra_compile_options = "\n".join(
|
extra_compile_options = "\n".join(
|
||||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
||||||
for flag in project_compile_opts
|
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
|
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
||||||
# emitted project-wide via idf_build_set_property in
|
# emitted project-wide via idf_build_set_property in
|
||||||
# get_project_cmakelists so they reach every component, not just src/.
|
# 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 = get_project_link_flags()
|
||||||
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
|
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# 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 import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||||
from esphome.core.config import BOARD_MAX_LENGTH
|
from esphome.core.config import BOARD_MAX_LENGTH
|
||||||
import esphome.final_validate as fv
|
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.helpers import write_file_if_changed
|
||||||
from esphome.storage_json import StorageJSON
|
from esphome.storage_json import StorageJSON
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
@@ -63,7 +68,7 @@ from .const import (
|
|||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6,
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7,
|
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
|
# force import gpio to register pin schema
|
||||||
from .gpio import nrf52_pin_to_code # noqa: F401
|
from .gpio import nrf52_pin_to_code # noqa: F401
|
||||||
@@ -99,9 +104,6 @@ FAKE_BOARD_MANIFEST = """
|
|||||||
|
|
||||||
|
|
||||||
def set_core_data(config: ConfigType) -> ConfigType:
|
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)
|
zephyr_set_core_data(config)
|
||||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
||||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||||
@@ -112,6 +114,12 @@ def set_core_data(config: ConfigType) -> ConfigType:
|
|||||||
return config
|
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:
|
def set_framework(config: ConfigType) -> ConfigType:
|
||||||
if CONF_VERSION not in config[CONF_FRAMEWORK]:
|
if CONF_VERSION not in config[CONF_FRAMEWORK]:
|
||||||
default_version = "2.6.1-b" if CORE.using_toolchain_platformio else "2.9.2"
|
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:
|
def _detect_bootloader(config: ConfigType) -> ConfigType:
|
||||||
"""Detect the bootloader for the given board."""
|
"""Detect the bootloader for the given board."""
|
||||||
config = config.copy()
|
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),
|
cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
_resolve_toolchain,
|
||||||
set_framework,
|
set_framework,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -565,6 +581,47 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
|||||||
return False
|
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:
|
def run_compile(args, config: ConfigType) -> bool:
|
||||||
if CORE.using_toolchain_platformio:
|
if CORE.using_toolchain_platformio:
|
||||||
return False
|
return False
|
||||||
@@ -574,4 +631,35 @@ def run_compile(args, config: ConfigType) -> bool:
|
|||||||
"Supported toolchains are 'platformio' and 'sdk-nrf'."
|
"Supported toolchains are 'platformio' and 'sdk-nrf'."
|
||||||
)
|
)
|
||||||
check_and_install()
|
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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
_WEST_VERSION = "1.5.0"
|
_REQUIREMENTS = Path(__file__).parent / "requirements.txt"
|
||||||
_TOOLCHAIN_VERSION = "0.17.4"
|
_TOOLCHAIN_VERSION = "0.17.4"
|
||||||
|
|
||||||
SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
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:
|
def _get_tools_path() -> Path:
|
||||||
return CORE.data_dir / "sdk-nrf"
|
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:
|
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:
|
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.
|
# 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
|
return sysname, machine, extension
|
||||||
|
|
||||||
|
|
||||||
def check_and_install() -> None:
|
def _get_version_str() -> str:
|
||||||
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
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)
|
python_env_path = _get_python_env_path(version)
|
||||||
env_python_path = get_python_env_executable_path(python_env_path, "python")
|
env_python_path = get_python_env_executable_path(python_env_path, "python")
|
||||||
sentinel = python_env_path / ".ready"
|
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:
|
if install_venv:
|
||||||
rmdir(python_env_path, msg=f"Clean up {version} Python environment")
|
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)
|
_install_sitecustomize(python_env_path)
|
||||||
|
|
||||||
_LOGGER.info("Installing west %s ...", _WEST_VERSION)
|
_LOGGER.info("Installing requirements ...")
|
||||||
cmd = [str(env_python_path), "-m", "pip", "install", f"west=={_WEST_VERSION}"]
|
cmd = [
|
||||||
|
str(env_python_path),
|
||||||
|
"-m",
|
||||||
|
"pip",
|
||||||
|
"install",
|
||||||
|
"-r",
|
||||||
|
str(_REQUIREMENTS),
|
||||||
|
]
|
||||||
if not run_command_ok(cmd):
|
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()
|
sentinel.touch()
|
||||||
|
|
||||||
framework_path = _get_framework_path(version)
|
framework_path = _get_framework_path(version)
|
||||||
sentinel = framework_path / ".ready"
|
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")
|
rmdir(framework_path, msg=f"Clean up {version} framework environment")
|
||||||
_LOGGER.info("Initializing nRF Connect SDK %s ...", version)
|
_LOGGER.info("Initializing nRF Connect SDK %s ...", version)
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -128,7 +176,7 @@ def check_and_install() -> None:
|
|||||||
"-m",
|
"-m",
|
||||||
"https://github.com/nrfconnect/sdk-nrf",
|
"https://github.com/nrfconnect/sdk-nrf",
|
||||||
"--mr",
|
"--mr",
|
||||||
f"{version}",
|
version,
|
||||||
str(framework_path),
|
str(framework_path),
|
||||||
]
|
]
|
||||||
if not run_command_ok(cmd):
|
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}")
|
raise EsphomeError(f"Can't update nRF Connect SDK {version}")
|
||||||
sentinel.touch()
|
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)
|
toolchains_dir = _get_toolchain_path(_TOOLCHAIN_VERSION)
|
||||||
sentinel = toolchains_dir / ".ready"
|
sentinel = toolchains_dir / ".ready"
|
||||||
if not sentinel.exists():
|
if not sentinel.exists():
|
||||||
rmdir(
|
rmdir(
|
||||||
toolchains_dir, msg=f"Clean up {_TOOLCHAIN_VERSION} toolchain environment"
|
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:
|
with tempfile.NamedTemporaryFile() as tmp:
|
||||||
_LOGGER.info("Downloading %s toolchain ...", _TOOLCHAIN_VERSION)
|
_LOGGER.info("Downloading %s toolchain ...", _TOOLCHAIN_VERSION)
|
||||||
|
|
||||||
sysname, machine, extension = _get_toolchain_platform_info()
|
|
||||||
|
|
||||||
download_from_mirrors(
|
download_from_mirrors(
|
||||||
SDK_NG_TOOLCHAIN_MIRRORS,
|
SDK_NG_TOOLCHAIN_MIRRORS,
|
||||||
{
|
{
|
||||||
@@ -167,5 +245,9 @@ def check_and_install() -> None:
|
|||||||
},
|
},
|
||||||
tmp.file,
|
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()
|
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__)
|
_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]:
|
def str_to_lst_of_str(a: str | list[str]) -> list[str]:
|
||||||
"""
|
"""
|
||||||
Convert a string to a list of string
|
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
|
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(
|
def test_get_project_cmakelists_emits_managed_components_property(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ from esphome.framework_helpers import (
|
|||||||
archive_extract_all,
|
archive_extract_all,
|
||||||
create_venv,
|
create_venv,
|
||||||
download_from_mirrors,
|
download_from_mirrors,
|
||||||
|
get_project_compile_flags,
|
||||||
|
get_project_link_flags,
|
||||||
get_python_env_executable_path,
|
get_python_env_executable_path,
|
||||||
get_system_python_path,
|
get_system_python_path,
|
||||||
rmdir,
|
rmdir,
|
||||||
@@ -952,3 +954,84 @@ class TestSevenZipExtractAll:
|
|||||||
out.mkdir()
|
out.mkdir()
|
||||||
archive_extract_all(archive, out)
|
archive_extract_all(archive, out)
|
||||||
assert (out / "hello.txt").exists()
|
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
|
toolchain_dir = tools / "toolchains" / _TOOLCHAIN_VERSION
|
||||||
for d in (python_env, framework, toolchain_dir):
|
for d in (python_env, framework, toolchain_dir):
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
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(
|
return SimpleNamespace(
|
||||||
python_env=python_env,
|
python_env=python_env,
|
||||||
framework=framework,
|
framework=framework,
|
||||||
@@ -102,6 +105,7 @@ class TestCheckAndInstall:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""All three sentinels present → nothing downloaded or compiled."""
|
"""All three sentinels present → nothing downloaded or compiled."""
|
||||||
(nrf52_dirs.python_env / ".ready").touch()
|
(nrf52_dirs.python_env / ".ready").touch()
|
||||||
|
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
|
||||||
(nrf52_dirs.framework / ".ready").touch()
|
(nrf52_dirs.framework / ".ready").touch()
|
||||||
(nrf52_dirs.toolchain / ".ready").touch()
|
(nrf52_dirs.toolchain / ".ready").touch()
|
||||||
|
|
||||||
@@ -121,11 +125,13 @@ class TestCheckAndInstall:
|
|||||||
check_and_install()
|
check_and_install()
|
||||||
|
|
||||||
mock_nrf52_ops.create_venv.assert_called_once()
|
mock_nrf52_ops.create_venv.assert_called_once()
|
||||||
# pip install west, west init, west update
|
# pip install requirements, west init, west update, pip install zephyr reqs
|
||||||
assert mock_nrf52_ops.run_command_ok.call_count == 3
|
assert mock_nrf52_ops.run_command_ok.call_count == 4
|
||||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
# minimal SDK + per-arch toolchain
|
||||||
mock_nrf52_ops.archive_extract_all.assert_called_once()
|
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 / ".ready").exists()
|
||||||
|
assert (nrf52_dirs.python_env / ".zephyr_reqs_ready").exists()
|
||||||
assert (nrf52_dirs.framework / ".ready").exists()
|
assert (nrf52_dirs.framework / ".ready").exists()
|
||||||
assert (nrf52_dirs.toolchain / ".ready").exists()
|
assert (nrf52_dirs.toolchain / ".ready").exists()
|
||||||
|
|
||||||
@@ -140,9 +146,10 @@ class TestCheckAndInstall:
|
|||||||
check_and_install()
|
check_and_install()
|
||||||
|
|
||||||
mock_nrf52_ops.create_venv.assert_not_called()
|
mock_nrf52_ops.create_venv.assert_not_called()
|
||||||
# west init + west update only (no pip install)
|
# west init, west update, pip install zephyr reqs
|
||||||
assert mock_nrf52_ops.run_command_ok.call_count == 2
|
assert mock_nrf52_ops.run_command_ok.call_count == 3
|
||||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
# minimal SDK + per-arch toolchain
|
||||||
|
assert mock_nrf52_ops.download_from_mirrors.call_count == 2
|
||||||
|
|
||||||
def test_toolchain_only_missing(
|
def test_toolchain_only_missing(
|
||||||
self,
|
self,
|
||||||
@@ -151,24 +158,26 @@ class TestCheckAndInstall:
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Venv and framework ready → only toolchain downloaded and extracted."""
|
"""Venv and framework ready → only toolchain downloaded and extracted."""
|
||||||
(nrf52_dirs.python_env / ".ready").touch()
|
(nrf52_dirs.python_env / ".ready").touch()
|
||||||
|
(nrf52_dirs.python_env / ".zephyr_reqs_ready").touch()
|
||||||
(nrf52_dirs.framework / ".ready").touch()
|
(nrf52_dirs.framework / ".ready").touch()
|
||||||
|
|
||||||
check_and_install()
|
check_and_install()
|
||||||
|
|
||||||
mock_nrf52_ops.create_venv.assert_not_called()
|
mock_nrf52_ops.create_venv.assert_not_called()
|
||||||
mock_nrf52_ops.run_command_ok.assert_not_called()
|
mock_nrf52_ops.run_command_ok.assert_not_called()
|
||||||
mock_nrf52_ops.download_from_mirrors.assert_called_once()
|
# minimal SDK + per-arch toolchain
|
||||||
mock_nrf52_ops.archive_extract_all.assert_called_once()
|
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,
|
self,
|
||||||
nrf52_dirs: SimpleNamespace,
|
nrf52_dirs: SimpleNamespace,
|
||||||
mock_nrf52_ops: SimpleNamespace,
|
mock_nrf52_ops: SimpleNamespace,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Failing pip install west raises EsphomeError."""
|
"""Failing pip install -r requirements.txt raises EsphomeError."""
|
||||||
mock_nrf52_ops.run_command_ok.return_value = False
|
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()
|
check_and_install()
|
||||||
|
|
||||||
def test_framework_init_failure_raises(
|
def test_framework_init_failure_raises(
|
||||||
|
|||||||
Reference in New Issue
Block a user