--- /dev/null
+# 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}