blob: 8b56b036de5c8085eb2f406ff81f2f5f412c986f [file]
# Copyright (c) 2020 Nordic Semiconductor ASA
# SPDX-License-Identifier: Apache-2.0
"""
Like gen_kconfig_rest.py, but for generating an index of existing
devicetree bindings.
"""
import argparse
import glob
import io
import logging
import os
import pprint
import re
import sys
import textwrap
from collections import defaultdict
from pathlib import Path
from typing import NamedTuple
import dts_binding_types
import gen_helpers
import yaml
from devicetree import edtlib
try:
from yaml import CSafeLoader as SafeLoader
except ImportError:
from yaml import SafeLoader
ZEPHYR_BASE = Path(__file__).parents[2]
GENERIC_OR_VENDOR_INDEPENDENT = 'Generic or vendor-independent'
UNKNOWN_VENDOR = 'Unknown vendor'
ZEPHYR_BASE = Path(__file__).parents[2]
# Base properties that have documentation in 'dt-important-props'.
DETAILS_IN_IMPORTANT_PROPS = {'compatible', 'label', 'reg', 'status', 'interrupts'}
logger = logging.getLogger('gen_devicetree_rest')
def format_value(value) -> str:
"""
Format a property value, preserving hexadecimal notation for HexInt values.
For lists/arrays, formats each element individually and joins with ", ".
"""
if isinstance(value, list):
return "[" + ", ".join(map(format_value, value)) + "]"
elif isinstance(value, edtlib.HexInt):
return hex(value)
else:
return str(value)
class VndLookup:
"""
A convenience class for looking up information based on a
devicetree compatible's vendor prefix 'vnd'.
"""
def __init__(self, vendor_prefixes, bindings):
self.vnd2vendor = self.load_vnd2vendor(vendor_prefixes)
self.vnd2bindings = self.init_vnd2bindings(bindings)
self.vnd2ref_target = self.init_vnd2ref_target()
def vendor(self, vnd):
return self.vnd2vendor.get(vnd, UNKNOWN_VENDOR)
def bindings(self, vnd, default=None):
return self.vnd2bindings.get(vnd, default)
def target(self, vnd):
return self.vnd2ref_target.get(
vnd, self.vnd2ref_target[(UNKNOWN_VENDOR,)])
@staticmethod
def load_vnd2vendor(vendor_prefixes):
# Load the vendor-prefixes.txt file. Return a dict mapping 'vnd'
# vendor prefixes as they are found in compatible properties to
# each vendor's full name.
#
# For example, this line:
#
# vnd A stand-in for a real vendor
#
# Gets split into a key 'vnd' and a value 'A stand-in for a real
# vendor' in the return value.
#
# The 'None' key maps to GENERIC_OR_VENDOR_INDEPENDENT.
vnd2vendor = {
None: GENERIC_OR_VENDOR_INDEPENDENT,
}
vnd2vendor.update(edtlib.load_vendor_prefixes_txt(vendor_prefixes))
logger.info('found %d vendor prefixes in %s', len(vnd2vendor) - 1,
vendor_prefixes)
if logger.isEnabledFor(logging.DEBUG):
logger.debug('vnd2vendor=%s', pprint.pformat(vnd2vendor))
return vnd2vendor
def init_vnd2bindings(self, bindings):
# Take a 'vnd2vendor' map and a list of bindings and return a dict
# mapping 'vnd' vendor prefixes prefixes to lists of bindings. The
# bindings in each list are sorted by compatible. The keys in the
# return value are sorted by vendor name.
#
# Special cases:
#
# - The 'None' key maps to bindings with no vendor prefix
# in their compatibles, like 'gpio-keys'. This is the first key.
# - The (UNKNOWN_VENDOR,) key maps to bindings whose compatible
# has a vendor prefix that exists, but is not known,
# like 'somethingrandom,device'. This is the last key.
# Get an unsorted dict mapping vendor prefixes to lists of bindings.
unsorted = defaultdict(list)
generic_bindings = []
unknown_vendor_bindings = []
for binding in bindings:
vnd = compatible_vnd(binding.compatible)
if vnd is None:
generic_bindings.append(binding)
elif vnd in self.vnd2vendor:
unsorted[vnd].append(binding)
else:
unknown_vendor_bindings.append(binding)
# Key functions for sorting.
def vnd_key(vnd):
return self.vnd2vendor[vnd].casefold()
def binding_key(binding):
return binding.compatible
# Sort the bindings for each vendor by compatible.
# Plain dicts are sorted in CPython 3.6+, which is what we
# support, so the return dict's keys are in the same
# order as vnd2vendor.
#
# The unknown-vendor bindings being inserted as a 1-tuple key is a
# hack for convenience that ensures they won't collide with a
# known vendor. The code that consumes the dict below handles
# this.
vnd2bindings = {
None: sorted(generic_bindings, key=binding_key)
}
for vnd in sorted(unsorted, key=vnd_key):
vnd2bindings[vnd] = sorted(unsorted[vnd], key=binding_key)
vnd2bindings[(UNKNOWN_VENDOR,)] = sorted(unknown_vendor_bindings,
key=binding_key)
if logger.isEnabledFor(logging.DEBUG):
logger.debug('vnd2bindings: %s', pprint.pformat(vnd2bindings))
return vnd2bindings
def init_vnd2ref_target(self):
# The return value, vnd2ref_target, is a dict mapping vendor
# prefixes to ref targets for their relevant sections in this
# file, with these special cases:
#
# - The None key maps to the ref target for bindings with no
# vendor prefix in their compatibles, like 'gpio-keys'
# - The (UNKNOWN_VENDOR,) key maps to the ref target for bindings
# whose compatible has a vendor prefix that is not recognized.
vnd2ref_target = {}
for vnd in self.vnd2bindings:
if vnd is None:
vnd2ref_target[vnd] = 'dt_no_vendor'
elif isinstance(vnd, str):
vnd2ref_target[vnd] = f'dt_vendor_{vnd}'
else:
assert vnd == (UNKNOWN_VENDOR,), vnd
vnd2ref_target[vnd] = 'dt_unknown_vendor'
return vnd2ref_target
class TypeLookup:
"""
A convenience class for looking up information based on a
devicetree compatible's binding type.
"""
def __init__(self, bindings):
self.type2name = {
"misc": [{"type": "text", "content": "Miscellaneous"}],
"generic": [{"type": "text", "content": "Generic"}],
}
self.type2name.update(dts_binding_types.load_binding_types())
self.type2bindings = self.init_type2bindings(bindings)
self.type2ref_target = self.init_type2ref_target()
def name(self, btype):
chunks = self.type2name.get(
btype,
[{"type": "text", "content": btype.capitalize()}]
)
parts = []
for chunk in chunks:
if chunk["type"] == "acronym":
parts.append(
f":abbr:`{chunk['abbr']} ({chunk['explanation']})`"
)
else:
parts.append(chunk["content"])
return "".join(parts)
def plain_name(self, btype):
"""
Human-readable type title without RST markup, for sorting and display logic.
"""
chunks = self.type2name.get(btype, [{"type": "text", "content": btype}])
parts = [
f"{c['abbr']} ({c['explanation']})" if c["type"] == "acronym" else c["content"]
for c in chunks
]
return "".join(parts)
def target(self, btype):
return self.type2ref_target.get(btype, f"dt_type_{btype}")
def init_type2bindings(self, bindings):
unsorted = defaultdict(list)
for binding in bindings:
if binding.path:
btype = dts_binding_types.get_binding_type_from_path(Path(binding.path))
else:
btype = "misc"
unsorted[btype].append(binding)
def binding_key(binding):
return binding.compatible
type2bindings = {}
for btype in sorted(unsorted, key=lambda t: self.plain_name(t).casefold()):
type2bindings[btype] = sorted(unsorted[btype], key=binding_key)
return type2bindings
def init_type2ref_target(self):
type2ref_target = {}
for btype in self.type2bindings:
type2ref_target[btype] = f'dt_type_{btype}'
return type2ref_target
def main():
args = parse_args()
setup_logging(args.verbose)
if args.turbo_mode:
# The dummy output only needs each binding's path and compatible;
# loading them without full edtlib parsing (and skipping the driver
# sources and property type gathering altogether) keeps this several
# times faster than a regular run.
bindings = load_bindings_turbo(args.dts_roots, args.dts_folders, args.dts_files)
vnd_lookup = VndLookup(args.vendor_prefixes, bindings)
dump_content(bindings, None, vnd_lookup, None, None, args.out_dir, True)
return
bindings = load_bindings(args.dts_roots, args.dts_folders, args.dts_files)
base_binding = load_base_binding()
driver_sources = load_driver_sources()
vnd_lookup = VndLookup(args.vendor_prefixes, bindings)
type_lookup = TypeLookup(bindings)
dump_content(bindings, base_binding, vnd_lookup, type_lookup, driver_sources, args.out_dir,
args.turbo_mode)
def parse_args():
# Parse command line arguments from sys.argv.
parser = argparse.ArgumentParser(allow_abbrev=False)
parser.add_argument('-v', '--verbose', default=0, action='count',
help='increase verbosity; may be given multiple times')
parser.add_argument('--vendor-prefixes', required=True,
help='vendor-prefixes.txt file path')
parser.add_argument('--dts-root', dest='dts_roots', action='append',
help='''additional DTS root directory as it would
be set in DTS_ROOTS''')
parser.add_argument('--dts-folder', dest='dts_folders', action='append', default=[],
help='additional DTS folders containing binding files')
parser.add_argument('--dts-file', dest='dts_files', action='append', default=[],
help='additional individual DTS binding files')
parser.add_argument('--turbo-mode', action='store_true',
help='Enable turbo mode (dummy references)')
parser.add_argument('out_dir', help='output files are generated here')
return parser.parse_args()
def setup_logging(verbose):
if verbose >= 2:
log_level = logging.DEBUG
elif verbose == 1:
log_level = logging.INFO
else:
log_level = logging.ERROR
logging.basicConfig(format='%(filename)s:%(levelname)s: %(message)s',
level=log_level)
def collect_binding_files(dts_roots, dts_folders, dts_files):
# Get the list of candidate binding files from searching 'dts_roots'.
if not dts_roots:
sys.exit('no DTS roots; use --dts-root to specify at least one')
binding_files = []
for dts_root in dts_roots:
binding_files.extend(glob.glob(f'{dts_root}/dts/bindings/**/*.yml',
recursive=True))
binding_files.extend(glob.glob(f'{dts_root}/dts/bindings/**/*.yaml',
recursive=True))
for folders in dts_folders:
binding_files.extend(glob.glob(f'{folders}/*.yml', recursive=False))
binding_files.extend(glob.glob(f'{folders}/*.yaml', recursive=False))
binding_files.extend(dts_files)
return binding_files
def load_bindings(dts_roots, dts_folders, dts_files):
# Get a list of edtlib.Binding objects from searching 'dts_roots'.
binding_files = collect_binding_files(dts_roots, dts_folders, dts_files)
bindings = edtlib.bindings_from_paths(binding_files, ignore_errors=True)
num_total = len(bindings)
# Remove bindings from the 'vnd' vendor, which is not a real vendor,
# but rather a stand-in we use for examples and tests when a real
# vendor would be inappropriate.
bindings = [binding for binding in bindings if
compatible_vnd(binding.compatible) != 'vnd']
logger.info('found %d bindings (ignored %d) in this dts_roots list: %s',
len(bindings), num_total - len(bindings), dts_roots)
return bindings
class TurboBinding(NamedTuple):
# Minimal stand-in for edtlib.Binding holding the only two attributes
# the turbo-mode output needs.
path: str
compatible: str
def load_bindings_turbo(dts_roots, dts_folders, dts_files):
# Turbo-mode variant of load_bindings(). Only each binding's path and
# compatible are needed, so the YAML files are parsed directly: creating
# full edtlib.Binding objects, which recursively resolve and merge
# included files, is much slower.
#
# Like edtlib.bindings_from_paths() with ignore_errors=True, files without
# a top-level 'compatible' (include-only files such as base.yaml) are
# skipped, as are files with no description at all: a description must
# either be given in the file itself or come from an included file
# (edtlib checks the merged result, but resolving includes is exactly the
# costly work turbo mode avoids, so the presence of 'include' is used as
# an approximation).
binding_files = collect_binding_files(dts_roots, dts_folders, dts_files)
bindings = []
for path in binding_files:
try:
with open(path, encoding='utf-8') as f:
raw = yaml.load(f, Loader=SafeLoader)
except (OSError, yaml.YAMLError):
continue
if not isinstance(raw, dict):
continue
compatible = raw.get('compatible')
if not isinstance(compatible, str):
continue
if 'description' not in raw and 'include' not in raw:
continue
if compatible_vnd(compatible) != 'vnd':
bindings.append(TurboBinding(path, compatible))
logger.info('turbo mode: found %d bindings in this dts_roots list: %s',
len(bindings), dts_roots)
return bindings
def load_base_binding():
# Make a Binding object for base.yaml.
#
# This helps separate presentation for properties common to all
# nodes from node-specific properties.
base_yaml = ZEPHYR_BASE / 'dts' / 'bindings' / 'base' / 'base.yaml'
base_includes = {"pm.yaml": os.fspath(ZEPHYR_BASE / 'dts' / 'bindings' / 'base'/ 'pm.yaml')}
if not base_yaml.is_file():
sys.exit(f'Expected to find base.yaml at {base_yaml}')
return edtlib.Binding(os.fspath(base_yaml), base_includes, require_compatible=False,
require_description=False)
def load_driver_sources():
driver_sources = {}
dt_drv_compat_occurrences = defaultdict(list)
dt_drv_compat_pattern = re.compile(r"#define DT_DRV_COMPAT\s+(.*)")
device_dt_inst_define_pattern = re.compile(r"DEVICE_DT_INST_DEFINE")
folders_to_scan = ["boards", "drivers", "modules", "soc", "subsys"]
# When looking at folders_to_scan, a file is considered as a likely driver source if:
# - There is only one and only one file with a "#define DT_DRV_COMPAT <compatible>" for a given
# compatible.
# - or, a file contains both a "#define DT_DRV_COMPAT <compatible>" and a
# DEVICE_DT_INST_DEFINE(...) call.
for folder in folders_to_scan:
for dirpath, _, filenames in os.walk(ZEPHYR_BASE / folder):
for filename in filenames:
if not filename.endswith(('.c', '.h')):
continue
filepath = Path(dirpath) / filename
with open(filepath, encoding="utf-8") as f:
content = f.read()
relative_path = filepath.relative_to(ZEPHYR_BASE)
# Find all DT_DRV_COMPAT occurrences in the file
dt_drv_compat_matches = dt_drv_compat_pattern.findall(content)
for compatible in dt_drv_compat_matches:
dt_drv_compat_occurrences[compatible].append(relative_path)
if dt_drv_compat_matches and device_dt_inst_define_pattern.search(content):
for compatible in dt_drv_compat_matches:
if compatible in driver_sources:
# Mark as ambiguous if multiple files define the same compatible
driver_sources[compatible] = None
else:
driver_sources[compatible] = relative_path
# Remove ambiguous driver sources
driver_sources = {k: v for k, v in driver_sources.items() if v is not None}
# Consider DT_DRV_COMPATs with only one occurrence as driver sources
for compatible, occurrences in dt_drv_compat_occurrences.items():
if compatible not in driver_sources and len(occurrences) == 1:
path = occurrences[0]
# Assume the driver is defined in the enclosing folder if it's a header file
if path.suffix == ".h":
path = path.parent
driver_sources[compatible] = path
return driver_sources
def dump_content(bindings, base_binding, vnd_lookup, type_lookup, driver_sources, out_dir,
turbo_mode):
# Dump the generated .rst files for a vnd2bindings dict.
# Files are only written if they are changed. Existing .rst
# files which would not be written by the 'vnd2bindings'
# dict are removed.
out_dir = Path(out_dir)
setup_bindings_dir(bindings, out_dir)
if turbo_mode:
write_dummy_index(bindings, vnd_lookup, out_dir)
else:
write_bindings_rst(vnd_lookup, type_lookup, out_dir)
write_orphans(bindings, base_binding, vnd_lookup, driver_sources, out_dir)
def setup_bindings_dir(bindings, out_dir):
# Make a set of all the Path objects we will be creating for
# out_dir / bindings / {binding_path}.rst. Delete all the ones that
# shouldn't be there. Make sure the bindings output directory
# exists.
paths = set()
bindings_dir = out_dir / 'bindings'
logger.info('making output subdirectory %s', bindings_dir)
bindings_dir.mkdir(parents=True, exist_ok=True)
for binding in bindings:
paths.add(bindings_dir / binding_filename(binding))
for dirpath, _, filenames in os.walk(bindings_dir):
for filename in filenames:
path = Path(dirpath) / filename
if path not in paths:
logger.info('removing unexpected file %s', path)
path.unlink()
def write_dummy_index(bindings, vnd_lookup, out_dir):
# Write out_dir / bindings.rst, with dummy anchors
# header
content = '\n'.join((
'.. _devicetree_binding_index:',
''
))
content += '\n'.join(
f'.. _dt_vendor_{vnd}:' for vnd in vnd_lookup.vnd2bindings if isinstance(vnd, str)
)
content += '\n'.join((
'',
'',
'Dummy bindings index',
'####################',
'',
))
# build compatibles set and dump it
compatibles = {binding.compatible for binding in bindings}
content += '\n'.join(
f'.. dtcompatible:: {compatible}' for compatible in compatibles
)
write_if_updated(out_dir / 'bindings.rst', content)
def write_bindings_rst(vnd_lookup, type_lookup, out_dir):
# Write out_dir / bindings.rst, the top level index of bindings.
string_io = io.StringIO()
print_block(f'''\
.. _devicetree_binding_index:
Bindings index
##############
This page documents the available devicetree bindings.
See {zref('dt-bindings')} for an introduction to the Zephyr bindings
file format.
The bindings are grouped both by **type** and by **vendor**. Within each group,
bindings are listed by the "compatible" property they apply to, like this:
- <compatible-A>
- <compatible-B> (on <bus-name> bus)
- <compatible-C>
- ...
The text "(on <bus-name> bus)" appears when bindings may behave
differently depending on the bus the node appears on.
For example, this applies to some sensor device nodes, which may
appear as children of either I2C or SPI bus nodes.
Type index
**********
This section contains an index of bindings by type.
Click on a type's name to go to the list of bindings for that type.
.. rst-class:: rst-columns
''', string_io)
for btype, bindings in type_lookup.type2bindings.items():
if len(bindings) == 0:
continue
print(f'- :ref:`{type_lookup.target(btype)}`', file=string_io)
print_block('''\
Vendor index
************
This section contains an index of hardware vendors.
Click on a vendor's name to go to the list of bindings for
that vendor.
.. rst-class:: rst-columns
''', string_io)
for vnd, bindings in vnd_lookup.vnd2bindings.items():
if len(bindings) == 0:
continue
print(f'- :ref:`{vnd_lookup.target(vnd)}`', file=string_io)
print_block('''\
Bindings by type
****************
.. rst-class:: rst-columns
''', string_io)
for btype, bindings in type_lookup.type2bindings.items():
if len(bindings) == 0:
continue
title = type_lookup.name(btype).strip()
underline = '=' * len(title)
print_block(f'''\
.. _{type_lookup.target(btype)}:
{title}
{underline}
.. rst-class:: rst-columns
''', string_io)
for binding in bindings:
print(f'- :ref:`{binding_ref_target(binding)}`', file=string_io)
print(file=string_io)
print_block('''\
Bindings by vendor
******************
**Vendor name (vendor prefix)**
.. rst-class:: rst-columns
''', string_io)
for vnd, bindings in vnd_lookup.vnd2bindings.items():
if isinstance(vnd, tuple):
title = vnd[0]
else:
title = vnd_lookup.vendor(vnd).strip()
if isinstance(vnd, str):
title += f' ({vnd})'
underline = '=' * len(title)
if len(bindings) == 0:
continue
print_block(f'''\
.. _{vnd_lookup.target(vnd)}:
{title}
{underline}
.. rst-class:: rst-columns
''', string_io)
for binding in bindings:
print(f'- :ref:`{binding_ref_target(binding)}`', file=string_io)
print(file=string_io)
write_if_updated(out_dir / 'bindings.rst', string_io.getvalue())
def write_orphans(bindings, base_binding, vnd_lookup, driver_sources, out_dir):
# Write out_dir / bindings / foo / binding_page.rst for each binding
# in 'bindings', along with any "disambiguation" pages needed when a
# single compatible string can be handled by multiple bindings.
#
# These files are 'orphans' in the Sphinx sense: they are not in
# any toctree.
logging.info('updating :orphan: files for %d bindings', len(bindings))
num_written = 0
# First, figure out which compatibles map to multiple bindings. We
# need this information to decide which of the generated files for
# a compatible are "disambiguation" pages that point to per-bus
# binding pages, and which ones aren't.
compat2bindings = defaultdict(list)
for binding in bindings:
compat2bindings[binding.compatible].append(binding)
dup_compat2bindings = {compatible: bindings for compatible, bindings
in compat2bindings.items() if len(bindings) > 1}
# Next, write the per-binding pages. These contain the
# per-compatible targets for compatibles not in 'dup_compats'.
# We'll finish up by writing per-compatible "disambiguation" pages
# for compatibles in 'dup_compats'.
# Names of properties in base.yaml.
base_names = set(base_binding.prop2specs.keys())
for binding in bindings:
string_io = io.StringIO()
print_binding_page(binding, base_names, vnd_lookup,
driver_sources, dup_compat2bindings, string_io)
written = write_if_updated(out_dir / 'bindings' /
binding_filename(binding),
string_io.getvalue())
if written:
num_written += 1
# Generate disambiguation pages for dup_compats.
compatibles_dir = out_dir / 'compatibles'
setup_compatibles_dir(dup_compat2bindings.keys(), compatibles_dir)
for compatible in dup_compat2bindings:
string_io = io.StringIO()
print_compatible_disambiguation_page(
compatible, dup_compat2bindings[compatible], string_io)
written = write_if_updated(compatibles_dir /
compatible_filename(compatible),
string_io.getvalue())
if written:
num_written += 1
logging.info('done writing :orphan: files; %d files needed updates',
num_written)
def make_sidebar(compatible, vendor_name, vendor_ref_target, driver_path=None):
lines = [
".. sidebar:: Overview",
"",
f" :Name: ``{compatible}``",
f" :Vendor: :ref:`{vendor_name} <{vendor_ref_target}>`",
f" :Used in: :zephyr:board-catalog:`List of boards <#compatibles={compatible}>` using",
" this compatible",
]
if driver_path:
lines.append(f" :Driver: :zephyr_file:`{driver_path}`")
return "\n".join(lines) + "\n"
def print_binding_page(binding, base_names, vnd_lookup, driver_sources,dup_compats,
string_io):
# Print the rst content for 'binding' to 'string_io'. The
# 'dup_compats' argument should support membership testing for
# compatibles which have multiple associated bindings; if
# 'binding.compatible' is not in it, then the ref target for the
# entire compatible is generated in this page as well.
# :orphan:
#
# .. ref_target:
#
# Title [(on <bus> bus)]
# ######################
if binding.on_bus:
on_bus_title = f' (on {binding.on_bus} bus)'
else:
on_bus_title = ''
compatible = binding.compatible
title = f'{compatible}{on_bus_title}'
underline = '#' * len(title)
if compatible not in dup_compats:
# If this binding is the only one that handles this
# compatible, point the ".. dtcompatible:" directive straight
# to this page. There's no need for disambiguation.
dtcompatible = f'.. dtcompatible:: {binding.compatible}'
else:
# This compatible is handled by multiple bindings;
# its ".. dtcompatible::" should be in a disambiguation page
# instead.
dtcompatible = ''
print_block(f'''\
:orphan:
.. raw:: html
<!--
FIXME: do not limit page width until content uses another representation
format other than tables
-->
<style>.wy-nav-content {{ max-width: none; !important }}</style>
{dtcompatible}
.. _{binding_ref_target(binding)}:
{title}
{underline}
''', string_io)
vnd = compatible_vnd(compatible)
vendor_name = vnd_lookup.vendor(vnd)
vendor_target = vnd_lookup.target(vnd)
driver_path = driver_sources.get(re.sub("[-,.@/+]", "_", compatible.lower()))
sidebar_content = make_sidebar(
compatible=compatible,
vendor_name=vendor_name,
vendor_ref_target=vendor_target,
driver_path=driver_path,
)
print_block(sidebar_content, string_io)
# Binding description.
if binding.bus:
bus_help = f'These nodes are "{binding.bus}" bus nodes.'
else:
bus_help = ''
print_block(f'''\
Description
***********
{bus_help}
''', string_io)
if binding.title:
description = ("\n\n"
.join([binding.title, binding.description])
.strip())
else:
description = binding.description.strip()
print(to_code_block(description), file=string_io)
# Examples
if binding.examples:
print_block('''\
Examples
********
''', string_io)
blocks = [to_code_block(example, language='dts')
for example in binding.examples]
print("\n\n----\n\n".join(blocks), file=string_io)
# Properties.
print_block('''\
Properties
**********
''', string_io)
print_top_level_properties(binding, base_names, string_io)
print_child_binding_properties(binding, string_io)
# Specifier cells.
#
# This presentation isn't particularly nice. Perhaps something
# better can be done for future work.
if binding.specifier2cells:
print_block('''\
Specifier cell names
********************
''', string_io)
for specifier, cells in binding.specifier2cells.items():
print(f'- {specifier} cells: {", ".join(cells)}',
file=string_io)
def print_top_level_properties(binding, base_names, string_io):
# Print the RST for top level properties for 'binding' to 'string_io'.
#
# The 'base_names' set contains all the base.yaml properties.
def prop_table(filter_fn, deprecated):
# Get a properly formatted and indented table of properties.
specs = [prop_spec for prop_spec in binding.prop2specs.values()
if filter_fn(prop_spec)]
indent = ' ' * 14
if specs:
temp_io = io.StringIO()
print_property_table(specs, temp_io, deprecated=deprecated)
return textwrap.indent(temp_io.getvalue(), indent)
return indent + '(None)'
def node_props_filter(prop_spec):
return prop_spec.name not in base_names and not prop_spec.deprecated
def deprecated_node_props_filter(prop_spec):
return prop_spec.name not in base_names and prop_spec.deprecated
def base_props_filter(prop_spec):
return prop_spec.name in base_names
if binding.child_binding:
print_block('''\
Top level properties
====================
''', string_io)
if binding.prop2specs:
if binding.child_binding:
print_block(f'''
These property descriptions apply to "{binding.compatible}"
nodes themselves. This page also describes child node
properties in the following sections.
''', string_io)
print_block(f'''\
.. tabs::
.. group-tab:: Node specific properties
Properties not inherited from the base binding file.
{prop_table(node_props_filter, False)}
.. group-tab:: Deprecated node specific properties
Deprecated properties not inherited from the base binding file.
{prop_table(deprecated_node_props_filter, False)}
.. group-tab:: Base properties
Properties inherited from the base binding file, which defines
common properties that may be set on many nodes. Not all of these
may apply to the "{binding.compatible}" compatible.
{prop_table(base_props_filter, True)}
''', string_io)
else:
print('No top-level properties.\n', file=string_io)
def print_child_binding_properties(binding, string_io):
# Prints property tables for all levels of nesting of child
# bindings.
level = 1
child = binding.child_binding
while child is not None:
if level == 1:
level_string = 'Child'
elif level == 2:
level_string = 'Grandchild'
else:
level_string = f'Level {level} child'
if child.prop2specs:
title = f'{level_string} node properties'
underline = '=' * len(title)
print(f'{title}\n{underline}\n', file=string_io)
print_property_table(child.prop2specs.values(), string_io,
deprecated=True)
child = child.child_binding
level += 1
def print_property_table(prop_specs, string_io, deprecated=False):
# Writes a table of properties based on 'prop_specs', an iterable
# of edtlib.PropertySpec objects, to 'string_io'.
#
# If 'deprecated' is true and the property is deprecated, an extra
# line is printed mentioning that fact. We allow this to be turned
# off for tables where all properties are deprecated, so it's
# clear from context.
# Table header.
print_block('''\
.. list-table::
:widths: 1 1 4
:header-rows: 1
* - Name
- Type
- Details
''', string_io)
def to_prop_table_row(prop_spec):
# Get a multiline string for a PropertySpec table row.
# The description column combines the description field,
# along with things like the default value or enum values.
#
# The property 'description' field from the binding may span
# one or multiple lines. We try to come up with a nice
# presentation for each.
details = ''
raw_prop_descr = prop_spec.description
if raw_prop_descr:
details += to_code_block(raw_prop_descr)
if prop_spec.required:
details += '\n\nThis property is **required**.'
if prop_spec.default is not None:
details += f'\n\nDefault value: ``{format_value(prop_spec.default)}``'
if prop_spec.const is not None:
details += f'\n\nConstant value: ``{format_value(prop_spec.const)}``'
if prop_spec.enum is not None:
details += ('\n\nLegal values: ' +
', '.join(f'``{format_value(val)}``' for val in
prop_spec.enum))
if prop_spec.min is not None and prop_spec.max is not None:
details += (f'\n\nValue range: ``{format_value(prop_spec.min)}`` to '
f'``{format_value(prop_spec.max)}``')
elif prop_spec.min is not None:
details += f'\n\nMinimum value: ``{format_value(prop_spec.min)}``'
elif prop_spec.max is not None:
details += f'\n\nMaximum value: ``{format_value(prop_spec.max)}``'
if prop_spec.min_len is not None and prop_spec.max_len is not None:
details += (f'\n\nLength range: ``{format_value(prop_spec.min_len)}`` to '
f'``{format_value(prop_spec.max_len)}``')
elif prop_spec.min_len is not None:
details += f'\n\nMinimum length: ``{format_value(prop_spec.min_len)}``'
elif prop_spec.max_len is not None:
details += f'\n\nMaximum length: ``{format_value(prop_spec.max_len)}``'
if prop_spec.name in DETAILS_IN_IMPORTANT_PROPS:
details += (f'\n\nSee {zref("dt-important-props")} for more '
'information.')
if deprecated and prop_spec.deprecated:
details += '\n\nThis property is **deprecated**.'
return f"""\
* - ``{prop_spec.name}``
- ``{prop_spec.type}``
- {textwrap.indent(details, ' ' * 7).lstrip()}
"""
# Print each row.
for prop_spec in prop_specs:
print(to_prop_table_row(prop_spec), file=string_io)
def setup_compatibles_dir(compatibles, compatibles_dir):
# Make a set of all the Path objects we will be creating for
# out_dir / compatibles / {compatible_path}.rst. Delete all the ones that
# shouldn't be there. Make sure the compatibles output directory
# exists.
logger.info('making output subdirectory %s', compatibles_dir)
compatibles_dir.mkdir(parents=True, exist_ok=True)
paths = set(compatibles_dir / compatible_filename(compatible)
for compatible in compatibles)
for path in compatibles_dir.iterdir():
if path not in paths:
logger.info('removing unexpected file %s', path)
path.unlink()
def print_compatible_disambiguation_page(compatible, bindings, string_io):
# Print the disambiguation page for 'compatible', which can be
# handled by any of the bindings in 'bindings', to 'string_io'.
assert len(bindings) > 1, (compatible, bindings)
underline = '#' * len(compatible)
output_list = '\n '.join(f'- :ref:`{binding_ref_target(binding)}`'
for binding in bindings)
print_block(f'''\
:orphan:
.. dtcompatible:: {compatible}
{compatible}
{underline}
The devicetree compatible ``{compatible}`` may be handled by any
of the following bindings:
{output_list}
''', string_io)
def print_block(block, string_io):
# Helper for dedenting and printing a triple-quoted RST block.
# (Just a block of text, not necessarily just a 'code-block'
# directive.)
print(textwrap.dedent(block), file=string_io)
def to_code_block(s, indent=0, language='none'):
# Converts 's', a string, to an indented rst .. code-block::. The
# 'indent' argument is a leading indent for each line in the code
# block, in spaces.
indent = indent * ' '
return (f".. code-block:: {language}\n\n"
f"{textwrap.indent(s, f'{indent} ')}\n")
def compatible_vnd(compatible):
# Get the vendor prefix for a compatible string 'compatible'.
#
# For example, compatible_vnd('foo,device') is 'foo'.
#
# If 'compatible' has no comma (','), None is returned.
if ',' not in compatible:
return None
return compatible.split(',', 1)[0]
def compatible_filename(compatible):
# Name of the per-compatible disambiguation page within the
# out_dir / compatibles directory.
return f'{compatible}.rst'
def zref(target, text=None):
# Make an appropriate RST :ref:`text <target>` or :ref:`target`
# string to a zephyr documentation ref target 'target', and return
# it.
#
# By default, the bindings docs are in the main Zephyr
# documentation, but this script supports putting them in a
# separate Sphinx doc set. Since we also link to Zephyr
# documentation from the generated content, we have an environment
# variable based escape hatch for putting the target in the zephyr
# doc set.
#
# This relies on intersphinx:
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
docset = os.environ.get('GEN_DEVICETREE_REST_ZEPHYR_DOCSET', '')
if docset.strip():
target = f'{docset}:{target}'
if text:
return f':ref:`{text} <{target}>`'
return f':ref:`{target}`'
def binding_filename(binding):
# Returns the output file name for a binding relative to the
# directory containing documentation for all bindings. It does
# this by stripping off the '.../dts/bindings/' prefix common to
# all bindings files in a DTS_ROOT directory.
#
# For example, for .../zephyr/dts/bindings/base/base.yaml, this
# would return 'base/base.yaml'.
#
# Hopefully that's unique across roots. If not, we'll need to
# update this function.
as_posix = Path(binding.path).as_posix()
dts_bindings = 'dts/bindings/'
idx = as_posix.rfind(dts_bindings)
if idx == -1:
raise ValueError(f'binding path has no {dts_bindings}: {binding.path}')
# Cut past dts/bindings, strip off the extension (.yaml or .yml), and
# replace with .rst.
return os.path.splitext(as_posix[idx + len(dts_bindings):])[0] + '.rst'
def binding_ref_target(binding):
# Return the sphinx ':ref:' target name for a binding.
stem = Path(binding.path).stem
return 'dtbinding_' + re.sub('[/,-]', '_', stem)
def write_if_updated(path, s):
# gen_helpers.write_if_updated() wrapper that handles logging and
# creating missing parents, as needed.
if not path.parent.is_dir():
path.parent.mkdir(parents=True)
written = gen_helpers.write_if_updated(path, s)
logger.debug('%s %s', 'wrote' if written else 'did NOT write', path)
return written
if __name__ == '__main__':
main()
sys.exit(0)