Daily bit(e) of C++ | Modern documentation tools
Daily bit(e) of C++ #48. The suite of modern code documentation tools: Doxygen, Sphinx, Breathe, Exhale.
Today we will look at modern documentation tools.
Doxygen has been the defacto standard tool for annotating C++ code and generating HTML documentation for many years. For API-reference style documentation, it is the perfect tool.
However, lately, the preferences for documentation have been shifting towards a more example-oriented style, where the documentation focuses more on the how? and why? instead of what?.
One tool that has been used extensively is Sphinx. Sphinx was originally a documentation tool aimed at Python; however, with plugins, Sphinx can import the XML output from Doxygen and make that information embeddable and referenceable within the reStructuredText markup.
Source code to HTML
In this article, we will go over the basics of setting up Sphinx using GitHub actions. The starting point will be source code annotated with Doxygen markup, and the endpoint will be a website with documentation.
We will go over the following:
processing the code using Doxygen
importing that information into Sphinx
automating this process using GitHub actions
referring to Doxygen entities inside the reStructuredText format
automatically generating an API reference page
publishing the resulting HTML on GitHub pages
The article will demonstrate the process using a simple demo repository with the code in the “src” directory and the documentation input files and configuration in the “docs” directory. The final documentation is published through GitHub pages.
From Doxygen to Sphinx
There would be little point in redoing all of the code documentation in a different format; thankfully, we don’t have to. We can still document our code using the standard Doxygen syntax and then use the generated XML output as input for Sphinx.
The plugin that will allow us to do that is Breathe.
If you are setting this up from scratch and do not use Doxygen, the first step is creating the Doxygen configuration file. All we need from Doxygen is to generate the XML output; therefore, we can turn off all other outputs.
These are the settings you might want to tweak (to generate the initial config, run “doxygen -g”):
PROJECT_NAME = "Modern Documentation" # Adjust to your needs
OUTPUT_DIRECTORY = "../docs" # For the demo repository
EXTRACT_ALL = YES
EXTRACT_PRIVATE = YES
RECURSIVE = YES
VERBATIM_HEADERS = NO
GENERATE_HTML = NO
GENERATE_LATEX = NO
GENERATE_XML = YES
The second part of the equation we need is the configuration for Sphinx. This article uses the “sphinx-book-theme”, and minor changes might be required based on your chosen theme (each theme might offer different options).
The configuration resides in two files, “conf.py” for the configuration itself:
# Basic configuration
project = 'ModernDoc'
copyright = '2023, Šimon Tóth'
author = 'Šimon Tóth'
# Extensions to use
extensions = [ "breathe" ]
# Configuration for the breathe extension
# Which directory to read the Doxygen output from
breathe_projects = {"ModernDoc":"xml"}
breathe_default_project = "ModernDoc"
# Configuration for the theme
html_theme = "sphinx_book_theme"
html_theme_options = {
"repository_url": "https://github.com/HappyCerberus/modern-documentation",
"use_repository_button": True,
}
And “requirements.txt” to pull in the required python dependencies:
breathe==4.34.0
sphinx==4.5.0
sphinx_book_theme
We will also need, at minimum, the “index.rst” file to start our documentation; for now, we can leave it empty.
The GitHub actions
With this configuration, we can test the documentation generation locally:
> cd src
> doxygen ../docs/Doxyfile
> cd ../docs
> pip install -r requirements.txt
> sphinx-build -b html . sphinx
However, we never want to do anything manually, so let’s integrate this with GitHub actions.
name: Generate and publish documentation
on:
push:
branches:
- main
jobs:
documentation:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
uses: actions/checkout@v3
- name: Generate Doxygen documentation
uses: mattnotmitt/doxygen-action@v1.9.5
with:
working-directory: 'src/'
doxyfile-path: '../docs/Doxyfile'
- name: Process the Doxygen output using Sphinx
uses: ammaraskar/sphinx-action@master
with:
build-command: "sphinx-build -b html . sphinx"
docs-folder: 'docs/'
- name: Deploy to Github pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/sphinx
The GitHub workflow will:
checkout the repository
generate Doxygen XML output
use that output to generate HTML documentation using Sphinx
push that generated HTML documentation to GitHub pages
Alternatively, as the last step, you could commit the resulting HTML into the repository using the “stefanzweifel/git-auto-commit-action@v4” GitHub action.
Note that the GITHUB_TOKEN is available automatically; however, you will need to enable write-access for GitHub actions under Settings>Actions>General>Workflow Permissions.
The documentation
Now that we have all the bits, it’s time to focus on the documentation.
As I mentioned at the beginning of the article, the Sphinx documentation style is oriented towards example-first documentation. This means we need to write the documentation, starting from the “index.rst” file, which serves as the landing page.
While we will talk about this later, it is worth mentioning that all other parts of the documentation need to be referenced/linked from the “index.rst” file (indirectly is OK). As we write our documentation, we can reference the Doxygen entities that were imported into Sphinx through the Breathe plugin:
Some important topic
====================
When needed, different parts of the API can be pulled in as references:
.. doxygenfunction:: function
This reStructuredText input will generate the following output (source code with the Doxygen comment):
We can also insert portions of the source code into the documentation as examples:
We can also pull in parts of the code as examples:
.. literalinclude:: ../src/main.cc
:language: cpp
:lines: 4-7
Will generate:
Sphinx will correctly interpret the advanced MarkDown used in Doxygen comments. However, if we desire, we can also go the other way and include reStructuredText as part of the Doxygen comments. The reStructuredText will, of course, not render correctly in the Doxygen-only output (if you are still using it).
/** \brief A custom type
This is a longer description for a custom type.
\verbatim embed:rst
Some extended information:
.. warning::
This is a warning.
An inline example for MyType.
.. code-block:: cpp
:linenos:
MyType x;
x.foo();
\endverbatim
*/
struct MyType {
/** \brief Do a lot of foo */
void foo();
};
This snippet will render like this:
The most typical thing you probably want to do is refer to the various Doxygen entities. The full range of supported directives with their options can be found in the Breathe plugin documentation.
What about the API?
We now have complete control over when a particular piece of information from Doxygen is displayed. However, there is one downside. Doxygen was great for generating the reference API documentation, and we no longer have that unless we manually construct it from scratch, referencing every entity in our code.
Fortunately, we can use another plugin that automatically generates an API overview page and the related sub-pages: Exhale.
To enable it, we need to add it to the “requirements.txt” file and adjust our configuration in “conf.py”:
# Extensions to use
extensions = [ "breathe", "exhale" ]
# Configuration for the exhale extensions
exhale_args = {
"containmentFolder": "./api",
"doxygenStripFromPath": "../src",
"rootFileName": "library_root.rst",
"rootFileTitle": "Library API",
}
The output can be further customized. However, for this demonstration, the only customization we do here is setting a custom title for the main generated page.
The plugin will automatically generate files in the reStructuredText format, which we can link to our documentation. To do that, we need to discuss the final topic: how Sphinx handles multi-file documentation.
Multi-file documentation
I already mentioned that the “index.rst” file is the core of our documentation, as it needs to reference all the files that are part of it.
Alternatively, if you do not want to do that (or cannot because the content being referenced is generated dynamically), there are options for replacing the entire or parts of the navigation with custom HTML.
Again, for our demonstration, we will stick to the simple approach and reference the other files in our documentation through the table of contents.
.. toctree::
:maxdepth: 2
self
api/library_root
other
The two things worth mentioning here are the “self”, which refers to this document and “api/library_root”, which is the file generated by Exhale (we have specified the directory and the filename in our configuration for the plugin).
The typical place to put this information is at the end of the “index.rst” file. The table of contents can also be hidden, at which point it will only serve for navigation.
Each referenced file can further adjust the navigation by including a local table of contents that is then expanded recursively.
The nontechnical part
The aim of this article is to cover the technical part of creating modern documentation for your project. However, I would be negligent if I didn’t mention the other side of the coin: what content your documentation should have and how you should structure it.
Fortunately, there is a great CppCon 2021 talk on this topic from Christopher Di Bella & Sy Brand: