This is an automated email from the ASF dual-hosted git repository.

xiaoxiang781216 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nuttx-ntfc.git

commit 38c204b1966fdea7f193a3b447f9b54224a344fd
Author: raiden00pl <[email protected]>
AuthorDate: Tue Aug 4 18:01:45 2026 +0200

    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 <[email protected]>
    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 @@ class NuttXBuilder:
                 )
                 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 @@ class NuttXBuilder:
         """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"}

Reply via email to