doc: extensions: boards: add zephyr:board-supported-runners directive

Use runners.yaml from build metadata to gather info regarding board
supported runners, store the info in the board catalog, and allow
to display it as a table in a board's doc page using the
.. zephyr:board-supported-runner:: directive.

Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
This commit is contained in:
Benjamin Cabé 2025-02-25 08:26:09 +01:00 committed by Benjamin Cabé
commit f06c26a6d0
5 changed files with 210 additions and 9 deletions

View file

@ -16,6 +16,10 @@ Directives
- ``zephyr:code-sample-listing::`` - Shows a listing of code samples found in a given category.
- ``zephyr:board-catalog::`` - Shows a listing of boards supported by Zephyr.
- ``zephyr:board::`` - Flags a document as being the documentation page for a board.
- ``zephyr:board-supported-hw::`` - Shows a table of supported hardware features for all the targets
of the board documented in the current page.
- ``zephyr:board-supported-runners::`` - Shows a table of supported runners for the board documented
in the current page.
Roles
-----
@ -58,6 +62,7 @@ __version__ = "0.2.0"
sys.path.insert(0, str(Path(__file__).parents[4] / "scripts/dts/python-devicetree/src"))
sys.path.insert(0, str(Path(__file__).parents[4] / "scripts/west_commands"))
sys.path.insert(0, str(Path(__file__).parents[3] / "_scripts"))
from gen_boards_catalog import get_catalog
@ -729,6 +734,9 @@ class BoardDirective(SphinxDirective):
board_node["archs"] = board["archs"]
board_node["socs"] = board["socs"]
board_node["image"] = board["image"]
board_node["supported_runners"] = board["supported_runners"]
board_node["flash_runner"] = board["flash_runner"]
board_node["debug_runner"] = board["debug_runner"]
return [board_node]
@ -992,6 +1000,121 @@ class BoardSupportedHardwareDirective(SphinxDirective):
return result_nodes
class BoardSupportedRunnersDirective(SphinxDirective):
"""A directive for showing the supported runners of a board."""
has_content = False
required_arguments = 0
optional_arguments = 0
def run(self):
env = self.env
docname = env.docname
matcher = NodeMatcher(BoardNode)
board_nodes = list(self.state.document.traverse(matcher))
if not board_nodes:
logger.warning(
"board-supported-runners directive must be used in a board documentation page.",
location=(docname, self.lineno),
)
return []
if not env.app.config.zephyr_generate_hw_features:
note = nodes.admonition()
note += nodes.title(text="Note")
note["classes"].append("warning")
note += nodes.paragraph(
text="The list of supported runners was not generated. Run a full documentation "
"build for the required metadata to be available."
)
return [note]
board_node = board_nodes[0]
runners = board_node["supported_runners"]
flash_runner = board_node["flash_runner"]
debug_runner = board_node["debug_runner"]
result_nodes = []
paragraph = nodes.paragraph()
paragraph += nodes.Text("The ")
paragraph += nodes.literal(text=board_node["id"])
paragraph += nodes.Text(
" board supports the runners and associated west commands listed below."
)
result_nodes.append(paragraph)
env_runners = env.domaindata["zephyr"]["runners"]
commands = ["flash", "debug"]
for runner in env_runners:
if runner in board_node["supported_runners"]:
for cmd in env_runners[runner].get("commands", []):
if cmd not in commands:
commands.append(cmd)
# create the table
table = nodes.table(classes=["colwidths-given", "runners-table"])
tgroup = nodes.tgroup(cols=len(commands) + 1) # +1 for the Runner column
# Add colspec for Runner column
tgroup += nodes.colspec(colwidth=15, classes=["type"])
# Add colspecs for command columns
for _ in commands:
tgroup += nodes.colspec(colwidth=15, classes=["type"])
thead = nodes.thead()
row = nodes.row()
entry = nodes.entry()
row += entry
headers = [*commands]
for header in headers:
entry = nodes.entry(classes=[header.lower()])
entry += addnodes.literal_strong(text=header, classes=["command"])
row += entry
thead += row
tgroup += thead
tbody = nodes.tbody()
# add a row for each runner
for runner in sorted(runners):
row = nodes.row()
# First column - Runner name
entry = nodes.entry()
xref = addnodes.pending_xref(
"",
refdomain="std",
reftype="ref",
reftarget=f"runner_{runner}",
refexplicit=True,
refwarn=False,
)
xref += nodes.Text(runner)
entry += addnodes.literal_strong("", "", xref)
row += entry
# Add columns for each command
for command in commands:
entry = nodes.entry()
if command in env_runners[runner].get("commands", []):
entry += nodes.Text("")
if (command == "flash" and runner == flash_runner) or (
command == "debug" and runner == debug_runner
):
entry += nodes.Text(" (default)")
row += entry
tbody += row
tgroup += tbody
table += tgroup
result_nodes.append(table)
return result_nodes
class ZephyrDomain(Domain):
"""Zephyr domain"""
@ -1011,6 +1134,7 @@ class ZephyrDomain(Domain):
"board-catalog": BoardCatalogDirective,
"board": BoardDirective,
"board-supported-hw": BoardSupportedHardwareDirective,
"board-supported-runners": BoardSupportedRunnersDirective,
}
object_types: dict[str, ObjType] = {
@ -1247,6 +1371,7 @@ def load_board_catalog_into_domain(app: Sphinx) -> None:
app.env.domaindata["zephyr"]["boards"] = board_catalog["boards"]
app.env.domaindata["zephyr"]["vendors"] = board_catalog["vendors"]
app.env.domaindata["zephyr"]["socs"] = board_catalog["socs"]
app.env.domaindata["zephyr"]["runners"] = board_catalog["runners"]
def setup(app):

View file

@ -15,6 +15,7 @@ import list_hardware
import yaml
import zephyr_module
from gen_devicetree_rest import VndLookup
from runners.core import ZephyrBinaryRunner
ZEPHYR_BASE = Path(__file__).parents[2]
ZEPHYR_BINDINGS = ZEPHYR_BASE / "dts/bindings"
@ -22,6 +23,10 @@ EDT_PICKLE_PATHS = [
"zephyr/edt.pickle",
"hello_world/zephyr/edt.pickle" # for board targets using sysbuild
]
RUNNERS_YAML_PATHS = [
"zephyr/runners.yaml",
"hello_world/zephyr/runners.yaml" # for board targets using sysbuild
]
logger = logging.getLogger(__name__)
@ -108,20 +113,25 @@ def guess_doc_page(board_or_shield):
return doc_file
def gather_board_devicetrees(twister_out_dir):
"""Gather EDT objects for each board from twister output directory.
def gather_board_build_info(twister_out_dir):
"""Gather EDT objects and runners info for each board from twister output directory.
Args:
twister_out_dir: Path object pointing to twister output directory
Returns:
A dictionary mapping board names to a dictionary of board targets and their EDT objects.
The structure is: {board_name: {board_target: edt_object}}
A tuple of two dictionaries:
- A dictionary mapping board names to a dictionary of board targets and their EDT.
objects.
The structure is: {board_name: {board_target: edt_object}}
- A dictionary mapping board names to a dictionary of board targets and their runners
info.
The structure is: {board_name: {board_target: runners_info}}
"""
board_devicetrees = {}
board_runners = {}
if not twister_out_dir.exists():
return board_devicetrees
return board_devicetrees, board_runners
# Find all build_info.yml files in twister-out
build_info_files = list(twister_out_dir.glob("*/**/build_info.yml"))
@ -137,6 +147,13 @@ def gather_board_devicetrees(twister_out_dir):
if not edt_pickle_file:
continue
runners_yaml_file = None
for runners_yaml_path in RUNNERS_YAML_PATHS:
maybe_file = build_info_file.parent / runners_yaml_path
if maybe_file.exists():
runners_yaml_file = maybe_file
break
try:
with open(build_info_file) as f:
build_info = yaml.safe_load(f)
@ -155,10 +172,17 @@ def gather_board_devicetrees(twister_out_dir):
edt = pickle.load(f)
board_devicetrees.setdefault(board_name, {})[board_target] = edt
if runners_yaml_file:
with open(runners_yaml_file) as f:
runners_yaml = yaml.safe_load(f)
board_runners.setdefault(board_name, {})[board_target] = (
runners_yaml
)
except Exception as e:
logger.error(f"Error processing build info file {build_info_file}: {e}")
return board_devicetrees
return board_devicetrees, board_runners
def run_twister_cmake_only(outdir):
@ -174,6 +198,7 @@ def run_twister_cmake_only(outdir):
"--all",
"-M",
*[arg for path in EDT_PICKLE_PATHS for arg in ('--keep-artifacts', path)],
*[arg for path in RUNNERS_YAML_PATHS for arg in ('--keep-artifacts', path)],
"--cmake-only",
"--outdir", str(outdir),
]
@ -226,12 +251,13 @@ def get_catalog(generate_hw_features=False):
systems = list_hardware.find_v2_systems(args_find_boards)
board_catalog = {}
board_devicetrees = {}
board_runners = {}
if generate_hw_features:
logger.info("Running twister in cmake-only mode to get Devicetree files for all boards")
with tempfile.TemporaryDirectory() as tmp_dir:
run_twister_cmake_only(tmp_dir)
board_devicetrees = gather_board_devicetrees(Path(tmp_dir))
board_devicetrees, board_runners = gather_board_build_info(Path(tmp_dir))
else:
logger.info("Skipping generation of supported hardware features.")
@ -314,6 +340,15 @@ def get_catalog(generate_hw_features=False):
# Store features for this specific target
supported_features[board_target] = features
board_runner_info = {}
if board.name in board_runners:
# Assume all board targets have the same runners so only consider the runners
# for the first board target.
r = list(board_runners[board.name].values())[0]
board_runner_info["runners"] = r.get("runners")
board_runner_info["flash-runner"] = r.get("flash-runner")
board_runner_info["debug-runner"] = r.get("debug-runner")
# Grab all the twister files for this board and use them to figure out all the archs it
# supports.
archs = set()
@ -336,6 +371,10 @@ def get_catalog(generate_hw_features=False):
"revision_default": board.revision_default,
"supported_features": supported_features,
"image": guess_image(board),
# runners
"supported_runners": board_runner_info.get("runners", []),
"flash_runner": board_runner_info.get("flash-runner", ""),
"debug_runner": board_runner_info.get("debug-runner", ""),
}
socs_hierarchy = {}
@ -344,8 +383,16 @@ def get_catalog(generate_hw_features=False):
series = soc.series or "<no series>"
socs_hierarchy.setdefault(family, {}).setdefault(series, []).append(soc.name)
available_runners = {}
for runner in ZephyrBinaryRunner.get_runners():
available_runners[runner.name()] = {
"name": runner.name(),
"commands": runner.capabilities().commands,
}
return {
"boards": board_catalog,
"vendors": {**vnd_lookup.vnd2vendor, "others": "Other/Unknown"},
"socs": socs_hierarchy,
"runners": available_runners,
}

View file

@ -1266,6 +1266,21 @@ Boards
(``zephyr_generate_hw_features`` config option set to ``True``). If disabled, a warning message
will be shown instead of the hardware features tables.
.. rst:directive:: .. zephyr:board-supported-runners::
This directive is used to show the supported runners for the board documented in the current
page, including which runner is the default for flashing and debugging.
The directive must be used in a document that also contains a :rst:dir:`zephyr:board` directive,
as it relies on the board information to generate the table.
.. note::
Similar to :rst:dir:`zephyr:board-supported-hw`, this directive requires hardware features
generation to be enabled (``zephyr_generate_hw_features`` config option set to ``True``) to
produce a complete table. If disabled, a warning message will be shown instead of the runners
tables.
References
**********

View file

@ -13,6 +13,7 @@ file declares that support properly. See :ref:`west-build-flash-debug` for
more information on these commands.
.. _atmel_sam_ba_bootloader:
.. _runner_bossac:
SAM Boot Assistant (SAM-BA)
***************************
@ -207,6 +208,7 @@ Windows PATH. A specific bossac executable can be used by passing the
.. _linkserver-debug-host-tools:
.. _runner_linkserver:
LinkServer Debug Host Tools
****************************
@ -282,6 +284,7 @@ LinkServer west runner ``--probe`` option to pass the probe index.
will need to add a breakpoint at ``main`` or the reset handler manually.
.. _jlink-debug-host-tools:
.. _runner_jlink:
J-Link Debug Host Tools
***********************
@ -312,6 +315,7 @@ required.
Note that the J-Link GDB server does not yet support Zephyr RTOS-awareness.
.. _openocd-debug-host-tools:
.. _runner_openocd:
OpenOCD Debug Host Tools
************************
@ -339,6 +343,7 @@ Check if your SoC is listed in `OpenOCD Supported Devices`_.
- Add ``C:\Program Files\OpenOCD\bin`` to 'PATH' environment variable
.. _pyocd-debug-host-tools:
.. _runner_pyocd:
pyOCD Debug Host Tools
**********************
@ -358,6 +363,7 @@ These debug host tools are compatible with the following debug probes:
Check if your SoC is listed in `pyOCD Supported Devices`_.
.. _lauterbach-trace32-debug-host-tools:
.. _runner_trace32:
Lauterbach TRACE32 Debug Host Tools
***********************************
@ -409,6 +415,7 @@ To enable Zephyr RTOS awareness follow the steps described in
`Lauterbach TRACE32 Zephyr OS Awareness Manual`_.
.. _nxp-s32-debug-host-tools:
.. _runner_nxp_s32dbg:
NXP S32 Debug Probe Host Tools
******************************
@ -471,6 +478,8 @@ afterwards detach the debug session:
west debug --tool-opt='--batch'
.. _runner_probe_rs:
probe-rs Debug Host Tools
*************************
@ -482,7 +491,7 @@ Check `probe-rs Installation`_ for more setup details.
Check if your SoC is listed in `probe-rs Supported Devices`_.
.. _stm32cubeprog-flash-host-tools:
.. _runner_rfp:
Renesas Flash Programmer (RFP) Host Tools
*****************************************
@ -503,6 +512,9 @@ to ``rfp-cli`` when flashing:
west flash --rfp-cli ~/Downloads/RFP_CLI_Linux_V31800_x64/linux-x64/rfp-cli
.. _stm32cubeprog-flash-host-tools:
.. _runner_stm32cubeprogrammer:
STM32CubeProgrammer Flash Host Tools
************************************

View file

@ -29,6 +29,8 @@ to connect external components]
Programming and Debugging
*************************
.. zephyr:board-supported-runners::
Flashing
========
[How to use this board with Zephyr and how to flash a Zephyr binary on this