In this article, we will talk about the d word again; documentation. I know, most developers find open-heart surgery more appealing that having to write documentation for their own code, but it is an integral part of software engineering and ensuring other users can work with your software. I wouldn’t do you a favour by not showing you how we can automate the documentation step with CMake.
The end result will be a fully automated code documentation process, that will generated this documentation for our complex number class, that we have worked with in previous articles. The documentation will be automatically build every time we compile our library and so will be guaranteed to be up to date. We also look at how we can install our documentation along our library and header files.
If you follow this article to the end, you will have the power to forget about code documentation entirely; all you have to do is annotate your header files (interfaces), and CMake will generate the required documentation automatically with the help of Doxygen, our preferred tool of annotating C++-based projects.
Download Resources
All developed code and resources in this article are available for download. If you are encountering issues running any of the scripts, please refer to the instructions for running scripts downloaded from this website.
- Download: Build-Systems-Part-7
In this series
- Part 1: How to use Makefiles to automate CFD solver compilations
- Part 2: Introduction to CMake for CFD practitioners
- Part 3: Advanced CMake features to power up your CFD development
- Part 4: How to compile, install, and use custom libraries with CMake
- Part 5: How to add external libraries into your CFD solver using CMake
- Part 6: How to automate testing with CMake, CTest, and gtest
- Part 7: Never worry about Docs again! Automate it with CMake instead
- Part 8: How to use CMake/CPack to create beautiful GUI-installers
- Part 9: Complete walkthrough: Adding CMake to an existing library
- Part 10: How to use Conan to manage your own CMake-based libraries
In this article
Introduction
Up to this point, we have achieved quite a bit with CMake; from creating a simple hello world project with some advanced features to compiling our own libraries, installing them locally for other packages to consume them, integrating a package manager to handle our dependencies for us, as well as adding tests and letting CMake discover them for automatic test execution. This puts us already in a very strong position to automate a large portion of our build step.
Not too long ago, we looked at the dreaded topic of code documentation, and the various types that exists. In particular, we looked in detail at how to write a 1-page documentation, a full blown user-guide, and tools that create documentation automatically based on annotation in the source code.
In this article, we are going to return, for one final time, to the most hated topic a programmer can think of – documentation – and see how we can automate the generation of documentation so that we can pretty much forget about it. Well, we still need to write a user guide if we want to be kind to our users, but most of the time, annotating our classes will be sufficient to create a well-maintained and documented codebase, and CMake can use that, together with Doxygen, to create our source code documentation for us.
To do so, we will use our dummy complex number library, which we have (ab)used during this series, and we will first write some source code documentation. We will also add some simple pages that could serve as a user guide. However, a real user guide would likely have a bit more depth to it (and more than one class to talk about). I talk more about what should go into a user guide in my write-up, and you should use that article instead to make a decision of what to include and exclude from your user guide.
With all of this in-place, we will then look at how we can use CMake to automatically build and install the documentation, if it was indeed requested. We will use Conan here again to install Doxygen as a project dependency, however, since Doxygen is an executable and not a library, the dependency management workflow is slightly different to what we are used when using Conan, so we will look at that as well briefly.
Finally, in preparation for the next article, we will also clean up our CMake files a bit and separate them into individual modules. This is not essential but helps to manage the complexity of our top-level CMakeLists.txt
file. Let’s look through our codebase and see what changes we have to do to get our documentation built automatically.
Generating Documentation for the complex number library
In this section, I want to go through the various parts of our project – which has now grown quite a bit in size – and explain what changes we have to make to bring the documentation into our project. We’ll go through the sections in our usual order, i.e. we review the top-level structure first, and then look at the various source files, followed by the CMake files. Finally, we’ll look at the commands required to build and install our project.
Project structure
Given that we just have a single class, our project has become rather large, and this is reflected in the project structure that you can see below.
root
├── build/
├── cmake/
│ ├── complexNumbersConfig.cmake.in
│ ├── docs.cmake
│ ├── install.cmake
│ ├── makeLibrary.cmake
│ └── options.cmake
├── complexNumberLib/
│ ├── CMakeLists.txt
│ ├── complexNumbers.cpp
│ └── complexNumbers.hpp
├── docs/
│ ├── doxygen-awesome-css-2.3.2/
│ │ └── ...
│ ├── userGuide/
│ │ ├── index.md
│ │ ├── installation.md
│ │ ├── introduction.md
│ │ └── usage.md
│ └── Doxyfile.in
├── CMakeLists.txt
└── conanfile.txt
We’ll continue to use build/
as our folder to collect all generated files by CMake and retain the cmake/
folder to house all of our CMake scripts. This is done to debloat our top-level CMakeLists.txt
file, and we will see this shortly. The complexNumberLib/
folder houses our source code (the one single class that we have) and the docs/
folder deals with all documentation-related code.
The docs/
folder mirrors that of the project we worked on previously when introducing Doxygen but contains essentially three pieces: the all-important Doxyfile.in, where you have noticed that the *.in suffix indicates that this is a configuration file, and we want CMake to make some substitutions for us later (we’ll look into those in detail shortly), the userGuide/
folder, which contains additional Markdown files that form our user guide, and the doxygen-awesome-css-2.3.2/
folder, which holds a stylesheet for a modernised look for Doxygen.
Finally, we have our top-level CMakeLists.txt
file, which will steer the compilation and installation step, as well as the conanfile.txt
, which will help us bring Doxygen into our project.
Documentation
Okay, so we have an idea of how the project is structured. However, as we saw in our Doxygen article, we need to prepare our source code for Doxygen to be able to generate automated documentation. This will require some additional changes to the source code itself and this is what we will look at next.
Source code documentation
The first thing we need to do is to provide additional comments in our header file. For consistency, I also like to annotate my source files, i.e. the *.cpp
files, but this is not essential. This is more of a help for other developers to find their way around quickly. The header and source file templates can be found in the Doxygen article, and I have applied both them to the header and source files. The header file located at complexNumberLib/complexNumbers.hpp
now becomes:
#pragma once
// preprocessor statements
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
#if defined(COMPILEDLL)
#define EXPORT __declspec(dllexport)
#elif defined(COMPILELIB)
#define EXPORT
#else
#define EXPORT __declspec(dllimport)
#endif
#else
#define EXPORT
#endif
// c++ include headers
#include <iostream>
#include <cmath>
#include <stdexcept>
#include <limits>
// third-party include headers
// include headers from current project
// helper classes
// namespace declaration go here
/**
* \class ComplexNumber
* \brief A class for complex number arithmetic and manipulation
*
* This class implements complex numbers to allow for complex number arithmetic.
*
* The following demonstrates some example for how this class could be used:
* \code
* #include <cassert>
* #include "complexNumberLib/complexNumber.hpp"
*
* int main() {
* ComplexNumber a(3.0, 4.0);
* ComplexNumber b(1.8, 5.3);
*
* assert(a.magnitude() == 5.0);
*
* a.conjugate();
* assert(a.Re() == 3.0);
* assert(a.Im() == -4.0);
*
* auto c1 = a + b;
* auto c2 = a - b;
* auto c2 = a * b;
* auto c2 = a / b;
*
* return 0;
* }
* \endcode
*/
class ComplexNumber {
public:
/// \name Custom types used in this class
/// @{
/// @}
public:
/// \name Constructors and destructors
/// @{
EXPORT ComplexNumber(double real, double imaginary);
EXPORT ~ComplexNumber();
/// @}
public:
/// \name API interface that exposes behaviour to the caller
/// @{
EXPORT void conjugate();
EXPORT double magnitude() const;
/// @}
public:
/// \name Getters and setters
/// @{
EXPORT double Re() const;
EXPORT double Im() const;
EXPORT void setRe(double real);
EXPORT void setIm(double imaginary);
/// @}
public:
/// \name Overloaded operators
/// @{
EXPORT ComplexNumber operator+(const ComplexNumber &other);
EXPORT ComplexNumber operator-(const ComplexNumber &other);
EXPORT ComplexNumber operator*(const ComplexNumber &other);
EXPORT ComplexNumber operator/(const ComplexNumber &other);
EXPORT friend std::ostream &operator<<(std::ostream &os, const ComplexNumber &c);
/// @}
private:
/// \name Private or protected implementation details, not exposed to the caller
/// @{
void isNan(double real, double imaginary) const;
/// @}
private:
/// \name Encapsulated data (private or protected variables)
/// @{
double _real;
double _imaginary;
/// @}
};
Lines 1-28 simply break the code up into different sections, so we don’t mingle pre-processor calls with header include statements. Lines 30-59 are probably the most important part for documentation purposes, where we provide some details about the class and show anyone wanting to use this class how to do that with some example code. Always provide examples for how to use your class, this will help anyone going through your documentation much more than text!
Lines 61-113 contain the class definition and the internal structure of the class. The biggest (and only) difference here is that we have introduced sections that Doxygen will use to separate our functions later in the documentation. It is not technically required, but I like to provide some separations in my classes for constructors/destructors, getter/setter, API calls that are exposed to the user, overloaded operators, private implementation details and encapsulated data.
The source file, shown below and located at complexNumberLib/complexNumbers.cpp
, simply uses the same separated sections that we defined within the class, though as I said, this will not have any influence on how the documentation is parsed and build, it is simply for additional housekeeping and providing some separation in the source file for developers. If you want to be lazy, only document the header file (the interface), which is what most people do anyways.
// class declaration
#include "complexNumberLib/complexNumbers.hpp"
/// \name Constructors and destructors
/// @{
ComplexNumber::ComplexNumber(double real, double imaginary) : _real(real), _imaginary(imaginary) {
isNan(real, imaginary);
}
ComplexNumber::~ComplexNumber() = default;
/// @}
/// \name API interface that exposes behaviour to the caller
/// @{
void ComplexNumber::conjugate() { _imaginary *= -1.0; }
double ComplexNumber::magnitude() const { return std::sqrt(std::pow(_real, 2) + std::pow(_imaginary, 2)); }
/// @}
/// \name Getters and setters
/// @{
double ComplexNumber::Re() const { return _real; }
double ComplexNumber::Im() const { return _imaginary; }
void ComplexNumber::setRe(double real) { _real = real; }
void ComplexNumber::setIm(double imaginary) { _imaginary = imaginary; }
/// @}
/// \name Overloaded operators
/// @{
ComplexNumber ComplexNumber::operator+(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real + other._real;
c._imaginary = _imaginary + other._imaginary;
return c;
}
ComplexNumber ComplexNumber::operator-(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real - other._real;
c._imaginary = _imaginary - other._imaginary;
return c;
}
ComplexNumber ComplexNumber::operator*(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
ComplexNumber c(0, 0);
c._real = _real * other._real - _imaginary * other._imaginary;
c._imaginary = _real * other._imaginary + _imaginary * other._real;
return c;
}
ComplexNumber ComplexNumber::operator/(const ComplexNumber &other) {
isNan(_real, _imaginary);
isNan(other._real, other._imaginary);
double denominator = other._real * other._real + other._imaginary * other._imaginary;
if (std::abs(denominator) < std::numeric_limits<double>::epsilon())
throw std::runtime_error("Complex number division by zero");
ComplexNumber c(0, 0);
c._real = (_real * other._real + _imaginary * other._imaginary) / denominator;
c._imaginary = (_imaginary * other._real - _real * other._imaginary) / denominator;
return c;
}
std::ostream &operator<<(std::ostream &os, const ComplexNumber &c) {
os << "(" << c._real << ", " << c._imaginary << ")";
return os;
}
/// @}
/// \name Private or protected implementation details, not exposed to the caller
/// @{
void ComplexNumber::isNan(double real, double imaginary) const {
if (std::isnan(real) || std::isnan(imaginary))
throw std::runtime_error("Complex number is NaN");
}
/// @}
User-guide documentation
Ok, we have sorted out the code documentation for a code annotation point of view, but perhaps we also want to ship our documentation with an additional user guide. As mentioned in the introduction, a full blown user guide for a simple class is overkill, and there isn’t much to say here, but we’ll go through it anyway to look at how we could add a user guide for larger, more complex projects.
All the files for the user guide are located in the docs/userGuide/
folder. We have the index.md
, introduction.md
, installation.md
, and usage.md
Markdown files, that form the basis for our user-guide. A real project would likely have more files. For completeness, but without much discussion on the content, I’ll list the content of the files below, but feel free to download the project and browse through the files as well to see how they are all linked together.
index.md
This file serves as the entry point to our user guide, and we provide here a table of contents which mirrors the structure we discussed in the opening article for this series on what should go into a user guide (and any documentation, really, for that matter).
\page mainpage A simple C++ library for dealing with complex numbers
This is a simple and lightweight library that deals with complex number arithmetic.
## Table of contents
- [Introduction](\ref introduction)
- [Installation](\ref installation)
- [Usage](\ref usage)
introduction.md
Any thoughts you want to pass on to the user before they get started with your code should go here. This could be the motivation for why this code exists (and why the user should care about it), as well as any other opening thoughts that may be relevant.
\page introduction Introduction
A simple C++ library to work with complex numbers. This libraru was developed to showcase how to integrate `Doxygen` with `cmake` to automatically generate and deploy the latest documentation. Read the full write-up on how to automate your documentation generation on [cfd.university](https://cfd.university/learn).
installation.md
One of the more important pages in your user guide. Always show explicitly how to get your code to work, as there are just too many different ways to get code compiled and dependencies resolved. If you want your users to stand any chance of succeeding in using your code, give them specific build and installation instructions.
\page installation Installation
To build this library, use the following steps. You will need CMake and a compiler/linker isntalled, as a minimum. The Ninja build tool is recommended as well. The library will be first configured, then compiled, and finally installed. We can optionally install Doxygen through Conan, if required, which would be done before the configuration step.
### Install dependencies
Create a `build/` folder in the root directory, if not already present. If you wish to build the documentation, then install Doxygen through Conan first:
```bash
conan install . --output-folder=build --build=missing
```
This assumes that you have a terminal opened and have changed into the root directory of the project.
### Configuration
Next, let's configure the project. First, we need to change into the `build/` folder, and then we can execute the configure step with
```bash
cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_DOCS=ON -G Ninja ..
```
Here we are using Ninja as our build generator. If you don't have Ninja installed, omit the `-G Ninja` flag and let CMake chose the default compiler based on your platform. We require the documentation to be build as well, so we turn the `BUILD_DOCS` variable on.
### Compilation
Next, we need to compile the project, this is achieved using the following instructions:
```bash
cmake --build . --config Debug
```
### Installation
And finally, let's install the library into a desired location on our system. The following assumes that we are on Windows and want to install into the `C:\temp` directory, but you may change that depending on yoru preference and platform. For example, on UNIX, you may want to use `~/temp` instead.
```bash
cmake --install . --prefix C:\temp --config Debug
```
The library will now be installed in the specified directory, along with the documentation
usage.md
Getting the code base compiled and installed is only the first small step. We need to be able to use this code, so we should also provide example use cases in the documentation. This can focus on high-level usage, i.e. how different classes interact with each other. You still have the documentation for each class through the code annotation exercise, where you show how to use individual classes. Since we only have a single class in this example, both the usage.md
and complexNumbers.hpp
files use the same code example.
\page usage Usage
The following shows example use cases for the complex number library:
```cpp
#include <cassert>
#include "complexNumberLib/complexNumber.hpp"
int main() {
ComplexNumber a(3.0, 4.0);
ComplexNumber b(1.8, 5.3);
assert(a.magnitude() == 5.0);
a.conjugate();
assert(a.Re() == 3.0);
assert(a.Im() == -4.0);
auto c1 = a + b;
auto c2 = a - b;
auto c2 = a * b;
auto c2 = a / b;
return 0;
}
```
Doxygen setup file
We have provided annotated code, and we have provided a somewhat useful user guide. Now, it is time for Doxygen to find out about these settings. All documentation-related inputs are located in the docs/Doxyfile.in
, and we have discussed this file at length in the Doxygen article. Have a quick look through so you are aware of the most important settings within this file, as I won’t go through all of them. Download the project at the top of this article if you want to see all the options that I set in this file.
Before we can use the docs/Doxyfile.in
, though, we have to first process it with CMake. As mentioned above, the *.in
suffix indicates that this is a configuration file, and CMake will look through this file and replace specific CMake variables with those set in the project. In particular, this file contains 6 variables that need to be substituted:
PROJECT_NAME = @CMAKE_PROJECT_NAME@
PROJECT_NUMBER = @CMAKE_PROJECT_VERSION_MAJOR@.@CMAKE_PROJECT_VERSION_MINOR@.@CMAKE_PROJECT_VERSION_PATCH@
PROJECT_BRIEF = @CMAKE_PROJECT_DESCRIPTION@
PROJECT_LOGO = @CMAKE_SOURCE_DIR@/docs/figures/logo.png
HTML_EXTRA_STYLESHEET = @CMAKE_SOURCE_DIR@/docs/doxygen-awesome-css-2.3.2/doxygen-awesome.css
HTML_EXTRA_STYLESHEET += @CMAKE_SOURCE_DIR@/docs/doxygen-awesome-css-2.3.2/doxygen-awesome-sidebar-only.css
INPUT = @CMAKE_SOURCE_DIR@/complexNumberLib
INPUT += @CMAKE_SOURCE_DIR@/docs/userGuide/index.md
INPUT += @CMAKE_SOURCE_DIR@/docs/userGuide/introduction.md
INPUT += @CMAKE_SOURCE_DIR@/docs/userGuide/installation.md
INPUT += @CMAKE_SOURCE_DIR@/docs/userGuide/usage.md
We take the project name we defined with the call to project()
in our top-level CMakeLists.txt
file and set it equal to the project name in Doxygen. The project number (most people would call it version instead of number, but why go with contention if you can break it …) is also derived from the version triplet we specify in the call to project()
within CMake, as is the project brief/description.
We want to use a custom logo to be displayed in our documentation, and so we provide an absolute path to it. Here, ${CMAKE_SOURCE_DIR}
refers to the root directory of the project, i.e. the one containing the build/
folder. We do the same with the additional stylesheets, i.e. lines 5-6, where we bring in the stylesheets provided by the doxygen-awesome-css-2.3.2/
folder.
The INPUT
variable specified on lines 7-11 tells Doxygen where to find input files. In this case, we want to include the ${CMAKE_SOURCE_DIR}/complexNumberLib
folder to catch all annotated classes, as well as the ${CMAKE_SOURCE_DIR}/docs/userGuide/
folder, to capture all pages for the user-guide. We do provide each page here explicitly, as this will allow us to sort the pages in a specific order (otherwise, Doxygen will sort all pages in alphabetical order, which is just useless).
We will see shortly how we use this docs/Doxyfile.in
to generate our actual Doxyfile
to steer the generation of our documentation. But before we do that, let’s look at how we can get Doxygen into our project with the least pain.
Project dependencies
At this point, let’s quickly go through the conanfile.txt
that brings in all required dependencies. In this case, we only have Doxygen as an external dependency, and so this is the only item within the conanfile.txt
, shown below:
[tool_requires]
doxygen/1.9.4
To get this entry, look through conan center; the Doxygen entry will show you that the above code is what you’ll need to include in your conanfile.txt
. Previously, we saw the [requires]
section, which listed all the libraries we required. Since Doxygen is not a library but rather an executable, it is listed under [tools_required]
. This produces a different sets of outputs, specifically, some scripts that you can run that will tell your current console where to find all of the tools that were installed by Conan.
Getting Doxygen through Conan
With the tools specified that we want to use, we are ready to bring in Doxygen through Conan. Since I am on Windows, though, and I am trying to embrace the future (that is PowerShell, good bye cmd.exe
), I have to provide some additional settings to make PowerShell scripts available, not just the older version of *.bat
scripts.
In Conan, we can specify different profiles to give a hint about the type of hardware and build type we are interested in when running Conan. These are located in your ~/.conan2/profiles/
folder. I have created a dev
profile, which looks like the following:
[settings]
arch=x86_64
build_type=Debug
compiler=msvc
compiler.cppstd=20
compiler.runtime=dynamic
compiler.version=193
os=Windows
[conf]
tools.cmake.cmaketoolchain:generator=Ninja
tools.env.virtualenv:powershell=True
In particular, notice the last two lines, where I specify that I want to use Ninja
as my default build tool, as well as telling Conan that I do want to use PowerShell as a virtual environment, i.e. I want to create scripts that I can run from within my PowerShell that will discover the path where Conan has placed Doxygen after it has been installed.
Next, and within the root folder, i.e. not the build/
folder, we bring in our Conan dependencies with the usual command:
conan install . -pr dev --output-folder=build --build=missing
Here, we use the dev profile indicated by the -pr
flag and we specify the location where to store all generated output (build/
), and we want to build missing packages as well. This will create a bunch of executable scripts within the build/
folder.
I’ve noticed that, at least on my machine, Doxygen needs to be compiled from scratch. Doing so on Windows works just fine, but I am getting (strangely) issues on Linux, seemingly due to outdated commands in the Conan receipt to build this dependency. If you experience the same, simply get Doxygen through your package manager, e.g. sudo apt install -y doxygen
or brew install doxygen
. In this case, you can skip all Conan-related instructions.
Making Doxygen available
After we execute Conan, we will see a few files within the build/
folder, specifically the conanbuild.ps1
file (on Windows). If your Linux distro did not act up, you will see a file called conanbuild.sh
. We need to execute this file, which will then tell our current console where to find Doxygen (which will be in some hidden Conan directory). After we execute this script, i.e. with .\conanbuild.ps1
or ./conanbuild.sh
, we can run doxygen -h
to verify that Doxygen is now available. You’ll also notice that the last line states the following:
C:\Users\tom\.conan2\p\b\doxyg975c2f6880eb8\p\bin\doxygen.exe -d prints additional usage flags for debugging purposes
Doxygen gives us the absolute path to itself, and we can see that this is indeed in the hidden ~/.conan2/*
folder (where the dot (.
) indicates that this is a hidden folder). However, after running the conanbuild.ps1
script, we see that it has correctly found the executable.
You’ll also notice that calling conanbuild.ps1
(or conanbuild.sh
) generates a new script called deactivate_conanbuild.ps1
(or deactivate_conanbuild.sh
). Once you run this, Doxygen will no longer be available, though once you close your console, everything will be reset anyway, so there is no need to call this script. It may be more useful on a continuous integration server (for example), where you want to clean up after yourself after your project has been built and tested.
Ok, so we have annotated code, a user guide, and Doxygen available to build our documentation. Let’s get to the interesting part of automating Doxygen. For this, we need to look through our various CMake files to see how we can integrate Doxygen into our CMake build process, which we’ll look at next.
CMake build scripts
As alluded to above, I have decided to split up the top-level CMakeLists.txt
file into separate modules. In this section, I want to look at all of them, most of which we have already seen if you followed along in this series. There will be a new entry for the documentation, and we will examine that part in more detail while we go through the remaining modules faster.
Root-level CMake file
Within our root directory (the one containing the build/
folder), our CMakeLists.txt
file has been reduced to the version shown below:
cmake_minimum_required(VERSION 3.23)
project(complexNumbers
LANGUAGES CXX
VERSION 1.0.0
DESCRIPTION "A simple C++ library for complex numbers"
)
# include custom options
include(cmake/options.cmake)
# create complex number library target
include(cmake/makeLibrary.cmake)
# go through the complexNumberLib/ subdirectory and add any files to the library as needed
add_subdirectory(complexNumberLib)
# create the documentation if requested
include(cmake/docs.cmake)
# install the library and its headers
include(cmake/install.cmake)
We call cmake_minimum_version()
and project()
as per usual, once for each CMake-based project, and then split the remaining sections into:
- Processing additional options set on the command line during the configuration step within the
cmake/options.cmake
file (line 10) - Creating the library, either a shared (dynamic) or static version within the
cmake/makeLibrary.cmake
file (line 13). - Processing all required source files by going through the
complexNumberLib/
folder on line 16 - Creating all documentation automatically within the
cmake/docs.cmake
file (line 19). This is the file we are interested in this article - And finally, installing the library within the
cmake/install.cmake
file (line 22)
There is no real convention here when it comes to the top-level CMakeLists.txt
file. I have seen projects defining pretty much everything in this file, including adding source files to the executable (i.e. they only use a single CMake file to steer the entire process), to an approach followed above where tasks are delegated into other files to help separate the build process.
Perhaps the most common approach, though, is an inconsistent mix of both, where some parts of the build process are defined in the top-level CMakeLists.txt
file, while others are split up into files which are located within the cmake/
folder. Given the complexity a CMake-based project can reach, I find it only natural to apply the same rules to my build scripts as I do to my source files (single responsibility principle and a manageable number of lines per document).
For completeness, the CMakeLists.txt
file within the complexNumberLib/
folder is shown below, which hasn’t changed from previous articles. It simply collects the source file, adds it to our library target (which we will define in the next section), and adds the header file to the public FILE_SET
property, so we know that it is required later when we install the library.
target_sources(${CMAKE_PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.cpp
PUBLIC FILE_SET HEADERS
BASE_DIRS ${PROJECT_SOURCE_DIR}
FILES ${CMAKE_CURRENT_SOURCE_DIR}/complexNumbers.hpp
)
Exposing additional options to the user
The first include statement that we saw was for the cmake/options.cmake
file. Previously, we used the CMakeOptions.txt
file the root directory to store all of our configuration options, which we have now moved (for consistency) into the cmake/
folder. It doesn’t really matter where it is, as I said, there is no convention in this regard so we are free to handle this as we wish. The content is shown below:
option(ENABLE_SHARED "Build shared (dynamic) library? If set to off, a static library will be built" OFF)
option(BUILD_DOCS "Build the documentation. This requires doxygen to be available" OFF)
We saw the first option before, i.e. the option asking us if we want to build a static or dynamic library by default. This is set to static, which are generally easier to handle than dynamic (shared) libraries. We also have a second option now, which is asking us to build the documentation along with the project. This is turned off by default, and so we have to remember to later switch it on when we configure our project.
Creating the library
The next include statement that we saw was for the cmake/makeLibrary.cmake
file, and this essentially just encapsulates what we have seen before when creating our static or dynamic (shared) library. Please refer to that if anything in the script below is unclear. We simply go through the library creation and add relevant pre-processor commands to the compiler flags if we use Microsoft’s C++ compiler. Also, we use this ingenious Windows-only hack for adding the letter d
to the library name if we build a debug version of our library.
# Add a postfix for debug library builds to avoid name clashes with release builds
if(WIN32)
set(CMAKE_DEBUG_POSTFIX d)
endif()
if(${ENABLE_SHARED})
# Create a shared (dynamic) library for the complex number class
add_library(${CMAKE_PROJECT_NAME} SHARED)
# add correct compiler flags so that we can correctly import/export symbols
if (MSVC)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILEDLL)
endif()
else()
# Create a static library for the complex number class
add_library(${CMAKE_PROJECT_NAME} STATIC)
# add correct compiler flags so that we can correctly import/export symbols
if (MSVC)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE COMPILELIB)
endif()
endif()
target_include_directories(${CMAKE_PROJECT_NAME}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
Building the documentation automatically
Now, then, let’s get to the juicy part (I am easily excited by building scripts). We want to now build our documentation, and we saw from our previous article on Doxygen that the command to do so is simply running Doxygen with a single argument, which is the location of the Doxyfile. In our article before, we had this Doxyfile located in the docs/
folder, and so running Doxygen from the command line may have simply been doxygen docs/Doxyfile
, or doxygen Doxyfile
if we were in the docs/
folder already.
But this is not all. In the current project, we don’t have a Doxyfile
yet, but instead a Doxyfile.in
within the docs/
folder. This is a CMake configuration file, and this will be used to generate the Doxyfile
for us. But we are getting ahead of ourselves, let’s look through the file first, and then we look at the various steps involved.
# use doxygen for building the documentation, check that it is available
find_program(DOXYGEN_EXECUTABLE doxygen)
if (BUILD_DOCS)
if (DOXYGEN_EXECUTABLE)
# create Doxyfile
configure_file(${PROJECT_SOURCE_DIR}/docs/Doxyfile.in ${CMAKE_BINARY_DIR}/Doxyfile @ONLY)
# note the option ALL which allows to build the docs together with the application
add_custom_target(doxygenDocs ALL
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_BINARY_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Generating API documentation with Doxygen"
)
else (DOXYGEN_EXECUTABLE)
message(WARNING "Doxygen need to be installed to generate the doxygen documentation")
endif (DOXYGEN_EXECUTABLE)
endif (BUILD_DOCS)
The first thing we need to do is to make sure Doxygen is found and available. We do that by calling find_program()
on line 2, and we store the result of this search in the DOXYGEN_EXECUTABLE
variable. Then, on line 4, we check if the documentation should be built, and if so, we check that the executable for Doxygen was found. If it wasn’t and no documentation is requested, then there is no problem, but if we want to build documentation without Doxygen available, we will get a warning message printed to the console as defined on line 16.
However, if Doxygen is found (and we can use the result provided by the DOXYGEN_EXECUTABLE
variable here), we proceed to line 7, where we transform our docs/Doxyfile.in
to build/Doxyfile
. Notice here that PROJECT_SOURCE_DIR
is the absolute path to the project’s root directory, while CMAKE_BINARY_DIR
is the directory containing all files generated during the build, which pretty much always is build/
, but we could break this convention.
The @ONLY
keyword is required to replace only variables of the form @VAR@
, and not ${VAR}
. This is handy if you want to make sure that you are not accidentally replacing variables that could be defined using the ${}
syntax. There likely isn’t any variable defined this way in the Doxyfile.in
(I say likely, as the file is rather long to remember all of its content), but it is good practice to be restrictive here.
With the Doxfile
generated, we can now run Doxygen itself, and we use the add_custom_target()
function to do that on lines 10-14. First, we provide a name for the target, here doxygenDocs
, and we specify the ALL
keyword to make sure this command runs when we build the project with cmake --build .
If we don’t set the ALL
target here, we would have to manually specify that we want to build this target along with the project using the cmake --build . --target doxygenDocs
command.
The COMMAND
shown on line 11 now calls Doxygen through the DOXYGEN_EXECUTABLE
variable (which, in this context, will point to the Doxygen executable, e.g. doxygen.exe
) and our input file argument points to build/Doxyfile
. We set the working directory to the CMAKE_CURRENT_BINARY_DIR
, which is the same as the CMAKE_BINARY_DIR
in our case (we can have more than one build/
directory, if we wanted, and CMAKE_CURRENT_BINARY_DIR
can then differentiate between different build/
folders, CMAKE_BINARY_DIR
can’t).
That’s it. With these steps in place, we are now able to build our documentation automatically. If you want to use something similar for your own projects, you can pretty much copy and paste the above cmake/docs.cmake
file, and then only make changes to the configuration file if indeed required.
Creating the configuration files for the install step
The file cmake/complexNumberConfig.cmake.in
was discussed at length in my article on installing libraries with CMake. There is quite a bit to untangle here, so repeating this here would probably make this article longer than it needs to be. Feel free to look at the article linked above and then come back. I’ll wait for you here.
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
# any additional libraries we depend on should go here, e.g.
# find_dependency(Gtest::Gtest)
# notice that we use find_dependency(), not find_package() here
include("${CMAKE_CURRENT_LIST_DIR}/complexNumbersTargets.cmake")
Oh, you are back. Should we crack on with the installation file, then?
Specific rules for the installation step
So the cmake/install.cmake
file has actually slightly changed. and if you just looked at the file below and at the above-linked article, you may not easily spot what has changed. The most obvious change is the additional install target on lines 33-39, which installs the documentation folder generated by Doxygen. But since we do have now to separate targets, essentially, that we want to install, i.e. the library and the documentation, it makes sense to split them into separate components, which we have achieved by adding the COMPONENT
keyword.
# install the headers that we specified with FILE_SET (header files for the user to include in their projects)
install(TARGETS ${CMAKE_PROJECT_NAME}
EXPORT complexNumbersTargets
FILE_SET HEADERS
COMPONENT complexLib
)
# provide some basic meta data about the library to generate the library's config file
install(
EXPORT complexNumbersTargets
FILE complexNumbersTargets.cmake
DESTINATION "lib/cmake/complexNumbers"
NAMESPACE complexNumbers::
COMPONENT complexLib
)
# generate the library's config file
include(CMakePackageConfigHelpers)
configure_package_config_file(
cmake/complexNumbersConfig.cmake.in
"${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
INSTALL_DESTINATION "lib/cmake/complexNumbers"
)
# install the config file
install(
FILES "${CMAKE_CURRENT_BINARY_DIR}/complexNumbersConfig.cmake"
DESTINATION "lib/cmake/complexNumbers"
COMPONENT complexLib
)
# install the documentation if requested
if (BUILD_DOCS)
install(
DIRECTORY ${CMAKE_BINARY_DIR}/docs/html
DESTINATION docs/complexNumberLib/
COMPONENT documentation
)
endif(BUILD_DOCS)
You’ll find that we have defined two components now; complexLib
and documentation
. The names are not necessarily important here, but they help CMake recognise to which group, essentially, each install step belongs (otherwise, they will land in the default UNSPECIFIED
component group).
It has little consequence for now, but in the next article, we’ll put the installation steps on steroids, and then having components defined is extremely useful. So, we acknowledge the presence of components for the moment and then return back to them in the next article.
Lines 33-39, dealing with the documentation install step, should hopefully also be familiar at this stage. But if it isn’t, we simply specify a DIRECTORY
that we want to install, which is the generated HTML documentation by Doxygen, and we want to install (copy) it to the DESTINATION
directory. We don’t have to specify the absolute path for the destination (to make things in CMake super inconsistent, as we do have to specify it for the source DIRECTORY
(also, SOURCE
would have been a better name than DIRECTORY
!)), so just provide the relative path within the installation directory here.
Once we have completed all of this, we are ready to build and install our project once again!
Building and installing the project
In this section, after we have modified our project, which now includes documentation, I want to look at the various commands we have to hack into our keyboard to get this project compiled and installed. It follows mostly what we have seen before, with some minor twists. However, repetition is good here as it will build up our muscle memory, so let’s go through it step by step.
Making Doxygen available
First things first, we want to make Doxygen available in our project, for which we use Conan, as discussed above. If you are one of the unlucky ones that encounter the issues mentioned in the Conan section above and had to use your package manager to get Doxygen installed, then you can skip this step.
Open the terminal and change into your top-level directory, which should contain an empty build/
folder. Then, execute Conan. I am using the following command:
conan install . -pr dev --output-folder=build --build=missing
Your command may be slightly different, as I am using my dev
profile here, and you may have a differently named one (or you use the default profile, in which case, you can ignore the -pr dev
part entirely).
This step will generate a bunch of batch files in our build/
directory. The one I am interested in is the build/conanbuild.ps1
file on Windows, and build/conanbuild.sh
on UNIX. I haven’t tested all shells, but you may get different file endings if you are running something different to the bourne shell (sh or bash) or Z shell (zsh, mainly macOS). Either way, whatever file is generated for your unique and exotic operating system, you’ll have to execute that file. To do that, change to the build/
directory and then execute
.\conanbuild.ps1
on Windows, and
./conanbuild.sh
on UNIX. This should make Doxygen available, and you can verify that by typing doxygen -h
, which should print some help messages to the screen. If all of this has worked, great, we are ready for building the project with CMake.
Configuration
The configuration step is almost the same as what we are used to. Here, I am also specifying that I want to build the documentation using the -DBUILD_DOCS=ON
declaration, as we have set it to OFF
by default. The full command for me is:
cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_DOCS=ON -G Ninja ..
Again, yours may differ if you are not using Ninja as your build tool. Incidentally, if you want to use Ninja but have no idea how to get it installed on your system, go back to your conanfile.txt
and change it to
[tool_requires]
doxygen/1.9.4
ninja/1.12.1
This will install Ninja together with Doxygen on your system and make it available when you run the conanbuild.ps1
or conanbuild.sh
scripts. A handy little trick to keep all of your dependencies in one area. Though Ninja is so common these days that you’ll likely get the latest stable version rather easily through your preferred package manager, yes. even on Windows.
Compiling and documentation building
The compilation step is the same one we have used in pretty much all previous articles and hopefully does not hold any surprises anymore:
cmake --build . --config Debug
Since we have specified the ALL
keyword in our custom target that we created for building the documentation. Once we call this command, CMake will execute Doxygen here and thus build our documentation as part of this command.
Installation of compiled libraries and documentation
Finally, we install the library as per our usual instructions:
cmake --install . --prefix C:\temp --config Debug
You can change the prefix to something more meaningful on your system (especially if you are on UNIX. Good luck finding your C:\
drive). On UNIX, your first hard drive will be sda
, your second hard drive will be sdb
and the third sdc
, and so on. Why is Windows starting with the letter C
? Well, if you ask that question, you probably have never used the A:\
and B:\
drive on Windows (and that’s a good thing).
If you have heard of floppy disks, congratulations, you know the secret, and we still reserve the letters A:\
and B:\
for them. But if you haven’t, think about a mobile, with no internet connection, no Bluetooth, not GPS, it can barely make a call and hold 2-3 compressed whatsApp images, while coming in at a weight of 100kg. Sounds appealing?
You can still buy USB-connected floppy disk readers, and for the cheap price of £30, you can by a pack of 10 floppy disks, with a total storage of 14.4 MBs. You can get a NVMe SSD with 500 GBs for the same price these days (and probably even that statement will age badly) …
Where was I? I am going off the rails again. Let’s quickly finish up and do something else with our lives for a while.
Once we execute the above command, notice that you will have a docs/
folder in your installation directory, and you should now be able to open the index.html page within this folder. Doing so will provide you with the documentation, which I have uploaded here as well so you can have a look and see what it should look like.
Summary
So then, this should be the last time you have heard me talk about documentation. It is a crucial part of software engineering and you will need it if you are interested in making your work available to others, either as end users or as developers to extend your project. We looked at how we can take Doxygen-based documentation and automatically build it during the CMake build process. All it takes is a configurable Doxyfile.in
and a mechanism to ensure Doxygen is available to build the documentation itself.
We are nearing the end of what we need to know about CMake and build automation. In the next article, as already alluded to earlier, we will take this project and provide a graphical user interface to install our software. Windows users will be familiar with this process, but we’ll explore how we can bring the same installation experience to UNIX systems as well. In this article, we already laid the foundation for some of that work to be realised with little additional changes, and I hope to see you in the next one!
Tom-Robin Teschner is a senior lecturer in computational fluid dynamics and course director for the MSc in computational fluid dynamics and the MSc in aerospace computational engineering at Cranfield University.