diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 69ef6167da..396728ceaf 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -20,6 +20,13 @@ __attribute__((weak)) void print_coredump() {} namespace esphome::logger { +// Zephyr 3.7 renamed z_arch_esf_t to struct arch_esf; the old name was later removed. +#if KERNEL_VERSION_NUMBER >= 0x030700 +using FatalErrorEsf = ::arch_esf; +#else +using FatalErrorEsf = z_arch_esf_t; +#endif + __attribute__((section(".noinit"))) struct { uint32_t magic; uint32_t reason; @@ -168,7 +175,7 @@ void Logger::dump_crash_() { } } -void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { +void k_sys_fatal_error_handler(unsigned int reason, const FatalErrorEsf *esf) { crash_buf.magic = App.get_config_hash(); crash_buf.reason = reason; if (esf) { @@ -192,7 +199,7 @@ void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { extern "C" { -void k_sys_fatal_error_handler(unsigned int reason, const z_arch_esf_t *esf) { +void k_sys_fatal_error_handler(unsigned int reason, const esphome::logger::FatalErrorEsf *esf) { esphome::logger::k_sys_fatal_error_handler(reason, esf); } } diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 8e91cff049..5ebf5b747f 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -438,7 +438,8 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("SERIAL", False, False) # disable stuff to make image smaller by default - zephyr_add_prj_conf("NCS_BOOT_BANNER", False, False) + if framework_ver >= cv.Version(2, 9, 2): + zephyr_add_prj_conf("NCS_BOOT_BANNER", False, False) zephyr_add_prj_conf("BOOT_BANNER", False, False) zephyr_add_prj_conf("PRINTK", False, False) zephyr_add_prj_conf("CONSOLE", False, False) @@ -495,8 +496,8 @@ def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]: types = [] UF2_PATH = "zephyr/zephyr.uf2" DFU_PATH = "firmware.zip" - HEX_PATH = "zephyr/zephyr.hex" # SDK 2.6.1, only generated when OTA is disabled - HEX_MERGED_PATH = "zephyr/merged.hex" # SDK 2.9.2, always generated + HEX_PATH = "zephyr/zephyr.hex" # SDK 2.6.1 without OTA, SDK 3.4.0+ + HEX_MERGED_PATH = "zephyr/merged.hex" # SDK 2.9.2 to 3.3.x, always generated APP_IMAGE_PATH = "zephyr/app_update.bin" build_dir = Path(storage_json.firmware_bin_path).parent if (build_dir / UF2_PATH).is_file(): @@ -968,6 +969,11 @@ def run_compile(args, config: ConfigType) -> bool: _LOGGER.info("Build inputs changed, cleaning %s", build_dir) rmtree(build_dir) + # SDK 3.4.0+ no longer generates merged.hex; drop one left by an older SDK + # build so it is never packaged or offered for download. + for stale_hex in (build_dir / "merged.hex", build_dir / "zephyr" / "merged.hex"): + stale_hex.unlink(missing_ok=True) + if pch_on: pch.log_pch_in_use() # Zephyr turns ccache on by itself when it is installed @@ -1012,13 +1018,18 @@ def run_compile(args, config: ConfigType) -> bool: 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(west_out / "zephyr.hex", zephyr_dir / "zephyr.hex") _copy_if_exists(build_dir / "merged.hex", zephyr_dir / "merged.hex") - # For Adafruit bootloader builds, regenerate the UF2 from merged.hex, - # whose records carry the correct flash addresses. The build's own - # zephyr.uf2 uses the board's default offset, which is wrong in some cases. - merged_hex = zephyr_dir / "merged.hex" - if bootloader in _UF2_FAMILY_IDS and merged_hex.is_file(): + # For Adafruit bootloader builds, regenerate the UF2 from a hex file. + # merged.hex carries the correct flash addresses; SDK 3.4.0+ no longer + # generates it, so use zephyr.hex there. Chosen by version so a merged.hex + # left by an older SDK build is never picked. + if framework_ver >= cv.Version(3, 4, 0): + hex_file = zephyr_dir / "zephyr.hex" + else: + hex_file = zephyr_dir / "merged.hex" + if bootloader in _UF2_FAMILY_IDS and hex_file.is_file(): # Drop the build's own wrong-offset UF2 so it isn't shipped alongside. app_uf2 = west_out / "zephyr.uf2" if app_uf2.is_file(): @@ -1035,12 +1046,12 @@ def run_compile(args, config: ConfigType) -> bool: "-c", "-o", str(zephyr_dir / "zephyr.uf2"), - str(merged_hex), + str(hex_file), ], env=env, stream_output=True, ): - raise EsphomeError("Failed to generate UF2 from merged hex") + raise EsphomeError(f"Failed to generate UF2 from {hex_file.name}") if bootloader in ( BOOTLOADER_ADAFRUIT, @@ -1048,9 +1059,6 @@ def run_compile(args, config: ConfigType) -> bool: BOOTLOADER_ADAFRUIT_NRF52_SD140_V6, BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, ): - # no fallback is needed for adafruit case. merged merged.hex is always generated. - # get_download_types needs fallback for mcuboot (non adafruit) - hex_file = zephyr_dir / "merged.hex" dfu_package = build_dir / "firmware.zip" genpkg_cmd = [ str(paths["python_executable"]), diff --git a/esphome/components/nrf52/framework.py b/esphome/components/nrf52/framework.py index 48af47e8ff..58c87619bf 100644 --- a/esphome/components/nrf52/framework.py +++ b/esphome/components/nrf52/framework.py @@ -28,6 +28,26 @@ _LOGGER = logging.getLogger(__name__) _REQUIREMENTS = Path(__file__).parent / "requirements.txt" TOOLCHAIN_VERSION = "0.17.4" +# Zephyr SDK used by nRF Connect SDK 3.4.0 and newer. +_TOOLCHAIN_VERSION_NCS_3_4_0 = "1.0.1" +_TOOLCHAIN_VERSIONS = (TOOLCHAIN_VERSION, _TOOLCHAIN_VERSION_NCS_3_4_0) + + +def _uses_sdk_ng_1_toolchain() -> bool: + """True when the framework needs Zephyr SDK 1.0+. + + SDK 1.0 moved the GNU toolchain under gnu/ and renamed its archive + to toolchain_gnu_*. + """ + return CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(3, 4, 0) + + +def _get_toolchain_version() -> str: + """Return the Zephyr SDK toolchain version for the current framework.""" + if _uses_sdk_ng_1_toolchain(): + return _TOOLCHAIN_VERSION_NCS_3_4_0 + return TOOLCHAIN_VERSION + # Packages the PlatformIO toolchain's Zephyr build script needs beyond west # (which comes from requirements.txt). Keep the pin in sync with @@ -40,6 +60,20 @@ SDK_NG_TOOLCHAIN_MIRRORS = str_to_lst_of_str( "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{VERSION}/toolchain_{sysname}-{machine}_arm-zephyr-eabi.{extension}", ) ) +_SDK_NG_TOOLCHAIN_GNU_MIRRORS = str_to_lst_of_str( + os.environ.get( + "ESPHOME_SDK_NG_TOOLCHAIN_GNU_MIRRORS", + "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v{VERSION}/toolchain_gnu_{sysname}-{machine}_arm-zephyr-eabi.{extension}", + ) +) + + +def _get_sdk_ng_toolchain_mirrors() -> list[str]: + """Return toolchain mirror URLs for the current framework version.""" + if _uses_sdk_ng_1_toolchain(): + return _SDK_NG_TOOLCHAIN_GNU_MIRRORS + return SDK_NG_TOOLCHAIN_MIRRORS + # Minimal SDK provides cmake discovery files (Zephyr-sdkConfig.cmake) and # host tools (dtc etc.) required by the Zephyr cmake build system. @@ -85,15 +119,22 @@ def _get_toolchain_path(version: str) -> Path: return get_sdk_nrf_tools_path() / "toolchains" / version -def toolchain_tool(name: str) -> Path: - """Path to one of the pinned Zephyr SDK's tools (objdump, readelf, ...). +def _get_arm_toolchain_path() -> Path: + """The arm-zephyr-eabi directory inside the pinned Zephyr SDK. - The single owner of the ``arm-zephyr-eabi/bin/arm-zephyr-eabi-`` - layout and the Windows suffix. + The single owner of the SDK 0.x (arm-zephyr-eabi/) and SDK 1.0+ + (gnu/arm-zephyr-eabi/) layouts. """ + toolchain_root = _get_toolchain_path(_get_toolchain_version()) + if _uses_sdk_ng_1_toolchain(): + return toolchain_root / "gnu" / "arm-zephyr-eabi" + return toolchain_root / "arm-zephyr-eabi" + + +def toolchain_tool(name: str) -> Path: + """Path to one of the pinned Zephyr SDK's tools (objdump, readelf, ...).""" suffix = ".exe" if os.name == "nt" else "" - bin_path = _get_toolchain_path(TOOLCHAIN_VERSION) / "arm-zephyr-eabi" / "bin" - return bin_path / f"arm-zephyr-eabi-{name}{suffix}" + return _get_arm_toolchain_path() / "bin" / f"arm-zephyr-eabi-{name}{suffix}" _SITECUSTOMIZE = """\ @@ -169,7 +210,7 @@ def get_build_env() -> dict: # "Zephyr-sdk_DIR" environment hint proved unreliable here: containerized # non-root builds failed to locate the SDK with it, while # ZEPHYR_SDK_INSTALL_DIR fixed the same invocation. - env["ZEPHYR_SDK_INSTALL_DIR"] = str(_get_toolchain_path(TOOLCHAIN_VERSION)) + env["ZEPHYR_SDK_INSTALL_DIR"] = str(_get_toolchain_path(_get_toolchain_version())) return env @@ -561,36 +602,41 @@ def _check_and_install(version: str) -> None: raise EsphomeError(f"Install Zephyr requirements for {version} failure") zephyr_sentinel.touch() - # Shared by every SDK version; locked only while missing - if not (_get_toolchain_path(TOOLCHAIN_VERSION) / ".ready").exists(): - with _install_lock(f"toolchain-{TOOLCHAIN_VERSION}"): + # Shared by every SDK version that uses the same toolchain; locked only + # while missing + toolchain_version = _get_toolchain_version() + if not (_get_toolchain_path(toolchain_version) / ".ready").exists(): + with _install_lock(f"toolchain-{toolchain_version}"): _install_toolchain() def _install_toolchain() -> None: - toolchains_dir = _get_toolchain_path(TOOLCHAIN_VERSION) + toolchain_version = _get_toolchain_version() + 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") + rmdir(toolchains_dir, msg=f"Clean up {toolchain_version} toolchain environment") sysname, machine, extension = _get_toolchain_platform_info() substitutions = { - "VERSION": TOOLCHAIN_VERSION, + "VERSION": toolchain_version, "sysname": sysname, "machine": machine, "extension": extension, } # Downloaded next to the destination (not a temp file) so an # interrupted download's .part file resumes on the next run. + # SDK 1.0+ Zephyr-sdkConfig.cmake looks for the toolchain in + # gnu/arm-zephyr-eabi/; extraction strips the archive's single root. for mirrors, extract_dir, what, slug in ( (SDK_NG_MINIMAL_MIRRORS, toolchains_dir, "Zephyr SDK minimal", "minimal"), ( - SDK_NG_TOOLCHAIN_MIRRORS, - toolchains_dir / "arm-zephyr-eabi", + _get_sdk_ng_toolchain_mirrors(), + _get_arm_toolchain_path(), "toolchain", "toolchain", ), ): - _LOGGER.info("Downloading %s %s ...", TOOLCHAIN_VERSION, what) + _LOGGER.info("Downloading %s %s ...", toolchain_version, what) download_and_extract( mirrors, substitutions, @@ -598,10 +644,17 @@ def _install_toolchain() -> None: extract_dir, progress_header="Extracting", ) - # Best-effort prune of resume leftovers, including a previous - # TOOLCHAIN_VERSION's orphans; the SDK archives are hundreds of MB. - # A locked file must not discard the just-completed install. + # Best-effort prune of resume leftovers, including orphans of retired + # toolchain versions; the SDK archives are hundreds of MB. The other + # toolchain still in use may be downloading under its own lock, so its + # leftovers are kept. A locked file must not discard the just-completed + # install. + other_versions = tuple( + f"{v}." for v in _TOOLCHAIN_VERSIONS if v != toolchain_version + ) for leftover in toolchains_dir.parent.glob("*.archive.part*"): + if leftover.name.startswith(other_versions): + continue try: leftover.unlink() except OSError as err: diff --git a/tests/unit_tests/components/nrf52/test_pch.py b/tests/unit_tests/components/nrf52/test_pch.py index 5af55586c3..0dcac3cad4 100644 --- a/tests/unit_tests/components/nrf52/test_pch.py +++ b/tests/unit_tests/components/nrf52/test_pch.py @@ -7,6 +7,7 @@ import pytest from esphome.components import nrf52 from esphome.components.zephyr.const import KEY_BOARD +import esphome.config_validation as cv from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain from esphome.core import CORE, EsphomeError @@ -110,6 +111,7 @@ def run_cmd(tmp_path: Path) -> Mock: CORE.build_path = tmp_path / "build" CORE.name = "livingroom" CORE.toolchain = Toolchain.SDK_NRF + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: cv.Version(3, 2, 0)} with ( patch.object(nrf52, "check_and_install"), patch.object(nrf52, "_generate_cmake_lists", return_value=False), diff --git a/tests/unit_tests/components/nrf52/test_run_compile_hex.py b/tests/unit_tests/components/nrf52/test_run_compile_hex.py new file mode 100644 index 0000000000..f86a29a9d2 --- /dev/null +++ b/tests/unit_tests/components/nrf52/test_run_compile_hex.py @@ -0,0 +1,130 @@ +"""nrf52 sdk-nrf build: the hex file used for the Adafruit UF2 and DFU package.""" + +from collections.abc import Iterator +from pathlib import Path +from unittest.mock import Mock, patch + +import pytest + +from esphome.components import nrf52 +from esphome.components.nrf52.const import BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 +from esphome.components.zephyr.const import KEY_BOARD, KEY_BOOTLOADER +import esphome.config_validation as cv +from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION, Toolchain +from esphome.core import CORE + + +@pytest.fixture +def build_dir(tmp_path: Path) -> Path: + CORE.config_path = tmp_path / "test.yaml" + CORE.build_path = tmp_path / "build" + CORE.name = "livingroom" + CORE.toolchain = Toolchain.SDK_NRF + return CORE.relative_pioenvs_path(CORE.name) + + +@pytest.fixture +def run_cmd(tmp_path: Path) -> Iterator[Mock]: + with ( + patch.object(nrf52, "check_and_install"), + patch.object(nrf52, "_generate_cmake_lists", return_value=False), + patch.object( + nrf52, + "get_build_paths", + return_value={"python_executable": "python3", "framework_path": tmp_path}, + ), + patch.object(nrf52, "get_build_env", return_value={}), + patch.object( + nrf52, + "zephyr_data", + return_value={ + KEY_BOARD: "board", + KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT_NRF52_SD140_V7, + }, + ), + patch.object(nrf52.pch, "pch_enabled", return_value=False), + patch.object(nrf52, "run_command_ok", return_value=True) as run, + ): + yield run + + +def _uf2_hex(run_cmd: Mock) -> str: + cmd = next(c.args[0] for c in run_cmd.call_args_list if "-f" in c.args[0]) + return cmd[-1] + + +def _dfu_hex(run_cmd: Mock) -> str: + cmd = next(c.args[0] for c in run_cmd.call_args_list if "genpkg" in c.args[0]) + return cmd[cmd.index("--application") + 1] + + +def _build( + build_dir: Path, + version: cv.Version, + outputs: list[str], + run_cmd: Mock, + leftovers: tuple[str, ...] = (), +) -> None: + """Run run_compile; *outputs* appear when west builds, *leftovers* before.""" + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: version} + (build_dir / "zephyr" / "zephyr").mkdir(parents=True) + # Without a CMake cache run_compile wipes the build dir first + for name in ["CMakeCache.txt", *leftovers]: + (build_dir / name).write_text("") + + def west_build(cmd: list[str], **kwargs: object) -> bool: + if "west" in cmd: + for name in outputs: + (build_dir / name).write_text("") + return True + + run_cmd.side_effect = west_build + nrf52.run_compile(None, {}) + + +def test_uf2_and_dfu_use_merged_hex(build_dir: Path, run_cmd: Mock) -> None: + _build( + build_dir, + cv.Version(2, 9, 2), + ["merged.hex", "zephyr/zephyr/zephyr.hex"], + run_cmd, + ) + merged = str(build_dir / "zephyr" / "merged.hex") + assert _uf2_hex(run_cmd) == merged + assert _dfu_hex(run_cmd) == merged + + +def test_uf2_and_dfu_fall_back_to_zephyr_hex(build_dir: Path, run_cmd: Mock) -> None: + """The nRF Connect SDK 3.4.0 build produces no merged.hex.""" + _build(build_dir, cv.Version(3, 4, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd) + app_hex = build_dir / "zephyr" / "zephyr.hex" + assert app_hex.is_file() + assert _uf2_hex(run_cmd) == str(app_hex) + assert _dfu_hex(run_cmd) == str(app_hex) + + +def test_older_sdk_does_not_fall_back_to_zephyr_hex( + build_dir: Path, run_cmd: Mock +) -> None: + """Before SDK 3.4.0 a missing merged.hex must not be replaced by zephyr.hex.""" + _build(build_dir, cv.Version(3, 2, 0), ["zephyr/zephyr/zephyr.hex"], run_cmd) + assert not any("-f" in c.args[0] for c in run_cmd.call_args_list) + assert _dfu_hex(run_cmd) == str(build_dir / "zephyr" / "merged.hex") + + +def test_stale_merged_hex_is_removed_before_building( + build_dir: Path, run_cmd: Mock +) -> None: + """A merged.hex left by an older SDK build must not be packaged.""" + _build( + build_dir, + cv.Version(3, 4, 0), + ["zephyr/zephyr/zephyr.hex"], + run_cmd, + leftovers=("merged.hex", "zephyr/merged.hex"), + ) + assert not (build_dir / "merged.hex").exists() + assert not (build_dir / "zephyr" / "merged.hex").exists() + app_hex = str(build_dir / "zephyr" / "zephyr.hex") + assert _uf2_hex(run_cmd) == app_hex + assert _dfu_hex(run_cmd) == app_hex diff --git a/tests/unit_tests/test_nrf52_framework.py b/tests/unit_tests/test_nrf52_framework.py index d6b27bdbef..17d583d139 100644 --- a/tests/unit_tests/test_nrf52_framework.py +++ b/tests/unit_tests/test_nrf52_framework.py @@ -20,6 +20,7 @@ from esphome.components.nrf52.framework import ( _get_penv_site_packages, _get_platformio_penv_path, _get_toolchain_platform_info, + _install_toolchain, _needs_venv_rebuild, _wanted_west_projects, check_and_install, @@ -808,6 +809,63 @@ class TestCheckAndInstall: assert substitutions["machine"] == "x86_64" assert substitutions["extension"] == "tar.xz" + def test_toolchain_download_uses_gnu_url_for_sdk_3_4_0( + self, + tmp_path: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """For nRF Connect SDK >= 3.4.0 the toolchain archive name includes 'toolchain_gnu_'.""" + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse("3.4.0")} + sdk_version = "3.4.0" + tools = get_sdk_nrf_tools_path() + python_env = tools / "penvs" / f"v{sdk_version}" + framework = tools / "frameworks" / f"v{sdk_version}" + toolchain_dir = tools / "toolchains" / "1.0.1" + for d in (python_env, framework, toolchain_dir): + d.mkdir(parents=True, exist_ok=True) + (framework / "zephyr" / "scripts").mkdir(parents=True, exist_ok=True) + (framework / "zephyr" / "scripts" / "requirements.txt").touch() + _mark_venv_ready(python_env) + (framework / ".ready").touch() + + check_and_install() + + # Two download calls: minimal SDK first, toolchain second + toolchain_call = mock_nrf52_ops.download_from_mirrors.call_args_list[1] + mirrors = toolchain_call.args[0] + assert all("toolchain_gnu_" in m for m in mirrors) + + def test_toolchain_extracts_under_gnu_for_sdk_3_4_0( + self, + tmp_path: Path, + mock_nrf52_ops: SimpleNamespace, + ) -> None: + """SDK 1.0+ toolchain archive must land in gnu/arm-zephyr-eabi/. + + Zephyr-sdkConfig.cmake validates the toolchain at gnu/arm-zephyr-eabi/ + in SDK 1.0+; if the archive is extracted to arm-zephyr-eabi/ instead, + cmake reports the package as not found. + """ + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse("3.4.0")} + sdk_version = "3.4.0" + tools = get_sdk_nrf_tools_path() + python_env = tools / "penvs" / f"v{sdk_version}" + framework = tools / "frameworks" / f"v{sdk_version}" + toolchain_dir = tools / "toolchains" / "1.0.1" + for d in (python_env, framework, toolchain_dir): + d.mkdir(parents=True, exist_ok=True) + (framework / "zephyr" / "scripts").mkdir(parents=True, exist_ok=True) + (framework / "zephyr" / "scripts" / "requirements.txt").touch() + _mark_venv_ready(python_env) + (framework / ".ready").touch() + + check_and_install() + + # Two extract calls: minimal SDK first (to toolchain root), toolchain second + extract_calls = mock_nrf52_ops.archive_extract_all.call_args_list + _, toolchain_extract_dir = extract_calls[1].args[:2] + assert toolchain_extract_dir == toolchain_dir / "gnu" / "arm-zephyr-eabi" + # --------------------------------------------------------------------------- # setup_platformio_python_env tests @@ -1040,6 +1098,44 @@ def test_get_build_env( assert env["SOME_PREEXISTING_VAR"] == "kept" +def test_get_build_env_sdk_3_4_0_uses_toolchain_root( + setup_core: Path, +) -> None: + """For NCS >= 3.4.0, ZEPHYR_SDK_INSTALL_DIR still points at the toolchain root.""" + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse("3.4.0")} + + env = get_build_env() + + tools = get_sdk_nrf_tools_path() + assert env["ZEPHYR_SDK_INSTALL_DIR"] == str(tools / "toolchains" / "1.0.1") + assert "Zephyr-sdk_DIR" not in env + + +def test_install_toolchain_keeps_other_live_toolchain_leftovers( + setup_core: Path, +) -> None: + """Pruning must not delete a partial download of the other toolchain in use.""" + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse("3.4.0")} + toolchains = get_sdk_nrf_tools_path() / "toolchains" + toolchains.mkdir(parents=True) + own = toolchains / "1.0.1.toolchain.archive.part" + other = toolchains / f"{TOOLCHAIN_VERSION}.toolchain.archive.part" + retired = toolchains / "0.16.8.toolchain.archive.part" + for leftover in (own, other, retired): + leftover.write_text("") + + with patch( + "esphome.components.nrf52.framework.download_and_extract", + side_effect=lambda *args, **kwargs: args[3].mkdir(parents=True), + ): + _install_toolchain() + + assert not own.exists() + assert other.exists() + assert not retired.exists() + assert (toolchains / "1.0.1" / ".ready").exists() + + # --------------------------------------------------------------------------- # get_sdk_nrf_tools_path tests # --------------------------------------------------------------------------- diff --git a/tests/unit_tests/test_nrf52_toolchain.py b/tests/unit_tests/test_nrf52_toolchain.py index ee8f6eaf49..b09540bb80 100644 --- a/tests/unit_tests/test_nrf52_toolchain.py +++ b/tests/unit_tests/test_nrf52_toolchain.py @@ -24,6 +24,7 @@ def _set_sdk_version(version: cv.Version) -> None: def test_binutils_come_from_the_pinned_zephyr_sdk() -> None: + _set_sdk_version(cv.Version(2, 9, 0)) bin_path = ( get_sdk_nrf_tools_path() / "toolchains" @@ -37,6 +38,21 @@ def test_binutils_come_from_the_pinned_zephyr_sdk() -> None: assert toolchain.get_readelf_path() == bin_path / f"arm-zephyr-eabi-readelf{suffix}" +def test_binutils_use_new_toolchain_for_sdk_3_4_0() -> None: + _set_sdk_version(cv.Version(3, 4, 0)) + bin_path = ( + get_sdk_nrf_tools_path() + / "toolchains" + / "1.0.1" + / "gnu" + / "arm-zephyr-eabi" + / "bin" + ) + suffix = ".exe" if os.name == "nt" else "" + assert toolchain.get_objdump_path() == bin_path / f"arm-zephyr-eabi-objdump{suffix}" + assert toolchain.get_readelf_path() == bin_path / f"arm-zephyr-eabi-readelf{suffix}" + + def test_elf_uses_the_nested_layout_from_sdk_2_9_2(nrf52_build: Path) -> None: _set_sdk_version(cv.Version(2, 9, 2)) # A flat image left by an older SDK must not be picked over the current