Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Documentation/config-yaml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ configuration section:
config:
cwd: './external'
build_dir: './build' # Build output directory
nuttx_dir: './external/nuttx' # Optional explicit NuttX tree
apps_dir: './external/nuttx-apps' # Optional explicit apps tree
build_env: # Optional env vars for cmake configure/build
CC: gcc-14
CXX: g++-14
Expand Down Expand Up @@ -350,6 +352,7 @@ 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``).

Example usage with ``st-flash`` tool:

Expand Down Expand Up @@ -414,6 +417,8 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`.
- Human-readable core name
* - ``device``
- Device type: ``sim``, ``qemu``, or ``serial``
* - ``os``
- Target shell type: ``nuttx`` (default) or ``linux``
* - ``exec_path``
- QEMU executable name or serial port device (``/dev/ttyACM0``, ``COM1``,
etc.)
Expand All @@ -426,6 +431,9 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`.
* - ``boot_timeout``
- (Optional) Seconds to wait for the first shell prompt after device
start. Defaults to ``5``
* - ``read_poll_interval``
- (Optional) Console polling interval in seconds used when reading
device output. Must be positive. Defaults to ``0.1``
* - ``app_bindir``
- (Optional) Directory with kernel-mode application binaries. Defaults
to the ``bin/`` directory next to the NuttX ELF for kernel-mode
Expand Down Expand Up @@ -455,3 +463,34 @@ These fields are parsed by :class:`ntfc.coreconfig.CoreConfig`.
* - ``kv``
- Per-core Kconfig overrides applied before build. Overrides matching
keys from global ``config.kv``

Linux Targets
=============

