Documentation generator for CMake. It parses CMake sources with
tree-sitter, extracts doxygen-like comments
from function() definitions and command calls, and renders them through your
own Jinja templates.
Nothing about the output format is baked in: cmake2doc hands your template a parsed model of the file and gets out of the way.
Table of contents
- Major features
- Getting started
- Integration into a CMake project
- Advanced features
- Development
- Prior art
- License
- Doxygen-like
@-tags for arguments, options, params and more, extracted from comments abovefunction(),macro()and command calls. - Custom Jinja templates, with three built-in ones to start from — Markdown or reStructuredText for Sphinx, out of the box.
- A checking pass that compares the doc comment against what the CMake code
actually accepts — and against
@exampleblocks, which are parsed as CMake — catching drift a plain doc generator can't. --injectto keep generated docs inside an existing file, such as a README, between two markers.--checkfor CI, to fail a pipeline when generated docs are stale.- A
cmake2doc.tomlconfig file, so a whole project's generation settings live in one place instead of scattered across a Makefile and workflow files. - A
cmake/cmake2doc.cmakemodule to run cmake2doc as part of the build. --jsonoutput for tools that aren't templates.- Cross-platform: Linux, macOS and Windows.
The only system requirement is Python 3.10 or newer.
The supported OSes are the following:
- Linux
- MacOS
- Windows
Ubuntu 22.04 or newer official repository already has the python of the required version.
pipx install cmake2doc
# Or:
pip install cmake2docDocument a function (or a macro) with @-tags in the comment block directly
above it:
# Adds a library target together with its tests and install rules.
#
# @arg NAME the name of the resulting target
# @option EXCLUDE_FROM_ALL do not build this target by default
# @param OUTPUT_NAME @required file name of the produced artifact
# @multiparam SOURCES the source files to compile
function(example_add_library)
endfunction()Render it with the built-in template:
cmake2doc --template function.md.jinja --output docs/reference.md CMakeLists.txtThat function becomes docs/reference.md:
## example_add_library
Adds a library target together with its tests and install rules.
```
example_add_library(
<NAME>
[EXCLUDE_FROM_ALL]
OUTPUT_NAME <value>
[SOURCES <value>...]
)
```
* <**NAME**> the name of the resulting target
* **EXCLUDE_FROM_ALL** do not build this target by default
* **OUTPUT_NAME <value>** file name of the produced artifact
* **SOURCES <value>...** the source files to compileA complete, runnable example lives in
examples/ — one
per output flavour: Markdown, reStructuredText, and reStructuredText using
Sphinx's CMake domain.
A doc comment is the run of # comment lines immediately above a function(),
a macro() or a command call. A blank line ends the run. The block is dedented
as a whole, so the space in the conventional # disappears while indentation
inside the comment — nested lists, code blocks — is preserved.
Both cmake comment forms carry documentation. A bracket comment works the same way,
including the #[==[.rst: style CMake's own modules use, where the .rst:
marker and the # of the closing #]==] are punctuation rather than text:
#[==[.rst:
@brief Adds a library target.
#]==]
function(example_add_library)
endfunction()| Tag | Applies to | Meaning |
|---|---|---|
@arg NAME |
function, macro | Positional argument. Always required. |
@option NAME |
function, macro | Valueless flag. |
@param NAME |
function, macro | Keyword taking a single value. |
@multiparam NAME |
function, macro | Keyword taking one or more values. |
@set_parent_scope NAME |
function, macro | Variable the symbol sets in its caller's scope, which is how CMake hands a result back. |
@required |
function, macro | Marks the preceding parameter as required. |
@type NAME |
function, macro | What the preceding parameter's value should be. |
@default VALUE |
function, macro | What the preceding parameter is worth when left out. |
@ingroup NAME |
function, macro, command | Assigns the symbol to a group. |
@defgroup NAME <title> |
a comment block of its own | Defines a group: its title runs to the next blank line, like @brief does, and its description is the paragraphs below. |
@file |
a comment block of its own | Marks the block as documenting the file it is in. |
@deprecated |
function, macro, command | Marks the whole symbol as deprecated. Text after it stays in the description, where it reads as the reason. |
@internal |
function, macro, command | Marks the symbol as not part of the public interface. The public filter drops it. |
@brief |
anything | A one-paragraph summary, distinct from the description. |
@example |
anything | A sample, held as a block so blank lines inside it survive. Checked to parse as CMake. |
@note, @warning |
anything | A paragraph set apart from the description. |
@since, @todo, @see |
anything | A paragraph each: a version, a task, a cross-reference. |
A tag that carries prose — @brief, @note, @warning, @since, @todo,
@see — ends at a blank line, as Doxygen's do, and what follows the blank
line belongs to the description again. @example and the parameter tags run
to the next tag instead, so a sample or a parameter description may span
paragraphs.
doc.brief is a plain string; the rest arrive as doc.sections, in the order
they were written, and doc.of_kind('note') selects one kind of them.
Text that is not part of a tag becomes the description: text before the first parameter tag describes the symbol, text after a parameter tag describes that parameter.
An @ only starts a tag at the beginning of a line or after whitespace, so
maintainer@example.com stays literal. Write @@ for a literal @ at the
start of a word — for instance when prose mentions a tag, as in
not tagged with @@ingroup.
Two things are left in the text and reported rather than acted on: a tag
cmake2doc does not recognise, and a known tag that is not followed by something
that looks like a name (@ingroup, so … is prose, not a group named ,).
Both fail the run; pass --no-strict to have them reported as warnings and
carry on. A tag that takes a name but has nothing at all after it — not even
prose, just the end of the comment or another tag — is a harder error still:
there is no literal text to fall back to keeping, so it fails the run even
under --no-strict.
A CMake function states its interface twice — once in the doc comment, once in its own body — and the two drift apart. cmake2doc reads the second one and fails the run on the disagreement:
# @option QUIET be quiet
# @multiparam SRCS the source files
function(example_add_library)
cmake_parse_arguments(ARG "QUIET" "" "SOURCES" ${ARGN})
endfunction()cmake2doc: error: CMakeLists.txt:2: function example_add_library: SRCS is
documented as @multiparam but example_add_library does not accept it
Under --no-strict the run carries on and every disagreement is reported:
CMakeLists.txt:2: function example_add_library: warning: SRCS is documented as
@multiparam but example_add_library does not accept it
CMakeLists.txt:3: function example_add_library: warning: example_add_library
takes SOURCES but it is not documented; add @multiparam SOURCES
Four things are read out of the code:
- both call forms of
cmake_parse_arguments() - the named parameters of
function(f NAME TYPE) set(VAR ... PARENT_SCOPE)return(PROPAGATE VAR)
The last two being what @set_parent_scope documents.
What the code does not state plainly is never guessed at, and so never warned
about. A keyword list built from a variable, a body with two
cmake_parse_arguments() calls in it, a macro that reaches for ${ARGV0}, or
an output variable whose name the caller supplies
(set(${ARG_OUTPUT_VARIABLE} ... PARENT_SCOPE)) all leave the matching tags
unchecked. A symbol with no doc comment at all is not reported either — that
is what --require-docs is for — but one documented at all, even with no
parameter mentioned yet, is checked against its own code from the start.
An @example is checked the same way: it is CMake, so cmake2doc parses it and
reports a sample that does not parse. Prose or another language belongs in a
fenced code block, which is left alone unless it is fenced as cmake.
--no-strict demotes these to warnings as well.
cmake2doc [-t TEMPLATE -o OUTPUT]... [-I DIR]... [-c FILE] [--inject]
[--json OUTPUT] [--exclude PATTERN]... [--require-docs]
[--strict] [--check] CMAKE_FILE...
| Flag | Effect |
|---|---|
-t, --template |
Template to render: a path, or the name of a built-in. Repeatable. |
-o, --output |
Where to write the matching --template, or - for stdout. Repeatable, paired in order. |
-I, --template-dir |
Extra directory to search for templates. Repeatable. |
-c, --config |
Read the arguments from this TOML file instead of the nearest cmake2doc.toml. |
--inject / --no-inject |
Write between the markers of an existing --output file instead of replacing it. |
--json |
Also write the parsed model as JSON, for tools that are not templates. |
--exclude |
Skip sources matching a glob, against the whole path or the file name. Repeatable. |
--require-docs / --no-require-docs |
Exit non-zero if a public function() or macro() has no doc comment. |
--strict / --no-strict |
Whether to treat a documentation problem — a doubtful @tag, or a comment that disagrees with the code — as an error rather than a warning. On by default; --no-strict reports them as warnings and carries on. |
--check / --no-check |
Write nothing; exit non-zero if any output is missing or stale. |
--list-templates |
List the built-in template names and exit. |
--version |
Print the version and exit. |
Each of --inject, --require-docs, --strict and --check has a --no-
form, so any of them recorded as true in cmake2doc.toml can still be
turned off for one run.
Each CMAKE_FILE is a file, a directory to search for CMakeLists.txt and
*.cmake (dot-directories are skipped), or a glob pattern. cmake2doc expands
directories and patterns itself, so it behaves the same in shells that do not,
such as those on Windows.
cmake2doc does not follow include() or add_subdirectory() to find more
sources — it only reads what CMAKE_FILE itself resolves to. This is usually
invisible: pointing it at a project's root directory already reaches every
subdirectory a normal add_subdirectory() tree does, since the search above
is a plain walk of the filesystem, not of the build. It matters only when a
directory is reached solely through an include() or add_subdirectory()
that leads outside the given root — a sibling cmake/ module directory, or
a dependency pulled in with FetchContent, say. List that directory
explicitly as another CMAKE_FILE (or add it to path in
cmake2doc.toml); cmake2doc will not find it on its own.
--check is meant for CI, to verify that generated documentation was
regenerated after a change to the CMake sources; it prints a diff of what
differs, since nobody in CI can re-run the generator to find out.
--require-docs is the other CI gate, the equivalent of rustdoc's
missing_docs: a public symbol with no doc comment fails the run. A name
starting with _ is private by CMake convention, and @internal says so
outright; neither is required to be documented.
A .cmake2docignore file at the project root lists further --exclude
patterns, one per line, # starting a comment.
A CI step that renders three templates needs six paired arguments to say so,
and they then have to be kept in step across a Makefile, a workflow file and a
pre-commit hook. Say it once instead, in cmake2doc.toml beside your CMake
code:
template = ["reference.md.jinja"]
output = ["docs/reference.md"]
path = ["."]
require-docs = trueand the CI step is cmake2doc with nothing after it. Every long option has a
setting of the same name, with - or _ between words. A setting of the
wrong type is refused rather than coerced, down to the items of a list:
strict = "no" is a string, and every non-empty string is true, so taking it
would mean doing the opposite of what it says; template = "reference.md.jinja"
is a string where a list belongs, and template = [1] a number where a name
does. Anything given on
the command line wins over the file, so cmake2doc --output - . still prints to
the terminal — and a flag turned off explicitly counts as given, so
--no-strict wins over a strict = true in the file.
The file is looked for in the working directory and then in each directory
above it, stopping at a repository, so cmake2doc does the same thing from a
build directory as from the project root. A relative path in it is relative
to the file, not to wherever cmake2doc was run from — otherwise output = "docs/reference.md" would name a different file from every directory. A
template that is not a file is left as written, so a built-in name still names
a built-in. --config names a different file, which must then exist, and its
directory is the project root instead.
The one setting with no option behind it is the [tags] table of
tags of your own: a vocabulary is a property of the
project, not of the run.
--inject keeps the documentation inside a file the author writes, rather
than in one of its own. Mark the place once:
# My project
<!-- BEGIN_CMAKE2MD -->
<!-- END_CMAKE2MD -->and everything between the markers is replaced on each run, leaving the prose
around them alone. It composes with --check.
The marker syntax depends on --output's own extension, so it reads as a
comment in the file it is written into: an .rst output gets a
reStructuredText comment, since docutils does not hide an HTML comment the
way a Markdown renderer does; anything else, including .md, gets the HTML
comment above.
My project
==========
.. BEGIN_CMAKE2MD
.. END_CMAKE2MDcmake/cmake2doc.cmake adds targets that run cmake2doc as part of the build, so
a project documents its own CMake code without a separate script to remember.
Copy it into your module path, or fetch it:
include(cmake2doc)
# All options are read from cmake2doc.toml
cmake2doc_generate(
# `cmake --build build --target docs` regenerates the documentation.
# A second target, `docs-check`, verifies instead that it is up to date
# and fails with a diff when it is not, which is what a CI job wants.
TARGET docs
# Build `docs` as part of the default build.
ALL
)That is the whole of it: what to render, where to write it and which files to
read are in cmake2doc.toml, said once. The targets run cmake2doc from the
current source directory, which is where it looks for that file.
The module is documented with cmake2doc's own tags, so it also serves as a worked example.
A pre-commit hook:
repos:
- repo: https://github.com/segoon/cmake2doc
rev: v0.1.0
hooks:
- id: cmake2doc-check
args: [--template, reference.md.jinja, --output, docs/reference.md, .]cmake2doc-check fails when the documentation is out of date and shows what
differs; cmake2doc regenerates it instead, so the commit picks it up.
A GitHub Action:
- uses: segoon/cmake2doc@v0.1.0
with:
args: --check --template reference.md.jinja --output docs/reference.md .Templates receive six lists:
symbols— everyfunction()andmacro(), documented or notvariables— every cache entry a user can set:option()andset(... CACHE ...), parsedtargets— everyadd_library(),add_executable(),add_test()andadd_custom_target()call, with its own name and kind, so a template wanting a table of them does not have to parseargsitselfcommands— every command call (option(),set(), …), including calls nested in afunction()body or anif()blockgroups— every@defgroup, in the order they were defined, each with aname, atitleand adescriptionfiles— the@filecomment blocks, each with thedocof the block
They are unfiltered on purpose: the documented filter drops the entries
that carry no comment, public drops the ones marked @internal, and
only_command selects the commands you actually document.
Each entry is a dict with:
| Key | Description |
|---|---|
name |
Function, macro or command name. |
doc |
Parsed comment: .description, .brief, .group, .deprecated, .internal, .args, .options, .params, .multi_params, .returns, .sections, .warnings, and the .of_kind(kind) method. |
group |
Shorthand for doc.group, i.e. the @ingroup value or None. |
pretty |
Symbol rendered via function.md.jinja; for commands, the plain description. |
comments |
The raw comment lines, dedented. |
comments_line |
Line the comment block starts on, or 0 when there is none. |
type_ |
Symbols: 'function' or 'macro'. Variables: the cache type, BOOL, PATH, FILEPATH, STRING or INTERNAL. |
signature |
Symbols only: what the code itself accepts, as signature.accepts.arg, .option, .param, .multiparam and .return. Each is a list of names, or None where the code does not say. |
args |
Commands and targets: the raw argument list, e.g. ['FOO', '"desc"', 'ON']. |
command |
Variables: 'option' or 'set'. Targets: 'add_library', 'add_executable', 'add_test' or 'add_custom_target'. |
kind |
Targets only: 'library', 'executable', 'test' or 'custom target', derived from command. |
default |
Variables only: the value the entry holds unless the user overrides it. |
docstring |
Variables only: the help string the command itself gives, which is what cmake-gui shows. |
choices |
Variables only: the values set_property(CACHE … PROPERTY STRINGS …) restricts the entry to, or None. |
advanced |
Variables only: whether mark_as_advanced() hides it from the ordinary user. |
filepath, line, location |
Where the symbol was found. |
Each parameter in doc.args / doc.options / doc.params /
doc.multi_params / doc.returns has .name, .description, .required,
.kind, .line, and .type_ and .default from @type and @default.
Each entry of doc.sections has .kind — the tag that opened it, without the
@ — .text, .name, .line and .label, which is what the tag says a
template should call it.
@ingroup puts a symbol in a group; @defgroup, in a comment block that
documents nothing else, gives that group a title and a description:
# @defgroup build Build targets
#
# What gets built, and what is left out.They arrive as groups, in the order they were defined, so a template writes
the whole document without naming a single group:
{% for group in groups %}
## {{ group.title }}
{{ group.description }}
{{ render(symbols | documented | only_group(group.name)) }}
{% endfor %}Once any group is defined, an @ingroup naming one that is not is reported —
until then @ingroup is a bare label, which is how it worked before, and
nothing is checked.
option(NAME "help" ON) and set(NAME value CACHE TYPE "help") declare the
same thing in a different order, so variables gives both of them one shape:
| Option | Description | Default |
|--------|-------------|---------|
{%- for v in variables | only_group('build') %}
| `{{ v.name }}` | {{ v.docstring | md_escape }} | `{{ v.default }}` |
{%- endfor %}A set() that writes no cache entry is a local variable rather than something
to configure, so it is not in the list; it is still in commands. A
set_property(CACHE … PROPERTY STRINGS …) in the same file fills choices.
The same is true of targets: an add_library() call is also in commands,
promoted rather than removed, exactly as option()/set(... CACHE ...) are
promoted into variables. add_custom_command() is not promoted — it names
no target of its own, only an OUTPUT file or an existing TARGET — so it
stays a plain command.
| Filter | Purpose |
|---|---|
unquote |
Strip surrounding double quotes from a CMake argument. |
escape |
Quote a value containing $ so it does not read as a variable reference. |
md_escape |
Escape | and \ so a string is safe inside a Markdown table cell. |
oneline |
Join CMake line continuations. |
only_command(name) |
Keep only commands with the given name. |
only_group(name) |
Keep only entries in the given @ingroup (use None for ungrouped). |
documented |
Keep only entries that carry a doc comment. |
public |
Drop the entries marked @internal. |
anchor |
The anchor a Markdown heading holding the given text gets. |
symbol_link(symbols) |
Link a name to its own section when symbols defines it, else leave it as written. |
render |
Concatenate the pretty field of a collection. |
Three templates ship with cmake2doc, and --list-templates names them.
reference.md.jinja is a whole document: a table of contents, every
documented function and macro laid out by @defgroup, and a table of the
build options. A project that wants documentation without writing a template
needs only:
cmake2doc --template reference.md.jinja --output docs/reference.md .reference.rst.jinja is the same document in reStructuredText, for a project
whose documentation is built with Sphinx. It uses only directives docutils
itself understands — code, note, warning, admonition, list-table —
so the output parses with or without Sphinx, and .. contents:: leaves the
table of contents to the renderer. For Sphinx's CMake domain
(.. cmake:command::, and the cross-references that come with it) see
examples/sphinx/,
which is a template rather than a built-in because the domain is an extension
a site either installs or does not.
function.md.jinja renders a single symbol, and is what fills
symbol.pretty; put a file of that name in a --template-dir (or the working
directory) to change how every symbol is rendered. It also works as a whole
document, listing every documented symbol and nothing else.
symbol.pretty is Markdown, in every run: function.md.jinja is what
fills it, and the name of that template is not configurable. A template that
emits anything else — the built-in reStructuredText one included — has to lay
symbols out from doc.args, doc.params and the rest itself. Read
reference.rst.jinja for how.
The vocabulary above is what cmake2doc means by a tag; what a project wants to record — an owner, a rationale, a ticket — is its own business. Declare it in the config file:
[tags]
author = { label = "Author:" }
rationale = { text = "block", label = "Why:" }
ticket = { takes_name = true, label = "Ticket:" }and @author is a tag like any other: recognised rather than reported,
rendered by the built-in template as > **Author:** …, and reachable from a
template of your own as doc.of_kind('author').
| Setting | Meaning |
|---|---|
text |
paragraph, the default, ends at a blank line as @note does; block runs to the next tag, so the text may span paragraphs. |
takes_name |
Whether a name follows the tag, as after @ingroup. It arrives as the section's .name. |
label |
What a template calls it. Defaults to the tag's own name. |
A declared tag opens a section, which is where anything a template has to
render lives. A flag such as @internal or a field such as @ingroup cannot
be declared this way: those write to a field of the parsed comment, and one of
your own devising would have nowhere to be written.
--json writes the same model a template is given:
{
"schema_version": 1,
"symbols": [{"name": "example_add_library", "doc": {"brief": "…"}}],
"variables": [], "targets": [], "commands": [], "groups": [], "files": []
}schema_version is bumped when a field disappears or changes meaning, never
when one is added, so a consumer must ignore the fields it does not know.
See docs/DEVELOPMENT.md: the workflow, how the modules fit together, and how to add a tag.
| Tool | How it compares |
|---|---|
| Doxygen | Where the @tag vocabulary comes from, down to a paragraph tag ending at a blank line. It has no CMake parser. |
| CMinx | The other CMake documentation generator. It derives signatures from the grammar as cmake2doc does, and targets a Sphinx site: reStructuredText is what it emits, and the docstrings are written in it. cmake2doc renders through templates, so it emits either — but a project already built with Sphinx will find CMinx the closer fit. |
| CMake's own Sphinx domain | Where the #[==[.rst: comment style comes from, and how CMake's own modules are documented. |
| terraform-docs, helm-docs | The same problem for another declarative language: a typed table of inputs, injection into an existing README, a config file, a pre-commit hook. |
| rustdoc | Doc examples that are checked rather than trusted, and missing_docs — here @example and --require-docs. |
| shdoc | The same shape of problem for shell: a dynamic language whose interface is only stated in comments. |
| godoc | No @tag vocabulary at all — a doc comment is just the prose directly above a declaration, taken as-is. It relies on the language having no equivalent of cmake_parse_arguments() to drift from; CMake's own argument parsing is exactly what a doc comment can fall out of sync with, which is why cmake2doc checks it. |
| JSDoc, TypeDoc | @param-style tags close to cmake2doc's own, and TypeScript's type system catches a caller passing the wrong type. Neither checks that a documented parameter is one the function actually accepts, which is the gap cmake2doc's checking pass closes for CMake's own untyped, string-based argument parsing. |
Apache License 2.0 — see LICENSE and NOTICE. cmake2doc started life as a set of scripts inside the userver framework.