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!
Then, you'll want to specify the name, languages and version (needed for packaging purposes).
This can be done easily with the
project(YOUR_PROJECT_NAME VERSION 0.1.0 LANGUAGES C CXX)
Comments start with the character
# and can be multiline (bracket comments) if following the bracket_open syntax.
# This is a single line comment
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:
PRIVATEmeans there is no propagation and the property is only applied to the target.
INTERFACEmeans the property will be propagated but not applied to the target, it is only used as part of the interface.
PUBLICmeans it is both used by the target and propagated to its dependents.
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
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
This is done by calling
add_library without sources and the
INTERFACE keyword, for example
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.
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
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
Of course you can also mix
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.
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.
Let's assume the following filesystem tree, with 3 subprojects
libB depend on
programA depend on
libA should not leak properties into
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?
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.
add_subdirectory(dir), CMake will add
dir to the build by opening
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:
cmake_minimum_required(VERSION 3.14) project(myawesomeproject VERSION 0.10 LANGUAGES C CXX) add_subdirectory(libs)
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
add_library(libA INTERFACE) target_include_directories(libA INTERFACE include/)
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)
add_executable(programA src/main.cpp) target_link_libraries(programA PRIVATE libB)
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.