Introduction
Overview
Teaching: 10 min
Exercises: 0 minQuestions
What is the difference between a build system and a build system generator?
Objectives
Learn about build systems and build system generators.
Understand why CMake is used.
Newer CMake is better.
Building code is hard. You need long commands to build each part of your code; and you need do to this on many parts of your code.
So people came up with Build Systems; these had ways to set up dependencies (such as file A needs
to be built to build file B), and ways to store the commands used to build each file or type of
file. These are language independent (mostly), allowing you to setup builds of almost anything; you
can use make
to build LaTeX documents if you wish. Some common build systems include make (the
classic pervasive one), ninja (a newer one from Google designed in the age of build system
generators), invoke (a Python one), and rake (Ruby make, nice syntax for Ruby users).
However, this is:
- Mostly hand coded: You have to know all the proper commands
- Platform/compiler dependent: You have to build the commands for each compiler.
- Not aware of dependencies: If you require a library, you have to handle the paths, etc.
- Hard to extend; if you want to use an IDE instead, good luck.
Enter Build System Generators (hereby labeled BSGs for brevity). These understand the concepts of your programming language build; they usually support common compilers, languages, libraries, and output formats. These usually write a build system (or IDE) file and then let that do the actual build. The most popular BSG is CMake, which stands for Cross-platform Make. But as we’ve just shown, it is not really in the same category as make. Other BSGs include Autotools (old, inflexible), Bazel (by Google), SCons (older Python system), Meson (young Python system, very opinionated), and a few others. But CMake has unparalleled support by IDEs, libraries, and compilers. It also scales very well, with small projects able to pick it up easily (modern CMake, anyway), and massive projects like the CERN experiments being able to use it for thousands of modules.
Note that both CMake and Make are custom languages rather than being built in an existing language, like rake and SCons, etc. While it is nice to consolidate languages, the requirement that you have an external language installed and configured was too high for any of these to catch on for general use.
To recap, you should use CMake if:
- You want to avoid hard-coding paths
- You need to build a package on more than one computer
- You want to use CI (continuous integration)
- You need to support different OSs (maybe even just flavors of Unix)
- You want to support multiple compilers
- You want to use an IDE, but maybe not all of the time
- You want to describe how your program is structured logically, not flags and commands
- You want to use a library
- You want to use tools, like Clang-Tidy, to help you code
- You want to use a debugger
(More) Modern CMake
CMake has really changed dramatically since it was introduced around 2000. And, by the time of 2.8, it was available in lots of Linux Distribution package managers. However, this means there often are really old versions of CMake “available by default” in your environment. Please, please upgrade and design for newer CMake. No one likes writing or debugging build systems. Using a newer version can cut your build system code in less than half, reduce bugs, integrate better with external dependents, and more. Installing CMake can be as little as one line, and doesn’t require sudo access. See more info here.
Somehow, this is difficult to understand, so I’ll shout it to make it clearer. Writing Modern CMake reduces your chances of build problems. The tools CMake provides are better than the ones you will try to write yourself. CMake works with more compilers, in more situations, than you do. So if you try to add a flag, you’ll likely get it wrong for some compilers or OSs, but if you can use a tool CMake gives you instead, then that’s CMake’s business to get right and not yours.
It’s not a big deal to install a newer CMake. The issues people open for “this is broken” far outnumber the issues people open because you required a CMake newer than they want to run (I think I’ve only seen one of those, and they came around when they saw the feature they would have to give up).
Example of Modern CMake
Bad 2.8 style CMake: Adding a C++11 flag manually. This is compiler specific, is different for CUDA, and locks in a set version, rather than a minimum version.
If you require CMake 3.1+, you can set
CXX_STANDARD
, but only on a final target. Or you can manually listcompile_features
for individual C++11 and C++14 features, and, and all targets using yours will get at least that level set on them.If you require CMake 3.8+, you can just use
compile_features
to set a minimium standard level, likecxx_std_11
, instead of manually listing a bunch of features. This was used for C++17 and later C++20 and C__23, exclusively.
Selecting a minimum in 2023:
What minimum CMake should you run locally, and what minimum should you support for people using your code? Since you are reading this, you should be able to get a release in the last few versions of CMake; do that, it will make your development easier. For support, there are two ways to pick minimums: based on features added (which is what a developer cares about), or on common pre-installed CMakes (which is what a user cares about).
Never select a minimum version older than the oldest compiler version you support. CMake should always be at least as new as your compiler.
What minimum to choose - OS support:
- 3.4: The bare minimum. Never set less.
- 3.7: Debian old-stable.
- 3.10: Ubuntu 18.04.
- 3.11: CentOS 8 (use EPEL or AppSteams, though)
- 3.13: Debian stable.
- 3.16: Ubuntu 20.04.
- 3.19: First to support Apple Silicon.
- latest: pip/conda-forge/homebew/chocolaty, etc.
What minimum to choose - Features:
- 3.8: C++ meta features, CUDA, lots more
- 3.11:
IMPORTED INTERFACE
setting, faster, FetchContent,COMPILE_LANGUAGE
in IDEs - 3.12: C++20,
cmake --build build -j N
,SHELL:
, FindPython - 3.14/3.15: CLI, FindPython updates
- 3.16: Unity builds / precompiled headers, CUDA meta features
- 3.17/3.18: Lots more CUDA, metaprogramming
- 3.20: C++23, CUDARCHS, IntelLLVM, NVHPC
- 3.21: Different message types, MSVC 2022, C17 & C23, HIP, MSYS
- 3.24: Package finder integration with downloads,
--fresh
- 3.25: C++26 support, LTO for CUDA
Other sources
There are some other places to find good information on the web. Here are some of them:
- Modern CMake: The book this tutorial derives from.
- The official help: Really amazing documentation. Nicely organized, great search, and you can toggle versions at the top. It just doesn’t have a great “best practices tutorial”, which is what this book tries to fill in.
- Effective Modern CMake: A great list of do’s and don’ts.
- Embracing Modern CMake: A post with good description of the term
- It’s time to do CMake Right: A nice set of best practices for Modern CMake projects.
- The Ultimate Guide to Modern CMake: A slightly dated post with similar intent.
- More Modern CMake: A great presentation from Meeting C++ 2018 that recommends CMake 3.12+. This talk makes calls CMake 3.0+ “Modern CMake” and CMake 3.12+ “More Modern CMake”.
- toeb/moderncmake: A nice presentation and examples about CMake 3.5+, with intro to syntax through project organization
Key Points
Build systems describe exactly how to build targets.
Build system generators describe general relationships.
Modern CMake is simpler and reduces the chance of build problems.
Building with CMake
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How do I build a project?
Objectives
Have a reference for installing CMake.
Learn how to build an existing project.
Customize the build.
Learn how to do some basic debugging.
Installing CMake
It’s usually only one line or maybe two to install a recent version of CMake almost anywhere; see CMake Instructions.
Building with CMake
Before writing CMake, let’s make sure you know how to run it to make things. This is true for almost all CMake projects, which is almost everything.
Try it out
Let’s get a project and try to build it. For fun, let’s build CLI11:
git clone https://github.com/CLIUtils/CLI11.git cd CLI11
Now, from the newly downloaded directory, let’s try the modern CMake (3.14) build procedure:
cmake -S . -B build cmake --build build cmake --build build -t test
This will make a build directory (-B
) if it does not exist, with the source directory
defined as -S
. CMake will configure and generate makefiles by default, as well as set all options
to their default settings and cache them into a file called CMakeCache.txt
, which will sit in the
build directory. You can call the build directory anything you want; by convention it should have
the word build
in it to be ignored by most package’s .gitignore
files.
You can then invoke your build system (line 2). Regardless of whether you used make
(the default),
ninja
, or even an IDE-based system, you can build with a uniform command. You can add -j 2
to
build on two cores, or -v
to verbosely show commands used to build.
Finally, you can even run your tests from here, by passing the “test” target to the underlying build
system. -t
(--target
before CMake 3.15) lets you select a target. There’s also a cmake <dir>
--install
command in CMake 3.15+ that does the install - without invoking the underlying build system!
Warning about in-source builds
Never do an “in-source” build - that is, run
cmake .
from the source directory. It will pollute your source directory with build outputs, CMake configuration files, and will disable out-of-source builds. A few packages do not allow the source directory to even sit inside the build directory; if that is the case, you need to change the relative path..
accordingly.Just to clarify, you can point CMake at either the source directory from the build directory, or at an existing build directory from anywhere.
Other syntax choices
The classic, battle hardened method should be shown for completeness:
mkdir build cd build cmake .. make make test
This has several downsides. If the directory already exists, you have to add
-p
, but that doesn’t work on Windows. You can’t as easily change between build directories, because you are in it. It’s more lines, and if you forget to change to the build directory, and you usecmake .
instead ofcmake ..
, then you can pollute your source directory.
Picking a compiler
Selecting a compiler must be done on the first run in an empty directory. It’s not CMake syntax per se, but you might not be familiar with it. To pick Clang:
CC=clang CXX=clang++ cmake -S . -B build
That sets the environment variables in bash for CC
and CXX
, and CMake will respect those
variables. This sets it just for that one line, but that’s the only time you’ll need those;
afterwards CMake continues to use the paths it deduces from those values.
Picking a generator
You can build with a variety of tools; make
is usually the default. To see all the tools CMake
knows about on your system, run
cmake --help
And you can pick a tool with -G"My Tool"
(quotes only needed if spaces are in the tool name). You
should pick a tool on your first CMake call in a directory, just like the compiler. Feel free to
have several build directories, like build
and build-xcode
. You can set the environment variable
CMAKE_GENERATOR
to control the default generator (CMake 3.15+). Note that makefiles will only
run in parallel if you explicitly pass a number of threads, such as make -j2
, while Ninja will
automatically run in parallel. You can directly pass a parallelization option such as -j 2
to the
cmake --build .
command in recent versions of CMake as well.
Setting options
You set options in CMake with -D
. You can see a list of options with -L
, or a list with
human-readable help with -LH
.
Verbose and partial builds
Again, not really CMake, but if you are using a command line build tool like make
, you can get
verbose builds:
cmake --build build -v
If you are using make directly, you can write VERBOSE=1 make
or even make VERBOSE=1
, and make
will also do the right thing, though writing a variable after a command is a feature of make
and
not the command line in general.
You can also build just a part of a build by specifying a target, such as the name of a library or
executable you’ve defined in CMake, and make will just build that target. That’s the --target
(-t
in CMake 3.15+) option.
Options
CMake has support for cached options. A Variable in CMake can be marked as “cached”, which means it
will be written to the cache (a file called CMakeCache.txt
in the build directory) when it is
encountered. You can preset (or change) the value of a cached option on the command line with -D
.
When CMake looks for a cached variable, it will use the existing value and will not overwrite it.
Standard options
These are common CMake options to most packages:
CMAKE_BUILD_TYPE
: Pick fromRelease
,RelWithDebInfo
,Debug
, or sometimes more.CMAKE_INSTALL_PREFIX
: The location to install to. System install on UNIX would often be/usr/local
(the default), user directories are often~/.local
, or you can pick a folder.BUILD_SHARED_LIBS
: You can set thisON
orOFF
to control the default for shared libraries (the author can pick one vs. the other explicitly instead of using the default, though)BUILD_TESTING
: This is a common name for enabling tests, not all packages use it, though, sometimes with good reason.
Try it out
In the CLI11 repository you cloned:
- Check to see what options are available
- Change a value; maybe set
CMAKE_CXX_STANDARD
to14
or turn off testing.- Configure with
CMAKE_INSTALL_PREFIX=install
, then install it into that local directory. Make sure it shows up there!
Debugging your CMake files
We’ve already mentioned verbose output for the build, but you can also see verbose CMake configure
output too. The --trace
option will print every line of CMake that is run. Since this is very
verbose, CMake 3.7 added --trace-source="filename"
, which will print out every executed line of
just the file you are interested in when it runs. If you select the name of the file you are
interested in debugging (usually with a parent directory if you are debugging a CMakeLists.txt,
since all of those have the same name), you can just see the lines that run in that file. Very
useful!
Try it out
Run the following from the source directory:
cmake build --trace-source="CMakeLists.txt"
Answer this
Question: Does
cmake build
build anything?Answer
No, the “build” here is the directory. This will configure (create build system files). To build, you would add
--build
before the directory, or use your build tool, such asmake
.
More reading
- Based on Modern CMake intro/running
Key Points
Build a project.
Use out-of-source builds.
Build options and customization.
Debug a CMakeLists easily.
Your first CMakeLists.txt file
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How little can I get away with in my CMakeLists?
Objectives
Understand the deep implications of
cmake_minimum_version
Know how to set up a project
Know how to make at least one target
Writing a CMakeLists
The following file is fine for the following examples:
This file can be compiled with C or C++.
Starting off
This is the simplest possible CMakeLists.txt
:
cmake_minimum_required(VERSION 3.15)
project(MyProject)
add_executable(myexample simple.cpp)
Let’s look at the three lines:
- The
cmake_minimum_required
command sets the policies so that the build is exactly like it would be on the listed version of CMake - in other words, CMake “dumbs itself down” to the version you request for any features that could produce a different build. This makes CMake almost perfectly backwards compatible. - You need to be working on a
project
, and it needs at least a name. CMake assumes aCXX
(that’s C++) andC
mixed project if you don’t give anyLANGUAGES
. - You need at least one library or executable to do anything interesting. The “thing” you make here
is called a “target”, and the executable/library has the same name, by default, and it has to be
unique in the project. You use
add_executable
for programs, andadd_library
for libraries.
Those commands have a few extra arguments that you can give:
cmake_minimum_required(VERSION 3.15...3.25)
project(MyProject
VERSION
1.0
DESCRIPTION
"Very nice project"
LANGUAGES
CXX
)
add_executable(myexample simple.cpp)
- You can specify a range of versions - this will cause the policies to be set to the highest supported value in that range. As a general rule, set the highest version you’ve tested with here.
- Projects can have versions, descriptions, and languages.
- Whitespace doesn’t matter. Be clear/pretty, or use cmake-format.
Try it out
Build and run the example code with a CMakeLists.txt
similar to the one above.
git clone https://github.com/hsf-training/hsf-training-cmake-webpage.git
cd hsf-training-cmake-webpage/code/00-intro
More reading
- Based on Modern CMake basics
Key Points
The
cmake_minimum_version
setting has deep implicationsYou need a project line.
You should prepare one or more targets to do anything interesting.
Working with Targets
Overview
Teaching: 10 min
Exercises: 15 minQuestions
How do targets work?
Objectives
Know how to set up targets
Understand linking and INTERFACE properties
Make INTERFACE targets
Targets
Now you know how to compile a single file using three lines of CMake. But what happens if you have more than one file with dependencies? You need to be able to tell CMake about the structure of your project, and it will help you build it. To do so, you will need targets.
You’ve already seen a target:
add_executable(myexample simple.cpp)
This creates an “executable” target with the name myexample
. Target names must be unique (and there
is a way to set the executable name to something other than the target name if you really want to).
Targets are much like “objects” in other languages; they have properties (member variables) that
hold information. The SOURCES
property, for example, will have simple.cpp
in it.
Another type of target is a library:
add_library(mylibrary simplelib.cpp)
You can add the keywords STATIC
, SHARED
, or MODULE
if you know what kind of library you want
to make; the default is sort-of an “auto” library that is user selectable with BUILD_SHARED_LIBS
.
You can make non-built libraries too. More on that later, once we see what we can do with targets.
Linking
Once you have several targets, you can describe the relationship between them with
target_link_libraries
and a keyword; one of PUBLIC
, PRIVATE
, and INTERFACE
. Don’t forget
this keyword when making a library! CMake goes into an old compatibility mode for this target that
generally breaks things.
Question
You have a library,
my_lib
, made frommy_lib.hpp
andmy_lib.cpp
. It requires at least C++14 to compile. If you then addmy_exe
, and it needsmy_lib
, should that forcemy_exe
to compile with C++14 or better?Answer
This depends on the header. If the header contains C++14, this is a PUBLIC requirement - both the library and it’s users need it. However, if the header is valid in all versions of C++, and only the implementations inside
my_lib.cpp
require C++14, then this is aPRIVATE
requirement
- users don’t need to be forced into C++14 mode.
Maybe you do require users have C++14, but your library can compile with any version of C++. This would be an
INTERFACE
requirement.
Figure 1: Example of PUBLIC, PRIVATE, and INTERFACE. myprogram
will build the three libraries it
sees through mylibrary
; the private library will not affect it.
There are two collections of properties on every target that can be filled with values; the
“private” properties control what happens when you build that target, and the “interface” properties
tell targets linked to this one what to do when building. The PUBLIC
keyword fills both property
fields at the same time.
Example 1: Include directories
When you run target_include_directories(TargetA PRIVATE mydir)
, then
the INCLUDE_DIRECTORIES
property of TargetA
has mydir
appended. If you use the keyword
INTERFACE
instead, then INTERFACE_INCLUDE_DIRECTORIES
is appended to, instead. If you use
PUBLIC
, then both properties are appended to at the same time.
Example 2: C++ standard
There is a C++ standard property - CXX_STANDARD
. You can set this property, and like many
properties in CMake, it gets it’s default value from a CMAKE_CXX_STANDARD
variable if it is set,
but there is no INTERFACE version - you cannot force a CXX_STANDARD
via a target. What would you
do if you had a C++11 interface target and a C++14 interface target and linked to both?
By the way, there is a way to handle this - you can specify the minimum compile features you need
to compile a target; the cxx_std_11
and similar meta-features are perfect for this - your target
will compile with at least the highest level specified, unless CXX_STANDARD
is set (and that’s
a nice, clear error if you set CXX_STANDARD
too low). target_compile_features
can fill
COMPILE_FEATURES
and INTERFACE_COMPILE_FEATURES
, just like directories in example 1.
Try it out
Get this repository and go to the example. Try to write a CMakeLists that will correctly build.
git clone https://github.com/hsf-training/hsf-training-cmake-webpage.git
cd hsf-training-cmake-webpage/code/01-simple
The files here are:
- simple_lib.cpp: Must be compiled with
MYLIB_PRIVATE
andMYLIB_PUBLIC
defined. - simple_example.cpp: Must be compiled with
MYLIB_PUBLIC
defined, but notMYLIB_PRIVATE
Use [target_compile_definitions(<target> <private or public>
<definition(s)>)
][target_compile_definitions
] to set the definitions on simple_lib
.
Things you can set on targets
target_link_libraries
: Other targets; can also pass library names directlytarget_include_directories
: Include directoriestarget_compile_features
: The compiler features you need activated, likecxx_std_11
target_compile_definitions
: Definitionstarget_compile_options
: More general compile flagstarget_link_directories
: Don’t use, give full paths instead (CMake 3.13+)target_link_options
: General link flags (CMake 3.13+)target_sources
: Add source files
See more commands here.
Other types of targets
You might be really excited by targets and are already planning out how you can describe your programs in terms of targets. That’s great! However, you’ll quickly run into two more situations where the target language is useful, but you need some extra flexibility over what we’ve covered.
First, you might have a library that conceptually should be a target, but doesn’t actually have any built components - a “header-only” library. These are called interface libraries in CMake and you would write:
add_library(some_header_only_lib INTERFACE)
Notice you didn’t need to add any source files. Now you can set INTERFACE
properties on this only
(since there is no built component).
The second situation is if you have a pre-built library that you want to use. This is called an
imported library in CMake, and uses the keyword IMPORTED
. Imported libraries can also be
INTERFACE
libraries, they can be built and modified using the same syntax as other libraries
(starting in CMake 3.11), and they can have ::
in their name. (ALIAS
libraries, which simply
rename some other library, are also allowed to have ::
). Most of the time you will get imported
libraries from other places, and will not be making your own.
INTERFACE IMPORTED
What about
INTERFACE IMPORTED
? The difference comes down to two things:
IMPORTED
targets are not exportable. If you save your targets, you can’t save IMPORTED ones - they need to be recreated (or found again).IMPORTED
header include directories will always be marked asSYSTEM
.Therefore, an
IMPORTED
target should represent something that is not directly part of your package.
More reading
- Based on Modern CMake basics
- Also see CMake’s docs
Key Points
Libraries and executables are targets.
Targets have lots of useful properties.
Targets can be linked to other target.
You can control what parts of a target get inherited when linking.
You can make INTERFACE targets instead of making variables.
Variables explained
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How do variables work?
Objectives
Learn about local variables.
Understand that cached variables persist across runs.
Know how to glob, and why you might not do it.
Variables
For this exercise, we will just directly run a CMake Script, instead of running CMakeLists.txt
.
The command to do so is:
# Assuming you have a file called example.cmake:
cmake -P example.cmake
This way, we don’t have so many little builds sitting around.
Local variables
Let’s start with a local variable.
# local.cmake
set(MY_VARIABLE "I am a variable")
message(STATUS "${MY_VARIABLE}")
Here we see the set
command, which sets a variable, and the message
command, which prints
out a string. We are printing a STATUS
message
- there are other types (many other types in
CMake 3.15+).
More about variables
Try the following:
- Remove the quotes in set. What happens?
- Remove the quotes in message. What happens? Why?
- Try setting a cached variable using
-DMY_VARIABLE=something
before the-P
. Which variable is shown?
Cached variables
Now, let’s look at cached variables; a key ingredient in all CMake builds. In a build, cached
variables are set in the command line or in a graphical tool (such as ccmake
, cmake-gui
), and
then written to a file called CMakeCache.txt
. When you rerun, the cache is read in before
starting, so that CMake “remembers” what you ran it with. For our example, we will use CMake in
script mode, and that will not write out a cache, which makes it easier to play with. Feel free to
look back at the example you built in the last lesson and investigate the CMakeCache.txt
file in
your build directory there. Things like the compiler location, as discovered or set on the first
run, are cached.
Here’s what a cached variable looks like:
# cache.cmake
set(MY_CACHE_VAR "I am a cached variable" CACHE STRING "Description")
message(STATUS "${MY_CACHE_VAR}")
We have to include the variable type here, which we didn’t have to do before (but we could have) -
it helps graphical CMake tools show the correct options. The main difference is the CACHE
keyword
and the description. If you were to run cmake -L
or cmake -LH
, you would see all the cached
variables and descriptions.
The normal set command only sets the cached variable if it is not already set - this allows you to
override cached variables with -D
. Try:
cmake -DMY_CACHE_VAR="command line" -P cache.cmake
You can use FORCE
to set a cached variable even if it already set; this should not be very common.
Since cached variables are global, sometimes they get used as a makeshift global variable - the
keyword INTERNAL
is identical to STRING FORCE
, and hides the variable from listings/GUIs.
Since bool cached variables are so common for builds, there is a shortcut syntax for making one
using option
:
option(MY_OPTION "On or off" OFF)
Other variables
You can get environment variables with $ENV{name}
. You can check to see if an environment variable
is defined with if(DEFINED ENV{name})
(notice the missing $
).
Properties are a form of variable that is attached to a target; you can use get_property
and
set_property
, or [get_target_properties
][] and set_target_properties
(stylistic preference) to
access and set these. You can see a list of all
properties by CMake version;
there is no way to get this programmatically.
Handy tip:
Use
include(CMakePrintHelpers)
to add the useful commandscmake_print_properties
andcmake_print_variables
to save yourself some typing when debugging variables and properties.
Target properties and variables
You have seen targets; they have properties attached that control their behavior. Many of these
properties, such as CXX_EXTENSIONS
, have a matching variable that starts with CMAKE_
, such
as CMAKE_CXX_EXTENSIONS
, that will be used to initialize them. So you can using set property
on each target by setting a variable before making the targets.
Globbing
There are several commands that help with string
s, file
s, [lists
][], and the like.
Let’s take a quick look at one of the most interesting: glob.
file(GLOB OUTPUT_VAR *.cxx)
This will make a list of all files that match the pattern and put it into OUTPUT_VAR
. You can also
use GLOB_RECURSE
, which will recurse subdirectories. There are several useful options, which you
can look at in the
documentation, but
one is particularly important: CONFIGURE_DEPENDS
(CMake 3.12+).
When you rerun the build step (not the configure step), then unless you setCONFIGURE_DEPENDS
,
your build tool will not check to see if you have added any new files that now pass the glob. This
is the reason poorly written CMake projects often have issues when you are trying to add files; some
people are in the habit of rerunning cmake
before every build because of this. You shouldn’t ever
have to manually reconfigure; the build tool will rerun CMake as needed with this one exception. If
you add CONFIGURE_DEPENDS
, then most build tools will actually start checking glob too. The
classic rule of CMake was “never glob”; the new rule is “never glob, but if you have to, add
CONFIGURE_DEPENDS
”.
More reading
- Based on Modern CMake basics/variables
- Also see CMake’s docs
Key Points
Local variables work in this directory or below.
Cached variables are stored between runs.
You can access environment variables, properties, and more.
You can glob to collect files from disk, but it might not always be a good idea.
Project Structure
Overview
Teaching: 10 min
Exercises: 10 minQuestions
What should my project look like?
Objectives
Know some best practices for project structure
For this section, we will be looking at the project in code/03-structure
.
code/03-structure/
├── CMakeLists.txt
├── README.md
├── apps
│ ├── CMakeLists.txt
│ └── app.cpp
├── cmake
│ └── FindSomeLib.cmake
├── docs
│ ├── CMakeLists.txt
│ └── mainpage.md
├── include
│ └── modern
│ └── lib.hpp
├── src
│ ├── CMakeLists.txt
│ └── lib.cpp
└── tests
├── CMakeLists.txt
└── testlib.cpp
First, take a look at the main CMakeLists.txt
file. This is an example of a nice project file
in CMake 3.14, so enjoy it for a minute. Now let’s look at specifics!
Protect project code
The parts of the project that only make sense if we are building this as the main project are
protected; this allows the project to be included in a larger master project with
add_subdirectory
.
Testing handled in the main CMakeLists
We have to do a little setup for testing in the main CMakeLists, because you can’t run
enable_testing
from a subdirectory (and thereby include(CTest)
). Also, notice that
BUILD_TESTING
does not turn on testing unless this is the main project.
Finding packages
We find packages in our main CMakeLists, then use them in subdirectories. We could have also put
them in a file that was included, such as cmake/find_pakages.cmake
. If your CMake is new enough,
you can even add a subdirectory with the find packages commands, but you have to set
IMPORTED_GLOBAL
on the targets you want to make available if you do that. For small to mid-size
projects, the first option is most common, and large projects use the second option (currently).
All the find packages here provide imported targets. If you do not have an imported target, make
one! Never use the raw variables past the lines immediately following the find_package
command.
There are several easy mistakes to make if you do not make imported targets, including forgetting to
add SYSTEM
, and the search order is better (especially before CMake 3.12).
In this project, I use the new FetchContent
(3.11/3.14) to download several dependencies; although
normally I prefer git submodules in /extern
.
Source
Now follow the add_subdirectory
command to see the src folder, where a library is created.
The headers are listed along with the sources in the add_library
command. This would have been
another way to do it in CMake 3.11+:
add_library(modern_library)
target_sources(modern_library
PRIVATE
lib.cpp
PUBLIC
${HEADER_LIST}
)
Notice that we have to use target_include_directories
; just adding a header to the sources
does not tell CMake what the correct include directory for it should be.
We also set up the target_link_libraries
with the appropriate targets.
App
Now take a look at apps/CMakeLists.txt
. This one is pretty simple, since all the leg work for
using our library was done on the library target, as it should be.
Click to see apps/CMakeLists.txt
Docs and Tests
Feel free to look at docs
and tests
for their CMakeLists.txt
.
Click to see docs/CMakeLists.txt
More reading
- Based on Modern CMake basics/structure
Key Points
Projects should be well organised.
Subproject CMakeLists are used for tests and more.
Common Problems and Solutions
Overview
Teaching: 5 min
Exercises: 0 minQuestions
What could go possibly wrong?
Objectives
Identify some common mistakes
Avoid making common mistakes
Now let’s take a look at some common problems with CMake code and with builds.
1: Low minimum CMake version
cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
Okay, I had to put this one in. But in some cases, just increasing this number fixes problems. 3.0 or less, for example, has a tendency to do the wrong thing when linking on macOS.
Solution: Either set a high minimum version or use the version range feature and CMake 3.12 or better. The lowest version you should ever choose is 3.4 even for an ultra-conservative project; several common issues were fixed by that version.
2: Building inplace
CMake should never be used to build in-place; but it’s easy to accidentally do so. And once it
happens, you have to manually clean the directory before you can do an out-of-source build again.
Because of this, while you can run cmake .
from the build directory after the initial run, it’s
best to avoid this form just in case you forget and run it from the source directory. Also, you can
add the following check to your CMakeLists.txt
:
### Require out-of-source builds
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH)
if(EXISTS "${LOC_PATH}")
message(FATAL_ERROR "You cannot build in a source directory (or any directory with "
"CMakeLists.txt file). Please make a build subdirectory. Feel free to "
"remove CMakeCache.txt and CMakeFiles.")
endif()
One or two generated files cannot be avoided, but if you put this near the top, you can avoid most of the generated files as well as immediately notifying the user (possibly you) that you’ve made a mistake.
3: Picking a compiler
CMake may pick the wrong compiler on systems with multiple compilers. You can use the environment
variables CC
and CXX
when you first configure, or CMake variables
CMAKE_CXX_COMPILER
, etc. - but you need to pick the compiler on the first run; you can’t
just reconfigure to get a new compiler.
4: Spaces in paths
CMake’s list and argument system is very crude (it is a macro language); you can use it to your
advantage, but it can cause issues. (This is also why there is no “splat” operator in CMake, like
f(*args)
in Python.) If you have multiple items, that’s a list (distinct arguments):
set(VAR a b v)
The value of VAR
is a list with three elements, or the string "a;b;c"
(the two things are exactly
the same). So, if you do:
set(MY_DIR "/path/with spaces/")
target_include_directories(target PRIVATE ${MY_DIR})
that is identical to:
target_include_directories(target PRIVATE /path/with spaces/)
which is two separate arguments, which is not at all what you wanted. The solution is to surround the original call with quotes:
set(MY_DIR "/path/with spaces/")
target_include_directories(target PRIVATE "${MY_DIR}")
Now you will correctly set a single include directory with spaces in it.
Key Points
Setting a CMake version too low.
Avoid building inplace.
How to select a compiler.
How to work with spaces in paths.
Debugging
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How do I debug everything?
Objectives
Know how to find problems in CMake
Know how to set up builds for debugging
Debugging is easy with CMake. We’ll cover two forms of debugging: debugging your CMake code, and debugging your C++ code.
CMake debugging
First, let’s look at ways to debug a CMakeLists or other CMake file.
Printing variables
The time honored method of print statements looks like this in CMake:
message(STATUS "MY_VARIABLE=${MY_VARIABLE}")
However, a built in module makes this even easier:
include(CMakePrintHelpers)
cmake_print_variables(MY_VARIABLE)
If you want to print out a property, this is much, much nicer! Instead of getting the properties one
by one of of each target (or other item with properties, such as SOURCES
, DIRECTORIES
, TESTS
,
or CACHE_ENTRIES
- global properties seem to be missing for some reason), you can simply list them
and get them printed directly:
cmake_print_properties(
TARGETS my_target
PROPERTIES POSITION_INDEPENDENT_CODE
)
Warning
You can’t actually access
SOURCES
, since it conflictes with theSOURCES
keyword in the function.
Tracing a run
Have you wanted to watch exactly what happens in your CMake file, and when? The
--trace-source="filename"
feature is fantastic. Every line run in the file that you give will be
echoed to the screen when it is run, letting you follow exactly what is happening. There are related
options as well, but they tend to bury you in output.
Watching a build
Let’s try this out. Let’s go to the
code/04-debug
folder and configure with trace mode on:cmake -S . -B build --trace-source=CMakeLists.txt
Try adding
--trace-expand
too. What is the difference? How about replacing--trace-source=CMakeLists.txt
with--trace
?
Find call information
CMake scripts can search for dependent libraries, executables and more. More details on this will be shown in the next section.
For now, let’s watch where CMake searches for find_...
locations in our current example!
You can print extra find call information during the cmake run to standard error by adding --debug-find
(CMake 3.17+).
Alternatively, CMAKE_FIND_DEBUG_MODE can be set around sections of your CMakeLists.txt
to limit debug printing to a specific region.
C++ debugging
To run a C++ debugger, you need to set several flags in your build. CMake does this for you with
“build types”. You can run CMake with CMAKE_BUILD_TYPE=Debug
for full debugging, or
RelWithDebInfo
for a release build with some extra debug info. You can also use Release
for an
optimized release build, or MinSizeRel
for a minimum size release (which I’ve never used).
Debug example
Let’s try it. Go to code/04-debug
, and build in debug mode. Our program has a bug. Let’s try it
out in a debugger.
cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build-debug
gdb build-debug/simple_example
Now, since we think there’s a problem in my_sin
, let’s set a breakpoint in my_sin
. Note that
I’m providing the gdb commands on the left, and lldb commands on the right.
# GDB # LLDB
break my_sin breakpoint set --name my_sin
r r
Now, let’s watch what happens to the sign variable. Set a watchpoint:
# GDB # LLDB
watch sign watchpoint set variable sign
c c
Keep running continue (c
). Do you find the problem in the code?
Aside: Linking to math
You may find that the example provided does not work unless it’s linked with the math library “m”, which looks like
-lm
when linking with gcc (llvm does not seem to need to link to it). Let’s look for the “m” library:# Does -lm work? (notice this is find_library, not find_package) find_library(MATH_LIBRARY m)
If it is found, this saves the location of the m library in a variable that we gave it the name of, in our case,
MATH_LIBRARY
. We can add the path (not a target) using the sametarget_link_libraries
command. It is very unfortunate that this command happens to accept both targets and raw paths and linker flags, but it’s a historical leftover.# If there is a -lm, let's use it if(MATH_LIBRARY) target_link_libraries(simple_lib PUBLIC ${MATH_LIBRARY}) endif()
Note that CMake defaults to an “empty” build type, which is neither optimized nor debug. You can fix this manually, or always specify a build type.
Adopting a convention from Linux, all build types append compiler flags from the environment variables CFLAGS
, CXXFLAGS
, CUDAFLAGS
, and LDFLAGS
(full list). This feature is often used by package management software, in conjunction with the already mentioned CC
, CXX
, CUDACXX
, and CUDAHOSTCXX
environment variables. Otherwise, you can set the release and debug flags separately.
Common needs
There are several common utilities that CMake can integrate with to help you with your builds. Here are just a few:
CMAKE_CXX_COMPILER_LAUNCHER
can set up a compiler launcher, likeccache
, to speed up your builds.CMAKE_CXX_CLANG_TIDY
can run clang-tidy to help you clean up your code.CMAKE_CXX_CPPCHECK
for cppcheck.CMAKE_CXX_CPPLINT
for cpplint.CMAKE_CXX_INCLUDE_WHAT_YOU_USE
for iwyu.
You can set these when building if you want.
Key Points
There are several methods for debugging your CMake code.
CMake can help you debug and profile your source code.
Finding Packages
Overview
Teaching: 10 min
Exercises: 0 minQuestions
How do I search for packages?
Objectives
Understand FindPackage.cmake
Understand PackageConfig.cmake
You can search for packages in CMake in two ways; both of them, however, use the same interface. Here’s what it would look like:
find_package(MyPackage 1.2)
This will look for a file in the CMAKE_MODULE_PATH
that is named FindMyPackage.cmake
. If it
does not find one, it will look for a file named MyPackageConfig.cmake
in several places,
including MyPackage_DIR
if that variable exists. You can only perform one of these searches with
MODULE
or CONFIG
, respectively.
You can add COMPONENTS
in some cases, if the package supports it, and you can also add QUIET
to
hide extra text, or REQUIRED
to cause a missing package to fail the configure step.
Aside: Environment Hints
Hinting the installation of software package that is installed outside of a system paths works can also be done with environment variables. In CMake 3.12+, individual packages locations can be hinted by setting their installation root path in
<PackageName>_ROOT
.export HDF5_ROOT=$HOME/software/hdf5-1.12.0
Similarly, the variable
CMAKE_PREFIX_PATH
can be used to hint a list of installation root paths at once:export CMAKE_PREFIX_PATH=$HOME/software/hdf5-1.12.0:$HOME/software/boost-1.74.0:$CMAKE_PREFIX_PATH
FindPackage
The older method for finding packages is the FindPackage.cmake method (MODULE
). This is a CMake or
user supplied search script that knows how to look for a package. While there are some conventions
here, and some tools to help, there are not many hard-and-fast requirements. A package should at
least set the variable Package_FOUND
. There are 100 or so find packages included in CMake, refer
to the documentation for each.
- See the included FindPackages here.
- Many of the packages took a long time to add imported targets
- The old
FindPythonLibs
andFindPythonInterp
are in the process of being replaced byFindPython
, but you need a very new version of CMake for the new ones; 3.12 minimum, 3.15 recommended (and 3.18.2+ ideal).
PackageConfig
The “better” way to do things is to have an installed package provide its own details to CMake;
these are “CONFIG” files and come with many packages. These files can be simpler and smarter, since
they don’t have to search for the package and query the options, but rather can be generated with
the correct paths and options for a particular install. ROOT is an example of a package that is now
providing a CONFIG file; another one that is just beginning to is Boost; while CMake includes a
FindBoost
, it has to be updated with each new Boost release, whereas BoostConfig.cmake
can
be included with each Boost release (first version in 1.70). One issue with some packages (TBB, for
example) is that they may provide optional CONFIG files, and your packager may not have activated
them.
To be clear: If you are a package author, never supply a Find<package>.cmake
, but instead always
supply a <package>Config.cmake
with all your builds. If you are depending on another package, try
to look for a Config first, and if that is not available, or often not available, then write a find
package for it for your use.
Key Points
A FindPackage.cmake file can factor out package discovery for a package you don’t own.
A PackageConfig.cmake helps others find your package.
ROOT
Overview
Teaching: 10 min
Exercises: 10 minQuestions
How do I use ROOT?
Objectives
Use ROOT a couple of different ways
ROOT is a data analysis framework.
Let’s try a couple of ROOT examples; one with the classic variable/global configure and one with the
newer target method. You will need a ROOT install or a ROOT docker container to run these examples.
You can use rootproject/root:latest
to test this, which is an official
Ubuntu based build. Conda-Forge ROOT + CMake would work too, if you like Conda. (ROOT has tags for
lots of other base images, too).
For these examples, you should be using a recent version of ROOT - especially for targets, which is still being worked on. The CONFIG files were added in 6.10, and targets received a lot of work in 6.14+. 6.16 has pretty decent targets.
Example 1: UseROOT
Change to the code/05a-root
directory. Run:
cmake -S . -B build
cd build
cmake --build .
root -b -q -x ../CheckLoad.C
Example 2: Targets
Change to the code/05b-root
directory. Run the same command above.
Key Points
ROOT has a CONFIG package you can use to integrate with CMake.
Functions in CMake
Overview
Teaching: 10 min
Exercises: 0 minQuestions
How do I write my own CMake commands?
Objectives
Know how to make a macro or a function in CMake.
Let’s take a look at making a CMake macro or function. The only difference is in scope; a macro does not make a new scope, while a function does.
function(EXAMPLE_FUNCTION AN_ARGUMENT)
set(${AN_ARGUMENT}_LOCAL "I'm in the local scope")
set(${AN_ARGUMENT}_PARENT "I'm in the parent scope" PARENT_SCOPE)
endfunction()
example_function() # Error
example_function(ONE)
example_function(TWO THREE) # Not error
message(STATUS "${ONE_LOCAL}") # What does this print?
message(STATUS "${ONE_PARENT}") # What does this print?
We see the basics of functions above. You can specify required positional arguments after the name;
all other arguments are set in ARGN
; ARGV
holds all arguments, even the listed positional ones.
Since you name variables with strings, you can set variables using names. This is enough to recreate
any of the CMake commands. But there’s one more thing…
Parsing arguments
You’ll have noticed that there are conventions to calling CMake commands; most commands have
all-caps keywords that take 0, 1, or an unlimited number of arguments. This handling is standardized
in the
cmake_parse_arguments
command. Here’s how it works:
Note: if you use a macro, then a scope is not created and the signature above will not work - remove
the PARSE_ARGV
keyword and the number of required arguments from the beginning, and add “${ARGN}”)
to the end.
The first argument after the PARSE_ARGV
keyword and number of required arguments is a prefix that
will be attached to the results. The next three arguments are lists, one with single keywords (no
arguments), one with keywords that take one argument each, and one with keywords that take any
number of arguments. The final argument is ${ARGN}
or ${ARGV}
, without quotes (it will be
expanded here). If you are in a function and not a macro, you can use PARSE_ARGV <N>
at the start
of the call, where N is the number of positional arguments to expect. This method allows semicolons
in the arguments.
Inside the function, you’ll find:
-- ARGV=something;SINGLE;ONE_VALUE;value;MULTI_VALUES;some;other;values;ANOTHER_MULTI_VALUES;even;more;values
-- ARGN=SINGLE;ONE_VALUE;value;MULTI_VALUES;some;other;values;ANOTHER_MULTI_VALUES;even;more;values
-- required_arg_1=something
-- COMPLEX_PREFIX_SINGLE=TRUE
-- COMPLEX_PREFIX_ANOTHER=FALSE
-- COMPLEX_PREFIX_ONE_VALUE=value
-- COMPLEX_PREFIX_ALSO_ONE_VALUE=
-- COMPLEX_PREFIX_MULTI_VALUES=some;other;values
-- COMPLEX_PREFIX_ANOTHER_MULTI_VALUES=even;more;values
-- COMPLEX_PREFIX_UNPARSED_ARGUMENTS=
The semicolons here are an explicit CMake list; you can use other methods to make this simpler at the cost of more lines of code.
More reading
- Based on Modern CMake basics/functions
Key Points
Functions and macros allow factorization.
CMake has an argument parsing function to help with making functions.