Table of Contents

  1. Doxygen Integration
  2. Sphinx Integration
  3. Documentation as Custom Target
  4. Markdown README
  5. Documentation Installation
  6. CI Documentation Deployment
  7. Conclusion & Next Steps
Back to CMake Mastery Series

Part 20: Building Documentation

June 4, 2026 Wasil Zafar 30 min read

Integrate Doxygen, Sphinx, and Breathe into your CMake build system to generate professional API documentation, deploy it in CI, and package it with your releases.

Doxygen Integration

CMake provides a first-class FindDoxygen module with the doxygen_add_docs() function that creates a documentation target from your source files. This is the simplest way to integrate Doxygen into a CMake project.

Key Insight: The doxygen_add_docs() function (added in CMake 3.9) handles Doxyfile generation internally — you set Doxygen options as CMake variables prefixed with DOXYGEN_, and CMake generates the Doxyfile for you. No manual Doxyfile needed for simple projects.
cmake_minimum_required(VERSION 3.21)
project(MyLibrary VERSION 2.1.0 LANGUAGES CXX)

# Find Doxygen — OPTIONAL so the build works without it
find_package(Doxygen OPTIONAL_COMPONENTS dot)

if(DOXYGEN_FOUND)
    # Set Doxygen configuration via CMake variables
    set(DOXYGEN_PROJECT_NAME "${PROJECT_NAME}")
    set(DOXYGEN_PROJECT_NUMBER "${PROJECT_VERSION}")
    set(DOXYGEN_PROJECT_BRIEF "A modern C++ library for data processing")
    set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/docs")

    set(DOXYGEN_EXTRACT_ALL YES)
    set(DOXYGEN_EXTRACT_PRIVATE NO)
    set(DOXYGEN_GENERATE_HTML YES)
    set(DOXYGEN_GENERATE_LATEX NO)
    set(DOXYGEN_GENERATE_XML YES)  # Needed for Breathe/Sphinx

    set(DOXYGEN_HTML_COLORSTYLE "TOGGLE")
    set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")

    # Create the "docs" target
    doxygen_add_docs(docs
        ${CMAKE_SOURCE_DIR}/include
        ${CMAKE_SOURCE_DIR}/src
        ${CMAKE_SOURCE_DIR}/README.md
        COMMENT "Generating API documentation with Doxygen..."
    )
    message(STATUS "Doxygen found: build target 'docs' available")
else()
    message(STATUS "Doxygen not found: documentation target disabled")
endif()
# Generate documentation
cmake -B build
cmake --build build --target docs

# Open the generated HTML
open build/docs/html/index.html

Doxygen Configuration

For complex projects that need full Doxyfile control, use configure_file() with a Doxyfile.in template:

# Doxyfile.in — Template with CMake variable substitution
PROJECT_NAME           = "@PROJECT_NAME@"
PROJECT_NUMBER         = "@PROJECT_VERSION@"
PROJECT_BRIEF          = "@PROJECT_DESCRIPTION@"
OUTPUT_DIRECTORY       = "@CMAKE_BINARY_DIR@/docs"

INPUT                  = @CMAKE_SOURCE_DIR@/include \
                         @CMAKE_SOURCE_DIR@/src \
                         @CMAKE_SOURCE_DIR@/README.md

RECURSIVE              = YES
EXTRACT_ALL            = YES
EXTRACT_PRIVATE        = NO
EXTRACT_STATIC         = YES

GENERATE_HTML          = YES
HTML_OUTPUT            = html
HTML_COLORSTYLE        = TOGGLE
HTML_TIMESTAMP         = YES
HTML_DYNAMIC_SECTIONS  = YES

GENERATE_LATEX         = NO
GENERATE_XML           = YES
XML_OUTPUT             = xml

USE_MDFILE_AS_MAINPAGE = @CMAKE_SOURCE_DIR@/README.md
FILE_PATTERNS          = *.h *.hpp *.cpp *.md

# Graphviz diagrams
HAVE_DOT               = @DOXYGEN_HAVE_DOT@
DOT_IMAGE_FORMAT       = svg
CLASS_DIAGRAMS         = YES
CALL_GRAPH             = YES
CALLER_GRAPH           = YES
COLLABORATION_GRAPH    = YES
# CMakeLists.txt — Using Doxyfile.in template
find_package(Doxygen REQUIRED OPTIONAL_COMPONENTS dot)

