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 1922d76d523af8140559be652f9f6532508b00da Author: raiden00pl <[email protected]> AuthorDate: Tue Aug 4 17:38:14 2026 +0200 lib/elf: add AppBinDir for kernel-mode application discovery Kernel builds install every application as a standalone ELF binary, so the command name is the file name; entry points are renamed to main and flat-mode symbols like hello_main do not exist. AppBinDir lists the application directory as the command set, supports the cmd_check marker syntax (alternatives, regex, trailing _main), and falls back to symbol lookups over the unstripped binaries in bin_debug for names that are not files (NSH builtins, cmocka entries). Signed-off-by: raiden00pl <[email protected]> Assisted-by: Claude Code --- src/ntfc/lib/elf/app_bindir.py | 137 +++++++++++++++++++++++++++++++++++++++++ tests/lib/test_app_bindir.py | 103 +++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) diff --git a/src/ntfc/lib/elf/app_bindir.py b/src/ntfc/lib/elf/app_bindir.py new file mode 100644 index 0000000..8f6e845 --- /dev/null +++ b/src/ntfc/lib/elf/app_bindir.py @@ -0,0 +1,137 @@ +############################################################################ +# 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. +# +############################################################################ + +"""Kernel-mode application binary directory handler. + +In kernel builds every application is a standalone ELF binary installed +to the target PATH, so the command name is the file name; entry points +are renamed to ``main`` and symbols like ``hello_main`` do not exist. +""" + +import os +import re +from functools import cached_property +from typing import Collection, Iterator, List, Optional, Pattern, Set, Union + +from ntfc.lib.elf.elf_parser import ElfParser + +_MAIN_SUFFIX = "_main" + + +def _match_one(name: str, commands: List[str]) -> bool: + """Match a single name against a list of command names.""" + if ".*" in name: + regex = re.compile(name) + return any(regex.fullmatch(cmd) for cmd in commands) + return name in commands + + +def match_command(pattern: str, commands: List[str]) -> bool: + """Match a cmd_check pattern against a list of command names. + + Accepts ``a|b`` alternatives and ``.*`` regex patterns. A trailing + ``_main`` is stripped so flat-mode symbol markers (e.g. + ``hello_main``) match the ``hello`` command. + """ + return any( + _match_one(name, commands) + for alt in pattern.split("|") + for name in (alt, alt.removesuffix(_MAIN_SUFFIX)) + ) + + +def symbol_patterns(pattern: str) -> Iterator[Union[str, Pattern[str]]]: + """Yield normalized ELF symbol patterns for a cmd_check expression.""" + for alternative in pattern.split("|"): + symbol = ( + f"{alternative}{_MAIN_SUFFIX}" + if "cmocka" in alternative + else alternative + ) + yield re.compile(symbol) if ".*" in symbol else symbol + + +def match_symbol(pattern: str, symbols: Collection[str]) -> bool: + """Match a cmd_check expression against ELF symbol names.""" + for candidate in symbol_patterns(pattern): + if isinstance(candidate, str): + if candidate in symbols: + return True + elif any(candidate.search(symbol) for symbol in symbols): + return True + + return False + + +class AppBinDir: + """Application binary directory of a kernel-mode build.""" + + #: sibling directory with the unstripped application binaries + DEBUG_DIR_NAME = "bin_debug" + + def __init__(self, bindir: str, debug_bindir: Optional[str] = None): + """Initialize application binary directory handler. + + :param bindir: directory with application binaries + :param debug_bindir: unstripped binaries for symbol lookups, + the sibling ``bin_debug`` by default + """ + self._bindir = bindir + self._debug_bindir = debug_bindir or os.path.join( + os.path.dirname(bindir), self.DEBUG_DIR_NAME + ) + + @staticmethod + def _scan(dirpath: Optional[str]) -> List[ElfParser]: + """Return a parser for every ELF file in a directory.""" + if not dirpath or not os.path.isdir(dirpath): + return [] + + parsers = [] + for name in sorted(os.listdir(dirpath)): + try: + parsers.append(ElfParser(os.path.join(dirpath, name))) + except AttributeError: + # not an ELF file, e.g. a script or a subdirectory + continue + + return parsers + + @cached_property + def commands(self) -> List[str]: + """Return available command names (application file names).""" + return [os.path.basename(p.elf_path) for p in self._scan(self._bindir)] + + @cached_property + def _symbols(self) -> Set[str]: + """Return the symbols defined by the debug application binaries.""" + return { + symbol.name + for parser in self._scan(self._debug_bindir) + for symbol in parser.symbols + } + + def has_command(self, pattern: str) -> bool: + """Check if a command is available, see :func:`match_command`.""" + return match_command(pattern, self.commands) + + def has_symbol(self, pattern: str) -> bool: + """Check symbols using cmd_check alternatives and regex syntax.""" + return match_symbol(pattern, self._symbols) diff --git a/tests/lib/test_app_bindir.py b/tests/lib/test_app_bindir.py new file mode 100644 index 0000000..af5ae38 --- /dev/null +++ b/tests/lib/test_app_bindir.py @@ -0,0 +1,103 @@ +############################################################################ +# 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. +# +############################################################################ + +import shutil + +import pytest + +from ntfc.lib.elf.app_bindir import AppBinDir + +ELF_MAGIC = b"\x7fELF" + b"\x00" * 12 + + [email protected] +def bindir(tmp_path): + path = tmp_path / "bin" + path.mkdir() + for name in ("init", "hello", "getprime", "sh"): + (path / name).write_bytes(ELF_MAGIC) + # non-ELF entries are not commands + (path / "README").write_text("not an application") + (path / "subdir").mkdir() + return path + + +def test_app_bindir_commands(bindir): + b = AppBinDir(str(bindir)) + assert b.commands == ["getprime", "hello", "init", "sh"] + + +def test_app_bindir_missing_dir(tmp_path): + # neither the binaries nor the sibling debug directory exist + b = AppBinDir(str(tmp_path / "nonexistent")) + assert b.commands == [] + assert b.has_command("hello") is False + assert b.has_symbol("hello_main") is False + + +def test_app_bindir_has_command(bindir): + b = AppBinDir(str(bindir)) + + # exact file name + assert b.has_command("hello") is True + assert b.has_command("free") is False + + # flat-mode symbol markers map to the file name + assert b.has_command("hello_main") is True + assert b.has_command("free_main") is False + + # alternatives + assert b.has_command("free|hello") is True + assert b.has_command("free|df") is False + + # regex + assert b.has_command("get.*") is True + assert b.has_command("xyz.*") is False + + +def test_app_bindir_has_symbol(bindir): + # unstripped sim ELF stands in for a debug application binary + debug = bindir.parent / "bin_debug" + debug.mkdir() + shutil.copy("./tests/resources/nuttx/sim/nuttx", debug / "sh") + (debug / "README").write_text("skipped: not an ELF") + + b = AppBinDir(str(bindir), str(debug)) + assert b.has_symbol("hello_main") is True + assert b.has_symbol("missing|hello_main") is True + assert b.has_symbol("missing.*|hello_main") is True + assert b.has_symbol("hello_.*") is True + assert b.has_symbol("no_such_symbol_xyz") is False + + # the sibling bin_debug directory is the default + assert AppBinDir(str(bindir)).has_symbol("hello_main") is True + + +def test_app_bindir_symbols_span_all_binaries(bindir): + debug = bindir.parent / "bin_debug" + debug.mkdir() + for name in ("a", "b"): + shutil.copy("./tests/resources/nuttx/sim/nuttx", debug / name) + + b = AppBinDir(str(bindir), str(debug)) + + # every binary is read, not just the first one + assert b.has_symbol("hello_main") is True + assert b.has_symbol("no_such_symbol_xyz") is False
