[nrf52] switch nrf52 builds to native sdk by default (#17319)

Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: ESPHome Device Builder <device-builder@esphome.io>
This commit is contained in:
tomaszduda23
2026-07-03 12:16:55 -04:00
committed by GitHub
co-authored by Jonathan Swoboda ESPHome Device Builder
parent ea14a93e67
commit fd16eec416
8 changed files with 126 additions and 30 deletions
+19 -11
View File
@@ -117,7 +117,7 @@ def set_core_data(config: ConfigType) -> ConfigType:
def _resolve_toolchain(config: ConfigType) -> ConfigType: def _resolve_toolchain(config: ConfigType) -> ConfigType:
if CORE.toolchain is None: if CORE.toolchain is None:
CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.PLATFORMIO) CORE.toolchain = config.get(CONF_TOOLCHAIN, Toolchain.SDK_NRF)
return config return config
@@ -439,8 +439,8 @@ def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
types = [] types = []
UF2_PATH = "zephyr/zephyr.uf2" UF2_PATH = "zephyr/zephyr.uf2"
DFU_PATH = "firmware.zip" DFU_PATH = "firmware.zip"
HEX_PATH = "zephyr/zephyr.hex" HEX_PATH = "zephyr/zephyr.hex" # SDK 2.6.1, only generated when OTA is disabled
HEX_MERGED_PATH = "zephyr/merged.hex" HEX_MERGED_PATH = "zephyr/merged.hex" # SDK 2.9.2, always generated
APP_IMAGE_PATH = "zephyr/app_update.bin" APP_IMAGE_PATH = "zephyr/app_update.bin"
build_dir = Path(storage_json.firmware_bin_path).parent build_dir = Path(storage_json.firmware_bin_path).parent
if (build_dir / UF2_PATH).is_file(): if (build_dir / UF2_PATH).is_file():
@@ -777,6 +777,11 @@ def _generate_cmake_lists() -> bool:
) )
def _copy_if_exists(src: Path, dst: Path) -> None:
if src.is_file():
shutil.copy2(src, dst)
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
@@ -828,15 +833,18 @@ def run_compile(args, config: ConfigType) -> bool:
): ):
raise EsphomeError("nRF52 native build failed") raise EsphomeError("nRF52 native build failed")
# Zephyr's cmake places kernel artifacts in build_dir/zephyr/zephyr/ and
# merged.hex at build_dir/. Normalize to build_dir/zephyr/ so paths match
# get_download_types (which mirrors the platformio build output layout).
zephyr_dir = build_dir / "zephyr" zephyr_dir = build_dir / "zephyr"
west_out = zephyr_dir / "zephyr" framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
for filename in ["zephyr.uf2"]: # SDK < 2.9.2 places artifacts directly in build_dir/zephyr/.
src = west_out / filename # SDK >= 2.9.2 nests them one level deeper (build_dir/zephyr/zephyr/);
if src.is_file(): # copy files to match get_download_types layout.
shutil.copy2(src, zephyr_dir / filename) if framework_ver < cv.Version(2, 9, 2):
west_out = zephyr_dir
else:
west_out = zephyr_dir / "zephyr"
_copy_if_exists(west_out / "zephyr.uf2", zephyr_dir / "zephyr.uf2")
_copy_if_exists(west_out / "zephyr.signed.bin", zephyr_dir / "app_update.bin")
_copy_if_exists(build_dir / "merged.hex", zephyr_dir / "merged.hex")
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes # (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
_GENPKG_PARAMS = { _GENPKG_PARAMS = {
+22
View File
@@ -2,10 +2,12 @@ import logging
import os import os
from pathlib import Path from pathlib import Path
import platform import platform
import shutil
import tempfile import tempfile
import platformdirs import platformdirs
import esphome.config_validation as cv
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
from esphome.core import CORE, EsphomeError from esphome.core import CORE, EsphomeError
from esphome.framework_helpers import ( from esphome.framework_helpers import (
@@ -134,6 +136,23 @@ def get_build_env() -> dict:
return env return env
def _patch_uf2conv_escape_sequences(framework_path: Path) -> None:
# SDK v2.6.1 ships uf2conv.py with '\s+' — an unrecognised escape that
# Python 3.12+ flags with SyntaxWarning (a future version will reject it).
uf2conv = framework_path / "zephyr" / "scripts" / "build" / "uf2conv.py"
if not uf2conv.exists():
return
content = uf2conv.read_text(encoding="utf-8")
patched = content.replace("re.split('\\s+', line)", "re.split('\\\\s+', line)")
if patched == content:
return
# Write atomically so a concurrent build never sees a truncated file
tmp = uf2conv.with_suffix(".py.tmp")
tmp.write_text(patched, encoding="utf-8")
shutil.copymode(uf2conv, tmp)
tmp.replace(uf2conv)
def check_and_install() -> None: def check_and_install() -> None:
version = _get_version_str() version = _get_version_str()
python_env_path = _get_python_env_path(version) python_env_path = _get_python_env_path(version)
@@ -195,6 +214,9 @@ def check_and_install() -> None:
] ]
if not run_command_ok(cmd, cwd=framework_path): if not run_command_ok(cmd, cwd=framework_path):
raise EsphomeError(f"Can't update nRF Connect SDK {version}") raise EsphomeError(f"Can't update nRF Connect SDK {version}")
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if framework_ver < cv.Version(2, 9, 2):
_patch_uf2conv_escape_sequences(framework_path)
sentinel.touch() sentinel.touch()
zephyr_sentinel = python_env_path / ".zephyr_reqs_ready" zephyr_sentinel = python_env_path / ".zephyr_reqs_ready"
+13
View File
@@ -18,6 +18,7 @@ from .const import (
KEY_OVERLAY, KEY_OVERLAY,
KEY_PM_STATIC, KEY_PM_STATIC,
KEY_PRJ_CONF, KEY_PRJ_CONF,
KEY_SYSBUILD,
KEY_USER, KEY_USER,
KEY_ZEPHYR, KEY_ZEPHYR,
zephyr_ns, zephyr_ns,
@@ -55,6 +56,7 @@ class ZephyrData(TypedDict):
pm_static: list[Section] pm_static: list[Section]
user: dict[str, list[str]] user: dict[str, list[str]]
kconfig: str kconfig: str
sysbuild: bool
def zephyr_set_core_data(config: ConfigType) -> None: def zephyr_set_core_data(config: ConfigType) -> None:
@@ -69,6 +71,10 @@ def zephyr_set_core_data(config: ConfigType) -> None:
pm_static=[], pm_static=[],
user={}, user={},
kconfig="", kconfig="",
# When OTA is disabled, the image is built without a bootloader even if the
# config says `bootloader: mcuboot`, so the image can be smaller. This was
# the default behaviour in SDK 2.6.1.
sysbuild=False,
) )
@@ -286,6 +292,13 @@ def copy_files() -> None:
CORE.relative_build_path("zephyr/Kconfig"), kconfig CORE.relative_build_path("zephyr/Kconfig"), kconfig
) )
sysbuild_conf = ""
if zephyr_data()[KEY_SYSBUILD]:
sysbuild_conf = "SB_CONFIG_BOOTLOADER_MCUBOOT=y\n"
changed |= _write_file_if_changed_or_remove_when_empty(
CORE.relative_build_path("zephyr/sysbuild.conf"), sysbuild_conf
)
if changed: if changed:
# A configure-time input changed; drop the CMake cache so the build # A configure-time input changed; drop the CMake cache so the build
# can't reuse stale configure results (the native sdk-nrf toolchain # can't reuse stale configure results (the native sdk-nrf toolchain
+1
View File
@@ -13,6 +13,7 @@ KEY_PRJ_CONF: Final = "prj_conf"
KEY_ZEPHYR = "zephyr" KEY_ZEPHYR = "zephyr"
KEY_BOARD: Final = "board" KEY_BOARD: Final = "board"
KEY_USER: Final = "user" KEY_USER: Final = "user"
KEY_SYSBUILD: Final = "sysbuild"
zephyr_ns = cg.esphome_ns.namespace("zephyr") zephyr_ns = cg.esphome_ns.namespace("zephyr")
CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component) CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component)
@@ -6,9 +6,19 @@ from esphome.components.zephyr import (
zephyr_add_prj_conf, zephyr_add_prj_conf,
zephyr_data, zephyr_data,
) )
from esphome.components.zephyr.const import BOOTLOADER_MCUBOOT, KEY_BOOTLOADER from esphome.components.zephyr.const import (
BOOTLOADER_MCUBOOT,
KEY_BOOTLOADER,
KEY_SYSBUILD,
)
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_HARDWARE_UART, CONF_ID, Framework from esphome.const import (
CONF_HARDWARE_UART,
CONF_ID,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
Framework,
)
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority from esphome.coroutine import CoroPriority
from esphome.types import ConfigType from esphome.types import ConfigType
@@ -139,3 +149,6 @@ async def to_code(config: ConfigType) -> None:
}}; }};
""" """
) )
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if framework_ver >= cv.Version(2, 9, 2):
zephyr_data()[KEY_SYSBUILD] = True
+54 -13
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Extract memory usage statistics from ESPHome build output. """Extract memory usage statistics from ESPHome build output.
This script parses the PlatformIO build output to extract RAM and flash This script parses the build output to extract RAM and flash usage
usage statistics for a compiled component. It's used by the CI workflow to statistics for a compiled component. It's used by the CI workflow to
compare memory usage between branches. compare memory usage between branches.
The script reads compile output from stdin and looks for the standard The script reads compile output from stdin and looks for the standard
@@ -10,6 +10,13 @@ PlatformIO output format:
RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes) RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)
Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes) Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)
or the linker memory usage table printed by Zephyr native builds
(e.g. nRF52 with the sdk-nrf toolchain):
Memory region Used Size Region Size %age Used
FLASH: 90624 B 796 KB 11.12%
RAM: 22432 B 256 KB 8.56%
IDT_LIST: 0 GB 32 KB 0.00%
Optionally performs detailed memory analysis if a build directory is provided. Optionally performs detailed memory analysis if a build directory is provided.
""" """
@@ -34,20 +41,43 @@ _RAM_PATTERN = re.compile(r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes"
_FLASH_PATTERN = re.compile(r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes") _FLASH_PATTERN = re.compile(r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes")
_BUILD_PATH_PATTERN = re.compile(r"Build path: (.+)") _BUILD_PATH_PATTERN = re.compile(r"Build path: (.+)")
# Zephyr native builds print the GNU ld --print-memory-usage table instead of
# the PlatformIO summary. Only the FLASH and RAM regions are real memory
# (IDT_LIST is a build-time pseudo-region discarded from the final image).
# Each cell is humanized to the largest unit that divides evenly, so used
# sizes are not always plain bytes (zero prints as "0 GB").
_ZEPHYR_RAM_PATTERN = re.compile(
r"^\s*RAM:\s+(\d+)\s*([KMG]?B)\s+\d+\s*[KMG]?B\s+\d+\.\d+%", re.MULTILINE
)
_ZEPHYR_FLASH_PATTERN = re.compile(
r"^\s*FLASH:\s+(\d+)\s*([KMG]?B)\s+\d+\s*[KMG]?B\s+\d+\.\d+%", re.MULTILINE
)
_ZEPHYR_UNIT_MULTIPLIERS = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3}
def _zephyr_bytes(matches: list[tuple[str, str]]) -> int:
"""Sum humanized (value, unit) pairs from the Zephyr memory table."""
return sum(int(value) * _ZEPHYR_UNIT_MULTIPLIERS[unit] for value, unit in matches)
def extract_from_compile_output( def extract_from_compile_output(
output_text: str, output_text: str,
) -> tuple[int | None, int | None, str | None]: ) -> tuple[int | None, int | None, str | None]:
"""Extract memory usage and build directory from PlatformIO compile output. """Extract memory usage and build directory from compile output.
Supports multiple builds (for component groups or isolated components). Supports multiple builds (for component groups or isolated components).
When test_build_components.py creates multiple builds, this sums the When test_build_components.py creates multiple builds, this sums the
memory usage across all builds. memory usage across all builds.
Looks for lines like: Looks for PlatformIO lines like:
RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes) RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)
Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes) Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)
and Zephyr (native west build) linker table rows like:
Memory region Used Size Region Size %age Used
FLASH: 90624 B 796 KB 11.12%
RAM: 22432 B 256 KB 8.56%
Also extracts build directory from lines like: Also extracts build directory from lines like:
INFO Compiling app... Build path: /path/to/build INFO Compiling app... Build path: /path/to/build
@@ -61,12 +91,20 @@ def extract_from_compile_output(
ram_matches = _RAM_PATTERN.findall(output_text) ram_matches = _RAM_PATTERN.findall(output_text)
flash_matches = _FLASH_PATTERN.findall(output_text) flash_matches = _FLASH_PATTERN.findall(output_text)
if not ram_matches or not flash_matches: # Zephyr native builds print the linker memory table instead
zephyr_ram_matches = _ZEPHYR_RAM_PATTERN.findall(output_text)
zephyr_flash_matches = _ZEPHYR_FLASH_PATTERN.findall(output_text)
if not (ram_matches or zephyr_ram_matches) or not (
flash_matches or zephyr_flash_matches
):
return None, None, None return None, None, None
# Sum all builds (handles multiple component groups) # Sum all builds (handles multiple component groups)
total_ram = sum(int(match) for match in ram_matches) total_ram = sum(int(match) for match in ram_matches)
total_flash = sum(int(match) for match in flash_matches) total_flash = sum(int(match) for match in flash_matches)
total_ram += _zephyr_bytes(zephyr_ram_matches)
total_flash += _zephyr_bytes(zephyr_flash_matches)
# Extract build directory from ESPHome's explicit build path output # Extract build directory from ESPHome's explicit build path output
# Look for: INFO Compiling app... Build path: /path/to/build # Look for: INFO Compiling app... Build path: /path/to/build
@@ -202,20 +240,23 @@ def main() -> int:
) )
if ram_bytes is None or flash_bytes is None: if ram_bytes is None or flash_bytes is None:
print("Failed to extract memory usage from compile output", file=sys.stderr)
print("Expected lines like:", file=sys.stderr)
print( print(
" RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)", "Failed to extract memory usage from compile output\n"
file=sys.stderr, "Expected lines like:\n"
) " RAM: [==== ] 36.1% (used 29548 bytes from 81920 bytes)\n"
print( " Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)\n"
" Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes)", "or a Zephyr linker memory usage table like:\n"
" Memory region Used Size Region Size %age Used\n"
" FLASH: 90624 B 796 KB 11.12%\n"
" RAM: 22432 B 256 KB 8.56%",
file=sys.stderr, file=sys.stderr,
) )
return 1 return 1
# Count how many builds were found # Count how many builds were found
num_builds = len(_RAM_PATTERN.findall(compile_output)) num_builds = len(_RAM_PATTERN.findall(compile_output)) + len(
_ZEPHYR_RAM_PATTERN.findall(compile_output)
)
if num_builds > 1: if num_builds > 1:
print( print(
@@ -1,7 +1,7 @@
<<: !include common.yaml
network: network:
enable_ipv6: true enable_ipv6: true
openthread: openthread:
tlv: 0E080000000000010000 tlv: 0E080000000000010000
api:
@@ -19,5 +19,3 @@ nrf52:
reg0: reg0:
voltage: 2.1V voltage: 2.1V
uicr_erase: true uicr_erase: true
framework:
version: "2.6.1-b"