[RFC PATCH v1 6/6] kernel-doc: add man page builder (target mandocs)

Markus Heiser
2017-01-24T19:53:57Z

This patch brings man page build we already know from the DocBook
toolchain. It adds a sphinx extension 'manKernelDoc' which consists of:

* '.. kernel-doc-man::' : directive implemented in class KernelDocMan
* 'kernel_doc_man' : *invisible* node
* 'kernel-doc-man' : an alternative man builder (class KernelDocManBuilder)

The 'kernel-doc-man' builder produces manual pages in the groff
format. It is a *man* page sphinx-builder mainly written to generate
manual pages from kernel-doc comments by:

* scanning the master doc-tree for sections marked with the
'.. kernel-doc-man::' directive and build manual pages for theses
sections.

* reorder / rename (sub-) sections according to the conventions that
should be employed when writing man pages for the Linux man-pages
project, see man-pages(7) ... (has to be discussed if we really need
such a reordering).

A few words about How the 'kernel-doc-man' builder an the kernel-doc
parser work together:

It starts with the kernel-doc parser which builds small reST-doctrees
from the comments of functions, structs and so on. The kernel-doc parser
has an option (see 'kernel_doc.ParseOptions.man_sect=n') which can be
activated e.g. on a *per* kernel-doc directive::

.. kernel-doc:: include/linux/debugobjects.h
:man-sect: 9

or alternative it can be activated globally with::

kernel_doc_mansect = 9

in the conf.py (this is what this patch does).

With option 'man_sect=n' the kernel-doc parser inserts *invisible*
'kernel_doc_man' nodes in the reST-doctree. Here is a view on such a
doctree where the <kernel_doc_man/> is a child of the
describing 'get_sd_load_idx' function::

<section docname="basics" dupnames="get_sd_load_idx"
ids="get-sd-load-idx id86" names="get_sd_load_idx">
get_sd_load_idx
<kernel_doc_man manpage="get_sd_load_idx.9"/>
...
<desc desctype="function" domain="c" noindex="False" objtype="function">
<desc_signature first="False" ids="c.get_sd_load_idx" names="c.get_sd_load_idx">
<desc_type>int</desc_type>
...

After the whole doctree is build by sphinx-build it is stored in the env
and the builder takes place. These *invisible* 'kernel_doc_man' nodes
will be ignored by all builders (html, pdf etc.) except the
'kernel-doc-man' builder. The later picks up those nodes from the
doctree and builds a manpage from the surrounding .

Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
---
Documentation/Makefile.sphinx | 5 +-
Documentation/conf.py | 3 +-
Documentation/media/Makefile | 1 +
Documentation/sphinx/manKernelDoc.py | 408 +++++++++++++++++++++++++++++++++++
4 files changed, 415 insertions(+), 2 deletions(-)
create mode 100755 Documentation/sphinx/manKernelDoc.py

diff --git a/Documentation/Makefile.sphinx b/Documentation/Makefile.sphinx
index 626dfd0..73bd71b 100644
--- a/Documentation/Makefile.sphinx
+++ b/Documentation/Makefile.sphinx
@@ -89,10 +89,12 @@ epubdocs:
xmldocs:
@$(foreach var,$(SPHINXDIRS),$(call loop_cmd,sphinx,xml,$(var),xml,$(var)))

+mandocs:
+ @$(foreach var,$(SPHINXDIRS),$(call loop_cmd,sphinx,kernel-doc-man,$(var),man,$(var)))
+
# no-ops for the Sphinx toolchain
sgmldocs:
psdocs:
-mandocs:
installmandocs:

cleandocs:
@@ -106,6 +108,7 @@ dochelp:
@echo ' htmldocs - HTML'
@echo ' latexdocs - LaTeX'
@echo ' pdfdocs - PDF'
+ @echo ' mandocs - man pages'
@echo ' epubdocs - EPUB'
@echo ' xmldocs - XML'
@echo ' cleandocs - clean all generated files'
diff --git a/Documentation/conf.py b/Documentation/conf.py
index 013af9a..97b826b 100644
--- a/Documentation/conf.py
+++ b/Documentation/conf.py
@@ -35,7 +35,7 @@ from load_config import loadConfig
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['rstKernelDoc', 'rstFlatTable', 'kernel_include', 'cdomain',
- 'sphinx.ext.todo' ]
+ 'manKernelDoc', 'sphinx.ext.todo' ]

