第十三章:构建文档
在本章中,我们将涵盖以下食谱:
引言
文档在所有软件项目中都是必不可少的:对于用户,解释如何获取和构建代码,并说明如何有效地使用您的代码或库,对于开发者,描述库的内部细节,并帮助其他程序员参与并贡献于您的项目。本章将展示如何使用 CMake 构建代码文档,使用两个流行的框架:Doxygen 和 Sphinx。
使用 Doxygen 构建文档
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-12/recipe-01
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Doxygen(www.doxygen.nl
)是一个非常流行的源代码文档工具。您可以在代码中添加文档标签作为注释。运行 Doxygen 将提取这些注释并在 Doxyfile 配置文件中定义的格式中创建文档。Doxygen 可以输出 HTML、XML,甚至是 LaTeX 或 PDF。本食谱将向您展示如何使用 CMake 构建您的 Doxygen 文档。
准备就绪
我们将使用之前章节中介绍的message
库的简化版本。源树组织如下:
. ├── cmake │ └── UseDoxygenDoc.cmake ├── CMakeLists.txt ├── docs │ ├── Doxyfile.in │ └── front_page.md └── src ├── CMakeLists.txt ├── hello-world.cpp ├── Message.cpp └── Message.hpp
我们的源代码仍然位于src
子目录下,自定义 CMake 模块位于cmake
子目录下。由于我们的重点是文档,我们删除了对 UUID 的依赖并简化了源代码。最显著的区别是头文件中的大量代码注释:
#pragma once #include <iosfwd> #include <string> /*! \file Message.hpp */ /*! \class Message * \brief Forwards string to screen * \author Roberto Di Remigio * \date 2018 */ class Message { public: /*! \brief Constructor from a string * \param[in] m a message */ Message(const std::string &m) : message_(m) {} /*! \brief Constructor from a character array * \param[in] m a message */ Message(const char *m) : message_(std::string(m)) {} friend std::ostream &operator<<(std::ostream &os, Message &obj) { return obj.printObject(os); } private: /*! The message to be forwarded to screen */ std::string message_; /*! \brief Function to forward message to screen * \param[in, out] os output stream */ std::ostream &printObject(std::ostream &os); };
这些注释采用/*! */
格式,并包含一些特殊标签,这些标签被 Doxygen 理解(参见www.stack.nl/~dimitri/doxygen/manual/docblocks.html
)。
如何操作
首先,让我们讨论根目录中的CMakeLists.txt
文件:
- 如您所熟悉,我们声明一个 C++11 项目,如下所示:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-01 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON)
- 我们定义共享和静态库以及可执行文件的输出目录,如下所示:
include(GNUInstallDirs) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR})
- 我们将
cmake
子目录附加到CMAKE_MODULE_PATH
。这是 CMake 找到我们的自定义模块所必需的:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
- 包含自定义模块
UseDoxygenDoc.cmake
。我们将在后面讨论其内容:
include(UseDoxygenDoc)
- 然后我们添加
src
子目录:
add_subdirectory(src)
src
子目录中的CMakeLists.txt
文件包含以下构建块:
- 我们添加一个
message
静态库,如下所示:
add_library(message STATIC Message.hpp Message.cpp )
- 然后我们添加一个可执行目标,
hello-world
:
add_executable(hello-world hello-world.cpp)
- 然后,
hello-world
可执行文件应该链接到消息库:
target_link_libraries(hello-world PUBLIC message )
在根CMakeLists.txt
文件的最后一节中,我们调用了add_doxygen_doc
函数。这添加了一个新的docs
目标,该目标将调用 Doxygen 来构建我们的文档:
add_doxygen_doc( BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/_build DOXY_FILE ${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile.in TARGET_NAME docs COMMENT "HTML documentation" )
最后,让我们看一下UseDoxygenDoc.cmake
模块,其中定义了add_doxygen_doc
函数:
- 我们找到
Doxygen
和Perl
可执行文件,如下所示:
find_package(Perl REQUIRED) find_package(Doxygen REQUIRED)
- 然后,我们声明
add_doxygen_doc
函数。该函数理解单值参数:BUILD_DIR
、DOXY_FILE
、TARGET_NAME
和COMMENT
。我们使用 CMake 的标准命令cmake_parse_arguments
来解析这些参数:
function(add_doxygen_doc) set(options) set(oneValueArgs BUILD_DIR DOXY_FILE TARGET_NAME COMMENT) set(multiValueArgs) cmake_parse_arguments(DOXY_DOC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) # ... endfunction()
Doxyfile
包含构建文档所需的所有 Doxygen 设置。模板Doxyfile.in
作为函数参数DOXY_FILE
传递,并被解析到DOXY_DOC_DOXY_FILE
变量中。我们按照以下方式配置模板文件Doxyfile.in
:
configure_file( ${DOXY_DOC_DOXY_FILE} ${DOXY_DOC_BUILD_DIR}/Doxyfile @ONLY )
- 然后,我们定义一个名为
DOXY_DOC_TARGET_NAME
的自定义目标,它将使用Doxyfile
中的设置执行 Doxygen,并将结果输出到DOXY_DOC_BUILD_DIR
:
add_custom_target(${DOXY_DOC_TARGET_NAME} COMMAND ${DOXYGEN_EXECUTABLE} Doxyfile WORKING_DIRECTORY ${DOXY_DOC_BUILD_DIR} COMMENT "Building ${DOXY_DOC_COMMENT} with Doxygen" VERBATIM )
- 最终,会向用户打印一条状态消息:
message(STATUS "Added ${DOXY_DOC_TARGET_NAME} [Doxygen] target to build documentation")
我们可以像往常一样配置项目:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build .
通过调用我们的自定义docs
目标,可以构建文档:
$ cmake --build . --target docs
你会注意到,在构建树中会出现一个_build
子目录。这包含 Doxygen 从你的源文件生成的 HTML 文档。使用你喜欢的浏览器打开index.html
将显示 Doxygen 欢迎页面。
如果你导航到类列表,你可以例如浏览Message
类的文档:
工作原理
CMake 默认不支持文档构建。但是,我们可以使用add_custom_target
来执行任意操作,这是我们在本食谱中利用的机制。需要注意的是,我们需要确保系统上存在构建文档所需的工具(在本例中为 Doxygen 和 Perl)。
此外,请注意UseDoxygenDoc.cmake
自定义模块仅执行以下操作:
- 执行对 Doxygen 和 Perl 可执行文件的搜索
- 定义一个函数
实际创建docs
目标的操作留给了稍后调用add_doxygen_doc
函数。这是一种“显式优于隐式”的模式,我们认为这是良好的 CMake 实践:不要使用模块包含来执行类似宏(或函数)的操作。
我们通过使用函数而不是宏来实现add_doxygen_doc
,以限制变量定义的作用域和可能的副作用。在这种情况下,函数和宏都可以工作(并且会产生相同的结果),但我们建议除非需要修改父作用域中的变量,否则应优先使用函数而不是宏。
CMake 3.9 中添加了一个新的改进的FindDoxygen.cmake
模块。实现了便利函数doxygen_add_docs
,它将作为我们在本食谱中介绍的宏。有关更多详细信息,请查看在线文档cmake.org/cmake/help/v3.9/module/FindDoxygen.html
。
使用 Sphinx 构建文档
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-12/recipe-02
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
Sphinx 是一个 Python 程序,也是一个非常流行的文档系统(www.sphinx-doc.org
)。当与 Python 项目一起使用时,它可以解析源文件中的所谓 docstrings,并自动为函数和类生成文档页面。然而,Sphinx 不仅限于 Python,还可以解析 reStructuredText、Markdown 纯文本文件,并生成 HTML、ePUB 或 PDF 文档。与在线 Read the Docs 服务(readthedocs.org
)结合使用,它提供了一种快速开始编写和部署文档的绝佳方式。本食谱将向您展示如何使用 CMake 基于 Sphinx 构建文档。
准备工作
我们希望构建一个简单的网站来记录我们的消息库。源树现在看起来如下:
. ├── cmake │ ├── FindSphinx.cmake │ └── UseSphinxDoc.cmake ├── CMakeLists.txt ├── docs │ ├── conf.py.in │ └── index.rst └── src ├── CMakeLists.txt ├── hello-world.cpp ├── Message.cpp └── Message.hpp
我们在cmake
子目录中有一些自定义模块,docs
子目录包含我们网站的主页,以纯文本 reStructuredText 格式,index.rst
,以及一个 Python 模板文件,conf.py.in
,用于 Sphinx 的设置。此文件可以使用 Sphinx 安装的一部分sphinx-quickstart
实用程序自动生成。
如何操作
与之前的食谱相比,我们将修改根CMakeLists.txt
文件,并实现一个函数(add_sphinx_doc
):
- 在将
cmake
文件夹附加到CMAKE_MODULE_PATH
之后,我们如下包含UseSphinxDoc.cmake
自定义模块:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") include(UseSphinxDoc)
UseSphinxDoc.cmake
模块定义了add_sphinx_doc
函数。我们使用关键字参数调用此函数,以设置我们的 Sphinx 文档构建。自定义文档目标将被称为docs
:
add_sphinx_doc( SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/docs BUILD_DIR ${CMAKE_CURRENT_BINARY_DIR}/_build CACHE_DIR ${CMAKE_CURRENT_BINARY_DIR}/_doctrees HTML_DIR ${CMAKE_CURRENT_BINARY_DIR}/sphinx_html CONF_FILE ${CMAKE_CURRENT_SOURCE_DIR}/docs/conf.py.in TARGET_NAME docs COMMENT "HTML documentation" )
UseSphinxDoc.cmake
模块遵循我们在前一个食谱中使用的相同“显式优于隐式”模式:
- 我们需要找到 Python 解释器和
Sphinx
可执行文件,如下所示:
find_package(PythonInterp REQUIRED) find_package(Sphinx REQUIRED)
- 然后我们定义带有单值关键字参数的
add_sphinx_doc
函数。这些参数由cmake_parse_arguments
命令解析:
function(add_sphinx_doc) set(options) set(oneValueArgs SOURCE_DIR BUILD_DIR CACHE_DIR HTML_DIR CONF_FILE TARGET_NAME COMMENT ) set(multiValueArgs) cmake_parse_arguments(SPHINX_DOC "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) # ... endfunction()
- 模板文件
conf.py.in
,作为CONF_FILE
关键字参数传递,配置为在SPHINX_DOC_BUILD_DIR
中的conf.py
:
configure_file( ${SPHINX_DOC_CONF_FILE} ${SPHINX_DOC_BUILD_DIR}/conf.py @ONLY )
- 我们添加了一个名为
SPHINX_DOC_TARGET_NAME
的自定义目标,以协调使用 Sphinx 构建文档:
add_custom_target(${SPHINX_DOC_TARGET_NAME} COMMAND ${SPHINX_EXECUTABLE} -q -b html -c ${SPHINX_DOC_BUILD_DIR} -d ${SPHINX_DOC_CACHE_DIR} ${SPHINX_DOC_SOURCE_DIR} ${SPHINX_DOC_HTML_DIR} COMMENT "Building ${SPHINX_DOC_COMMENT} with Sphinx" VERBATIM )
- 最后,我们向用户打印出一条状态消息:
message(STATUS "Added ${SPHINX_DOC_TARGET_NAME} [Sphinx] target to build documentation")
- 我们配置项目并构建
docs
目标:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . --target docs
这将在构建树的SPHINX_DOC_HTML_DIR
子目录中生成 HTML 文档。再次,您可以使用您喜欢的浏览器打开index.html
并查看闪亮(但仍然稀疏)的文档:
它是如何工作的
再次,我们利用了add_custom_target
的强大功能,向我们的构建系统添加了一个任意构建目标。在这种情况下,文档将使用 Sphinx 构建。由于 Sphinx 是一个可以与其他 Python 模块扩展的 Python 程序,因此docs
目标将依赖于 Python 解释器。我们确保通过使用find_package
来满足依赖关系。请注意,FindSphinx.cmake
模块还不是标准的 CMake 模块;它的副本包含在项目源代码的cmake
子目录下。
结合 Doxygen 和 Sphinx
本食谱的代码可在github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-12/recipe-03
找到,并包含一个 C++示例。该食谱适用于 CMake 版本 3.5(及以上),并在 GNU/Linux、macOS 和 Windows 上进行了测试。
我们有一个 C++项目,因此,Doxygen 是生成源代码文档的理想选择。然而,我们也希望发布面向用户的文档,例如解释我们的设计选择。我们更愿意使用 Sphinx 来实现这一点,因为生成的 HTML 也可以在移动设备上工作,而且我们可以将文档部署到 Read the Docs(readthedocs.org
)。本食谱将说明如何使用 Breathe 插件(breathe.readthedocs.io
)来桥接 Doxygen 和 Sphinx。
CMake 秘籍(七)(2)https://developer.aliyun.com/article/1525424