Loading...
# SPDX-License-Identifier: GPL-2.0+
# Copyright (c) 2026 Simon Glass <sjg@chromium.org>
#
"""Sphinx extension to auto-generate binman entry and bintool documentation.

This parses etype and btool source files using the ast module to extract
class docstrings, avoiding the need to import binman modules (which have
dependencies like libfdt that may not be available in the doc-build
environment).

The generated files are written to doc/develop/package/ (alongside
binman.rst) and included via toctree directives. They are .gitignore'd
since they are always regenerated during the build.

To use, add 'binman_docs' to the extensions list in conf.py.
"""

import ast
import os


def get_entry_docstring(source_file):
    """Extract the Entry_ class docstring from an etype source file.

    Some files contain helper classes before the Entry_ class, so we look
    specifically for a class whose name starts with 'Entry_'.

    Args:
        source_file: Path to the source file

    Returns:
        The docstring of the Entry_ class, or None
    """
    with open(source_file) as inf:
        tree = ast.parse(inf.read())
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.ClassDef) and node.name.startswith('Entry_'):
            return ast.get_docstring(node, clean=False)
    return None


def get_bintool_docstring(source_file):
    """Extract the Bintool class docstring from a btool source file.

    Args:
        source_file: Path to the source file

    Returns:
        The docstring of the Bintool class, or None
    """
    with open(source_file) as inf:
        tree = ast.parse(inf.read())
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.ClassDef) and node.name.startswith('Bintool'):
            return ast.get_docstring(node, clean=False)
    return None


def generate_entry_docs(srcdir):
    """Generate entries.rst content from etype source files.

    Args:
        srcdir: Root of the U-Boot source tree

    Returns:
        String containing RST content
    """
    etype_dir = os.path.join(srcdir, 'tools', 'binman', 'etype')
    modules = sorted([
        os.path.splitext(f)[0] for f in os.listdir(etype_dir)
        if f.endswith('.py') and not f.startswith('_') and f != '__init__.py'
    ])

    parts = ['''\
Binman Entry Documentation
==========================

This file describes the entry types supported by binman. These entry types can
be placed in an image one by one to build up a final firmware image. It is
fairly easy to create new entry types. Just add a new file to the 'etype'
directory. You can use the existing entries as examples.

Note that some entries are subclasses of others, using and extending their
features to produce new behaviours.


''']

    missing = []
    for name in modules:
        source = os.path.join(etype_dir, name + '.py')
        docs = get_entry_docstring(source)
        if docs:
            lines = docs.splitlines()
            first_line = lines[0]
            rest = [line[4:] for line in lines[1:]]
            hdr = 'Entry: %s: %s' % (name.replace('_', '-'), first_line)

            ref_name = 'etype_%s' % name
            parts.append('.. _%s:' % ref_name)
            parts.append('')
            parts.append(hdr)
            parts.append('-' * len(hdr))
            parts.append('\n'.join(rest))
            parts.append('')
            parts.append('')
        else:
            missing.append(name)

    if missing:
        raise ValueError('Documentation is missing for modules: %s' %
                         ', '.join(missing))

    return '\n'.join(parts)


def generate_bintool_docs(srcdir):
    """Generate bintools.rst content from btool source files.

    Args:
        srcdir: Root of the U-Boot source tree

    Returns:
        String containing RST content
    """
    btool_dir = os.path.join(srcdir, 'tools', 'binman', 'btool')
    fnames = [
        f for f in os.listdir(btool_dir)
        if f.endswith('.py') and not f.startswith('_') and f != '__init__.py'
    ]

    def tool_sort_name(fname):
        name = os.path.splitext(fname)[0]
        if name.startswith('btool_'):
            name = name[6:]
        return name

    fnames.sort(key=tool_sort_name)

    parts = ['''\
.. SPDX-License-Identifier: GPL-2.0+

Binman bintool Documentation
============================

This file describes the bintools (binary tools) supported by binman. Bintools
are binman's name for external executables that it runs to generate or process
binaries. It is fairly easy to create new bintools. Just add a new file to the
'btool' directory. You can use existing bintools as examples.


''']

    missing = []
    for fname in fnames:
        name = os.path.splitext(fname)[0]
        # Strip btool_ prefix used for modules that conflict with Python libs
        if name.startswith('btool_'):
            name = name[6:]
        source = os.path.join(btool_dir, fname)
        docs = get_bintool_docstring(source)
        if docs:
            lines = docs.splitlines()
            first_line = lines[0]
            rest = [line[4:] for line in lines[1:]]
            hdr = 'Bintool: %s: %s' % (name, first_line)
            parts.append(hdr)
            parts.append('-' * len(hdr))
            parts.append('\n'.join(rest))
            parts.append('')
            parts.append('')
        else:
            missing.append(name)

    if missing:
        raise ValueError('Documentation is missing for modules: %s' %
                         ', '.join(missing))

    return '\n'.join(parts)


def generate_docs(app):
    """Generate binman documentation RST files.

    Called by Sphinx during the builder-inited event, before any RST files
    are read.

    Args:
        app: The Sphinx application object
    """
    srcdir = os.path.abspath(os.path.join(app.srcdir, '..'))
    outdir = os.path.join(app.srcdir, 'develop', 'package')

    entries_rst = os.path.join(outdir, 'entries.rst')
    content = generate_entry_docs(srcdir)
    with open(entries_rst, 'w') as outf:
        outf.write(content)

    bintools_rst = os.path.join(outdir, 'bintools.rst')
    content = generate_bintool_docs(srcdir)
    with open(bintools_rst, 'w') as outf:
        outf.write(content)


def setup(app):
    app.connect('builder-inited', generate_docs)
    return {'version': '1.0', 'parallel_read_safe': True}