diff --git a/docs/source/api_examples.md b/docs/source/api_examples.md index 25915ce..9acd009 100644 --- a/docs/source/api_examples.md +++ b/docs/source/api_examples.md @@ -100,3 +100,36 @@ for path in copied_paths: # Clean up when done cleanup_tmp_copies(copied_paths) ``` + +## List available boards and defconfigs with Board Explorer + +The NuttxBoardExplorer allows the user to retrieve a list of boards, which +can be filtered by arch, soc or individual board. Each returned board has +a list of all defconfigs, which can be iterated and its content can be retrieved. + +This example demonstrates how to discover boards and defconfigs available +in an existing NuttX repository using the lightweight helpers in +`ntxbuild.nuttx`. + +```python +from pathlib import Path +from ntxbuild.nuttx import NuttxBoardExplorer + +# Path to the nuttx repository inside your nuttxspace +nuttx_repo = Path.cwd() / "nuttx" + +# Create a filter and list all boards for a specific architecture +nbf = NuttxBoardExplorer(nuttx_repo) +boards = nbf.set_arch("arm").boards + +for board in boards: + print(f"Board: {board.name} (arch={board.arch} soc={board.soc})") + for cfg in board.defconfigs: + print(f" - defconfig: {d.name}") + # optionally read the defconfig content + # print(d.content) + +# You can also find boards by soc or by exact board name +boards_by_soc = nbf.set_soc("stm32").boards +boards_by_name = nbf.set_board("nuttx-stm32").boards +``` diff --git a/docs/source/features.md b/docs/source/features.md index 7e2b0a8..e11747a 100644 --- a/docs/source/features.md +++ b/docs/source/features.md @@ -39,6 +39,19 @@ See usage examples on {doc}`api_examples`. - Configurable target directory - Automatic cleanup +## Board Explorer + +The Board Explorer feature provides a simple way to discover and inspect +the boards and available defconfigs inside a NuttX repository. It is available +both through the CLI (commands that list boards) and the Python API +(see `ntxbuild.nuttx` helpers). + +- Quickly find which boards support a particular architecture or SoC. +- Programmatically build a list of candidate boards for automated testing or + CI matrix generation. +- Provide a lightweight UI (or CLI output) that helps users choose a board + and defconfig before running `start` or `build` commands. + ## Curses Support Menuconfig works just as usual through this tool. diff --git a/docs/source/ntxbuild.rst b/docs/source/ntxbuild.rst index d4c972b..5576428 100644 --- a/docs/source/ntxbuild.rst +++ b/docs/source/ntxbuild.rst @@ -73,6 +73,17 @@ Utilities Module :show-inheritance: :undoc-members: +NuttX Module +--------------------- + +Utilities for discovering and representing NuttX boards and defconfigs. This +module provides small data classes to represent boards under `/nuttx/boards`. + +.. automodule:: ntxbuild.nuttx + :members: + :show-inheritance: + :undoc-members: + Module contents --------------- diff --git a/docs/source/quick_start.md b/docs/source/quick_start.md index ad4ef19..2f279ae 100644 --- a/docs/source/quick_start.md +++ b/docs/source/quick_start.md @@ -79,6 +79,68 @@ builder.build(parallel=10) builder.distclean() ``` +## View Available Boards and Configs +It is possible to quickly see a table of available boards for a SoC or defconfigs for a board. + +For the board list, provide the SoC name as in `boards//`. +```bash +$ ntxbuild list boards qemu +╒═════════════╕ +│ Boards │ +╞═════════════╡ +│ qemu-armv7a │ +├─────────────┤ +│ qemu-armv7r │ +├─────────────┤ +│ qemu-armv8a │ +├─────────────┤ +│ qemu-i486 │ +╘═════════════╛ +Total boards: 4 + +$ ntxbuild list boards esp32c6 +╒═════════════════╕ +│ Boards │ +╞═════════════════╡ +│ esp32c6-devkitc │ +├─────────────────┤ +│ esp32c6-devkitm │ +├─────────────────┤ +│ esp32c6-xiao │ +╘═════════════════╛ +Total boards: 3 +``` + +To view defconfigs, use the board name under `boards///`. + +``` +$ ntxbuild list defconfigs esp32c6-devkitc +╒══════════════════════╤════════════════════╕ +│ Defconfigs │ Defconfigs │ +╞══════════════════════╪════════════════════╡ +│ adc │ bmp180 │ +├──────────────────────┼────────────────────┤ +│ buttons │ capture │ +├──────────────────────┼────────────────────┤ +│ crypto │ efuse │ +├──────────────────────┼────────────────────┤ +│ gpio │ i2c │ +|──────────────────────┼────────────────────┤ +[...] +|──────────────────────┼────────────────────┤ +│ twai │ ulp │ +├──────────────────────┼────────────────────┤ +│ usbconsole │ watchdog │ +├──────────────────────┼────────────────────┤ +│ wifi │ │ +╘══════════════════════╧════════════════════╛ +╒═════════╤═════════════════╤════════╤═════════╤════════════════════════════════╕ +│ Total │ Board │ Arch │ Soc │ Path (nuttx/boards/) │ +╞═════════╪═════════════════╪════════╪═════════╪════════════════════════════════╡ +│ 37 │ esp32c6-devkitc │ risc-v │ esp32c6 │ risc-v/esp32c6/esp32c6-devkitc │ +╘═════════╧═════════════════╧════════╧═════════╧════════════════════════════════╛ +``` + ## Downloading Toolchains To visualize currently available toolchains, execute the `toolchain list` command: diff --git a/ntxbuild/cli.py b/ntxbuild/cli.py index 00f3210..1959140 100644 --- a/ntxbuild/cli.py +++ b/ntxbuild/cli.py @@ -12,6 +12,7 @@ from .build import BuildTool, nuttx_builder from .config import ConfigManager from .env_data import clear_ntx_env, create_base_env_file, load_ntx_env +from .nuttx import NuttxBoardExplorer from .setup import download_nuttx_apps_repo, download_nuttx_repo from .toolchains import ManagePath, ToolchainInstaller from .utils import NUTTX_APPS_DEFAULT_DIR_NAME, NUTTX_DEFAULT_DIR_NAME, find_nuttx_root @@ -512,5 +513,77 @@ def menuconfig(menuconfig): sys.exit(1) +@main.group() +def list(): # noqa: F811 + """List available boards and defconfigs. + + Provides commands to list boards and defconfigs in the NuttX repository. + """ + pass + + +@list.command() +@click.argument("soc") +@click.option("--nuttx-dir", help="NuttX directory", default=NUTTX_DEFAULT_DIR_NAME) +@click.option("--apps-dir", help="Apps directory", default=NUTTX_APPS_DEFAULT_DIR_NAME) +def boards(soc, nuttx_dir, apps_dir): + """List available boards for a specific SoC/chip. + + Example usage: + ntxbuild list boards + """ + current_dir = Path.cwd() + logger.debug(f"Search for nuttx directory in: {current_dir}") + + # This validates the directory structure. We don't use prepare_env + # because we don't need to load the environment just to check + # available boards. + nuttxspace = find_nuttx_root(current_dir, nuttx_dir, apps_dir) + + try: + nuttx_path = nuttxspace / nuttx_dir + explorer = NuttxBoardExplorer(nuttx_path) + boards_list = explorer.set_soc(soc).boards + if not boards_list: + click.echo(f"No boards found for SoC: {soc}") + sys.exit(0) + explorer.print_board_summary() + sys.exit(0) + except Exception as e: + click.echo(f"❌ {e}") + sys.exit(1) + + +@list.command() +@click.argument("board") +@click.option("--nuttx-dir", help="NuttX directory", default=NUTTX_DEFAULT_DIR_NAME) +@click.option("--apps-dir", help="Apps directory", default=NUTTX_APPS_DEFAULT_DIR_NAME) +def defconfigs(board, nuttx_dir, apps_dir): + """List available defconfigs for a specific board. + + Example usage: + ntxbuild list defconfigs + """ + current_dir = Path.cwd() + logger.debug(f"Search for nuttx directory in: {current_dir}") + + # This validates the directory structure. We don't use prepare_env + # because we don't need to load the environment just to check + # available defconfigs. + nuttxspace = find_nuttx_root(current_dir, nuttx_dir, apps_dir) + try: + nuttx_path = nuttxspace / nuttx_dir + explorer = NuttxBoardExplorer(nuttx_path) + boards_list = explorer.set_board(board).boards + if not boards_list: + click.echo(f"Board not found: {board}") + sys.exit(0) + boards_list[0].print_defconfig_summary() + sys.exit(0) + except Exception as e: + click.echo(f"❌ {e}") + sys.exit(1) + + if __name__ == "__main__": main() diff --git a/ntxbuild/nuttx.py b/ntxbuild/nuttx.py new file mode 100644 index 0000000..1233ec9 --- /dev/null +++ b/ntxbuild/nuttx.py @@ -0,0 +1,193 @@ +"""Helpers to discover boards and defconfigs inside a NuttX repository. + +This module defines small data classes that represent defconfig directories +and board directories under the NuttX source tree (typically +`/nuttx/boards////configs//defconfig`). + +The classes are intentionally lightweight and used by the CLI and +other higher-level helpers to enumerate available boards and defconfigs. +""" + +import logging +from dataclasses import dataclass, field +from pathlib import Path + +from tabulate import tabulate + +logger = logging.getLogger(__name__) + + +@dataclass +class Defconfig: + path: Path + name: str = None + """Representation of a defconfig directory. + + Attributes: + path: Path to the defconfig directory (the folder that contains the + file named `defconfig`). + name: Optional explicit name for the defconfig. If not provided, the + directory name (path.stem) is used. + """ + + def __post_init__(self): + if not self.name: + logger.debug(f"Set defconfig name to {self.path.stem} from {self.path}") + self.name = self.path.stem + + @property + def content(self): + """Return the text content of the `defconfig` file. + + Returns: + The defconfig file contents as a string. + """ + return (self.path / "defconfig").read_text() + + +@dataclass +class Board: + path: Path + name: str = None + arch: str = None + soc: str = None + defconfigs: list[Defconfig] = field(default_factory=list) + """Representation of a NuttX board directory. + + The expected layout for `path` is: + `.../boards///`. + + If only the Path is provided, the other files are inferred from it. + + Attributes: + path: Path to the board directory. + name: Board name (inferred from path.stem if not provided). + arch: Architecture name (inferred from path.parents[1].stem if not + provided). + soc: SoC/chip name (inferred from path.parents[0].stem if not + provided). + defconfigs: List of `Defconfig` objects found under the + `configs/` subdirectory. + """ + + def __post_init__(self): + if not self.name: + self.name = self.path.stem + if not self.arch: + self.arch = self.path.parents[1].stem + if not self.soc: + self.soc = self.path.parents[0].stem + self._parse_defconfigs() + + def _parse_defconfigs(self): + """Discover defconfig directories under the board `configs/` folder. + + This will look for `configs/*` and create a `Defconfig` object for + each match. + """ + matches = self.path.glob("configs/*") + self.defconfigs = [Defconfig(m) for m in matches] + self.defconfigs.sort(key=lambda x: x.name) + logger.debug(f"Found {len(self.defconfigs)} configs") + + def print_defconfig_summary(self): + """Print the defconfigs for the board.""" + if len(self.defconfigs) == 0: + logger.error(f"No defconfigs found for board {self.name}") + return + + if len(self.defconfigs) > 15: + names = [config.name for config in self.defconfigs] + rows = [names[i : i + 2] for i in range(0, len(names), 2)] + print( + tabulate( + rows, headers=["Defconfigs", "Defconfigs"], tablefmt="fancy_grid" + ) + ) + else: + names = [[config.name] for config in self.defconfigs] + print(tabulate(names, headers=["Defconfigs"], tablefmt="fancy_grid")) + + board_path = self.path.relative_to(self.path.parents[2]) + print( + tabulate( + [[len(self.defconfigs), self.name, self.arch, self.soc, board_path]], + headers=["Total", "Board", "Arch", "Soc", "Path (nuttx/boards/)"], + tablefmt="fancy_grid", + ) + ) + + +class NuttxBoardExplorer: + """Helper to search for boards inside a NuttX repository. + User must set filtering criteria before searching and + retrieve results via the `boards` property. + + Args: + nuttx_path: Path to the root of the NuttX repository (the folder + that contains the `boards/` subdirectory). + """ + + def __init__(self, nuttx_path: Path): + self.boards_dir = Path(nuttx_path) / "boards" + + @property + def boards(self) -> list: + return self._search_board() + + def set_arch(self, arch: str): + """Filter search results by architecture name. + + Example filter pattern produced: `/*/*/configs`. + """ + self.filter = f"{arch}/*/*/configs" + return self + + def set_soc(self, soc: str): + """Filter search results by SoC/chip name. + + Example filter pattern produced: `*//*/configs`. + """ + self.filter = f"*/{soc}/*/configs" + return self + + def set_board(self, board: str): + """Filter search results by board name. + + Example filter pattern produced: `*/*//configs`. + """ + self.filter = f"*/*/{board}/configs" + return self + + def _search_board(self): + """Execute the glob search and return a list of `Board` objects. + + The `filter` attribute must be set by one of the `set_*` helpers + before calling this method (or accessing the `boards` property). + """ + matches = self.boards_dir.glob(self.filter) + board_list = [] + for match in matches: + board_path = match.parent + board = Board(board_path) + board_list.append(board) + board_list.sort(key=lambda x: x.name) + logger.debug(f"Found {len(board_list)} boards") + return board_list + + def print_board_summary(self): + """Print the board summary.""" + logger.debug("Printing board summary") + if len(self.boards) == 0: + logger.error("No boards found") + return + + if len(self.boards) > 15: + names = [board.name for board in self.boards] + rows = [names[i : i + 2] for i in range(0, len(names), 2)] + print(tabulate(rows, headers=["Boards", "Boards"], tablefmt="fancy_grid")) + else: + names = [[board.name] for board in self.boards] + print(tabulate(names, headers=["Boards"], tablefmt="fancy_grid")) + + print(f"Total boards: {len(self.boards)}\n") diff --git a/pyproject.toml b/pyproject.toml index c78488a..fb69178 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ dependencies = [ "click>=8.0.0", "gitpython>=3.1.45", "kconfiglib==14.1.0", + "tabulate==0.9.0", ] [project.urls] diff --git a/tests/test_nuttx.py b/tests/test_nuttx.py new file mode 100644 index 0000000..d532262 --- /dev/null +++ b/tests/test_nuttx.py @@ -0,0 +1,85 @@ +from pathlib import Path + +import pytest + +from ntxbuild.nuttx import Board, Defconfig, NuttxBoardExplorer + + +def _find_some_defconfig(nuttx_repo: Path): + # Look for defconfig files inside the nuttx repo created by tests/conftest + pattern = "boards/*/*/*/configs/*/defconfig" + matches = list(nuttx_repo.glob(pattern)) + return matches[0] if matches else None + + +def test_defconfig_name_and_content(nuttxspace_path: Path): + nuttx_repo = nuttxspace_path / "nuttx" + if not nuttx_repo.exists(): + pytest.skip("nuttx repo not available in tests/nuttxspace") + + defconfig_file = _find_some_defconfig(nuttx_repo) + if not defconfig_file: + pytest.skip("No defconfig files found in nuttx repo; skipping test") + + defconfig_dir = defconfig_file.parent + d = Defconfig(path=defconfig_dir) + + # name should be inferred from path.stem (the defconfig folder name) + assert d.name == defconfig_dir.stem + # content should be readable and non-empty + assert d.content is not None + assert len(d.content) > 0 + + +def test_board_parsing_and_defconfigs(nuttxspace_path: Path): + nuttx_repo = nuttxspace_path / "nuttx" + if not nuttx_repo.exists(): + pytest.skip("nuttx repo not available in tests/nuttxspace") + + defconfig_file = _find_some_defconfig(nuttx_repo) + if not defconfig_file: + pytest.skip("No defconfig files found in nuttx repo; skipping test") + + defconfig_dir = defconfig_file.parent + # board path is two parents up from defconfig dir: .../configs// -> board + board_path = defconfig_dir.parent.parent + b = Board(path=board_path) + + assert b.name == board_path.stem + # arch is parents[1].stem (boards//...) + assert b.arch == board_path.parents[1].stem + # soc is parents[0].stem + assert b.soc == board_path.parents[0].stem + + # defconfigs should include at least the one we found + assert any(d.path == defconfig_dir for d in b.defconfigs) + + +def test_nuttx_board_filter_setters_and_search(nuttxspace_path: Path): + nuttx_repo = nuttxspace_path / "nuttx" + if not nuttx_repo.exists(): + pytest.skip("nuttx repo not available in tests/nuttxspace") + + defconfig_file = _find_some_defconfig(nuttx_repo) + if not defconfig_file: + pytest.skip("No defconfig files found in nuttx repo; skipping test") + + # Derive arch/soc/board from an existing defconfig + defconfig_dir = defconfig_file.parent + board_path = defconfig_dir.parent.parent + arch = board_path.parents[1].stem + soc = board_path.parents[0].stem + board_name = board_path.stem + + nbf = NuttxBoardExplorer(nuttx_path=nuttx_repo) + + boards_arch = nbf.set_arch(arch).boards + assert len(boards_arch) >= 1 + assert all(b.arch == arch for b in boards_arch) + + boards_soc = nbf.set_soc(soc).boards + assert any(b.soc == soc for b in boards_soc) + + boards_board = nbf.set_board(board_name).boards + # There should be at least one board matching the exact board name + assert any(b.name == board_name for b in boards_board)