# Check if dot (Graphviz) is available
if(DOXYGEN_DOT_FOUND)
    set(DOXYGEN_HAVE_DOT "YES")
else()
    set(DOXYGEN_HAVE_DOT "NO")
endif()

# Generate Doxyfile from template
configure_file(
    ${CMAKE_SOURCE_DIR}/docs/Doxyfile.in
    ${CMAKE_BINARY_DIR}/Doxyfile
    @ONLY
)

# Create custom target using the generated Doxyfile
add_custom_target(docs
    COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    COMMENT "Generating Doxygen documentation..."
    VERBATIM
)

Graphviz Diagrams

When Graphviz's dot tool is available, Doxygen automatically generates class hierarchy diagrams, call graphs, and collaboration diagrams. CMake's FindDoxygen detects dot via the OPTIONAL_COMPONENTS dot argument.

Documentation Generation Pipeline
        flowchart TD
            A[Source Code] --> B[Doxygen]
            A --> C[Comment Blocks]
            D[Doxyfile.in] --> E[configure_file]
            E --> F[Doxyfile]
            F --> B
            B --> G[HTML Output]
            B --> H[XML Output]
            B --> I[LaTeX Output]
            H --> J[Breathe]
            K[RST Files] --> L[Sphinx]
            J --> L
            L --> M[Sphinx HTML]
            G --> N[Deploy]
            M --> N
            N --> O[GitHub Pages]
            N --> P[ReadTheDocs]
    
# Enabling specific diagram types
set(DOXYGEN_HAVE_DOT YES)
set(DOXYGEN_DOT_IMAGE_FORMAT svg)
set(DOXYGEN_INTERACTIVE_SVG YES)

# Class diagrams: show inheritance hierarchies
set(DOXYGEN_CLASS_DIAGRAMS YES)
set(DOXYGEN_CLASS_GRAPH YES)

# Call graphs: show which functions call which
set(DOXYGEN_CALL_GRAPH YES)
set(DOXYGEN_CALLER_GRAPH YES)

# Collaboration diagrams: show class relationships
set(DOXYGEN_COLLABORATION_GRAPH YES)

# Include dependency graphs
set(DOXYGEN_INCLUDE_GRAPH YES)
set(DOXYGEN_INCLUDED_BY_GRAPH YES)

# Limit graph depth to keep diagrams readable
set(DOXYGEN_MAX_DOT_GRAPH_DEPTH 3)
set(DOXYGEN_DOT_GRAPH_MAX_NODES 50)
Experiment Beginner

Try It: Doxygen with Graphviz

Create a small C++ project with 3-4 classes forming an inheritance hierarchy. Install Graphviz (sudo apt install graphviz or brew install graphviz), enable class diagrams and call graphs, then build the docs target. Inspect the SVG diagrams in the generated HTML output.

doxygen graphviz diagrams

Sphinx Integration

Sphinx excels at narrative documentation — tutorials, guides, and architecture descriptions — using reStructuredText or Markdown. While CMake doesn't have a built-in FindSphinx module, creating one is straightforward:

# cmake/FindSphinx.cmake
find_program(SPHINX_EXECUTABLE
    NAMES sphinx-build
    DOC "Path to sphinx-build executable"
)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Sphinx
    REQUIRED_VARS SPHINX_EXECUTABLE
    VERSION_VAR SPHINX_VERSION
)
# CMakeLists.txt — Sphinx documentation target
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(Sphinx)

if(SPHINX_EXECUTABLE)
    set(SPHINX_SOURCE "${CMAKE_SOURCE_DIR}/docs")
    set(SPHINX_BUILD "${CMAKE_BINARY_DIR}/docs/sphinx")

    add_custom_target(sphinx-docs
        COMMAND ${SPHINX_EXECUTABLE}
            -b html
            -d "${SPHINX_BUILD}/doctrees"
            "${SPHINX_SOURCE}"
            "${SPHINX_BUILD}/html"
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
        COMMENT "Building Sphinx documentation..."
    )
endif()

Breathe Bridge: Doxygen + Sphinx

Breathe is a Sphinx extension that reads Doxygen's XML output and exposes it as Sphinx directives. This lets you combine Doxygen's automatic API extraction with Sphinx's superior narrative documentation capabilities.

