CMake basics, how does one write a good CMake project?

CMake basics, how does one write a good CMake project?

03/16/2020
cmake,tutorial,C++,buildsystem

Why CMake?

At siliceum, we decided to use CMake for our C++ based projects. There are many (relatively) new (meta) buildsystems around lately such as build2 or meson.

We based this choice on the fact that most of the ecosystem is using CMake, so IDEs now have decent support for it, so do most libraries and major package managers. It also requires no additional dependency such as Python which usually makes it a tiny bit easier to install for Continuous Integration on platforms such as Windows. So while CMake is far from perfect, for now, it does the job.

But how does one write a good CMake project? We hope to answer this question so that the C++ community can thrive, and make it easier for everyone to integrate any library in a project.

This is the first article of a mini series related to build systems and continuous integration.

The CMakeLists.txt file

All build systems require some kind of entrypoint for the definition of the project. CMake uses a file named CMakeLists.txt, and it is written in its own scripting language.

CMake version specification

The first thing you will need in it, is to specify the minimum version of CMake you will be using. This is important because CMake can have different behaviours based on its versions, which are named policies.

Depending on the features you will be using, you might need to ask for more recent versions of CMake. In our case we choose CMake 3.14 as it is not too old and supports most recent features.

Please do not use something older than 3.1, which dates back to 2014!

cmake_minimum_required(VERSION 3.14)

Project description

Then, you'll want to specify the name, languages and version (needed for packaging purposes). This can be done easily with the project command:

project(YOUR_PROJECT_NAME VERSION 0.1.0 LANGUAGES C CXX)

Comments

Comments start with the character # and can be multiline (bracket comments) if following the bracket_open syntax.

# This is a single line comment

Targets

The different types of targets

A target usually is an executable or a library, but can also be a custom target if your project needs to run some custom tools.
Those can be created respectively using the commands add_executable, add_library and add_custom_target.

Dependencies between targets can then be defined to determine the build order and link commands.

A sample project

Let's say we want to create a project with a library, and a commandline interface application using it. You can start by telling cmake what files are used to build the library. Note that we will be using the pitchfork convention for file layout.

add_library(myawesomelib
  source/myawesomelib.cpp
  source/implementation-details.h
  include/myawesomelib.h
)

Since we put our public headers in a different directory than the other source files, we'll need to tell the compiler where to find those. This is done with target_include_directories.

target_include_directories(myawesomelib PUBLIC include)

Target properties and transitive usage requirements

You probably wonder what the PUBLIC parameter means. In CMake, targets have a list of properties that are used when building, and you can populate those using various commands. Some of those properties are called transitive usage requirements and can be propagated from one target to another when a dependency is declared.

There are 3 keywords controlling the propagation:

This means that when linking our myawesomelib, we will be able to include myawesomelib.h, and we will also be able to include it from myawesomelib.cpp.

In the case of a header only library, you need to tell CMake that no compilation is required by creating an interface library. It will use only INTERFACE properties. This is done by calling add_library without sources and the INTERFACE keyword, for example add_library(myheaderonlylib INTERFACE).

Other types of libraries exist but won't be covered in this article, refer to the documentation for an exhaustive list.

Same thing for more information about targets, transitive usage requirements and the buildsystem in general, you can take a look at the documentation here.

Linking libraries

Now that we have a library, we want to use it in our commandline tool. We will use the add_executable command to create the executable target, and target_link_libraries to indicate what libraries we are using:

add_executable(myawesomecli main.cpp)
target_link_libraries(myawesomecli PRIVATE myawesomelib)

And that's it!

Building with cmake

It is highly recommended to create a build directory before building a CMake project so that it doesn't pollute the rest of your project directory. That way you can also have one build directory per compiler or platform for example.

Then simply place yourself in this directory (usually called build) and start CMake from it. You can use CMake-GUI (which is very helpful for configuration) or use the cmake command line directly.

CMake is meta-buildsystem, this means that it does not compile your code directly but will generate projects for other buildsystems. The main advantage of this technique is that it can generate files that are used by default by your platform and/or IDE. This is handled by chosing a generator (or using the default one).

Viewed from the commandline perspective, you basicly need to do the following:

mkdir build
cd build
cmake -G "Your generator" ..

And you have generated the files needed to build your project. You can omit the -G "Your generator" and cmake will chose a generator by default. It can be a Visual Studio project on Windows or a Makefile on Linux.

In both cases, you can build your project directly using the cmake commandline instead of using the underlying buildsystem, effectively working as an abstraction:

cmake --build . [--target yourtarget]

Note that you might prefer to work from the project root directory and not change your working directory to the build one. This can be done with the following command lines:

cmake -B build -G "Your generator" -S .
cmake --build build

More control

Keep your dependencies under control

As you might have guessed, transitive properties are really practical as they can propagate across multiple levels of indirection when linking targets.
That's also why it is recommended to always specify the propagation type. If one of your library needs to link against another but does not want to expose it, for example if you are wrapping different libraries, link against those with PRIVATE.