Setting ``os: linux`` on a core switches the shell abstraction from NuttX
to Linux: shell prompt (``#`` by default), command-not-found marker,
``poweroff``/``reboot``/``uname`` commands and kernel crash signatures.
This allows running the same test suites against Linux and NuttX, which is
useful for comparing the two systems (e.g. benchmarks).

Linux images are pre-built, so NTFC does not build them: point ``elf_path``
at the kernel image and pass boot arguments via ``exec_args`` (with the
``$IMAGE_ELF`` placeholder) or a ``flash`` command. ELF symbol parsing and
NuttX core topology discovery are skipped for Linux cores, which also means
the ``cmd_check`` pytest marker is not supported on them.

Example QEMU Linux core:

.. code-block:: yaml

cores:
core0:
name: 'linux'
os: 'linux'
device: 'qemu'
exec_path: 'qemu-system-x86_64'
exec_args: '-M q35 -m 2G -nographic -kernel $IMAGE_ELF
-initrd ./initramfs.img -append "console=ttyS0"'
elf_path: './bzImage'
prompt: '# '
boot_timeout: 60
44 changes: 43 additions & 1 deletion src/ntfc/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class NuttXBuilder:

IMAGE_BIN_STR = "$IMAGE_BIN"
IMAGE_HEX_STR = "$IMAGE_HEX"
IMAGE_ELF_STR = "$IMAGE_ELF"
_KCONFIG_DISABLED_RE = re.compile(
r"^#\s+(CONFIG_[A-Za-z0-9_]+)\s+is not set"
)
Expand Down Expand Up @@ -100,6 +101,12 @@ def _get_cmake_defines(
{str(key): str(val) for key, val in custom_defines.items()}
)

apps_dir = self._cfg_values.get("config", {}).get("apps_dir")
if apps_dir:
defines["NUTTX_APPS_DIR"] = os.path.abspath(
os.path.expanduser(os.path.expandvars(str(apps_dir)))
)

return defines

def _get_kconfig_overrides(
Expand Down Expand Up @@ -348,6 +355,20 @@ def _run_build(

self._run_command(cmd, env=run_env)

def _run_build_target(
self,
build: str,
target: str,
env: Optional[Dict[str, str]] = None,
) -> None:
"""Run one named CMake build target."""
cmd = ["cmake", "--build", str(Path(build)), "--target", target]
run_env = os.environ.copy()
if env:
run_env.update(env)

self._run_command(cmd, env=run_env)

def _build_core(
self, core: str, cores: Dict[str, Any], product: str
) -> None:
Expand All @@ -371,13 +392,24 @@ def _build_core(
if not cfg_cwd: # pragma: no cover
raise BuilderConfigError("not found cwd in YAML configuration")

cfg_build_dir = os.path.expanduser(
os.path.expandvars(str(cfg_build_dir))
)
build_path = os.path.join(cfg_build_dir, build_dir)
build_cfg = cores[core]["defconfig"]
logger.info(
f"build image " f"conf: {build_cfg}, out: {build_path}"
)

nuttx_dir = os.path.join(cfg_cwd, "nuttx")
cfg_cwd = os.path.expanduser(os.path.expandvars(str(cfg_cwd)))
configured_nuttx_dir = self._cfg_values["config"].get("nuttx_dir")
nuttx_dir = (
os.path.expanduser(
os.path.expandvars(str(configured_nuttx_dir))
)
if configured_nuttx_dir
else os.path.join(cfg_cwd, "nuttx")
)
nuttx_elf_path = os.path.join(build_path, "nuttx")
nuttx_conf_path = os.path.join(build_path, ".config")

Expand Down Expand Up @@ -410,6 +442,15 @@ def _build_core(
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:
self._run_build_target(
build_path, "olddefconfig", env=build_env
)

# build
self._run_build(build_path, env=build_env)

Expand Down Expand Up @@ -439,6 +480,7 @@ def _flash_core(

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))

cmd = flash_cmd.split()

Expand Down
2 changes: 1 addition & 1 deletion src/ntfc/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def get_core_info(self) -> Tuple[str, ...]:

def init(self) -> None:
"""Finish product initialization."""
cores = self.get_core_info()
cores = self.get_core_info() if self._conf.os == "nuttx" else ()
self._core0 = cores[0] if cores else "core0"
self._cur_core = self._core0
self._cores = cores if cores else ("core0",)
Expand Down
15 changes: 14 additions & 1 deletion src/ntfc/coreconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(self, cfg: Dict[str, Any]) -> None:
self._load_core_config()

elf_path = self._config.get("elf_path", None)
if elf_path:
if elf_path and self.os == "nuttx":
# load ELF
self._elf = ElfParser(elf_path)

Expand Down Expand Up @@ -86,11 +86,24 @@ def uptime(self) -> Any:
"""Return core uptime."""
return self._config.get("uptime", 3)

@property
def read_poll_interval(self) -> float:
"""Return console polling interval in seconds."""
value = float(self._config.get("read_poll_interval", 0.1))
if value <= 0:
raise ValueError("read_poll_interval must be positive")
return value

@property
def device(self) -> Any:
"""Return core device."""
return self._config.get("device", None)

@property
def os(self) -> str:
"""Return target operating system name."""
return str(self._config.get("os", "nuttx")).lower()

@property
def name(self) -> Any:
"""Return core name."""
Expand Down
4 changes: 2 additions & 2 deletions src/ntfc/device/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def __init__(self, conf: "CoreConfig", echo: bool = True):
)
self.clear_fault_flags()

self._read_all_sleep = 0.1
self._read_all_sleep = conf.read_poll_interval
self._has_echo = echo
self._start_time: Optional[float] = None
self._output_tail_buf = bytearray()
Expand Down Expand Up @@ -273,7 +273,7 @@ def _read_until_pattern_loop( # noqa: C901
ret = CmdStatus.TIMEOUT

while True:
chunk = self._read_all(0.1)
chunk = self._read_all(self._read_all_sleep)
output += chunk
self._console_log(chunk)

Expand Down
10 changes: 9 additions & 1 deletion src/ntfc/device/getos.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from typing import TYPE_CHECKING

from .linux import DeviceLinux
from .nuttx import DeviceNuttx

if TYPE_CHECKING:
Expand All @@ -36,4 +37,11 @@

def get_os(conf: "CoreConfig") -> "OSCommon":
"""Get OS abstraction."""
return DeviceNuttx(conf) # only NuttX supported now
operating_systems = {
"linux": DeviceLinux,
"nuttx": DeviceNuttx,
}
factory = operating_systems.get(conf.os)
if factory is None:
raise ValueError(f"unsupported operating system: {conf.os}")
return factory(conf)
87 changes: 87 additions & 0 deletions src/ntfc/device/linux.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
############################################################################
# SPDX-License-Identifier: Apache-2.0
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership. The
# ASF licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
############################################################################

"""Linux shell abstraction for serial and QEMU test devices."""

from typing import TYPE_CHECKING, Dict, List

from ntfc.device.state import CrashType

from .oscommon import OSCommon

if TYPE_CHECKING:
from ntfc.coreconfig import CoreConfig


class DeviceLinux(OSCommon):
"""Describe the Linux shell contract used by NTFC devices."""

_PROMPT = b"#"
_CRASH_SIGNATURES: Dict[CrashType, List[bytes]] = {
CrashType.PANIC: [
b"Kernel panic - not syncing",
b"not syncing: Fatal exception",
],
CrashType.ASSERTION: [b"BUG: unable to handle kernel"],
}

def __init__(self, conf: "CoreConfig") -> None:
"""Initialize the Linux shell abstraction."""
self._prompt = conf.prompt.encode() if conf.prompt else self._PROMPT

@property
def prompt(self) -> bytes:
"""Get shell prompt."""
return self._prompt

@property
def no_cmd(self) -> str:
"""Get command-not-found marker."""
return "not found"

@property
def help_cmd(self) -> bytes:
"""Get shell help command."""
return b"help"

@property
def poweroff_cmd(self) -> bytes:
"""Get shell poweroff command."""
return b"poweroff"

@property
def reboot_cmd(self) -> bytes:
"""Get shell reboot command."""
return b"reboot"

@property
def uname_cmd(self) -> bytes:
"""Get operating-system identification command."""
return b"uname -s"

@property
def crash_signatures(self) -> Dict[CrashType, List[bytes]]:
"""Get Linux kernel crash signatures."""
return self._CRASH_SIGNATURES

@property
def panic_char(self) -> str:
"""Linux does not define a console panic character here."""
return ""
26 changes: 24 additions & 2 deletions tests/device/test_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@

class DeviceMock(DeviceCommon):

def __init__(self, _):
def __init__(self, config):
"""Mock."""

DeviceCommon.__init__(self, _)
if not isinstance(config.os, str):
config.os = "nuttx"
if not isinstance(config.read_poll_interval, (int, float)):
config.read_poll_interval = 0.1
DeviceCommon.__init__(self, config)

def _read(self, _=0):
"""Mock."""
Expand Down Expand Up @@ -101,6 +105,11 @@ def test_device_common_init():
assert d.busyloop is False
assert d.flood is False

config.os = "nuttx"
config.read_poll_interval = 0.01
explicit = DeviceMock(config)
assert explicit._read_all_sleep == 0.01


def test_device_common_send_cmd_pattern():

Expand Down Expand Up @@ -223,6 +232,19 @@ def test_device_common_read_until_pattern():
dev.read_until_pattern("PASS", 10)


def test_device_common_read_until_pattern_uses_configured_poll_interval():
with patch("ntfc.envconfig.EnvConfig") as mockdevice:
config = mockdevice.return_value
config.read_poll_interval = 0.001
dev = DeviceMock(config)

with patch.object(dev, "_read_all", return_value=b"PASS") as read_all:
ret = dev.read_until_pattern(b"PASS", 1)

assert ret.status == CmdStatus.SUCCESS
read_all.assert_called_once_with(0.001)


def test_device_common_panic_char():

with patch("ntfc.device.common.get_os") as mock_get_os:
Expand Down
Loading