doc: extensions: boards: Add hardware features filter to board catalog

Add a new hardware features filter to the board catalog that allows
users to filter boards based on their supported hardware capabilities.
The features are extracted from the devicetree files and organized by
binding type.

Key changes:
- Extract hardware feature descriptions from devicetree bindings
- Add HW_FEATURES_TURBO_MODE option to skip feature generation for
  faster builds (similar to DT_TURBO_MODE)
- Add tag-based UI for filtering boards by hardware features

The feature can be disabled for faster documentation builds using
-DHW_FEATURES_TURBO_MODE=1 or by using 'make html-fast'.

Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
This commit is contained in:
Benjamin Cabé 2024-10-12 21:58:18 +02:00 committed by Fabio Baltieri
commit 444964e031
12 changed files with 356 additions and 25 deletions

View file

@ -24,6 +24,10 @@ this page <boards-shields>`.
single field, selecting multiple options (such as two architectures) will show boards matching single field, selecting multiple options (such as two architectures) will show boards matching
**either** option. **either** option.
* The list of supported hardware features for each board is automatically generated using
information from the Devicetree. It may not be reflecting the full list of supported features
since some of them may not be enabled by default.
* Can't find your exact board? Don't worry! If a similar board with the same or a closely related * Can't find your exact board? Don't worry! If a similar board with the same or a closely related
MCU exists, you can use it as a :ref:`starting point <create-your-board-directory>` for adding MCU exists, you can use it as a :ref:`starting point <create-your-board-directory>` for adding
support for your own board. support for your own board.

View file

@ -16,6 +16,7 @@ set(SPHINXOPTS "-j auto -W --keep-going -T" CACHE STRING "Default Sphinx Options
set(SPHINXOPTS_EXTRA "" CACHE STRING "Extra Sphinx Options (added to defaults)") set(SPHINXOPTS_EXTRA "" CACHE STRING "Extra Sphinx Options (added to defaults)")
set(LATEXMKOPTS "-halt-on-error -no-shell-escape" CACHE STRING "Default latexmk options") set(LATEXMKOPTS "-halt-on-error -no-shell-escape" CACHE STRING "Default latexmk options")
set(DT_TURBO_MODE OFF CACHE BOOL "Enable DT turbo mode") set(DT_TURBO_MODE OFF CACHE BOOL "Enable DT turbo mode")
set(HW_FEATURES_TURBO_MODE OFF CACHE BOOL "Enable HW features turbo mode")
set(DOC_TAG "development" CACHE STRING "Documentation tag") set(DOC_TAG "development" CACHE STRING "Documentation tag")
set(DTS_ROOTS "${ZEPHYR_BASE}" CACHE STRING "DT bindings root folders") set(DTS_ROOTS "${ZEPHYR_BASE}" CACHE STRING "DT bindings root folders")
@ -149,6 +150,16 @@ set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${GEN_DEVICETREE_
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
# html # html
set(SPHINX_TAGS "${DOC_TAG}")
if(HW_FEATURES_TURBO_MODE)
list(APPEND SPHINX_TAGS "hw_features_turbo")
endif()
set(SPHINX_TAGS_ARGS "")
foreach(tag ${SPHINX_TAGS})
list(APPEND SPHINX_TAGS_ARGS "-t" "${tag}")
endforeach()
add_doc_target( add_doc_target(
html html
COMMAND ${CMAKE_COMMAND} -E env ${SPHINX_ENV} OUTPUT_DIR=${DOCS_HTML_DIR} COMMAND ${CMAKE_COMMAND} -E env ${SPHINX_ENV} OUTPUT_DIR=${DOCS_HTML_DIR}
@ -157,7 +168,7 @@ add_doc_target(
-c ${DOCS_CFG_DIR} -c ${DOCS_CFG_DIR}
-d ${DOCS_DOCTREE_DIR} -d ${DOCS_DOCTREE_DIR}
-w ${DOCS_BUILD_DIR}/html.log -w ${DOCS_BUILD_DIR}/html.log
-t ${DOC_TAG} ${SPHINX_TAGS_ARGS}
${SPHINXOPTS} ${SPHINXOPTS}
${SPHINXOPTS_EXTRA} ${SPHINXOPTS_EXTRA}
${DOCS_SRC_DIR} ${DOCS_SRC_DIR}
@ -187,7 +198,7 @@ add_doc_target(
-c ${DOCS_CFG_DIR} -c ${DOCS_CFG_DIR}
-d ${DOCS_DOCTREE_DIR} -d ${DOCS_DOCTREE_DIR}
-w ${DOCS_BUILD_DIR}/html.log -w ${DOCS_BUILD_DIR}/html.log
-t ${DOC_TAG} ${SPHINX_TAGS_ARGS}
${SPHINXOPTS} ${SPHINXOPTS}
${SPHINXOPTS_EXTRA} ${SPHINXOPTS_EXTRA}
${DOCS_SRC_DIR} ${DOCS_SRC_DIR}
@ -214,7 +225,7 @@ add_doc_target(
-c ${DOCS_CFG_DIR} -c ${DOCS_CFG_DIR}
-d ${DOCS_DOCTREE_DIR} -d ${DOCS_DOCTREE_DIR}
-w ${DOCS_BUILD_DIR}/latex.log -w ${DOCS_BUILD_DIR}/latex.log
-t ${DOC_TAG} ${SPHINX_TAGS_ARGS}
-t convertimages -t convertimages
${SPHINXOPTS} ${SPHINXOPTS}
${SPHINXOPTS_EXTRA} ${SPHINXOPTS_EXTRA}
@ -266,7 +277,7 @@ add_doc_target(
-c ${DOCS_CFG_DIR} -c ${DOCS_CFG_DIR}
-d ${DOCS_DOCTREE_DIR} -d ${DOCS_DOCTREE_DIR}
-w ${DOCS_BUILD_DIR}/linkcheck.log -w ${DOCS_BUILD_DIR}/linkcheck.log
-t ${DOC_TAG} ${SPHINX_TAGS_ARGS}
${SPHINXOPTS} ${SPHINXOPTS}
${SPHINXOPTS_EXTRA} ${SPHINXOPTS_EXTRA}
${DOCS_SRC_DIR} ${DOCS_SRC_DIR}

View file

@ -8,6 +8,7 @@ SPHINXOPTS ?= -j auto -W --keep-going -T
SPHINXOPTS_EXTRA ?= SPHINXOPTS_EXTRA ?=
LATEXMKOPTS ?= -halt-on-error -no-shell-escape LATEXMKOPTS ?= -halt-on-error -no-shell-escape
DT_TURBO_MODE ?= 0 DT_TURBO_MODE ?= 0
HW_FEATURES_TURBO_MODE ?= 0
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Documentation targets # Documentation targets
@ -15,10 +16,10 @@ DT_TURBO_MODE ?= 0
.PHONY: configure clean html html-fast html-live html-live-fast latex pdf doxygen .PHONY: configure clean html html-fast html-live html-live-fast latex pdf doxygen
html-fast: html-fast:
${MAKE} html DT_TURBO_MODE=1 ${MAKE} html DT_TURBO_MODE=1 HW_FEATURES_TURBO_MODE=1
html-live-fast: html-live-fast:
${MAKE} html-live DT_TURBO_MODE=1 ${MAKE} html-live DT_TURBO_MODE=1 HW_FEATURES_TURBO_MODE=1
html html-live latex pdf linkcheck doxygen: configure html html-live latex pdf linkcheck doxygen: configure
cmake --build ${BUILDDIR} --target $@ cmake --build ${BUILDDIR} --target $@
@ -32,7 +33,8 @@ configure:
-DSPHINXOPTS="${SPHINXOPTS}" \ -DSPHINXOPTS="${SPHINXOPTS}" \
-DSPHINXOPTS_EXTRA="${SPHINXOPTS_EXTRA}" \ -DSPHINXOPTS_EXTRA="${SPHINXOPTS_EXTRA}" \
-DLATEXMKOPTS="${LATEXMKOPTS}" \ -DLATEXMKOPTS="${LATEXMKOPTS}" \
-DDT_TURBO_MODE=${DT_TURBO_MODE} -DDT_TURBO_MODE=${DT_TURBO_MODE} \
-DHW_FEATURES_TURBO_MODE=${HW_FEATURES_TURBO_MODE}
clean: clean:
cmake --build ${BUILDDIR} --target clean cmake --build ${BUILDDIR} --target clean

View file

@ -2,7 +2,7 @@
Zephyr Extension Zephyr Extension
################ ################
Copyright (c) 2023 The Linux Foundation Copyright (c) 2023-2025 The Linux Foundation
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
This extension adds a new ``zephyr`` domain for handling the documentation of various entities This extension adds a new ``zephyr`` domain for handling the documentation of various entities
@ -708,6 +708,7 @@ class BoardCatalogDirective(SphinxDirective):
"boards": domain_data["boards"], "boards": domain_data["boards"],
"vendors": domain_data["vendors"], "vendors": domain_data["vendors"],
"socs": domain_data["socs"], "socs": domain_data["socs"],
"hw_features_present": self.env.app.config.zephyr_generate_hw_features,
}, },
) )
return [nodes.raw("", rendered, format="html")] return [nodes.raw("", rendered, format="html")]
@ -954,7 +955,7 @@ def install_static_assets_as_needed(
def load_board_catalog_into_domain(app: Sphinx) -> None: def load_board_catalog_into_domain(app: Sphinx) -> None:
board_catalog = get_catalog() board_catalog = get_catalog(generate_hw_features=app.config.zephyr_generate_hw_features)
app.env.domaindata["zephyr"]["boards"] = board_catalog["boards"] app.env.domaindata["zephyr"]["boards"] = board_catalog["boards"]
app.env.domaindata["zephyr"]["vendors"] = board_catalog["vendors"] app.env.domaindata["zephyr"]["vendors"] = board_catalog["vendors"]
app.env.domaindata["zephyr"]["socs"] = board_catalog["socs"] app.env.domaindata["zephyr"]["socs"] = board_catalog["socs"]
@ -962,6 +963,7 @@ def load_board_catalog_into_domain(app: Sphinx) -> None:
def setup(app): def setup(app):
app.add_config_value("zephyr_breathe_insert_related_samples", False, "env") app.add_config_value("zephyr_breathe_insert_related_samples", False, "env")
app.add_config_value("zephyr_generate_hw_features", False, "env")
app.add_domain(ZephyrDomain) app.add_domain(ZephyrDomain)

View file

@ -1,5 +1,5 @@
/** /**
* Copyright (c) 2024, The Linux Foundation. * Copyright (c) 2024-2025, The Linux Foundation.
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
@ -82,6 +82,49 @@
white-space: nowrap; white-space: nowrap;
} }
.tag-container {
display: flex;
flex-wrap: wrap;
border: 1px solid #ccc;
border-radius: 50px;
padding: 5px 18px;
}
.tag-container:focus-within {
border-color: var(--input-focus-border-color);
}
.tag {
background-color: var(--admonition-note-background-color);
color: var(--admonition-note-color);
padding: 2px 12px 4px 16px;
border-radius: 30px;
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 14px;
margin-right: 8px;
}
.tag:hover {
background-color: #0056b3;
}
.tag::after {
content: '\00D7'; /* multiplication sign */
margin-left: 8px;
font-size: 12px;
cursor: pointer;
}
.filter-form input.tag-input {
flex: 1;
border: none;
padding: 5px;
outline: none;
background-color: transparent;
}
#catalog { #catalog {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View file

@ -1,5 +1,5 @@
/** /**
* Copyright (c) 2024, The Linux Foundation. * Copyright (c) 2024-2025, The Linux Foundation.
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
@ -29,6 +29,29 @@ function populateFormFromURL() {
} }
}); });
// Restore supported features from URL
if (hashParams.has("features")) {
const features = hashParams.get("features").split(",");
setTimeout(() => {
features.forEach(feature => {
const tagContainer = document.getElementById('tag-container');
const tagInput = document.getElementById('tag-input');
const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = feature;
tagElement.onclick = () => {
const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
selectedTags.splice(selectedTags.indexOf(feature), 1);
tagElement.remove();
filterBoards();
};
tagContainer.insertBefore(tagElement, tagInput);
});
filterBoards();
}, 0);
}
filterBoards(); filterBoards();
} }
@ -47,6 +70,10 @@ function updateURL() {
} }
}); });
// Add supported features to URL
const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
selectedTags.length ? hashParams.set("features", selectedTags.join(",")) : hashParams.delete("features");
window.history.replaceState({}, "", `#${hashParams.toString()}`); window.history.replaceState({}, "", `#${hashParams.toString()}`);
} }
@ -84,6 +111,81 @@ function fillSocSocSelect(families, series = undefined, selectOnFill = false) {
}); });
} }
function setupHWCapabilitiesField() {
let selectedTags = [];
const tagContainer = document.getElementById('tag-container');
const tagInput = document.getElementById('tag-input');
const datalist = document.getElementById('tag-list');
const tagCounts = Array.from(document.querySelectorAll('.board-card')).reduce((acc, board) => {
board.getAttribute('data-supported-features').split(' ').forEach(tag => {
acc[tag] = (acc[tag] || 0) + 1;
});
return acc;
}, {});
const allTags = Object.keys(tagCounts).sort();
function addTag(tag) {
if (selectedTags.includes(tag) || tag === "" || !allTags.includes(tag)) return;
selectedTags.push(tag);
const tagElement = document.createElement('span');
tagElement.classList.add('tag');
tagElement.textContent = tag;
tagElement.onclick = () => removeTag(tag);
tagContainer.insertBefore(tagElement, tagInput);
tagInput.value = '';
updateDatalist();
}
function removeTag(tag) {
selectedTags = selectedTags.filter(t => t !== tag);
document.querySelectorAll('.tag').forEach(el => {
if (el.textContent.includes(tag)) el.remove();
});
updateDatalist();
}
function updateDatalist() {
datalist.innerHTML = '';
const filteredTags = allTags.filter(tag => !selectedTags.includes(tag));
filteredTags.forEach(tag => {
const option = document.createElement('option');
option.value = tag;
datalist.appendChild(option);
});
filterBoards();
}
tagInput.addEventListener('input', () => {
if (allTags.includes(tagInput.value)) {
addTag(tagInput.value);
}
});
// Add tag when pressing the Enter key
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && allTags.includes(tagInput.value)) {
addTag(tagInput.value);
e.preventDefault();
}
});
// Delete tag when pressing the Backspace key
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && tagInput.value === '' && selectedTags.length > 0) {
removeTag(selectedTags[selectedTags.length - 1]);
}
});
updateDatalist();
}
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const form = document.querySelector(".filter-form"); const form = document.querySelector(".filter-form");
@ -101,9 +203,10 @@ document.addEventListener("DOMContentLoaded", function () {
fillSocFamilySelect(); fillSocFamilySelect();
fillSocSeriesSelect(); fillSocSeriesSelect();
fillSocSocSelect(); fillSocSocSelect();
populateFormFromURL(); populateFormFromURL();
setupHWCapabilitiesField();
socFamilySelect = document.getElementById("family"); socFamilySelect = document.getElementById("family");
socFamilySelect.addEventListener("change", () => { socFamilySelect.addEventListener("change", () => {
const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value); const selectedFamilies = [...socFamilySelect.selectedOptions].map(({ value }) => value);
@ -142,6 +245,11 @@ function resetForm() {
fillSocFamilySelect(); fillSocFamilySelect();
fillSocSeriesSelect(); fillSocSeriesSelect();
fillSocSocSelect(); fillSocSocSelect();
// Clear supported features
document.querySelectorAll('.tag').forEach(tag => tag.remove());
document.getElementById('tag-input').value = '';
filterBoards(); filterBoards();
} }
@ -160,8 +268,10 @@ function filterBoards() {
const vendorSelect = document.getElementById("vendor").value; const vendorSelect = document.getElementById("vendor").value;
const socSocSelect = document.getElementById("soc"); const socSocSelect = document.getElementById("soc");
const selectedTags = [...document.querySelectorAll('.tag')].map(tag => tag.textContent);
const resetFiltersBtn = document.getElementById("reset-filters"); const resetFiltersBtn = document.getElementById("reset-filters");
if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length) { if (nameInput || archSelect || vendorSelect || socSocSelect.selectedOptions.length || selectedTags.length) {
resetFiltersBtn.classList.remove("btn-disabled"); resetFiltersBtn.classList.remove("btn-disabled");
} else { } else {
resetFiltersBtn.classList.add("btn-disabled"); resetFiltersBtn.classList.add("btn-disabled");
@ -174,6 +284,7 @@ function filterBoards() {
const boardArchs = board.getAttribute("data-arch").split(" "); const boardArchs = board.getAttribute("data-arch").split(" ");
const boardVendor = board.getAttribute("data-vendor"); const boardVendor = board.getAttribute("data-vendor");
const boardSocs = board.getAttribute("data-socs").split(" "); const boardSocs = board.getAttribute("data-socs").split(" ");
const boardSupportedFeatures = board.getAttribute("data-supported-features").split(" ");
let matches = true; let matches = true;
@ -183,7 +294,8 @@ function filterBoards() {
!(nameInput && !boardName.includes(nameInput)) && !(nameInput && !boardName.includes(nameInput)) &&
!(archSelect && !boardArchs.includes(archSelect)) && !(archSelect && !boardArchs.includes(archSelect)) &&
!(vendorSelect && boardVendor !== vendorSelect) && !(vendorSelect && boardVendor !== vendorSelect) &&
(selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))); (selectedSocs.length === 0 || selectedSocs.some((soc) => boardSocs.includes(soc))) &&
(selectedTags.length === 0 || selectedTags.every((tag) => boardSupportedFeatures.includes(tag)));
board.classList.toggle("hidden", !matches); board.classList.toggle("hidden", !matches);
}); });

View file

@ -1,5 +1,5 @@
{# {#
Copyright (c) 2024, The Linux Foundation. Copyright (c) 2024-2025, The Linux Foundation.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
#} #}
@ -15,7 +15,7 @@
data-arch="{{ board.archs | join(" ") }}" data-arch="{{ board.archs | join(" ") }}"
data-vendor="{{ board.vendor }}" data-vendor="{{ board.vendor }}"
data-socs="{{ board.socs | join(" ") }}" data-socs="{{ board.socs | join(" ") }}"
tabindex="0"> data-supported-features="{{ board.supported_features | join(" ") }}" tabindex="0">
<div class="vendor">{{ vendors[board.vendor] }}</div> <div class="vendor">{{ vendors[board.vendor] }}</div>
{% if board.image -%} {% if board.image -%}
<img alt="A picture of the {{ board.full_name }} board" <img alt="A picture of the {{ board.full_name }} board"

View file

@ -1,5 +1,5 @@
{# {#
Copyright (c) 2024, The Linux Foundation. Copyright (c) 2024-2025, The Linux Foundation.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
#} #}
@ -60,6 +60,20 @@
<select id="soc" name="soc" size="10" multiple></select> <select id="soc" name="soc" size="10" multiple></select>
</div> </div>
<div class="form-group" style="flex-basis: 100%">
<label for="hw-capabilities">Supported Hardware Capabilities</label>
<div class="tag-container" id="tag-container">
<input list="tag-list" class="tag-input" id="tag-input"
placeholder="{% if hw_features_present -%}
Type a tag...
{%- else -%}
List of supported hardware capabilities is not available
{%- endif %}"
{% if not hw_features_present %}disabled{% endif %}>
<datalist id="tag-list"></datalist>
</div>
</div>
</form> </form>
<div id="form-options" style="text-align: center; margin-bottom: 20px"> <div id="form-options" style="text-align: center; margin-bottom: 20px">

View file

@ -1,7 +1,11 @@
# Copyright (c) 2024 The Linux Foundation # Copyright (c) 2024-2025 The Linux Foundation
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import logging import logging
import os
import pickle
import subprocess
import sys
from collections import namedtuple from collections import namedtuple
from pathlib import Path from pathlib import Path
@ -12,6 +16,8 @@ import zephyr_module
from gen_devicetree_rest import VndLookup from gen_devicetree_rest import VndLookup
ZEPHYR_BASE = Path(__file__).parents[2] ZEPHYR_BASE = Path(__file__).parents[2]
ZEPHYR_BINDINGS = ZEPHYR_BASE / "dts/bindings"
EDT_PICKLE_PATH = "zephyr/edt.pickle"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -38,6 +44,7 @@ def guess_image(board_or_shield):
return (img_file.relative_to(ZEPHYR_BASE)).as_posix() if img_file else None return (img_file.relative_to(ZEPHYR_BASE)).as_posix() if img_file else None
def guess_doc_page(board_or_shield): def guess_doc_page(board_or_shield):
patterns = [ patterns = [
"doc/index.{ext}", "doc/index.{ext}",
@ -51,7 +58,92 @@ def guess_doc_page(board_or_shield):
return doc_file return doc_file
def get_catalog(): def gather_board_devicetrees(twister_out_dir):
"""Gather EDT objects 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}}
"""
board_devicetrees = {}
if not twister_out_dir.exists():
return board_devicetrees
# Find all build_info.yml files in twister-out
build_info_files = list(twister_out_dir.glob("*/**/build_info.yml"))
for build_info_file in build_info_files:
# Look for corresponding zephyr.dts
edt_pickle_file = build_info_file.parent / EDT_PICKLE_PATH
if not edt_pickle_file.exists():
continue
try:
with open(build_info_file) as f:
build_info = yaml.safe_load(f)
board_info = build_info.get('cmake', {}).get('board', {})
board_name = board_info.get('name')
qualifier = board_info.get('qualifiers', '')
revision = board_info.get('revision', '')
board_target = board_name
if qualifier:
board_target = f"{board_name}/{qualifier}"
if revision:
board_target = f"{board_target}@{revision}"
with open(edt_pickle_file, 'rb') as f:
edt = pickle.load(f)
board_devicetrees.setdefault(board_name, {})[board_target] = edt
except Exception as e:
logger.error(f"Error processing build info file {build_info_file}: {e}")
return board_devicetrees
def run_twister_cmake_only(outdir):
"""Run twister in cmake-only mode to generate build info files.
Args:
outdir: Directory where twister should output its files
"""
twister_cmd = [
sys.executable,
f"{ZEPHYR_BASE}/scripts/twister",
"-T", "samples/hello_world/",
"--all",
"-M",
"--keep-artifacts", "zephyr/edt.pickle",
"--cmake-only",
"--outdir", str(outdir),
]
minimal_env = {
'PATH': os.environ.get('PATH', ''),
'ZEPHYR_BASE': str(ZEPHYR_BASE),
'HOME': os.environ.get('HOME', ''),
'PYTHONPATH': os.environ.get('PYTHONPATH', '')
}
try:
subprocess.run(twister_cmd, check=True, cwd=ZEPHYR_BASE, env=minimal_env)
except subprocess.CalledProcessError as e:
logger.warning(f"Failed to run Twister, list of hw features might be incomplete.\n{e}")
def get_catalog(generate_hw_features=False):
"""Get the board catalog.
Args:
generate_hw_features: If True, run twister to generate hardware features information.
"""
import tempfile
vnd_lookup = VndLookup(ZEPHYR_BASE / "dts/bindings/vendor-prefixes.txt", []) vnd_lookup = VndLookup(ZEPHYR_BASE / "dts/bindings/vendor-prefixes.txt", [])
module_settings = { module_settings = {
@ -78,6 +170,15 @@ def get_catalog():
boards = list_boards.find_v2_boards(args_find_boards) boards = list_boards.find_v2_boards(args_find_boards)
systems = list_hardware.find_v2_systems(args_find_boards) systems = list_hardware.find_v2_systems(args_find_boards)
board_catalog = {} board_catalog = {}
board_devicetrees = {}
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))
else:
logger.info("Skipping generation of supported hardware features.")
for board in boards.values(): for board in boards.values():
# We could use board.vendor but it is often incorrect. Instead, deduce vendor from # We could use board.vendor but it is often incorrect. Instead, deduce vendor from
@ -91,6 +192,40 @@ def get_catalog():
vendor = folder.name vendor = folder.name
break break
socs = {soc.name for soc in board.socs}
full_name = board.full_name or board.name
doc_page = guess_doc_page(board)
supported_features = {}
targets = set()
# Use pre-gathered build info and DTS files
if board.name in board_devicetrees:
for board_target, edt in board_devicetrees[board.name].items():
targets.add(board_target)
okay_nodes = [
node
for node in edt.nodes
if node.status == "okay" and node.matching_compat is not None
]
target_features = {}
for node in okay_nodes:
binding_path = Path(node.binding_path)
binding_type = (
binding_path.relative_to(ZEPHYR_BINDINGS).parts[0]
if binding_path.is_relative_to(ZEPHYR_BINDINGS)
else "misc"
)
target_features.setdefault(binding_type, set()).add(node.matching_compat)
# for now we do the union of all supported features for all of board's targets but
# in the future it's likely the catalog will be organized so that the list of
# supported features is also available per target.
supported_features.update(target_features)
# Grab all the twister files for this board and use them to figure out all the archs it # Grab all the twister files for this board and use them to figure out all the archs it
# supports. # supports.
archs = set() archs = set()
@ -103,10 +238,6 @@ def get_catalog():
except Exception as e: except Exception as e:
logger.error(f"Error parsing twister file {twister_file}: {e}") logger.error(f"Error parsing twister file {twister_file}: {e}")
socs = {soc.name for soc in board.socs}
full_name = board.full_name or board.name
doc_page = guess_doc_page(board)
board_catalog[board.name] = { board_catalog[board.name] = {
"name": board.name, "name": board.name,
"full_name": full_name, "full_name": full_name,
@ -114,6 +245,8 @@ def get_catalog():
"vendor": vendor, "vendor": vendor,
"archs": list(archs), "archs": list(archs),
"socs": list(socs), "socs": list(socs),
"supported_features": supported_features,
"targets": list(targets),
"image": guess_image(board), "image": guess_image(board),
} }

View file

@ -1,6 +1,7 @@
/** /**
* Copyright (c) 2019-2020, Juan Linietsky, Ariel Manzur and the Godot community * Copyright (c) 2019-2020, Juan Linietsky, Ariel Manzur and the Godot community
* Copyright (c) 2021, Teslabs Engineering S.L. * Copyright (c) 2021, Teslabs Engineering S.L.
* Copyright (c) 2023-2025, The Linux Foundation.
* SPDX-License-Identifier: CC-BY-3.0 * SPDX-License-Identifier: CC-BY-3.0
* *
* Various tweaks to the Read the Docs theme to better conform with Zephyr's * Various tweaks to the Read the Docs theme to better conform with Zephyr's

View file

@ -325,6 +325,7 @@ external_content_keep = [
# -- Options for zephyr.domain -------------------------------------------- # -- Options for zephyr.domain --------------------------------------------
zephyr_breathe_insert_related_samples = True zephyr_breathe_insert_related_samples = True
zephyr_generate_hw_features = not tags.has("hw_features_turbo") # pylint: disable=undefined-variable # noqa: F821
# -- Options for sphinx.ext.graphviz -------------------------------------- # -- Options for sphinx.ext.graphviz --------------------------------------

View file

@ -253,11 +253,19 @@ To enable this mode, set the following option when invoking cmake::
-DDT_TURBO_MODE=1 -DDT_TURBO_MODE=1
or invoke make with the following target:: Another step that typically takes a long time is the generation of the list of
supported features for each board. This can be disabled by setting the following
option when invoking cmake::
-DHW_FEATURES_TURBO_MODE=1
Invoking :command:`make` with the following target will build the documentation
without either of the aforementioned features::
cd ~/zephyrproject/zephyr/doc cd ~/zephyrproject/zephyr/doc
# To generate HTML output without detailed Kconfig # To generate HTML output without detailed Devicetree bindings documentation
# and supported features index
make html-fast make html-fast
Viewing generated documentation locally Viewing generated documentation locally