Of course you can also mix PRIVATE, INTERFACE and PUBLIC libraries for a single target

add_library(mywrapper [...])
target_link_libraries(mywrapper
  PRIVATE
    otherLib1
    otherLib2
  PUBLIC
    libNeededByConsumers
)

Note that if a static library A links against library B privately, a binary using it will link against both as it is necessary, but only the properties of A will propagate to it.

Compile definitions and features

Often you will need to pass macro definitions to the compiler, this is done with target_compile_definitions.

For example:

target_compile_definitions(myawesomelib
  PRIVATE
    USE_SIMD=1
    INTERNAL_MACRO
  INTERFACE
    ONLY_CONSUMERS_CAN_SEE_THIS_DEFINE=42
)

If you are using recent features of C++, you will want to specify which ones you are using.
CMake will automatically detect what flags need to be added to the compiler command and if it is not supported by your compiler, will show you an error. For exemple if you want to require the usage of the c++14 standard:

target_compile_features(myawesomelib PRIVATE cxx_std_14)

While CMake used to provide more fine grained control, it is now mostly exposing only the standard versions.
The list of compile features for C++ are available here.

You can also use target_compile_options to pass flags directly to the compiler.
However if you decide to do so, it is best to make it optional if the flag is non-mondatory. More details about this in the next article!

Splitting the CMakeLists.txt

As projects grow, you quickly need to organize your files and splitting the CMakeLists.txt file becomes necessary.

Example structure

Let's assume the following filesystem tree, with 3 subprojects libA, libB and programA. We'll have libB depend on libA and programA depend on libB, but libA should not leak properties into programA.

myawesomeproject
└── libs
    ├── libA
    │   └── include
    │       └── libA.h
    ├── libB
    │   ├── include
    │   │   └── libB.h
    │   └── src
    │       └── libB.cpp
    └── programA
        └── src
            └── main.cpp

The suggested approach is to have CMakeLists.txt files placed the following way:

myawesomeproject
├── CMakeLists.txt
└── libs
    ├── CMakeLists.txt
    ├── libA
    │   ├── CMakeLists.txt
    │   └── include
    │       └── libA.h
    ├── libB
    │   ├── CMakeLists.txt
    │   ├── include
    │   │   └── libB.h
    │   └── src
    │       └── libB.cpp
    └── programA
        ├── CMakeLists.txt
        └── src
            └── main.cpp

Content of the CMakeLists.txt files

Now that we have our directory structure, what do we put in our files?

Subfolders

CMake has two main ways of handling multi-directories projects, the add_subdirectory and include commands.

If you use add_subdirectory, you will be creating a new scope for variables, while with include, variables will be declared in the current scope. Both have their use case.
We advise to use add_subdirectory by default. Note that include can also be used in the context of CMake Modules which we will discuss in a later post.

When calling add_subdirectory(dir), CMake will add dir to the build by opening dir/CMakeLists.txt.

In some cases you might want to add directories but not have them be built by default unless required, either by dependency or by the user.
In that case you can add the EXCLUDE_FROM_ALL parameter which will effectively tell CMake not to add the targets from the subdirectory to the dependencies of the all target nor in the IDEs (think make all). eg add_subdirectory( optionalTargetsSubDir EXCLUDE_FROM_ALL ).

You will also need to call add_subdirectory in the correct order if you want to have dependencies across the different subdirectories.

A view of a minimalistic content of the files:

./CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(myawesomeproject VERSION 0.10 LANGUAGES C CXX)

add_subdirectory(libs)

libs/CMakeLists.txt

add_subdirectory(libA)
add_subdirectory(libB) # After libA so that we can link it
add_subdirectory(programA) # After libB so that we can link it

libs/libA/CMakeLists.txt

add_library(libA INTERFACE)
target_include_directories(libA INTERFACE include/)

libs/libB/CMakeLists.txt

add_library(libB src/libB.cpp include/libB.h )
target_include_directories(libB PUBLIC include/)

# PRIVATE so that libA doesn't leak into programA
target_link_libraries(libB PRIVATE libA)

libs/programA/CMakeLists.txt

add_executable(programA src/main.cpp)
target_link_libraries(programA PRIVATE libB)

What's next?

This article covered the basics of modern CMake, and should be enough to give you a headstart.

Some topics were deliberatly not addressed such as variables, build configurations, toolchain files, modules and packages as those can be less simple than they seem.

In the next article we will dive a bit deeper and take a look at variables, configurations and generator expressions.

For those in a quest for knowledge and can't wait for the next article, we recommend having a look at this C++ boilerplate. It is a concrete example of a C++ project using modern CMake, from which you can start and build on.

And of course, do not forget the official CMake documentation.

You can discuss this article on reddit r/cpp.


Clément Grégoire