# The name of the math extension changed on Sphinx 1.4
if major == 1 and minor > 3:
@@ -508,6 +508,7 @@ pdf_documents = [
# line arguments.
kernel_doc_verbose_warn = False
kernel_doc_raise_error = False
+kernel_doc_mansect = 9

# ------------------------------------------------------------------------------
# Since loadConfig overwrites settings from the global namespace, it has to be
diff --git a/Documentation/media/Makefile b/Documentation/media/Makefile
index 3266360..289ca6d 100644
--- a/Documentation/media/Makefile
+++ b/Documentation/media/Makefile
@@ -103,6 +103,7 @@ html: all
epub: all
xml: all
latex: $(IMGPDF) all
+kernel-doc-man: $(BUILDDIR) ${TARGETS}

clean:
-rm -f $(DOTTGT) $(IMGTGT) ${TARGETS} 2>/dev/null
diff --git a/Documentation/sphinx/manKernelDoc.py b/Documentation/sphinx/manKernelDoc.py
new file mode 100755
index 0000000..3e27983
--- /dev/null
+++ b/Documentation/sphinx/manKernelDoc.py
@@ -0,0 +1,408 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8; mode: python -*-
+
+u"""
+ kernel-doc-man
+ ~~~~~~~~~~~~~~
+
+ Implementation of the sphinx builder ``kernel-doc-man``.
+
+ :copyright: Copyright (C) 2016 Markus Heiser
+ :license: GPL Version 2, June 1991 see Linux/COPYING for details.
+
+ The ``kernel-doc-man`` (:py:class:`KernelDocManBuilder`) produces manual
+ pages in the groff format. It is a *man* page sphinx-builder mainly written
+ to generate manual pages from kernel-doc comments by:
+
+ * scanning the master doc-tree for sections marked with the
+ ``.. kernel-doc-man::`` directive and build manual pages for theses
+ sections.
+
+ * reorder / rename (sub-) sections according to the conventions that should
+ be employed when writing man pages for the Linux man-pages project, see
+ man-pages(7)
+
+ Usage::
+
+ $ sphinx-build -b kernel-doc-man
+
+ rest-Markup entry (e.g)::
+
+ .. kernel-doc-man:: manpage-name.9
+
+ Since the ``kernel-doc-man`` is an extension of the common `sphinx *man*
+ builder
+ <http://www.sphinx-doc.org/en/stable/config.html#confval-man_pages>`_, it is
+ also a full replacement, building booth, the common sphinx man-pages and
+ those marked with the ``.. kernel-doc-man::`` directive.
+
+ Mostly authors will use this feature in their reST documents in conjunction
+ with the ``.. kernel-doc::`` :ref:`directive
+ <kernel-doc:kernel-doc-directive>`, to create man pages from kernel-doc
+ comments. This could be done, by setting the man section number with the
+ option ``man-sect``, e.g.::
+
+ .. kernel-doc:: include/linux/debugobjects.h
+ :man-sect: 9
+ :internal:
+
+ With this ``:man-sect: 9`` option, the kernel-doc parser will insert a
+ ``.. kernel-doc-man:: <declaration-name>.<man-sect no>`` directive in the
+ reST output, for every section describing a function, union etc.
+
+"""
+
+# ==============================================================================
+# imports
+# ==============================================================================
+
+import re
+import collections
+from os import path
+
+from docutils.io import FileOutput
+from docutils.frontend import OptionParser
+from docutils import nodes
+from docutils.utils import new_document
+from docutils.parsers.rst import Directive
+from docutils.transforms import Transform
+
+from sphinx import addnodes
+from sphinx.util.nodes import inline_all_toctrees
+from sphinx.util.console import bold, darkgreen # pylint: disable=E0611
+from sphinx.writers.manpage import ManualPageWriter
+
+from sphinx.builders.manpage import ManualPageBuilder
+
+from kernel_doc import Container
+
+# ==============================================================================
+# common globals
+# ==============================================================================
+
+DEFAULT_MAN_SECT = 9
+
+# The version numbering follows numbering of the specification
+# (Documentation/books/kernel-doc-HOWTO).
+__version__ = '1.0'
+
+# ==============================================================================
+def setup(app):
+# ==============================================================================
+
+ app.add_builder(KernelDocManBuilder)
+ app.add_directive("kernel-doc-man", KernelDocMan)
+ app.add_config_value('author', "", 'env')
+ app.add_node(kernel_doc_man
+ , html = (skip_kernel_doc_man, None)
+ , latex = (skip_kernel_doc_man, None)
+ , texinfo = (skip_kernel_doc_man, None)
+ , text = (skip_kernel_doc_man, None)
+ , man = (skip_kernel_doc_man, None) )
+
+ return dict(
+ version = __version__
+ , parallel_read_safe = True
+ , parallel_write_safe = True
+ )
+
+# ==============================================================================
+class kernel_doc_man(nodes.Invisible, nodes.Element): # pylint: disable=C0103
+# ==============================================================================
+ """Node to mark a section as *manpage*"""
+
+def skip_kernel_doc_man(self, node): # pylint: disable=W0613
+ raise nodes.SkipNode
+
+
+# ==============================================================================
+class KernelDocMan(Directive):
+# ==============================================================================
+
+ required_arguments = 1
+ optional_arguments = 0
+
+ def run(self):
+ man_node = kernel_doc_man()
+ man_node["manpage"] = self.arguments[0]
+ return [man_node]
+
+# ==============================================================================
+class Section2Manpage(Transform):
+# ==============================================================================
+ u"""Transforms a *section* tree into an *manpage* tree.
+
+ The structural layout of a man-page differs from the one produced, by the
+ kernel-doc parser. The kernel-doc parser produce reST which fits to *normal*
+ documentation, e.g. the declaration of a function in reST is like.
+
+ .. code-block:: rst
+
+ user_function
+ =============
+
+ .. c:function:: int user_function(int a)
+
+ The *purpose* description.
+
+ :param int a:
+ Parameter a description
+
+ Description
+ ===========
+
+ lorem ipsum ..
+
+ Return
+ ======
+
+ Returns first argument
+
+ On the other side, in man-pages it is common (see ``man man-pages``) to
+ print the *purpose* line in the "NAME" section, function's prototype in the
+ "SYNOPSIS" section and the parameter description in the "OPTIONS" section::
+
+ NAME
+ user_function -- The *purpose* description.
+
+ SYNOPSIS
+ int user_function(int a)
+
+ OPTIONS
+ a
+
+ DESCRIPTION
+ lorem ipsum
+
+ RETURN VALUE
+ Returns first argument
+
+ """
+ # The common section order is:
+ manTitles = [
+ (re.compile(r"^SYNOPSIS|^DEFINITION"
+ , flags=re.I), "SYNOPSIS")
+ , (re.compile(r"^CONFIG", flags=re.I), "CONFIGURATION")
+ , (re.compile(r"^DESCR", flags=re.I), "DESCRIPTION")
+ , (re.compile(r"^OPTION", flags=re.I), "OPTIONS")
+ , (re.compile(r"^EXIT", flags=re.I), "EXIT STATUS")
+ , (re.compile(r"^RETURN", flags=re.I), "RETURN VALUE")
+ , (re.compile(r"^ERROR", flags=re.I), "ERRORS")
+ , (re.compile(r"^ENVIRON", flags=re.I), "ENVIRONMENT")
+ , (re.compile(r"^FILE", flags=re.I), "FILES")
+ , (re.compile(r"^VER", flags=re.I), "VERSIONS")
+ , (re.compile(r"^ATTR", flags=re.I), "ATTRIBUTES")
+ , (re.compile(r"^CONFOR", flags=re.I), "CONFORMING TO")
+ , (re.compile(r"^NOTE", flags=re.I), "NOTES")
+ , (re.compile(r"^BUG", flags=re.I), "BUGS")
+ , (re.compile(r"^EXAMPLE", flags=re.I), "EXAMPLE")
+ , (re.compile(r"^SEE", flags=re.I), "SEE ALSO")
+ , ]
+
+ manTitleOrder = [t for r,t in manTitles]
+
+ @classmethod
+ def getFirstChild(cls, subtree, *classes):
+ for c in classes:
+ if subtree is None:
+ break
+ idx = subtree.first_child_matching_class(c)
+ if idx is None:
+ subtree = None
+ break
+ subtree = subtree[idx]
+ return subtree
+
+ def strip_man_info(self):
+ section = self.document[0]
+ man_info = Container(authors=[])
+ man_node = self.getFirstChild(section, kernel_doc_man)
+ name, sect = (man_node["manpage"].split(".", -1) + [DEFAULT_MAN_SECT])[:2]
+ man_info["manpage"] = name
+ man_info["mansect"] = sect
+
+ # strip field list
+ field_list = self.getFirstChild(section, nodes.field_list)
+ if field_list:
+ field_list.parent.remove(field_list)
+ for field in field_list:
+ name = field[0].astext().lower()
+ value = field[1].astext()
+ man_info[name] = man_info.get(name, []) + [value,]
+
+ # normalize authors
+ for auth, adr in zip(man_info.get("author", [])
+ , man_info.get("address", [])):
+ man_info["authors"].append("%s <%s>" % (auth, adr))
+
+ # strip *purpose*
+ desc_content = self.getFirstChild(
+ section, addnodes.desc, addnodes.desc_content)
+ if not len(desc_content):
+ # missing initial short description in kernel-doc comment
+ man_info.subtitle = ""
+ else:
+ man_info.subtitle = desc_content[0].astext()
+ del desc_content[0]
+
+ # remove section title
+ old_title = self.getFirstChild(section, nodes.title)
+ old_title.parent.remove(old_title)
+
+ # gather type of the declaration
+ decl_type = self.getFirstChild(
+ section, addnodes.desc, addnodes.desc_signature, addnodes.desc_type)
+ if decl_type is not None:
+ decl_type = decl_type.astext().strip()
+ man_info.decl_type = decl_type
+
+ # complete infos
+ man_info.title = man_info["manpage"]
+ man_info.section = man_info["mansect"]
+
+ return man_info
+
+ def isolateSections(self, sec_by_title):
+ section = self.document[0]
+ while True:
+ sect = self.getFirstChild(section, nodes.section)
+ if not sect:
+ break
+ sec_parent = sect.parent
+ target_idx = sect.parent.index(sect) - 1
+ sect.parent.remove(sect)
+ if isinstance(sec_parent[target_idx], nodes.target):
+ # drop target / is useless in man-pages
+ del sec_parent[target_idx]
+ title = sect[0].astext().upper()
+ for r, man_title in self.manTitles:
+ if r.search(title):
+ title = man_title
+ sect[0].replace_self(nodes.title(text = title))
+ break
+ # we dont know if there are sections with the same title
+ sec_by_title[title] = sec_by_title.get(title, []) + [sect]
+
+ return sec_by_title
+
+ def isolateSynopsis(self, sec_by_title):
+ synopsis = None
+ c_desc = self.getFirstChild(self.document[0], addnodes.desc)
+ if c_desc is not None:
+ c_desc.parent.remove(c_desc)
+ synopsis = nodes.section()
+ synopsis += nodes.title(text = 'synopsis')
+ synopsis += c_desc
+ sec_by_title["SYNOPSIS"] = sec_by_title.get("SYNOPSIS", []) + [synopsis]
+ return sec_by_title
+
+ def apply(self):
+ self.document.man_info = self.strip_man_info()
+ sec_by_title = collections.OrderedDict()
+
+ self.isolateSections(sec_by_title)
+ # On struct, enum, union, typedef, the SYNOPSIS is taken from the
+ # DEFINITION section.
+ if self.document.man_info.decl_type not in [
+ "struct", "enum", "union", "typedef"]:
+ self.isolateSynopsis(sec_by_title)
+
+ for sec_name in self.manTitleOrder:
+ sec_list = sec_by_title.pop(sec_name,[])
+ self.document[0] += sec_list
+
+ for sec_list in sec_by_title.values():
+ self.document[0] += sec_list
+
+# ==============================================================================
+class KernelDocManBuilder(ManualPageBuilder):
+# ==============================================================================
+
+ """
+ Builds groff output in manual page format.
+ """
+ name = 'kernel-doc-man'
+ format = 'man'
+ supported_image_types = []
+
+ def init(self):
+ pass
+
+ def is_manpage(self, node): # pylint: disable=R0201
+ if isinstance(node, nodes.section):
+ return bool(Section2Manpage.getFirstChild(
+ node, kernel_doc_man) is not None)
+ else:
+ return False
+
+ def prepare_writing(self, docnames):
+ """A place where you can add logic before :meth:`write_doc` is run"""
+ pass
+
+ def write_doc(self, docname, doctree):
+ """Where you actually write something to the filesystem."""
+ pass
+
+ def get_partial_document(self, children): # pylint: disable=R0201
+ doc_tree = new_document(' ')
+ doc_tree += children
+ return doc_tree
+
+ def write(self, *ignored):
+ if self.config.man_pages:
+ # build manpages from config.man_pages as usual
+ ManualPageBuilder.write(self, *ignored)
+ # FIXME:
+
+ self.info(bold("scan master tree for kernel-doc man-pages ... ") + darkgreen("{"), nonl=True)
+
+ master_tree = self.env.get_doctree(self.config.master_doc)
+ master_tree = inline_all_toctrees(
+ self, set(), self.config.master_doc, master_tree, darkgreen, [self.config.master_doc])
+ self.info(darkgreen("}"))
+ man_nodes = master_tree.traverse(condition=self.is_manpage)
+ if not man_nodes and not self.config.man_pages:
+ self.warn('no "man_pages" config value nor manual section found; no manual pages '
+ 'will be written')
+ return
+
+ self.info(bold('writing man pages ... '), nonl=True)
+
+ for man_parent in man_nodes:
+
+ doc_tree = self.get_partial_document(man_parent)
+ Section2Manpage(doc_tree).apply()
+
+ if not doc_tree.man_info["authors"] and self.config.author:
+ doc_tree.man_info["authors"].append(self.config.author)
+
+ doc_writer = ManualPageWriter(self)
+ doc_settings = OptionParser(
+ defaults = self.env.settings
+ , components = (doc_writer,)
+ , read_config_files = True
+ , ).get_default_values()
+
+ doc_settings.__dict__.update(doc_tree.man_info)
+ doc_tree.settings = doc_settings
+ targetname = '%s.%s' % (doc_tree.man_info.title, doc_tree.man_info.section)
+ if doc_tree.man_info.decl_type in [
+ "struct", "enum", "union", "typedef"]:
+ targetname = "%s_%s" % (doc_tree.man_info.decl_type, targetname)
+
+ destination = FileOutput(
+ destination_path = path.join(self.outdir, targetname)
+ , encoding='utf-8')
+
+ self.info(darkgreen(targetname) + " ", nonl=True)
+ self.env.resolve_references(doc_tree, doc_tree.man_info.manpage, self)
+
+ # remove pending_xref nodes
+ for pendingnode in doc_tree.traverse(addnodes.pending_xref):
+ pendingnode.replace_self(pendingnode.children)
+ doc_writer.write(doc_tree, destination)
+ self.info()
+
+
+ def finish(self):
+ pass
--
2.7.4