Doxygen + Sphinx + Breathe Architecture
        flowchart LR
            subgraph Sources
                A[C++ Headers]
                B[Comment Blocks]
            end
            subgraph Doxygen
                C[Parse & Extract]
                D[XML Output]
            end
            subgraph Sphinx
                E[RST Narrative Docs]
                F[Breathe Directives]
                G[Sphinx Builder]
            end
            A --> C
            B --> C
            C --> D
            D --> F
            E --> G
            F --> G
            G --> H[Combined HTML]
    
# docs/conf.py — Sphinx configuration with Breathe
project = '@PROJECT_NAME@'
version = '@PROJECT_VERSION@'
author = 'Your Name'

extensions = [
    'breathe',
    'sphinx.ext.autodoc',
    'sphinx.ext.intersphinx',
    'sphinx.ext.viewcode',
]

# Breathe configuration
breathe_projects = {
    '@PROJECT_NAME@': '@CMAKE_BINARY_DIR@/docs/xml'
}
breathe_default_project = '@PROJECT_NAME@'

# Theme
html_theme = 'furo'
# Full Doxygen + Breathe + Sphinx pipeline
find_package(Doxygen REQUIRED)
find_package(Sphinx REQUIRED)

# Step 1: Generate Doxygen XML
set(DOXYGEN_GENERATE_HTML NO)   # Sphinx produces the HTML
set(DOXYGEN_GENERATE_XML YES)   # Breathe reads this
set(DOXYGEN_XML_OUTPUT "${CMAKE_BINARY_DIR}/docs/xml")

doxygen_add_docs(doxygen-xml
    ${CMAKE_SOURCE_DIR}/include
    COMMENT "Generating Doxygen XML for Breathe..."
)

# Step 2: Configure Sphinx conf.py
configure_file(
    ${CMAKE_SOURCE_DIR}/docs/conf.py.in
    ${CMAKE_BINARY_DIR}/docs/conf.py
    @ONLY
)

# Step 3: Build Sphinx (depends on Doxygen XML)
add_custom_target(docs
    COMMAND ${SPHINX_EXECUTABLE}
        -b html
        -c "${CMAKE_BINARY_DIR}/docs"
        "${CMAKE_SOURCE_DIR}/docs"
        "${CMAKE_BINARY_DIR}/docs/html"
    DEPENDS doxygen-xml
    COMMENT "Building Sphinx + Breathe documentation..."
)

Using Breathe Directives in RST

# docs/api.rst — Example API documentation page
API Reference
=============

MyClass
-------

.. doxygenclass:: MyNamespace::MyClass
   :members:
   :protected-members:

Helper Functions
----------------

.. doxygenfunction:: MyNamespace::helper_function

.. doxygenfile:: utils.h
   :sections: func

Documentation as a Custom Target

The "docs" Target Pattern

# Option to include docs in the default ALL target
option(BUILD_DOCS "Build documentation as part of ALL" OFF)

if(BUILD_DOCS)
    # Adding ALL makes docs build with every 'cmake --build'
    doxygen_add_docs(docs ALL
        ${CMAKE_SOURCE_DIR}/include
        COMMENT "Building documentation..."
    )
else()
    # Without ALL, docs must be built explicitly
    doxygen_add_docs(docs
        ${CMAKE_SOURCE_DIR}/include
        COMMENT "Building documentation..."
    )
endif()

configure_file for Documentation Templates

Use configure_file() to inject version numbers, project names, and build information into documentation templates:

# Substitute project info into docs
configure_file(
    ${CMAKE_SOURCE_DIR}/docs/version.rst.in
    ${CMAKE_BINARY_DIR}/docs/version.rst
    @ONLY
)

configure_file(
    ${CMAKE_SOURCE_DIR}/docs/conf.py.in
    ${CMAKE_BINARY_DIR}/docs/conf.py
    @ONLY
)
# docs/version.rst.in
Version Information
===================

:Project: @PROJECT_NAME@
:Version: @PROJECT_VERSION@
:Date: @BUILD_DATE@
:Git Hash: @GIT_HASH@
# Get build date and git hash for docs
string(TIMESTAMP BUILD_DATE "%Y-%m-%d")
execute_process(
    COMMAND git rev-parse --short HEAD
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    OUTPUT_VARIABLE GIT_HASH
    OUTPUT_STRIP_TRAILING_WHITESPACE
    ERROR_QUIET
)
if(NOT GIT_HASH)
    set(GIT_HASH "unknown")
