From 8bd0f4ae1a4503baaa4c2acfb2e92b329b4a5618 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 17:29:44 +0200 Subject: [PATCH 1/8] builder.py: register app_bindir and exec_cwd for kernel-mode builds Kernel-mode CMake builds install applications to /bin. On qemu arm/riscv the target mounts them over a semihosting hostfs relative to the spawned process working directory. When the generated .config selects CONFIG_BUILD_KERNEL=y, register app_bindir=/bin and exec_cwd=, keeping values set in YAML. Signed-off-by: raiden00pl Assisted-by: Claude Code --- src/ntfc/builder.py | 17 ++++++++++++ tests/test_builder.py | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/ntfc/builder.py b/src/ntfc/builder.py index 6e104f5..fa8887b 100644 --- a/src/ntfc/builder.py +++ b/src/ntfc/builder.py @@ -458,6 +458,23 @@ def _build_core( cores[core]["elf_path"] = nuttx_elf_path cores[core]["conf_path"] = nuttx_conf_path + if self._is_kernel_config(nuttx_conf_path): + # kernel-mode: applications are installed to /bin + # and hostfs mounts resolve relative to the process cwd + cores[core].setdefault( + "app_bindir", os.path.join(build_path, "bin") + ) + cores[core].setdefault("exec_cwd", build_path) + + @staticmethod + def _is_kernel_config(conf_path: str) -> bool: + """Check if a generated .config selects a kernel build.""" + if not os.path.isfile(conf_path): + return False + + with open(conf_path, "r", encoding="utf-8") as f: + return any(line.strip() == "CONFIG_BUILD_KERNEL=y" for line in f) + def _reboot_core( self, core: str, cores: Dict[str, Any] ) -> None: # pragma: no cover diff --git a/tests/test_builder.py b/tests/test_builder.py index 1fb4ab8..ab64223 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -79,6 +79,70 @@ def test_builder_init(): ) +def test_builder_kernel_mode_core_config(tmp_path) -> None: + config = copy.deepcopy(conf_dir) + config["config"]["build_dir"] = str(tmp_path) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + + build_path = tmp_path / "product-xxx-dummy" + build_path.mkdir() + (build_path / ".config").write_text("CONFIG_BUILD_KERNEL=y\n") + + b = NuttXBuilder(config) + b._run_command = builder_run_command_dummy + b._make_dir = builder_make_dir_dummy + b.build_all() + + core = b.new_conf()["product"]["cores"]["core0"] + assert core["app_bindir"] == str(build_path / "bin") + assert core["exec_cwd"] == str(build_path) + + +def test_builder_kernel_mode_keeps_user_overrides(tmp_path) -> None: + config = copy.deepcopy(conf_dir) + config["config"]["build_dir"] = str(tmp_path) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + config["product"]["cores"]["core0"]["app_bindir"] = "/custom/bin" + config["product"]["cores"]["core0"]["exec_cwd"] = "/custom/cwd" + + build_path = tmp_path / "product-xxx-dummy" + build_path.mkdir() + (build_path / ".config").write_text("CONFIG_BUILD_KERNEL=y\n") + + b = NuttXBuilder(config) + b._run_command = builder_run_command_dummy + b._make_dir = builder_make_dir_dummy + b.build_all() + + core = b.new_conf()["product"]["cores"]["core0"] + assert core["app_bindir"] == "/custom/bin" + assert core["exec_cwd"] == "/custom/cwd" + + +def test_builder_flat_mode_no_kernel_keys(tmp_path) -> None: + config = copy.deepcopy(conf_dir) + config["config"]["build_dir"] = str(tmp_path) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + + build_path = tmp_path / "product-xxx-dummy" + build_path.mkdir() + (build_path / ".config").write_text("CONFIG_BUILD_FLAT=y\n") + + b = NuttXBuilder(config) + b._run_command = builder_run_command_dummy + b._make_dir = builder_make_dir_dummy + b.build_all() + + core = b.new_conf()["product"]["cores"]["core0"] + assert "app_bindir" not in core + assert "exec_cwd" not in core + + # missing .config (dummy build) is treated as a flat build + assert ( + NuttXBuilder._is_kernel_config(str(tmp_path / "nonexistent")) is False + ) + + def test_builder_passes_build_env() -> None: config = copy.deepcopy(conf_dir) config["config"]["build_env"] = {"CC": "gcc-13", "CXX": "g++-13"} From fe17d950d9a7d8dc0570a4a24f473a14eeb4a344 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 17:40:25 +0200 Subject: [PATCH 2/8] coreconfig.py: dispatch cmd_check to application binaries With app_bindir configured, cmd_check resolves commands against the application binary directory first and falls back to symbol lookups over bin_debug for names that are not applications (NSH builtins like cmd_df, cmocka entries). Flat-mode behavior is unchanged. Signed-off-by: raiden00pl Assisted-by: Claude Code --- Documentation/writing-test-cases.rst | 6 +++- src/ntfc/coreconfig.py | 34 +++++++++++++++++++-- src/ntfc/testfilter.py | 4 +-- tests/test_coreconfig.py | 44 ++++++++++++++++++++++++++++ tests/test_filtertest.py | 2 +- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/Documentation/writing-test-cases.rst b/Documentation/writing-test-cases.rst index 7637292..1f3a30e 100644 --- a/Documentation/writing-test-cases.rst +++ b/Documentation/writing-test-cases.rst @@ -192,7 +192,11 @@ Execute NSH command and verify output: Decorators: -- ``@pytest.mark.cmd_check("symbol_name")``: Verify ELF symbol exists +- ``@pytest.mark.cmd_check("symbol_name")``: Verify ELF symbol exists. + On kernel-mode targets (``CONFIG_BUILD_KERNEL=y``) the marker is first + matched against application file names (a trailing ``_main`` maps to + the file name, so ``hello_main`` matches the ``hello`` binary) and + then against symbols in the unstripped application binaries - ``@pytest.mark.dep_config("CONFIG_X", "CONFIG_Y")``: Skip if configs not enabled diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index 7026170..bb88e50 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -21,8 +21,10 @@ """Product core configuration handler.""" import os +from functools import cached_property from typing import Any, Dict, Optional, Union +from ntfc.lib.elf.app_bindir import AppBinDir, symbol_patterns from ntfc.lib.elf.elf_parser import ElfParser @@ -211,10 +213,36 @@ def kv_check(self, cfg: str) -> Any: return self._kv_values.get(cfg, False) + @cached_property + def _appbin(self) -> Optional[AppBinDir]: + """Return the installed kernel-mode application binaries.""" + bindir = self.app_bindir + if not bindir or not os.path.isdir(bindir): + return None + + return AppBinDir(bindir) + + @property + def has_app_bindir(self) -> bool: + """Return True when kernel-mode application binaries are found.""" + return self._appbin is not None + def cmd_check(self, cmd: str, core: int = 0) -> bool: - """Check if command is available in binary.""" + """Check if command is available in binary. + + Kernel-mode cores resolve commands against the application binary + directory; otherwise the symbol must exist in the core ELF. + """ + if self._appbin: + if self._appbin.has_command(cmd): + return True + # not an application: NSH builtins and test entry points + # are symbols inside the application binaries + return self._appbin.has_symbol(cmd) + if not self._elf: raise AttributeError("no elf data") - symbol_name = f"{cmd}_main" if "cmocka" in cmd else cmd - return self._elf.has_symbol(symbol_name) + return any( + self._elf.has_symbol(symbol) for symbol in symbol_patterns(cmd) + ) diff --git a/src/ntfc/testfilter.py b/src/ntfc/testfilter.py index 155c882..00d5bab 100644 --- a/src/ntfc/testfilter.py +++ b/src/ntfc/testfilter.py @@ -85,12 +85,12 @@ def check_test_support( reason = f"Required config '{d}' not enabled" break - # command available in ELF + # command available on the target if skip is False: for c in cmd: if self._config.cmd_check(c) is False: skip = True - reason = f"Required symbol '{c}' not found in ELF" + reason = f"Required command '{c}' not available" break # check extra parameters diff --git a/tests/test_coreconfig.py b/tests/test_coreconfig.py index 082d3f0..9eb666e 100644 --- a/tests/test_coreconfig.py +++ b/tests/test_coreconfig.py @@ -18,6 +18,8 @@ # ############################################################################ +import shutil + import pytest from ntfc.coreconfig import CoreConfig @@ -164,6 +166,48 @@ def test_core_config_app_bindir(tmp_path): } assert CoreConfig(conf).app_bindir is None + # a derived directory that does not exist is not a command source + conf = { + "name": "t", + "conf_path": str(kernel_cfg), + "elf_path": "./tests/resources/nuttx/sim/nuttx", + } + core_conf = CoreConfig(conf) + assert core_conf.app_bindir == "./tests/resources/nuttx/sim/bin" + assert core_conf.has_app_bindir is False + + +def test_core_config_cmd_check_kernel_mode(tmp_path): + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text("CONFIG_BUILD_KERNEL=y\n") + bindir = tmp_path / "bin" + bindir.mkdir() + (bindir / "hello").write_bytes(b"\x7fELF" + b"\x00" * 12) + + conf = { + "name": "t", + "conf_path": str(kernel_cfg), + "app_bindir": str(bindir), + } + + p = CoreConfig(conf) + assert p.has_app_bindir is True + # command resolution by application file name, no ELF configured + assert p.cmd_check("hello") is True + assert p.cmd_check("hello_main") is True + assert p.cmd_check("cmd_df") is False + + # symbol fallback over unstripped binaries in bin_debug + debug = tmp_path / "bin_debug" + debug.mkdir() + shutil.copy("./tests/resources/nuttx/sim/nuttx", debug / "sh") + + p = CoreConfig(conf) + assert p.cmd_check("cmd_df") is True + assert p.cmd_check("missing|cmd_df") is True + assert p.cmd_check("cmd_d.*") is True + assert p.cmd_check("no_such_symbol_xyz") is False + def test_core_config_read_poll_interval() -> None: assert CoreConfig({"name": "test"}).read_poll_interval == 0.1 diff --git a/tests/test_filtertest.py b/tests/test_filtertest.py index e7df74b..cf85ce6 100644 --- a/tests/test_filtertest.py +++ b/tests/test_filtertest.py @@ -57,7 +57,7 @@ def test_filterest_filter(): f.extract_test_requirements = mock_extract_test_requirements2 skip, reason = f.check_test_support(None) assert skip is True - assert reason == "Required symbol 'CMD1' not found in ELF" + assert reason == "Required command 'CMD1' not available" config.kv_check.return_value = True config.cmd_check.return_value = True From ef17f7a798c091d20aca77c15d8b798632c7b3fd Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 17:42:22 +0200 Subject: [PATCH 3/8] core.py: route runtime check_cmd through config on kernel builds Runtime checks searched the core ELF or the NSH help output, neither of which lists kernel-mode applications. Route check_cmd through CoreConfig.cmd_check when the core is a kernel build with a known application directory, so runtime checks agree with collection-time filtering. Signed-off-by: raiden00pl Assisted-by: Claude Code --- src/ntfc/core.py | 57 ++++++++++++++++++++-------------------------- tests/test_core.py | 36 +++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/src/ntfc/core.py b/src/ntfc/core.py index 239b541..bc58dd1 100644 --- a/src/ntfc/core.py +++ b/src/ntfc/core.py @@ -28,6 +28,7 @@ Any, List, Optional, + Pattern, Tuple, Union, ) @@ -528,66 +529,58 @@ def start(self) -> None: self._device.start() def check_cmd(self, cmd_pattern: str) -> bool: - """Check if a command pattern is available in the ELF binary. + """Check if a command pattern is available on the core. - This method validates whether a specific command or set of - commands is present in the core's ELF binary by searching for - corresponding symbols. It supports both single commands and - alternative command patterns separated by '|'. + Commands are resolved from the core configuration when it knows + the application binaries, from the device ELF parser or the + shell help output otherwise. It supports both single commands + and alternative command patterns separated by '|'. :param cmd_pattern: Command pattern to check for. Can contain alternatives separated by '|' (e.g., 'test1|test2') :return: True if the command pattern is found, False otherwise - - Note: - - Requires ELF parser to be available in device - - Supports alternative patterns with '|' - - Used to validate core capabilities before executing """ - # Check if device supports command checking + # Kernel-mode: resolve against the application binary directory + # so runtime checks agree with collection-time filtering + if self._conf.has_app_bindir: + return self._conf.cmd_check(cmd_pattern) + + # Devices that expose their own ELF parser resolve the command + # from its symbols if hasattr(self._device, "elf_parser") and self._device.elf_parser: logger.debug(f"Checking command pattern: {cmd_pattern}") - # Split by '|' to support alternative patterns - alternatives = ( - cmd_pattern.split("|") if "|" in cmd_pattern else [cmd_pattern] - ) - - for pattern in alternatives: + for pattern in cmd_pattern.split("|"): # For cmocka tests, append _main to symbol name - symbol_pattern_str: str = ( + symbol_str = ( f"{pattern}_main" if "cmocka" in pattern else pattern ) # Support regex wildcards - if ".*" in symbol_pattern_str: - symbol_pattern = re.compile(symbol_pattern_str) - else: - tmp = symbol_pattern_str - symbol_pattern = tmp # type: ignore[assignment] + symbol: Union[str, Pattern[str]] = ( + re.compile(symbol_str) + if ".*" in symbol_str + else symbol_str + ) - if self._device.elf_parser.has_symbol(symbol_pattern): + if self._device.elf_parser.has_symbol(symbol): return True return False - # Fallback: try to execute the command and check if it exists + # Fallback: check the command against the shell help output logger.warning( "ELF parser not available, trying command check for: " f"{cmd_pattern}" ) - # Send 'help' command to check if command exists result = self.sendCommandReadUntilPattern("help", timeout=5) if result.status == CmdStatus.SUCCESS: - # Check if any of the command alternatives are in the help output - alternatives = ( - cmd_pattern.split("|") if "|" in cmd_pattern else [cmd_pattern] + return any( + pattern.lower() in result.output.lower() + for pattern in cmd_pattern.split("|") ) - for pattern in alternatives: - if pattern.lower() in result.output.lower(): - return True return False diff --git a/tests/test_core.py b/tests/test_core.py index 838513d..ef67281 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -585,8 +585,6 @@ def test_core_check_cmd_with_elf_parser(envconfig_dummy): # Test regex wildcard pattern mock_elf_parser.has_symbol.side_effect = None mock_elf_parser.has_symbol.return_value = True - import re - assert p.check_cmd("test.*") is True # The pattern should be compiled as regex call_args = mock_elf_parser.has_symbol.call_args @@ -595,3 +593,37 @@ def test_core_check_cmd_with_elf_parser(envconfig_dummy): # Test all alternatives not found mock_elf_parser.has_symbol.return_value = False assert p.check_cmd("nonexistent1|nonexistent2") is False + + # Test with failed help command + dev.send_cmd_read_until_pattern.return_value = CmdReturn( + CmdStatus.TIMEOUT + ) + assert p.check_cmd("test") is False + + +def test_core_check_cmd_kernel_mode(tmp_path): + """Test check_cmd resolves via app_bindir on kernel builds.""" + from ntfc.coreconfig import CoreConfig + + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text("CONFIG_BUILD_KERNEL=y\n") + bindir = tmp_path / "bin" + bindir.mkdir() + (bindir / "hello").write_bytes(b"\x7fELF" + b"\x00" * 12) + + conf = CoreConfig( + { + "name": "t", + "conf_path": str(kernel_cfg), + "app_bindir": str(bindir), + } + ) + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + p = ProductCore(mockdevice.return_value, conf) + + # resolved from the application directory, device is not queried + assert p.check_cmd("hello") is True + assert p.check_cmd("hello_main") is True + assert p.check_cmd("nonexistent") is False + mockdevice.return_value.send_cmd_read_until_pattern.assert_not_called() From 2a32c08055d21186d5a32f2a6d9c3f91f2643445 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 17:50:28 +0200 Subject: [PATCH 4/8] device: resolve image paths before spawning with a custom exec_cwd With exec_cwd set, QEMU resolved a relative '-kernel' path against the spawn directory instead of the NTFC working directory it is expressed in, so boot timed out. Absolutize the image path in the qemu and sim start paths when a custom spawn directory is configured. Signed-off-by: raiden00pl Assisted-by: Claude Code --- src/ntfc/device/host.py | 12 ++++++++++++ src/ntfc/device/qemu.py | 5 +---- src/ntfc/device/sim.py | 6 +----- tests/device/test_qemu.py | 26 ++++++++++++++++++++++++++ tests/device/test_sim.py | 30 ++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/ntfc/device/host.py b/src/ntfc/device/host.py index d632387..e188e65 100644 --- a/src/ntfc/device/host.py +++ b/src/ntfc/device/host.py @@ -61,6 +61,18 @@ def pid(self) -> Optional[int]: """Return the spawned child PID, or ``None`` before start.""" return self._child.pid if self._child else None + def _image_path(self) -> str: + """Return the image path, absolute when spawning in another cwd. + + Relative paths are relative to the NTFC working directory, not + to the spawn directory. + """ + elf = self._conf.elf_path + if not elf: + raise IOError + + return os.path.abspath(elf) if self._cwd else str(elf) + def _dev_is_health_priv(self) -> bool: """Check if the host device is OK.""" if not self._child: diff --git a/src/ntfc/device/qemu.py b/src/ntfc/device/qemu.py index a655d26..a565003 100644 --- a/src/ntfc/device/qemu.py +++ b/src/ntfc/device/qemu.py @@ -44,12 +44,9 @@ def __init__(self, conf: "CoreConfig"): def _start_impl(self) -> None: """Start QEMU emulator implementation.""" - elf = self._conf.elf_path + elf = self._image_path() exec_path = self._conf.exec_path exec_args = self._conf.exec_args - - if not elf: - raise IOError if not exec_path: raise KeyError("no exec_path in configuration file!") diff --git a/src/ntfc/device/sim.py b/src/ntfc/device/sim.py index d4ebeb5..8da475f 100644 --- a/src/ntfc/device/sim.py +++ b/src/ntfc/device/sim.py @@ -44,11 +44,7 @@ def __init__(self, conf: "CoreConfig"): def _start_impl(self) -> None: """Start sim emulator implementation.""" - elf = self._conf.elf_path - if not elf: - raise IOError - - cmd = [elf] + cmd = [self._image_path()] uptime = self._conf.uptime # open host-based emulation diff --git a/tests/device/test_qemu.py b/tests/device/test_qemu.py index c99741e..008fd91 100644 --- a/tests/device/test_qemu.py +++ b/tests/device/test_qemu.py @@ -18,6 +18,7 @@ # ############################################################################ +import os from unittest.mock import patch import pytest @@ -89,6 +90,7 @@ def test_device_qemu_open(): config.exec_path = "" config.exec_args = "" config.elf_path = "" + config.exec_cwd = None qemu = DeviceQemu(config) @@ -175,3 +177,27 @@ def host_open_dummy4(cmd, uptime): config.uptime = 3 qemu.start() + + +def test_device_qemu_exec_cwd_absolute_image(tmp_path): + + from ntfc.coreconfig import CoreConfig + + config = CoreConfig( + { + "name": "t", + "device": "qemu", + "exec_path": "qemu-system-riscv64", + "exec_args": "-nographic", + "exec_cwd": str(tmp_path), + } + ) + # relative image path, resolved against the NTFC cwd at spawn time + config._config["elf_path"] = "some/image" + + qemu = DeviceQemu(config) + cmds = [] + qemu.host_open = lambda cmd, uptime: cmds.append(cmd) + qemu.start() + + assert cmds[0][2] == "-kernel " + os.path.abspath("some/image") diff --git a/tests/device/test_sim.py b/tests/device/test_sim.py index b4397ae..0d76ce6 100644 --- a/tests/device/test_sim.py +++ b/tests/device/test_sim.py @@ -139,3 +139,33 @@ def send(self, data): sent.clear() sim._write(b"abc\n") assert sent == [b"abc\n"] + + +def test_device_sim_exec_cwd_absolute_image(tmp_path): + + import os + + from ntfc.coreconfig import CoreConfig + + config = CoreConfig( + {"name": "t", "device": "sim", "exec_cwd": str(tmp_path)} + ) + # relative image path, resolved against the NTFC cwd at spawn time + config._config["elf_path"] = "some/image" + + sim = DeviceSim(config) + cmds = [] + sim.host_open = lambda cmd, uptime: cmds.append(cmd) + sim.start() + + assert cmds[0] == [os.path.abspath("some/image")] + + # without exec_cwd the image path is passed through unchanged + config = CoreConfig({"name": "t", "device": "sim"}) + config._config["elf_path"] = "some/image" + + sim = DeviceSim(config) + sim.host_open = lambda cmd, uptime: cmds.append(cmd) + sim.start() + + assert cmds[1] == ["some/image"] From ef48669de76e948a22d96bf965e69bc6f897e737 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 18:01:45 +0200 Subject: [PATCH 5/8] builder.py: add application image support for kernel-mode flashing Real hardware has no hostfs, so kernel-mode applications must be shipped as a filesystem image. Add the per-core apps_image option, which generates a ROMFS image from app_bindir with genromfs after the build, and the $APPS_BINDIR and $APPS_IMG flash command placeholders. Signed-off-by: raiden00pl Assisted-by: Claude Code --- Documentation/config-yaml.rst | 9 ++++ Documentation/config.yaml | 3 ++ src/ntfc/builder.py | 67 ++++++++++++++++++++++--- tests/test_builder.py | 93 +++++++++++++++++++++++++++++++++++ 4 files changed, 165 insertions(+), 7 deletions(-) diff --git a/Documentation/config-yaml.rst b/Documentation/config-yaml.rst index 8383404..57f4f1f 100644 --- a/Documentation/config-yaml.rst +++ b/Documentation/config-yaml.rst @@ -381,6 +381,10 @@ Flash command can use special tags that are handled by NTFC: - ``$IMAGE_BIN`` is replaced by path to ``nuttx.bin``. - ``$IMAGE_HEX`` is replaced by path to ``nuttx.hex``. - ``$IMAGE_ELF`` is replaced by path to the core image (``elf_path``). +- ``$APPS_BINDIR`` is replaced by the application binaries directory + (kernel-mode builds). +- ``$APPS_IMG`` is replaced by the generated application filesystem + image (requires ``apps_image``). Example usage with ``st-flash`` tool: @@ -466,6 +470,11 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`. - (Optional) Directory with kernel-mode application binaries. Defaults to the ``bin/`` directory next to the NuttX ELF for kernel-mode builds (``CONFIG_BUILD_KERNEL=y``) + * - ``apps_image`` + - (Optional) Generate a filesystem image with the application + binaries after build, e.g. ``apps_image: {type: romfs}``. The + image path is available as ``$APPS_IMG`` in the ``flash`` + command. Requires ``genromfs`` and a kernel-mode build * - ``defconfig`` - Path to NuttX defconfig (auto-build) * - ``elf_path`` diff --git a/Documentation/config.yaml b/Documentation/config.yaml index b091634..a0d6f90 100644 --- a/Documentation/config.yaml +++ b/Documentation/config.yaml @@ -77,6 +77,9 @@ product: # many products can be supported in tests (pro app_bindir: '' # (optional) directory with kernel-mode application binaries. # Defaults to the bin/ directory next to the NuttX ELF # for kernel-mode builds. + apps_image: # (optional) generate a filesystem image with the application + type: romfs # binaries after build (kernel-mode only, requires genromfs). + # The image path is available as $APPS_IMG in 'flash'. # NTFC can use pre-build image or build it from defconfig # the behavior will depend on the parameters specified in config. diff --git a/src/ntfc/builder.py b/src/ntfc/builder.py index fa8887b..f99820c 100644 --- a/src/ntfc/builder.py +++ b/src/ntfc/builder.py @@ -40,6 +40,9 @@ class NuttXBuilder: IMAGE_BIN_STR = "$IMAGE_BIN" IMAGE_HEX_STR = "$IMAGE_HEX" IMAGE_ELF_STR = "$IMAGE_ELF" + APPS_BINDIR_STR = "$APPS_BINDIR" + APPS_IMG_STR = "$APPS_IMG" + APPS_IMG_NAME = "apps.romfs.img" _KCONFIG_DISABLED_RE = re.compile( r"^#\s+(CONFIG_[A-Za-z0-9_]+)\s+is not set" ) @@ -466,6 +469,62 @@ def _build_core( ) cores[core].setdefault("exec_cwd", build_path) + self._make_apps_image(cores[core], build_path) + + def _make_apps_image( + self, core_cfg: Dict[str, Any], build_path: str + ) -> None: + """Generate a filesystem image with application binaries. + + Enabled with the per-core ``apps_image`` option; the image path + is registered as ``apps_img`` for the ``$APPS_IMG`` flash + placeholder. + """ + img_cfg = core_cfg.get("apps_image", None) + if not img_cfg: + return + + if not isinstance(img_cfg, dict): + raise BuilderConfigError("apps_image must be a mapping") + + img_type = img_cfg.get("type", "romfs") + if img_type != "romfs": + raise BuilderConfigError( + f"unsupported apps_image type: {img_type}" + ) + + bindir = core_cfg.get("app_bindir", None) + if not bindir: + raise BuilderConfigError( + "apps_image requires app_bindir (kernel-mode build)" + ) + + tool = shutil.which("genromfs") + if not tool: + raise BuilderConfigError("genromfs not found in PATH") + + img_path = os.path.join(build_path, self.APPS_IMG_NAME) + self._run_command([tool, "-f", img_path, "-d", bindir], env=None) + core_cfg["apps_img"] = img_path + + def _expand_flash_cmd( + self, flash_cmd: str, core_cfg: Dict[str, Any] + ) -> str: + """Expand image placeholders in a flash command.""" + parent = Path(core_cfg["elf_path"]).parent + values = { + self.IMAGE_BIN_STR: str(parent / "nuttx.bin"), + self.IMAGE_HEX_STR: str(parent / "nuttx.hex"), + self.IMAGE_ELF_STR: core_cfg["elf_path"], + self.APPS_BINDIR_STR: core_cfg.get("app_bindir", ""), + self.APPS_IMG_STR: core_cfg.get("apps_img", ""), + } + + for placeholder, value in values.items(): + flash_cmd = flash_cmd.replace(placeholder, value) + + return flash_cmd + @staticmethod def _is_kernel_config(conf_path: str) -> bool: """Check if a generated .config selects a kernel build.""" @@ -491,13 +550,7 @@ def _flash_core( """Flash single core image.""" flash_cmd = cores[core].get("flash", None) if flash_cmd: - img_path = Path(cores[core]["elf_path"]) - image_hex = str(img_path.parent) + "/nuttx.hex" - image_bin = str(img_path.parent) + "/nuttx.bin" - - flash_cmd = flash_cmd.replace(self.IMAGE_BIN_STR, image_bin) - flash_cmd = flash_cmd.replace(self.IMAGE_HEX_STR, image_hex) - flash_cmd = flash_cmd.replace(self.IMAGE_ELF_STR, str(img_path)) + flash_cmd = self._expand_flash_cmd(flash_cmd, cores[core]) cmd = flash_cmd.split() diff --git a/tests/test_builder.py b/tests/test_builder.py index ab64223..1dcb0fe 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -143,6 +143,99 @@ def test_builder_flat_mode_no_kernel_keys(tmp_path) -> None: ) +def test_builder_expand_flash_cmd() -> None: + b = NuttXBuilder(copy.deepcopy(conf_dir)) + core_cfg = { + "elf_path": "bbb/core/nuttx", + "app_bindir": "bbb/core/bin", + "apps_img": "bbb/core/apps.romfs.img", + } + + cmd = b._expand_flash_cmd( + "flash $IMAGE_BIN $IMAGE_HEX $APPS_BINDIR $APPS_IMG", core_cfg + ) + assert cmd == ( + "flash bbb/core/nuttx.bin bbb/core/nuttx.hex " + "bbb/core/bin bbb/core/apps.romfs.img" + ) + + # placeholders without registered values expand to empty strings + cmd = b._expand_flash_cmd( + "flash $APPS_BINDIR $APPS_IMG", {"elf_path": "bbb/core/nuttx"} + ) + assert cmd == "flash " + + +def test_builder_apps_image(tmp_path, monkeypatch) -> None: + config = copy.deepcopy(conf_dir) + config["config"]["build_dir"] = str(tmp_path) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + config["product"]["cores"]["core0"]["apps_image"] = {"type": "romfs"} + + build_path = tmp_path / "product-xxx-dummy" + build_path.mkdir() + (build_path / ".config").write_text("CONFIG_BUILD_KERNEL=y\n") + + calls = [] + + def run_command_capture(cmd, env): + calls.append(cmd) + + monkeypatch.setattr( + "ntfc.builder.shutil.which", lambda tool: f"/usr/bin/{tool}" + ) + + b = NuttXBuilder(config) + b._run_command = run_command_capture + b._make_dir = builder_make_dir_dummy + b.build_all() + + core = b.new_conf()["product"]["cores"]["core0"] + img_path = str(build_path / "apps.romfs.img") + assert core["apps_img"] == img_path + assert calls[-1] == [ + "/usr/bin/genromfs", + "-f", + img_path, + "-d", + str(build_path / "bin"), + ] + + +def test_builder_apps_image_errors(tmp_path, monkeypatch) -> None: + def make_builder(config_txt, apps_image): + config = copy.deepcopy(conf_dir) + config["config"]["build_dir"] = str(tmp_path) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + config["product"]["cores"]["core0"]["apps_image"] = apps_image + + build_path = tmp_path / "product-xxx-dummy" + build_path.mkdir(exist_ok=True) + (build_path / ".config").write_text(config_txt) + + b = NuttXBuilder(config) + b._run_command = builder_run_command_dummy + b._make_dir = builder_make_dir_dummy + return b + + # unsupported image type + with pytest.raises(BuilderConfigError): + make_builder("CONFIG_BUILD_KERNEL=y\n", {"type": "vfat"}).build_all() + + # not a mapping + with pytest.raises(BuilderConfigError): + make_builder("CONFIG_BUILD_KERNEL=y\n", "romfs").build_all() + + # no application directory (flat build) + with pytest.raises(BuilderConfigError): + make_builder("CONFIG_BUILD_FLAT=y\n", {"type": "romfs"}).build_all() + + # genromfs not installed + monkeypatch.setattr("ntfc.builder.shutil.which", lambda tool: None) + with pytest.raises(BuilderConfigError): + make_builder("CONFIG_BUILD_KERNEL=y\n", {"type": "romfs"}).build_all() + + def test_builder_passes_build_env() -> None: config = copy.deepcopy(conf_dir) config["config"]["build_env"] = {"CC": "gcc-13", "CXX": "g++-13"} From e05f4ca8ad8c9d0001c800bf81cc41465175faab Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 18:04:32 +0200 Subject: [PATCH 6/8] core.py: runtime command discovery fallback via target PATH listing Prebuilt kernel-mode images have no host application directory, so check_cmd could not resolve commands. List CONFIG_PATH_INITIAL (default /system/bin) once on the running target, cache it, and match cmd_check patterns against it with the matcher shared with AppBinDir. A failed listing is not cached and is retried. Signed-off-by: raiden00pl Assisted-by: Claude Code --- src/ntfc/core.py | 68 ++++++++++++++++++++++++------------ src/ntfc/coreconfig.py | 5 +++ tests/test_core.py | 78 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 21 deletions(-) diff --git a/src/ntfc/core.py b/src/ntfc/core.py index bc58dd1..9de18fc 100644 --- a/src/ntfc/core.py +++ b/src/ntfc/core.py @@ -28,7 +28,6 @@ Any, List, Optional, - Pattern, Tuple, Union, ) @@ -36,6 +35,7 @@ from ntfc.command_builder import CommandBuilder from ntfc.coreconfig import CoreConfig from ntfc.device.common import CmdReturn, CmdStatus +from ntfc.lib.elf.app_bindir import match_command, symbol_patterns from ntfc.log.logger import logger if TYPE_CHECKING: @@ -73,6 +73,8 @@ class CoreStatus(_Enum): class ProductCore: """This class implements product core under test.""" + _PATH_NAME_RE = re.compile(r"[A-Za-z0-9_.+-]+") + def __init__( self, device: "DeviceCommon", @@ -100,6 +102,7 @@ def __init__( list(ignored_cores) if ignored_cores is not None else ["dsp"] ) self._builder = CommandBuilder(device.prompt, device.no_cmd) + self._runtime_cmds: Optional[List[str]] = None self._prompt = device.prompt self._main_prompt = self._prompt @@ -528,17 +531,47 @@ def start(self) -> None: """Start device.""" self._device.start() + def _check_cmd_runtime(self, cmd_pattern: str) -> bool: + """Check a command against the target PATH listing. + + The listing is fetched once from the running target and cached; + a failed listing is not cached so it is retried on next use. + """ + if self._runtime_cmds is None: + result = self.sendCommandReadUntilPattern( + f"ls {self._conf.path_initial}", timeout=5 + ) + if result.status != CmdStatus.SUCCESS: + return False + + output_lower = result.output.lower() + no_cmd = str(self._device.no_cmd).lower() + if no_cmd in output_lower or any( + line.lstrip().lower().startswith("ls:") + for line in result.output.splitlines() + ): + return False + + # drop the command echo line; path headers and the prompt + # do not match the name pattern + tokens = " ".join(result.output.splitlines()[1:]).split() + self._runtime_cmds = [ + token + for token in tokens + if self._PATH_NAME_RE.fullmatch(token) + ] + + return match_command(cmd_pattern, self._runtime_cmds) + def check_cmd(self, cmd_pattern: str) -> bool: """Check if a command pattern is available on the core. - Commands are resolved from the core configuration when it knows - the application binaries, from the device ELF parser or the - shell help output otherwise. It supports both single commands - and alternative command patterns separated by '|'. + Commands are resolved from the application binaries, the running + target, the device ELF parser or the shell help output, in that + order. - :param cmd_pattern: Command pattern to check for. Can contain - alternatives separated by '|' - (e.g., 'test1|test2') + :param cmd_pattern: Command pattern, may contain alternatives + separated by '|' (e.g. 'test1|test2') :return: True if the command pattern is found, False otherwise """ # Kernel-mode: resolve against the application binary directory @@ -546,24 +579,17 @@ def check_cmd(self, cmd_pattern: str) -> bool: if self._conf.has_app_bindir: return self._conf.cmd_check(cmd_pattern) + if self._conf.is_kernel_build: + # prebuilt image without host binaries: discover the + # command set once from the running target + return self._check_cmd_runtime(cmd_pattern) + # Devices that expose their own ELF parser resolve the command # from its symbols if hasattr(self._device, "elf_parser") and self._device.elf_parser: logger.debug(f"Checking command pattern: {cmd_pattern}") - for pattern in cmd_pattern.split("|"): - # For cmocka tests, append _main to symbol name - symbol_str = ( - f"{pattern}_main" if "cmocka" in pattern else pattern - ) - - # Support regex wildcards - symbol: Union[str, Pattern[str]] = ( - re.compile(symbol_str) - if ".*" in symbol_str - else symbol_str - ) - + for symbol in symbol_patterns(cmd_pattern): if self._device.elf_parser.has_symbol(symbol): return True diff --git a/src/ntfc/coreconfig.py b/src/ntfc/coreconfig.py index bb88e50..ed06769 100644 --- a/src/ntfc/coreconfig.py +++ b/src/ntfc/coreconfig.py @@ -227,6 +227,11 @@ def has_app_bindir(self) -> bool: """Return True when kernel-mode application binaries are found.""" return self._appbin is not None + @property + def path_initial(self) -> str: + """Return the initial target PATH, see CONFIG_PATH_INITIAL.""" + return str(self.kv_check("CONFIG_PATH_INITIAL") or "/system/bin") + def cmd_check(self, cmd: str, core: int = 0) -> bool: """Check if command is available in binary. diff --git a/tests/test_core.py b/tests/test_core.py index ef67281..7439831 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -627,3 +627,81 @@ def test_core_check_cmd_kernel_mode(tmp_path): assert p.check_cmd("hello_main") is True assert p.check_cmd("nonexistent") is False mockdevice.return_value.send_cmd_read_until_pattern.assert_not_called() + + +def test_core_check_cmd_kernel_runtime_fallback(tmp_path, monkeypatch): + """Test check_cmd discovers commands from the running target.""" + from ntfc.coreconfig import CoreConfig + + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text( + 'CONFIG_BUILD_KERNEL=y\nCONFIG_PATH_INITIAL="/system/bin"\n' + ) + # a kernel core whose application binaries are not on the host: + # the command set has to come from the running target + conf = CoreConfig( + { + "name": "t", + "conf_path": str(kernel_cfg), + "elf_path": "./tests/resources/nuttx/sim/nuttx", + } + ) + assert conf.has_app_bindir is False + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + p = ProductCore(mockdevice.return_value, conf) + + calls = [] + status = [CmdStatus.TIMEOUT] + + def fake_send(cmd, pattern=None, args=None, timeout=30): + calls.append(cmd) + output = "ls /system/bin\n/system/bin:\n hello\n init\n sh\nnsh> " + return CmdReturn(status[0], output=output) + + monkeypatch.setattr(p, "sendCommandReadUntilPattern", fake_send) + + # listing failure is not cached + assert p.check_cmd("hello") is False + assert len(calls) == 1 + + status[0] = CmdStatus.SUCCESS + assert p.check_cmd("hello") is True + assert p.check_cmd("hello_main") is True + assert p.check_cmd("missing|sh") is True + assert p.check_cmd("missing") is False + + # target was listed once after the failed attempt + assert calls == ["ls /system/bin", "ls /system/bin"] + + +def test_core_check_cmd_kernel_runtime_does_not_cache_ls_error( + tmp_path, monkeypatch +): + """Do not interpret words from an ls error as target commands.""" + from ntfc.coreconfig import CoreConfig + + kernel_cfg = tmp_path / "kv_config" + kernel_cfg.write_text("CONFIG_BUILD_KERNEL=y\n") + conf = CoreConfig({"name": "t", "conf_path": str(kernel_cfg)}) + + with patch("ntfc.device.common.DeviceCommon") as mockdevice: + p = ProductCore(mockdevice.return_value, conf) + calls = [] + + def fake_send(cmd, pattern=None, args=None, timeout=30): + calls.append(cmd) + return CmdReturn( + CmdStatus.SUCCESS, + output=( + "ls /system/bin\n" + "ls: /system/bin: No such file or directory\n" + "nsh> " + ), + ) + + monkeypatch.setattr(p, "sendCommandReadUntilPattern", fake_send) + + assert p.check_cmd("No") is False + assert p.check_cmd("file") is False + assert calls == ["ls /system/bin", "ls /system/bin"] From c48ea37da95cad4f340b077667de03ae227d2e20 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 20:51:08 +0200 Subject: [PATCH 7/8] builder.py: reconfigure after applying Kconfig overrides CMake registers application targets at configure time from .config, but kv overrides are applied after configuring, so an override enabling a new application changed .config without ever creating its build target: code-level options took effect through the .config -> config.h rule while the application never appeared in the image. Overrides can also unlock suboptions (e.g. *_PROGNAME) whose missing defaults make cmake drop the application silently. Run olddefconfig and configure again after applying overrides. Signed-off-by: raiden00pl Assisted-by: Claude Code --- src/ntfc/builder.py | 22 ++++++++++++------ tests/test_builder.py | 52 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/src/ntfc/builder.py b/src/ntfc/builder.py index f99820c..22d10b1 100644 --- a/src/ntfc/builder.py +++ b/src/ntfc/builder.py @@ -24,6 +24,7 @@ import re import shutil import subprocess +from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional @@ -351,7 +352,6 @@ def _run_build( "--build", str(build_path), ] - run_env = os.environ.copy() if env: run_env.update(env) # pragma: no cover @@ -431,8 +431,8 @@ def _build_core( if not already_build or self._rebuild: # pragma: no cover self._log_kconfig_overrides(kv_overrides) - # configure build - self._run_cmake( + configure = partial( + self._run_cmake, source=nuttx_dir, build=build_path, generator="Ninja", @@ -440,20 +440,28 @@ def _build_core( env=build_env, ) + # configure build + configure() + # apply Kconfig overrides to generated .config before build self._apply_kconfig_overrides( nuttx_conf_path, kv_overrides, cfg_cwd ) - # Regenerate include/nuttx/config.h after changing .config. - # Otherwise CMake can relink an image built with stale Kconfig - # values while the saved .config claims the override applied. - if kv_overrides: + # fill in defaults of suboptions the overrides + # unlocked (e.g. *_PROGNAME): applications are + # silently dropped at configure when these are + # missing from .config self._run_build_target( build_path, "olddefconfig", env=build_env ) + # application targets are registered at configure + # time from .config: configure again so overrides + # that enable new applications take effect + configure() + # build self._run_build(build_path, env=build_env) diff --git a/tests/test_builder.py b/tests/test_builder.py index 1dcb0fe..cd5f7a0 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -143,6 +143,35 @@ def test_builder_flat_mode_no_kernel_keys(tmp_path) -> None: ) +def test_builder_reconfigures_after_kv_overrides(monkeypatch) -> None: + config = copy.deepcopy(conf_dir) + config["product"]["cores"]["core0"]["defconfig"] = "dummy/path" + config["product"]["cores"]["core0"]["kv"] = {"CONFIG_SYSTEM_X": "y"} + + calls = [] + + def run_command_capture(cmd, env): + calls.append(cmd) + + b = NuttXBuilder(config) + b._run_command = run_command_capture + b._make_dir = builder_make_dir_dummy + monkeypatch.setattr( + b, "_apply_kconfig_overrides", lambda *args, **kwargs: None + ) + + b.build_all() + + # application targets are registered at configure time: overrides + # require an olddefconfig (defaults of unlocked suboptions) and a + # second configure before the build + assert [cmd[0] for cmd in calls] == ["cmake"] * 4 + assert calls[1][-1] == "olddefconfig" + assert calls[0][:2] == calls[2][:2] + assert calls[3][:2] == ["cmake", "--build"] + assert "olddefconfig" not in calls[3] + + def test_builder_expand_flash_cmd() -> None: b = NuttXBuilder(copy.deepcopy(conf_dir)) core_cfg = { @@ -283,7 +312,8 @@ def test_builder_regenerates_config_after_kconfig_overrides() -> None: "--target", "olddefconfig", ] - assert calls[2] == ["cmake", "--build", "bbb/product-xxx-dummy"] + assert calls[0][:2] == calls[2][:2] + assert calls[3] == ["cmake", "--build", "bbb/product-xxx-dummy"] def test_builder_run_build_target_passes_env() -> None: @@ -655,11 +685,14 @@ def test_builder_applies_kv_before_build() -> None: def run_command_capture(cmd, env): calls.append(cmd) if "--build" not in cmd: + # cmake generates .config only when it does not exist yet expected_build_path.mkdir(parents=True, exist_ok=True) - expected_conf_path.write_text( - "# CONFIG_TEST_BOOL is not set\n" "CONFIG_TEST_STR=old\n", - encoding="utf-8", - ) + if not expected_conf_path.exists(): + expected_conf_path.write_text( + "# CONFIG_TEST_BOOL is not set\n" + "CONFIG_TEST_STR=old\n", + encoding="utf-8", + ) else: cfg_text = expected_conf_path.read_text(encoding="utf-8") assert "CONFIG_TEST_BOOL=y\n" in cfg_text @@ -679,11 +712,12 @@ def fake_apply_with_tool(conf_path, overrides, _cfg_cwd): with patch("ntfc.builder.logger.info", side_effect=logs.append): b.build_all() - assert len(calls) == 3 + # configure, olddefconfig and reconfigure after overrides, build + assert len(calls) == 4 assert calls[0][0] == "cmake" - assert calls[1][:2] == ["cmake", "--build"] - assert calls[1][-2:] == ["--target", "olddefconfig"] - assert calls[2][:2] == ["cmake", "--build"] + assert calls[1][-1] == "olddefconfig" + assert calls[2][0] == "cmake" + assert calls[3][:2] == ["cmake", "--build"] assert any( "Applying Kconfig overrides before build:" == msg for msg in logs ) From 0b7cfd4db9529e37213da391dfc0c4721b302457 Mon Sep 17 00:00:00 2001 From: raiden00pl Date: Tue, 4 Aug 2026 17:31:45 +0200 Subject: [PATCH 8/8] config: add kernel-mode reference configs add kernel-mode reference configs Signed-off-by: raiden00pl Assisted-by: Claude Code --- config/nuttx-qemu-armv7a-knsh.yaml | 32 +++++++++++++++++ config/nuttx-qemu-armv8a-knsh.yaml | 32 +++++++++++++++++ config/nuttx-qemu-intel64-knsh.yaml | 39 +++++++++++++++++++++ config/nuttx-qemu-riscv-rv-virt-knsh64.yaml | 31 ++++++++++++++++ config/nuttx-serial-knsh.yaml.example | 33 +++++++++++++++++ 5 files changed, 167 insertions(+) create mode 100644 config/nuttx-qemu-armv7a-knsh.yaml create mode 100644 config/nuttx-qemu-armv8a-knsh.yaml create mode 100644 config/nuttx-qemu-intel64-knsh.yaml create mode 100644 config/nuttx-qemu-riscv-rv-virt-knsh64.yaml create mode 100644 config/nuttx-serial-knsh.yaml.example diff --git a/config/nuttx-qemu-armv7a-knsh.yaml b/config/nuttx-qemu-armv7a-knsh.yaml new file mode 100644 index 0000000..aa431fd --- /dev/null +++ b/config/nuttx-qemu-armv7a-knsh.yaml @@ -0,0 +1,32 @@ +# NuttX kernel-mode (CONFIG_BUILD_KERNEL=y) target for QEMU armv7a. +# +# The CMake build installs application binaries to /bin and the +# builder sets exec_cwd= for kernel-mode cores, so the hostfs +# mount data 'fs=.' maps /system to the build directory and +# /system/bin/init resolves to /bin/init. Semihosting is +# required for the hostfs mount. + +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-armv7a-knsh" + cores: + core0: + name: 'main' + device: 'qemu' + exec_path: 'qemu-system-arm' + exec_args: '-semihosting -cpu cortex-a7 -nographic + -machine virt,highmem=off,virtualization=off,gic-version=2 + -chardev stdio,id=con,mux=on -serial chardev:con + -mon chardev=con,mode=readline' + defconfig: 'boards/arm/qemu/qemu-armv7a/configs/knsh' + boot_timeout: 15 + kv: + CONFIG_INIT_MOUNT_DATA: "fs=." + # required by the arch/os ostest test case + CONFIG_SYSTEM_SETLOGMASK: "y" + # required by the arch/os heap stability test case + CONFIG_TESTING_HEAP: "y" diff --git a/config/nuttx-qemu-armv8a-knsh.yaml b/config/nuttx-qemu-armv8a-knsh.yaml new file mode 100644 index 0000000..c54e37f --- /dev/null +++ b/config/nuttx-qemu-armv8a-knsh.yaml @@ -0,0 +1,32 @@ +# NuttX kernel-mode (CONFIG_BUILD_KERNEL=y) target for QEMU armv8a. +# +# The CMake build installs application binaries to /bin and the +# builder sets exec_cwd= for kernel-mode cores, so the hostfs +# mount data 'fs=.' maps /system to the build directory and +# /system/bin/init resolves to /bin/init. Semihosting is +# required for the hostfs mount. + +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-armv8a-knsh" + cores: + core0: + name: 'main' + device: 'qemu' + exec_path: 'qemu-system-aarch64' + exec_args: '-semihosting -cpu cortex-a53 -nographic + -machine virt,virtualization=on,gic-version=3 + -net none -chardev stdio,id=con,mux=on + -serial chardev:con -mon chardev=con,mode=readline' + defconfig: 'boards/arm64/qemu/qemu-armv8a/configs/knsh' + boot_timeout: 15 + kv: + CONFIG_INIT_MOUNT_DATA: "fs=." + # required by the arch/os ostest test case + CONFIG_SYSTEM_SETLOGMASK: "y" + # required by the arch/os heap stability test case + CONFIG_TESTING_HEAP: "y" diff --git a/config/nuttx-qemu-intel64-knsh.yaml b/config/nuttx-qemu-intel64-knsh.yaml new file mode 100644 index 0000000..95b727f --- /dev/null +++ b/config/nuttx-qemu-intel64-knsh.yaml @@ -0,0 +1,39 @@ +# NuttX kernel-mode (CONFIG_BUILD_KERNEL=y) target for QEMU intel64. +# +# The knsh_romfs configuration loads user-space applications from a +# ROMFS image linked into the kernel; there is no hostfs, so exec_cwd +# is not relevant for the mount. Application command discovery uses the +# /bin directory registered by the builder. +# +# Requires NuttX with CMake kernel-build support for x86_64 (branch +# fix-x86_64-cmake-kernel-build in the vendored checkout: arch_interface +# guard, CMAKE_LD, relocatable binary install, board ROMFS generation). + +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-intel64-knsh" + cores: + core0: + name: 'main' + device: 'qemu' + exec_path: 'qemu-system-x86_64' + exec_args: '-m 2G -cpu host -enable-kvm -nographic -serial mon:stdio' + defconfig: 'boards/x86_64/qemu/qemu-intel64/configs/knsh_romfs' + boot_timeout: 15 + kv: + # resolve bare command names against the ROMFS mount point + CONFIG_LIBC_ENVPATH: "y" + CONFIG_PATH_INITIAL: "/system/bin" + # required by the arch/os ostest test case + CONFIG_SYSTEM_SETLOGMASK: "y" + # required by the arch/os heap stability test case + CONFIG_TESTING_HEAP: "y" + # setlocale()/nl_langinfo(), referenced by the LTP strftime cases + CONFIG_LIBC_LOCALE: "y" + # without it SIGKILL is absent from the default action table and + # can be ignored, which the LTP sigignore cases check for + CONFIG_SIG_SIGKILL_ACTION: "y" diff --git a/config/nuttx-qemu-riscv-rv-virt-knsh64.yaml b/config/nuttx-qemu-riscv-rv-virt-knsh64.yaml new file mode 100644 index 0000000..08f4df1 --- /dev/null +++ b/config/nuttx-qemu-riscv-rv-virt-knsh64.yaml @@ -0,0 +1,31 @@ +# NuttX kernel-mode (CONFIG_BUILD_KERNEL=y) reference target. +# +# The knsh64 configuration is an S-mode build: QEMU boots its bundled +# OpenSBI firmware (no '-bios none') and NTFC appends '-kernel '. +# +# The CMake build installs application binaries to /bin and the +# builder sets exec_cwd= for kernel-mode cores, so the hostfs +# mount data 'fs=.' maps /system to the build directory and +# /system/bin/init resolves to /bin/init. + +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc-rv-virt-knsh64" + cores: + core0: + name: 'main' + device: 'qemu' + exec_path: 'qemu-system-riscv64' + exec_args: '-semihosting -M virt,aclint=on -cpu rv64 -smp 1 -nographic' + defconfig: 'boards/risc-v/qemu-rv/rv-virt/configs/knsh64' + boot_timeout: 15 + kv: + CONFIG_INIT_MOUNT_DATA: "fs=." + # required by the arch/os ostest test case + CONFIG_SYSTEM_SETLOGMASK: "y" + # required by the arch/os heap stability test case + CONFIG_TESTING_HEAP: "y" diff --git a/config/nuttx-serial-knsh.yaml.example b/config/nuttx-serial-knsh.yaml.example new file mode 100644 index 0000000..db4b083 --- /dev/null +++ b/config/nuttx-serial-knsh.yaml.example @@ -0,0 +1,33 @@ +# Template for a kernel-mode (CONFIG_BUILD_KERNEL=y) hardware target +# on a serial console. Copy, rename and fill in the board specifics. +# +# Kernel-mode hardware notes: +# - There is no hostfs on hardware: application binaries must reach the +# target filesystem. Either the board defconfig bakes a ROMFS into the +# kernel image, or 'apps_image' generates one from /bin and the +# 'flash' command writes it with $APPS_IMG. +# - When NTFC builds the image, command discovery uses /bin. For +# prebuilt images (no 'defconfig'), commands are discovered once from +# the running target by listing CONFIG_PATH_INITIAL. +# - Kernel boot (mount filesystem, load init ELF) plus a bootloader can +# exceed the 5 second default boot wait; raise 'boot_timeout'. + +config: + cwd: './external' + build_dir: './build' + +product: + + name: "ntfc--knsh" + cores: + core0: + name: 'main' + device: 'serial' + exec_path: '/dev/ttyUSB0' + exec_args: '115200,n,8,1' + defconfig: 'boards////configs/knsh' + boot_timeout: 30 + apps_image: + type: romfs + flash: ' write $IMAGE_BIN $APPS_IMG ' + reboot: ''