endif()

Markdown README Integration

Doxygen can use your project's README.md as the main page, keeping a single source of truth:

# Use README.md as the Doxygen main page
set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")

# Enable Markdown support in Doxygen
set(DOXYGEN_MARKDOWN_SUPPORT YES)
set(DOXYGEN_AUTOLINK_SUPPORT YES)

doxygen_add_docs(docs
    ${CMAKE_SOURCE_DIR}/README.md
    ${CMAKE_SOURCE_DIR}/CHANGELOG.md
    ${CMAKE_SOURCE_DIR}/include
    COMMENT "Generating documentation..."
)
Experiment Intermediate

Try It: Breathe Pipeline

Set up a project with C++ source documented with Doxygen comment blocks and a Sphinx docs/ directory with narrative RST files. Configure the full Doxygen → XML → Breathe → Sphinx pipeline and verify that both the auto-extracted API docs and your handwritten guides appear in the same HTML output.

sphinx breathe documentation

Documentation Installation

install(DIRECTORY) for Built Docs

# Install generated HTML documentation
install(DIRECTORY ${CMAKE_BINARY_DIR}/docs/html/
    DESTINATION ${CMAKE_INSTALL_DOCDIR}
    COMPONENT documentation
    OPTIONAL  # Don't fail if docs weren't built
)

# Also install raw markdown docs
install(FILES
    ${CMAKE_SOURCE_DIR}/README.md
    ${CMAKE_SOURCE_DIR}/CHANGELOG.md
    ${CMAKE_SOURCE_DIR}/LICENSE
    DESTINATION ${CMAKE_INSTALL_DOCDIR}
    COMPONENT documentation
)

CPack Inclusion

# Include documentation in packages
set(CPACK_COMPONENTS_ALL runtime development documentation)
set(CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "Documentation")
set(CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION
    "API reference and user guides")
set(CPACK_COMPONENT_DOCUMENTATION_GROUP "docs")

# Ensure docs are built before packaging
add_dependencies(package docs)  # Not always reliable — see below

# Better: add a custom target that builds docs then packages
add_custom_target(package-with-docs
    COMMAND ${CMAKE_COMMAND} --build . --target docs
    COMMAND ${CMAKE_COMMAND} --build . --target package
    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    COMMENT "Building docs and creating package..."
)

CI Documentation Deployment

Deploying to GitHub Pages

# .github/workflows/docs.yml
name: Documentation

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build-docs:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y doxygen graphviz
          pip install sphinx breathe furo

      - name: Configure and build docs
        run: |
          cmake -B build -DBUILD_DOCS=OFF
          cmake --build build --target docs

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: build/docs/html

  deploy:
    needs: build-docs
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

ReadTheDocs Integration

# .readthedocs.yaml
version: 2

build:
  os: ubuntu-22.04
  tools:
    python: "3.11"
  apt_packages:
    - doxygen
    - graphviz
    - cmake

sphinx:
  configuration: docs/conf.py
  builder: html

python:
  install:
    - requirements: docs/requirements.txt
# docs/requirements.txt
sphinx>=7.0
breathe>=4.35
furo
sphinx-copybutton
Experiment Advanced

Try It: GitHub Pages Deployment

Fork a C++ project, add Doxygen documentation via doxygen_add_docs(), create the GitHub Actions workflow above, and push to the main branch. Verify that your API documentation is live at https://<user>.github.io/<repo>/ within minutes of the push.

CI/CD GitHub Pages deployment

Conclusion & Next Steps

Documentation is a first-class deliverable, not an afterthought. By integrating it into your CMake build, docs stay synchronized with code, build automatically in CI, and ship with your releases. Key takeaways:

  • doxygen_add_docs() — The simplest path: set DOXYGEN_* variables and get a docs target
  • Doxyfile.in + configure_file() — Full control over Doxygen configuration with CMake variable substitution
  • Breathe + Sphinx — Combine auto-extracted API docs with handwritten narrative documentation
  • Graphviz — Automatic class diagrams, call graphs, and collaboration graphs
  • CI deployment — GitHub Pages and ReadTheDocs for always-up-to-date public documentation
  • install() + CPack — Include documentation in release packages
Official Reference: See the FindDoxygen module documentation for all DOXYGEN_* variables supported by doxygen_add_docs(), and the configure_file() command reference.