In this chapter, we will cover the following recipes:
- Compiling a single source file into an executable
- Switching generators
- Building and linking static and shared libraries
- Controlling compilation with conditionals
- Presenting options to the user
- Specifying the compiler
- Switching the build type
- Controlling compiler flags
- Setting the standard for the language
- Using control flow constructs
The recipes in this chapter will walk you through fairly basic tasks needed to build your code: compiling an executable, compiling a library, performing build actions based on user input, and so forth. CMake is a build system generator particularly suited to being platform- and compiler-independent. We have striven to show this aspect in this chapter. Unless stated otherwise, all recipes are independent of the operating system; they can be run without modifications on GNU/Linux, macOS, and Windows.
The recipes in this book are mainly designed for C++ projects and demonstrated using C++ examples, but CMake can be used for projects in other languages, including C and Fortran. For any given recipe and whenever it makes sense, we have tried to include examples in C++, C, and Fortran. In this way, you will be able to choose the recipe in your favorite flavor. Some recipes are tailor-made to highlight challenges to overcome when a specific language is chosen.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-01 and has C++, C, and Fortran examples. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
In this recipe, we will demonstrate how to run CMake to configure and build a simple project. The project consists of a single source file for a single executable. We will discuss the project in C++, but examples for C and Fortran are available in the GitHub repository.
We wish to compile the following source code into a single executable:
#include <cstdlib> #include <iostream> #include <string> std::string say_hello() { return std::string("Hello, CMake world!"); } int main() { std::cout << say_hello() << std::endl; return EXIT_SUCCESS; }
Alongside the source file, we need to provide CMake with a description of the operations to perform to configure the project for the build tools. The description is done in the CMake language, whose comprehensive documentation can be found online at https://cmake.org/cmake/help/latest/. We will place the CMake instructions into a file called CMakeLists.txt
.Â
Note
The name of the file is case sensitive; it has to be called CMakeLists.txt
for CMake to be able to parse it.
In detail, these are the steps to follow:
- Open a text file with your favorite editor. The name of this file will be
CMakeLists.txt
. - The first line sets a minimum required version for CMake. A fatal error will be issued if a version of CMake lower than that is used:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
- The second line declares the name of the project (recipe-01) and the supported language (
CXX
stands for C++):
project(recipe-01 LANGUAGES CXX)
add_executable(hello-world hello-world.cpp)
- Save the file in the same directory as the source file
hello-world.cpp
. Remember that it can only be namedCMakeLists.txt
. We are now ready to configure the project by creating and stepping into a build directory:
$ mkdir -p build $ cd build $ cmake .. -- The CXX compiler identification is GNU 8.1.0 -- Check for working CXX compiler: /usr/bin/c++ -- Check for working CXX compiler: /usr/bin/c++ -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done -- Generating done -- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-01/cxx-example/build
$ cmake --build .
Scanning dependencies of target hello-world
[ 50%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o
[100%] Linking CXX executable hello-world
[100%] Built target hello-world
In this recipe, we have used a simple CMakeLists.txt
to build a "Hello world" executable:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-01 LANGUAGES CXX) add_executable(hello-world hello-world.cpp)
Note
In CMake, C++ is the default programming language. However, we suggest to always explicitly state the projectâs language in theproject
 command using the LANGUAGES
option.
To configure the project and generate its build system, we have to run CMake through its command-line interface (CLI). The CMake CLI offers a number of switches, cmake --help
will output to screen the full help menu listing all of the available switches. We will learn more about them throughout the book. As you will notice from the output of cmake --help
, most of them will let you access the CMake manual. The typical series of commands issued for generating the build system is the following:
$ mkdir -p build $ cd build $ cmake ..
Here, we created a directory, build
, where the build system will be generated, we entered the build
directory, and invoked CMake by pointing it to the location of CMakeLists.txt
(in this case located in the parent directory). It is possible to use the following invocation to achieve the same effect:
$ cmake -H. -Bbuild
This invocation is cross-platform and introduces the -H
and -B
CLI switches. With -H.
we are instructing CMake to search for the root CMakeLists.txt
file in the current directory. -Bbuild
tells CMake to generate all of its files in a directory called build
.
Note
Note that the cmake -H. -Bbuild
invocation of CMake is still undergoing standardization: https://cmake.org/pipermail/cmake-developers/2018-January/030520.html. This is the reason why we will instead use the traditional approach in this book (create a build directory, step into it, and configure the project by pointing CMake to the location of CMakeLists.txt
).
Running the cmake
command outputs a series of status messages to inform you of the configuration:
$ cmake ..
-- The CXX compiler identification is GNU 8.1.0
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-01/cxx-example/build
Note
Running cmake .
 in the same directory as CMakeLists.txt
would in principle be enough to configure a project. However, CMake would then write all generated files into the root of the project. This would be an in-source build and is generally undesirable, as it mixes the source and the build tree of the project. The out-of-source build we have demonstrated is the preferred practice.
CMake is a build system generator. You describe what type of operations the build system, such as Unix Makefiles, Ninja, Visual Studio, and so on, will have to run to get your code compiled. In turn, CMake generates the corresponding instructions for the chosen build system. By default, on GNU/Linux and macOS systems, CMake employs the Unix Makefiles generator. On Windows, Visual Studio is the default generator. We will take a closer look at generators in the next recipe and also revisit generators in Chapter 13, Alternative Generators and Cross-compilation. On GNU/Linux, CMake will by default generate Unix Makefiles to build the project:
Makefile
:Â The set of instructions thatmake
will run to build the project.CMakeFiles
: Directory which contains temporary files, used by CMake for detecting the operating system, compiler, and so on. In addition, depending on the chosen generator,it also contains project-specific files.cmake_install.cmake
: A CMake script handling install rules, which is used at install time.CMakeCache.txt
: The CMake cache, as the filename suggests. This file is used by CMake when re-running the configuration.
To build the example project, we ran this command:
$ cmake --build .
This command is a generic, cross-platform wrapper to the native build command for the chosen generator, make
in this case. We should not forget to test our example executable:
$ ./hello-world
Hello, CMake world!
Finally, we should point out that CMake does not enforce a specific name or a specific location for the build directory. We could have placed it completely outside the project path. This would have worked equally well:
$ mkdir -p /tmp/someplace $ cd /tmp/someplace $ cmake /path/to/source $ cmake --build .
The official documentation at https://cmake.org/runningcmake/ gives a concise overview on running CMake. The build system generated by CMake, the Makefile
in the example given above, will contain targets and rules to build object files, executables, and libraries for the given project. The hello-world
executable was our only target in the current example, but running the command:
$ cmake --build . --target help
The following are some of the valid targets for this Makefile:
... all (the default if no target is provided)
... clean
... depend
... rebuild_cache
... hello-world
... edit_cache
... hello-world.o
... hello-world.i
... hello-world.s
reveals that CMake generates more targets than those strictly needed for building the executable itself. These targets can be chosen with the cmake --build . --target <target-name>
syntax and achieve the following:
all
(orALL_BUILD
with the Visual Studio generator) is the default target and will build all other targets in the project.clean
, is the target to choose if one wants to remove all generated files.depend
, will invoke CMake to generate the dependecies, if any, for the source files.rebuild_cache
, will once again invoke CMake to rebuild theCMakeCache.txt
. This is needed in case new entries from the source need to be added.edit_cache
, this target will let you edit cache entries directly.
For more complex projects, with a test stage and installation rules, CMake will generate additional convenience targets:
test
(orRUN_TESTS
with the Visual Studio generator) will run the test suite with the help of CTest. We will discuss testing and CTest extensively in Chapter 4, Creating and Running Tests.install
, will execute the installation rules for the project. We will discuss installation rules in Chapter 10, Writing an Installer.package
, this target will invoke CPack to generate a redistributable package for the project. Packaging and CPack will be discussed in Chapter 11, Packaging Projects.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-02 and has a C++, C, and Fortran example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
CMake is a build system generator and a single CMakeLists.txt
can be used to configure projects for different toolstacks on different platforms. You describe in CMakeLists.txt
the operations the build system will have to run to get your code configured and compiled. Based on these instructions, CMake will generate the corresponding instructions for the chosen build system (Unix Makefiles, Ninja, Visual Studio, and so on). We will revisit generators in Chapter 13, Alternative Generators and Cross-compilation.
CMake supports an extensive list of native build tools for different platforms. Both command-line tools, such as Unix Makefiles and Ninja, and integrated development environment (IDE) tools are supported. You can find an up-to-date list of the generators available on your platform and for your installed version of CMake by running the following:
$ cmake --help
The output of this command will list all options to the CMake command-line interface. At the bottom, you will find the list of available generators. For example, this is the output on a GNU/Linux machine with CMake 3.11.2 installed:
Generators The following generators are available on this platform: Unix Makefiles = Generates standard UNIX makefiles. Ninja = Generates build.ninja files. Watcom WMake = Generates Watcom WMake makefiles. CodeBlocks - Ninja = Generates CodeBlocks project files.
CodeBlocks - Unix Makefiles = Generates CodeBlocks project files. CodeLite - Ninja = Generates CodeLite project files. CodeLite - Unix Makefiles = Generates CodeLite project files. Sublime Text 2 - Ninja = Generates Sublime Text 2 project files. Sublime Text 2 - Unix Makefiles = Generates Sublime Text 2 project files. Kate - Ninja = Generates Kate project files. Kate - Unix Makefiles = Generates Kate project files. Eclipse CDT4 - Ninja = Generates Eclipse CDT 4.0 project files. Eclipse CDT4 - Unix Makefiles= Generates Eclipse CDT 4.0 project files.
With this recipe, we will show how easy it is to switch generators for the same project.
We will reuse hello-world.cpp
and CMakeLists.txt
 from the previous recipe. The only difference is in the invocation of CMake, since we will now have to pass the generator explicitly with the-G
CLI switch.
$ mkdir -p build $ cd build $ cmake -G Ninja .. -- The CXX compiler identification is GNU 8.1.0 -- Check for working CXX compiler: /usr/bin/c++ -- Check for working CXX compiler: /usr/bin/c++ -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Detecting CXX compile features -- Detecting CXX compile features - done -- Configuring done -- Generating done -- Build files have been written to: /home/user/cmake-cookbook/chapter-01/recipe-02/cxx-example/build
- In the second step, we build the project:
$ cmake --build .
[2/2] Linking CXX executable hello-world
We have seen that the output of the configuration step was unchanged compared to the previous recipe. The output of the compilation step and the contents of the build directory will however be different, as every generator has its own specific set of files:
build.ninja
andrules.ninja
:Â Contain all the build statements and build rules for Ninja.CMakeCache.txt
: CMake always generates its own cache in this file, regardless of the chosen generator.CMakeFiles
: Contains temporary files generated by CMake during configuration.cmake_install.cmake
: CMake script handling install rules and which is used at install time.
Note how cmake --build .
wrapped the ninja
command in a unified, cross-platform interface.
We will discuss alternative generators and cross-compilation in Chapter 13, Alternative Generators and Cross-compilation.
The CMake documentation is a good starting point to learn more about generators:Â https://cmake.org/cmake/help/latest/manual/cmake-generators.7.html.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-03 and has a C++ and Fortran example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
A project almost always consists of more than a single executable built from a single source file. Projects are split across multiple source files, often spread across different subdirectories in the source tree. This practice not only helps in keeping source code organized within a project, but greatly favors modularity, code reuse, and separation of concerns, since common tasks can be grouped into libraries. This separation also simplifies and speeds up recompilation of a project during development. In this recipe, we will show how to group sources into libraries and how to link targets against these libraries.
Let us go back to our very first example. However, instead of having one single source file for the executable, we will now introduce a class to wrap the message to be printed out to screen. This is our updated hello-world.cpp
:
#include "Message.hpp" #include <cstdlib> #include <iostream> int main() { Message say_hello("Hello, CMake World!"); std::cout << say_hello << std::endl; Message say_goodbye("Goodbye, CMake World"); std::cout << say_goodbye << std::endl; return EXIT_SUCCESS; }
The Message
class wraps a string, provides an overload for the <<
operator, and consists of two source files: the Message.hpp
header file and the corresponding Message.cpp
source file. The Message.hpp
 interface file contains the following:
#pragma once #include <iosfwd> #include <string> class Message { public: Message(const std::string &m) : message_(m) {} friend std::ostream &operator<<(std::ostream &os, Message &obj) { return obj.printObject(os); } private: std::string message_; std::ostream &printObject(std::ostream &os); };
The corresponding implementation is contained in Message.cpp
:
#include "Message.hpp" #include <iostream> #include <string> std::ostream &Message::printObject(std::ostream &os) { os << "This is my very nice message: " << std::endl; os << message_; return os; }
These two new files will also have to be compiled and we have to modify CMakeLists.txt
accordingly. However, in this example we want to compile them first into a library, and not directly into the executable:
- Create a new target, this time a static library. The name of the library will be the name of the target and the sources are listed as follows:
add_library(message STATIC Message.hpp Message.cpp )
- The creation of the target for the
hello-world
executable is unmodified:
add_executable(hello-world hello-world.cpp)
- Finally, tell CMake that the library target has to be linked into the executable target:
target_link_libraries(hello-world message)
- We can configure and build with the same commands as before. This time a library will be compiled, alongside theÂ
hello-world
 executable:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . Scanning dependencies of target message [ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o [ 50%] Linking CXX static library libmessage.a [ 50%] Built target message Scanning dependencies of target hello-world [ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o [100%] Linking CXX executable hello-world [100%] Built target hello-world $ ./hello-world This is my very nice message: Hello, CMake World! This is my very nice message: Goodbye, CMake World
The previous example introduced two new commands:
add_library(message STATIC Message.hpp Message.cpp)
: This will generate the necessary build tool instructions for compiling the specified sources into a library. The first argument toadd_library
is the name of the target. The same name can be used throughoutÂCMakeLists.txt
to refer to the library. The actual name of the generated library will be formed by CMake by adding the prefixlib
in front and the appropriate extension as a suffix. The library extension is determined based on the second argument,STATIC
orSHARED
, and the operating system.target_link_libraries(hello-world message)
: Links the library into the executable. This command will also guarantee that thehello-world
executable properly depends on the message library. We thus ensure that the message library is always built before we attempt to link it to thehello-world
executable.
After successful compilation, the build directory will contain the libmessage.a
 static library (on GNU/Linux) and thehello-world
executable.Â
CMake accepts other values as valid for the second argument to add_library
and we will encounter all of them in the rest of the book:
STATIC
, which we have already encountered, will be used to create static libraries, that is, archives of object files for use when linking other targets, such as executables.SHARED
 will be used to create shared libraries, that is, libraries that can be linked dynamically and loaded at runtime. Switching from a static library to a dynamic shared object (DSO) is as easy as usingÂadd_library(message SHARED Message.hpp Message.cpp)
 inÂCMakeLists.txt
.ÂOBJECT
 can be used to compile the sources in the list given toadd_library
to object files, but then neither archiving them into a static library nor linking them into a shared object. The use of object libraries is particularly useful if one needs to create both static and shared libraries in one go. We will demonstrate this in this recipe.MODULE
libraries are once again DSOs. In contrast toÂSHARED
libraries, they are not linked to any other target within the project, but may be loaded dynamically later on. This is the argument to use when building a runtime plugin.
CMake is also able to generate special types of libraries. These produce no output in the build system but are extremely helpful in organizing dependencies and build requirements between targets:
IMPORTED
, this type of library target represents a library located outside the project. The main use for this type of library is to model pre-existing dependencies of the project that are provided by upstream packages. As suchIMPORTED
libraries are to be treated as immutable. We will show examples of usingIMPORTED
libraries throughout the rest of the book. See also: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#imported-targetsINTERFACE
, this special type of CMake library is similar to anIMPORTED
library, but is mutable and has no location. Its main use case is to model usage requirements for a target that is outside our project. We will show a use case forINTERFACE
libraries in Recipe 5, Distributing a project with dependencies as Conda package, in Chapter 11, Packaging Projects. See also: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#interface-librariesALIAS
, as the name suggests, a library of this type defines an alias for a pre-existing library target within the project. It is thus not possible to choose an alias for anIMPORTED
library. See also: https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#alias-libraries
In this example, we have collected the sources directly using add_library
. In later chapters, we demonstrate the use of the target_sources
CMake command to collect sources, in particular in Chapter 7, Structuring Projects. See also this wonderful blog post by Craig Scott: https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/ which further motivates the use of the target_sources
command.
Let us now show the use of the object library functionality made available in CMake. We will use the same source files, but modify CMakeLists.txt
:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-03 LANGUAGES CXX) add_library(message-objs OBJECT Message.hpp Message.cpp ) # this is only needed for older compilers # but doesn't hurt either to have it set_target_properties(message-objs PROPERTIES POSITION_INDEPENDENT_CODE 1 ) add_library(message-shared SHARED $<TARGET_OBJECTS:message-objs> ) add_library(message-static STATIC $<TARGET_OBJECTS:message-objs> ) add_executable(hello-world hello-world.cpp) target_link_libraries(hello-world message-static)
First, notice that the add_library
command changed to add_library(message-objs OBJECT Message.hpp Message.cpp)
. Additionally, we have to make sure that the compilation to object files generates position-independent code. This is done by setting the corresponding property of the message-objs
target, with the set_target_properties
command.
Note
The need to explicitly set the POSITION_INDEPENDENT_CODE
property for the target might only arise on certain platforms and/or using older compilers.
This object library can now be used to obtain both the static library, called message-static
, and the shared library, called message-shared
. It is important to note the generator expression syntax used to refer to the object library:Â $<TARGET_OBJECTS:message-objs>
. Generator expressions are constructs that CMake evaluates at generation time, right after configuration time, to produce configuration-specific build output. See also: https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html. We will delve into generator expressions later in Chapter 5, Configure-time and Build-time Operations. Finally, the hello-world
executable is linked with the static version of the message
library.
Is it possible to have CMake generate the two libraries with the same name? In other words, can both of them be called message
instead of message-static
and message-shared
? We will need to modify the properties of these two targets:
add_library(message-shared SHARED $<TARGET_OBJECTS:message-objs> ) set_target_properties(message-shared PROPERTIES OUTPUT_NAME "message" ) add_library(message-static STATIC $<TARGET_OBJECTS:message-objs> ) set_target_properties(message-static PROPERTIES OUTPUT_NAME "message" )
Can we link against the DSO? It depends on the operating system and compiler:
- On GNU/Linux and macOS, it will work, regardless of the chosen compiler.
- On Windows, it will not work with Visual Studio, but it will work with MinGW and MSYS2.
Why? Generating good DSOs requires the programmer to limit symbol visibility. This is achieved with the help of the compiler, but conventions are different on different operating systems and compilers. CMake has a powerful mechanism for taking care of this and we will explain how it works in Chapter 10, Writing an Installer.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-04 and has a C++ example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
So far, we have looked at fairly simple projects, where the execution flow for CMake was linear: from a set of source files to a single executable, possibly via static or shared libraries. To ensure complete control over the execution flow of all the steps involved in building a project, configuration, compilation, and linkage, CMake offers its own language. In this recipe, we will explore the use of the conditional construct if-elseif-else-endif
.
Note
The CMake language is fairly large and consists of basic control constructs, CMake-specific commands, and infrastructure for modularly extending the language with new functions. A complete overview can be found online here: https://cmake.org/cmake/help/latest/manual/cmake-language.7.html.
Let us start with the same source code as for the previous recipe. We want to be able to toggle between two behaviors:
- Build
Message.hpp
andMessage.cpp
into a library, static or shared, and then link the resulting library into thehello-world
executable. - Build
Message.hpp
,Message.cpp
, andhello-world.cpp
into a single executable, without producing the library.
Let us construct CMakeLists.txt
to achieve this:
- We start out by defining the minimum CMake version, project name, and supported language:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-04 LANGUAGES CXX)
- We introduce a new variable,Â
USE_LIBRARY
. This is a logical variable and its value will be set toÂOFF
. We also print its value for the user:
set(USE_LIBRARY OFF) message(STATUS "Compile sources into a library? ${USE_LIBRARY}")
- Set the
BUILD_SHARED_LIBS
 global variable, defined in CMake, toOFF
. CallingÂadd_library
 and omitting the second argument will build a static library:
set(BUILD_SHARED_LIBS OFF)
- We then introduce a variable,
_sources
, listingMessage.hpp
andMessage.cpp
:
list(APPEND _sources Message.hpp Message.cpp)
- We then introduce anÂ
if-else
statement based on the value ofUSE_LIBRARY
. If the logical toggle is true,Message.hpp
andMessage.cpp
will be packaged into a library:
if(USE_LIBRARY) # add_library will create a static library # since BUILD_SHARED_LIBS is OFF add_library(message ${_sources}) add_executable(hello-world hello-world.cpp) target_link_libraries(hello-world message) else() add_executable(hello-world hello-world.cpp ${_sources}) endif()
- We can again build with the same set of commands. Since
USE_LIBRARY
is set toOFF
, thehello-world
executable will be compiled from all sources. This can be verified by running theobjdump -x
command on GNU/Linux.
We have introduced two variables: USE_LIBRARY
and BUILD_SHARED_LIBS
. Both of them have been set to OFF
. As detailed in the CMake language documentation, true or false values can be expressed in a number of ways:
- A logical variable is true if it is set to any of the following:
1
,ON
,YES
,TRUE
,Y
, or a non-zero number. - A logical variable is false if it is set to any of the following:Â
0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
, an empty string, or it ends in the suffix-NOTFOUND
.
The USE_LIBRARY
variable will toggle between the first and the second behavior. BUILD_SHARED_LIBS
is a global flag offered by CMake. Remember that the add_library
command can be invoked without passing the STATIC
/SHARED
/OBJECT
argument. This is because, internally, the BUILD_SHARED_LIBS
global variable is looked up; if false or undefined, a static library will be generated.
This example shows that it is possible to introduce conditionals to control the execution flow in CMake. However, the current setup does not allow the toggles to be set from outside, that is, without modifying CMakeLists.txt
 by hand. In principle, we want to be able to expose all toggles to the user, so that configuration can be tweaked without modifying the code for the build system. We will show how to do that in a moment.
Note
The ()
in else()
and endif()
may surprise you when starting to read and write CMake code. The historical reason for these is the ability to indicate the scope. For instance, it is possible instead to use if(USE_LIBRARY) ... else(USE_LIBRARY) ... endif(USE_LIBRARY)
if this helps the reader. This is a matter of taste.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-05 and has a C++ example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
In the previous recipe, we introduced conditionals in a rather rigid fashion: by introducing variables with a given truth value hardcoded. This can be useful sometimes, but it prevents users of your code from easily toggling these variables. Another disadvantage of the rigid approach is that the CMake code does not communicate to the reader that this is a value that is expected to be modified from outside. The recommended way to toggle behavior in the build system generation for your project is to present logical switches as options in your CMakeLists.txt
 using the option()
command. This recipe will show you how to use this command.
Let us have a look at our static/shared library example from the previous recipe. Instead of hardcoding USE_LIBRARY
to ON
or OFF
, we will now prefer to expose it as an option with a default value that can be changed from the outside:
- Replace the
set(USE_LIBRARY OFF)
command of the previous recipe with an option. The option will have the same name and its default value will beOFF
:
option(USE_LIBRARY "Compile sources into a library" OFF)
- Now, we can switch the generation of the library by passing the information to CMake via its
-D
CLI option:Â
$ mkdir -p build $ cd build $ cmake -D USE_LIBRARY=ON .. -- ... -- Compile sources into a library? ON -- ... $ cmake --build . Scanning dependencies of target message [ 25%] Building CXX object CMakeFiles/message.dir/Message.cpp.o [ 50%] Linking CXX static library libmessage.a [ 50%] Built target message Scanning dependencies of target hello-world [ 75%] Building CXX object CMakeFiles/hello-world.dir/hello-world.cpp.o [100%] Linking CXX executable hello-world [100%] Built target hello-world
The -D
 switch is used to set any type of variable for CMake: logicals, paths, and so forth.
The option
command accepts three arguments:
option(<option_variable> "help string" [initial value])
<option_variable>
is the name of variable representing the option."help string"
is a string documenting the option. This documentation becomes visible in terminal-based or graphical user interfaces for CMake.[initial value]
is the default value for the option, eitherON
orOFF
.
Sometimes there is the need to introduce options that are dependent on the value of other options. In our example, we might wish to offer the option to either produce a static or a shared library. However, this option would have no meaning if the USE_LIBRARY
logical was not set to ON
. CMake offers the cmake_dependent_option()
command to define options that depend on other options:
include(CMakeDependentOption) # second option depends on the value of the first cmake_dependent_option( MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF "USE_LIBRARY" ON ) # third option depends on the value of the first cmake_dependent_option( MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON "USE_LIBRARY" ON )
If USE_LIBRARY
is ON
, MAKE_STATIC_LIBRARY
defaults to OFF
, while MAKE_SHARED_LIBRARY
defaults to ON
. So we can run this:Â
$ cmake -D USE_LIBRARY=OFF -D MAKE_SHARED_LIBRARY=ON ..
This will still not build a library, since USE_LIBRARY
is still set to OFF
.
As mentioned earlier, CMake has mechanisms in place to extend its syntax and capabilities through the inclusion of modules, either shipped with CMake itself or custom ones. In this case, we have included a module called CMakeDependentOption
. Without the include statement, the cmake_dependent_option()
command would not be available for use. See also https://cmake.org/cmake/help/latest/module/CMakeDependentOption.html.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-06 and has a C++/C example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
One aspect that we have not given much thought to so far is the selection of compilers. CMake is sophisticated enough to select the most appropriate compiler given the platform and the generator. CMake is also able to set compiler flags to a sane set of defaults. However, often we wish to control the choice of the compiler, and in this recipe we will show how this can be done. In later recipes, we will also consider the choice of build type and show how to control compiler flags.
How can we select a specific compiler? For example, what if we want to use the Intel or Portland Group compilers? CMake stores compilers for each language in the CMAKE_<LANG>_COMPILER
variable, where <LANG>
is any of the supported languages, for our purposes CXX
, C
, or Fortran
. The user can set this variable in one of two ways:
- By using the
-D
 option in the CLI, for example:
$ cmake -D CMAKE_CXX_COMPILER=clang++ ..
- By exporting the environment variables
CXX
for the C++ compiler,ÂCC
for the C compiler, andFC
for the Fortran compiler. For example, use this command to use clang++ as the C++ compiler:
$ env CXX=clang++ cmake ..
Any of the recipes discussed so far can be configured for use with any other compiler by passing the appropriate option.
Note
CMake is aware of the environment and many options can either be set via the -D
 switch of its CLI or via an environment variable. The former mechanism overrides the latter, but we suggest to always set options explicitly with -D
. Explicit is better than implicit, since environment variables might be set to values that are not suitable for the project at hand.
We have here assumed that the additional compilers are available in the standard paths where CMake does its lookups. If that is not the case, the user will need to pass the full path to the compiler executable or wrapper.
Note
We recommend to set the compilers using the -D CMAKE_<LANG>_COMPILER
 CLI options instead of exportingCXX
,CC
, andFC
. This is the only way that is guaranteed to be cross-platform and compatible with non-POSIX shells. It also avoids polluting your environment with variables, which may affect the environment for external libraries built together with your project.
At configure time, CMake performs a series of platform tests to determine which compilers are available and if they are suitable for the project at hand. A suitable compiler is not only determined by the platform we are working on, but also by the generator we want to use. The first test CMake performs is based on the name of the compiler for the project language. For example, if cc
is a working C compiler, then that is what will be used as the default compiler for a C project. On GNU/Linux, using Unix Makefiles or Ninja, the compilers in the GCC family will be most likely chosen by default for C++, C, and Fortran. On Microsoft Windows, the C++ and C compilers in Visual Studio will be selected, provided Visual Studio is the generator. MinGW compilers are the default if MinGW or MSYS Makefiles were chosen as generators.
Where can we find which default compilers and compiler flags will be picked up by CMake for our platform? CMake offers the --system-information
flag, which will dump all information about your system to the screen or a file. To see this, try the following:
$ cmake --system-information information.txt
Searching through the file (in this case, information.txt
), you will find the default values for the CMAKE_CXX_COMPILER
, CMAKE_C_COMPILER
, and CMAKE_Fortran_COMPILER
options, together with their default flags. We will have a look at the flags in the next recipe.
CMake provides additional variables to interact with compilers:
CMAKE_<LANG>_COMPILER_LOADED
: This is set toTRUE
if the language,Â<LANG>
, was enabled for the project.CMAKE_<LANG>_COMPILER_ID
:Â The compiler identification string, unique to the compiler vendor. This is, for example,ÂGCC
 for the GNU Compiler Collection,ÂAppleClang
 for Clang on macOS, andÂMSVC
 for Microsoft Visual Studio Compiler. Note, however, that this variable is not guaranteed to be defined for all compilers or languages.CMAKE_COMPILER_IS_GNU<LANG>
: This logical variable is set toTRUE
if the compiler for the language<LANG>
is part of the GNU Compiler Collection. Notice that the<LANG>
portion of the variable name follows the GNU convention: it will beCC
for the C language,CXX
for the C++ language, andG77
for the Fortran language.CMAKE_<LANG>_COMPILER_VERSION
: This variable holds a string with the version of the compiler for the given language. The version information is given in theÂmajor[.minor[.patch[.tweak]]]
format. However, as forCMAKE_<LANG>_COMPILER_ID
, this variable is not guaranteed to be defined for all compilers or languages.
We can try to configure the following example CMakeLists.txt
with different compilers. In this example, we will use CMake variables to probe what compiler we are using and what version:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-06 LANGUAGES C CXX) message(STATUS "Is the C++ compiler loaded? ${CMAKE_CXX_COMPILER_LOADED}") if(CMAKE_CXX_COMPILER_LOADED) message(STATUS "The C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}") message(STATUS "Is the C++ from GNU? ${CMAKE_COMPILER_IS_GNUCXX}") message(STATUS "The C++ compiler version is: ${CMAKE_CXX_COMPILER_VERSION}") endif() message(STATUS "Is the C compiler loaded? ${CMAKE_C_COMPILER_LOADED}") if(CMAKE_C_COMPILER_LOADED) message(STATUS "The C compiler ID is: ${CMAKE_C_COMPILER_ID}") message(STATUS "Is the C from GNU? ${CMAKE_COMPILER_IS_GNUCC}") message(STATUS "The C compiler version is: ${CMAKE_C_COMPILER_VERSION}") endif()
Observe that this example does not contain any targets, so there is nothing to build and we will only focus on the configuration step:
$ mkdir -p build $ cd build $ cmake .. ... -- Is the C++ compiler loaded? 1 -- The C++ compiler ID is: GNU -- Is the C++ from GNU? 1 -- The C++ compiler version is: 8.1.0 -- Is the C compiler loaded? 1 -- The C compiler ID is: GNU -- Is the C from GNU? 1 -- The C compiler version is: 8.1.0 ...
The output will of course depend on the available and chosen compilers and compiler versions.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-07 and has a C++/C example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
CMake has the notion of build types or configurations, such as Debug
, Release
, and so forth. Within one configuration, one can collect related options or properties, such as compiler and linker flags, for a Debug
or Release
build. The variable governing the configuration to be used when generating the build system is CMAKE_BUILD_TYPE
. This variable is empty by default, and the values recognized by CMake are:
Debug
 for building your library or executable without optimization and with debug symbols,Release
 for building your library or executable with optimization and without debug symbols,RelWithDebInfo
 for building your library or executable with less aggressive optimizations and with debug symbols,MinSizeRel
 for building your library or executable with optimizations that do not increase object code size.
In this recipe, we will show how the build type can be set for an example project:
- We start out by defining the minimum CMake version, project name, and supported languages:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-07 LANGUAGES C CXX)
- Then, we set a default build type (in this case,
Release
) and print it in a message for the user. Note that the variable is set as aCACHE
variable, so that it can be subsequently edited through the cache:
if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
- Finally, we print corresponding compile flags set by CMake as a function of the build type:
message(STATUS "C flags, Debug configuration: ${CMAKE_C_FLAGS_DEBUG}") message(STATUS "C flags, Release configuration: ${CMAKE_C_FLAGS_RELEASE}") message(STATUS "C flags, Release configuration with Debug info: ${CMAKE_C_FLAGS_RELWITHDEBINFO}") message(STATUS "C flags, minimal Release configuration: ${CMAKE_C_FLAGS_MINSIZEREL}") message(STATUS "C++ flags, Debug configuration: ${CMAKE_CXX_FLAGS_DEBUG}") message(STATUS "C++ flags, Release configuration: ${CMAKE_CXX_FLAGS_RELEASE}") message(STATUS "C++ flags, Release configuration with Debug info: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") message(STATUS "C++ flags, minimal Release configuration: ${CMAKE_CXX_FLAGS_MINSIZEREL}")
- Let us now verify the output of a default configuration:
$ mkdir -p build $ cd build $ cmake .. ... -- Build type: Release -- C flags, Debug configuration: -g -- C flags, Release configuration: -O3 -DNDEBUG -- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG -- C flags, minimal Release configuration: -Os -DNDEBUG -- C++ flags, Debug configuration: -g -- C++ flags, Release configuration: -O3 -DNDEBUG -- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG -- C++ flags, minimal Release configuration: -Os -DNDEBUG
- And now, let us switch the build type:
$ cmake -D CMAKE_BUILD_TYPE=Debug ..
-- Build type: Debug
-- C flags, Debug configuration: -g
-- C flags, Release configuration: -O3 -DNDEBUG
-- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
-- C flags, minimal Release configuration: -Os -DNDEBUG
-- C++ flags, Debug configuration: -g
-- C++ flags, Release configuration: -O3 -DNDEBUG
-- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
-- C++ flags, minimal Release configuration: -Os -DNDEBUG
We have demonstrated how to set a default build type and how to override it from the command line. With this, we can control whether a project is built with optimization flags or with all optimizations turned off, and instead debugging information on. We have also seen what kind of flags are used for the various available configurations, as this depends on the compiler of choice. Instead of printing the flags explicitly during a run of CMake, one can also peruse the output of running cmake --system-information
to find out what the presets are for the current combination of platform, default compiler, and language. In the next recipe, we will discuss how to extend or adjust compiler flags for different compilers and different build types.
We have shown how the variable CMAKE_BUILD_TYPE
(documented at this link: https://cmake.org/cmake/help/v3.5/variable/CMAKE_BUILD_TYPE.html) defines the configuration of the generated build system. It is often helpful to build a project both in Release
and Debug
configurations, for example when assessing the effect of compiler optimization levels. For single-configuration generators, such as Unix Makefiles, MSYS Makefiles or Ninja, this requires running CMake twice, that is a full reconfiguration of the project. CMake however also supports multiple-configuration generators. These are usually project files offered by integrated-development environments, most notably Visual Studio and Xcode which can handle more than one configuration simultaneously. The available configuration types for these generators can be tweaked with the CMAKE_CONFIGURATION_TYPES
variable which will accept a list of values (documentation available at this link: https://cmake.org/cmake/help/v3.5/variable/CMAKE_CONFIGURATION_TYPES.html).
The following CMake invocation with the Visual Studio:
$ mkdir -p build $ cd build $ cmake .. -G"Visual Studio 12 2017 Win64" -D CMAKE_CONFIGURATION_TYPES="Release;Debug"
will generate a build tree for both the Release
and Debug
configuration. You can then decide which of the two to build by using the --config
flag:
$ cmake --build . --config Release
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08 and has a C++ example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
The previous recipes showed how to probe CMake for information on the compilers and how to tune compiler optimizations for all targets in your project. The latter task is a subset of the general need to control which compiler flags are used in your project. CMake offers a lot of flexibility for adjusting or extending compiler flags and you can choose between two main approaches:
- CMake treats compile options as properties of targets. Thus, one can set compile options on a per target basis, without overriding CMake defaults.
- You can directly modify the
CMAKE_<LANG>_FLAGS_<CONFIG>
variables by using theÂ-D
 CLI switch. These will affect all targets in the project and override or extend CMake defaults.
In this recipe, we will show both approaches.
We will compile an example program to calculate the area of different geometric shapes. The code has a main
 function in a file called compute-areas.cpp
:
#include "geometry_circle.hpp" #include "geometry_polygon.hpp" #include "geometry_rhombus.hpp" #include "geometry_square.hpp" #include <cstdlib> #include <iostream> int main() { using namespace geometry; double radius = 2.5293; double A_circle = area::circle(radius); std::cout << "A circle of radius " << radius << " has an area of " << A_circle << std::endl; int nSides = 19; double side = 1.29312; double A_polygon = area::polygon(nSides, side); std::cout << "A regular polygon of " << nSides << " sides of length " << side << " has an area of " << A_polygon << std::endl; double d1 = 5.0; double d2 = 7.8912; double A_rhombus = area::rhombus(d1, d2); std::cout << "A rhombus of major diagonal " << d1 << " and minor diagonal " << d2 << " has an area of " << A_rhombus << std::endl; double l = 10.0; double A_square = area::square(l); std::cout << "A square of side " << l << " has an area of " << A_square << std::endl; return EXIT_SUCCESS; }
The implementations of the various functions are contained in other files: each geometric shape has a header file and a corresponding source file. In total, we have four header files and five source files to compile:
. âââ CMakeLists.txt âââ compute-areas.cpp âââ geometry_circle.cpp âââ geometry_circle.hpp âââ geometry_polygon.cpp âââ geometry_polygon.hpp âââ geometry_rhombus.cpp âââ geometry_rhombus.hpp âââ geometry_square.cpp âââ geometry_square.hpp
We will not provide listings for all these files but rather refer the reader to https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-08.
Now that we have the sources in place, our goal will be to configure the project and experiment with compiler flags:
- We set the minimum required version of CMake:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
- We declare the name of the project and the language:
project(recipe-08 LANGUAGES CXX)
- Then, we print the current set of compiler flags. CMake will use these for all C++ targets:
message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
- We prepare a list of flags for our targets. Some of these will not be available on Windows and we make sure to account for that case:
list(APPEND flags "-fPIC" "-Wall") if(NOT WIN32) list(APPEND flags "-Wextra" "-Wpedantic") endif()
- We add a new target, theÂ
geometry
 library and list its source dependencies:
add_library(geometry STATIC geometry_circle.cpp geometry_circle.hpp geometry_polygon.cpp geometry_polygon.hpp geometry_rhombus.cpp geometry_rhombus.hpp geometry_square.cpp geometry_square.hpp )
- We set compile options for this library target:
target_compile_options(geometry PRIVATE ${flags} )
- We then add a target for theÂ
compute-areas
 executable:
add_executable(compute-areas compute-areas.cpp)
- We also set compile options for the executable target:
target_compile_options(compute-areas PRIVATE "-fPIC" )
- Finally, we link the executable to theÂ
geometry
 library:
target_link_libraries(compute-areas geometry)
In this example, the warning flags -Wall
, -Wextra
, and -Wpedantic
will be added to the compile options for the geometry
 target; both the compute-areas
 andgeometry
targets will use the-fPIC
flag. Compile options can be added with three levels of visibility:INTERFACE
,PUBLIC
, andPRIVATE
.
The visibility levels have the following meaning:
- With the
PRIVATE
attribute, compile options will only be applied to the given target and not to other targets consuming it. In our examples, compiler options set on thegeometry
target will not be inherited by thecompute-areas
, even thoughcompute-areas
will link against thegeometry
library. - With the
INTERFACE
attribute, compile options on a given target will only be applied to targets consuming it. - With the
PUBLIC
attribute, compile options will be applied to the given target and all other targets consuming it.
The visibility levels of target properties are at the core of a modern usage of CMake and we will revisit this topic often and extensively throughout the book. Adding compile options in this way does not pollute the CMAKE_<LANG>_FLAGS_<CONFIG>
 global CMake variables and gives you granular control over what options are used on which targets.
How can we verify whether the flags are correctly used as we intended to? Or in other words, how can you discover which compile flags are actually used by a CMake project? One approach is the following and it uses CMake to pass additional arguments, in this case the environment variable VERBOSE=1
, to the native build tool:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . -- VERBOSE=1 ... lots of output ... [ 14%] Building CXX object CMakeFiles/geometry.dir/geometry_circle.cpp.o /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_circle.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_circle.cpp [ 28%] Building CXX object CMakeFiles/geometry.dir/geometry_polygon.cpp.o /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_polygon.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_polygon.cpp [ 42%] Building CXX object CMakeFiles/geometry.dir/geometry_rhombus.cpp.o /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_rhombus.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_rhombus.cpp [ 57%] Building CXX object CMakeFiles/geometry.dir/geometry_square.cpp.o /usr/bin/c++ -fPIC -Wall -Wextra -Wpedantic -o CMakeFiles/geometry.dir/geometry_square.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/geometry_square.cpp ... more output ... [ 85%] Building CXX object CMakeFiles/compute-areas.dir/compute-areas.cpp.o /usr/bin/c++ -fPIC -o CMakeFiles/compute-areas.dir/compute-areas.cpp.o -c /home/bast/tmp/cmake-cookbook/chapter-01/recipe-08/cxx-example/compute-areas.cpp ... more output ...
The preceding output confirms that the compile flags were correctly set according to our instructions.
The second approach to controlling compiler flags involves no modifications to CMakeLists.txt
. If one wants to modify compiler options for the geometry
and compute-areas
targets in this project, it is as easy as invoking CMake with an additional argument:
$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..
As you might have guessed, this command will compile the project, deactivating exceptions and runtime type identification (RTTI).
The two approaches can also be coupled. One can use a basic set of flags globally, while keeping control of what happens on a per target basis. We can use CMakeLists.txt
and running this command:
$ cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..
This will configure the geometry
target with -fno-exceptions -fno-rtti -fPIC -Wall -Wextra -Wpedantic
, while configuring compute-areas
with -fno-exceptions -fno-rtti -fPIC
.
Note
In the rest of the book, we will generally set compiler flags on a per target basis and this is the practice that we recommend for your projects. Using target_compile_options()
not only allows for a fine-grained control over compilation options, but it also integrates better with more advanced features of CMake.
Most of the time, flags are compiler-specific. Our current example will only work with GCC and Clang; compilers from other vendors will not understand many, if not all, of those flags. Clearly, if a project aims at being truly cross-platform, this problem has to be solved. There are three approaches to this.
The most typical approach will append a list of desired compiler flags to each configuration type CMake variable, that is, to CMAKE_<LANG>_FLAGS_<CONFIG>
. These flags are set to what is known to work for the given compiler vendor, and will thus be enclosed in if-endif
 clauses that check the CMAKE_<LANG>_COMPILER_ID
variable, for example:
if(CMAKE_CXX_COMPILER_ID MATCHES GNU) list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions") list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override") list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused") endif() if(CMAKE_CXX_COMPILER_ID MATCHES Clang) list(APPEND CMAKE_CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics") list(APPEND CMAKE_CXX_FLAGS_DEBUG "-Wdocumentation") list(APPEND CMAKE_CXX_FLAGS_RELEASE "-O3" "-Wno-unused") endif()
A more refined approach does not tamper with the CMAKE_<LANG>_FLAGS_<CONFIG>
variables at all and rather defines project-specific lists of flags:
set(COMPILER_FLAGS) set(COMPILER_FLAGS_DEBUG) set(COMPILER_FLAGS_RELEASE) if(CMAKE_CXX_COMPILER_ID MATCHES GNU) list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions") list(APPEND CXX_FLAGS_DEBUG "-Wsuggest-final-types" "-Wsuggest-final-methods" "-Wsuggest-override") list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused") endif() if(CMAKE_CXX_COMPILER_ID MATCHES Clang) list(APPEND CXX_FLAGS "-fno-rtti" "-fno-exceptions" "-Qunused-arguments" "-fcolor-diagnostics") list(APPEND CXX_FLAGS_DEBUG "-Wdocumentation") list(APPEND CXX_FLAGS_RELEASE "-O3" "-Wno-unused") endif()
Later on, it uses generator expressions to set compiler flags on a per-configuration and per-target basis:
target_compile_option(compute-areas PRIVATE ${CXX_FLAGS} "$<$<CONFIG:Debug>:${CXX_FLAGS_DEBUG}>" "$<$<CONFIG:Release>:${CXX_FLAGS_RELEASE}>" )
We have shown both approaches in the current recipe and have clearly recommended the latter (project-specific variables and target_compile_options
) over the former (CMake variables).
Both approaches work and are widely used in many projects. However, they have shortcomings. As we have already mentioned, CMAKE_<LANG>_COMPILER_ID
is not guaranteed to be defined for all compiler vendors. In addition, some flags might become deprecated or might have been introduced in a later version of the compiler. Similarly to CMAKE_<LANG>_COMPILER_ID
, the CMAKE_<LANG>_COMPILER_VERSION
 variable is not guaranteed to be defined for all languages and vendors. Although checking on these variables is quite popular, we think that a more robust alternative would be to check whether a desired set of flags works with the given compiler, so that only effectively working flags are actually used in the project. Combined with the use of project-specific variables,target_compile_options
, and generator expressions, this approach is quite powerful. We will show how to use this check-and-set pattern in Recipe 3, Writing a function to test and set compiler flags, in Chapter 7,Structuring Projects.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09 and has a C++ and Fortran example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
Programming languages have different standards available, that is, different versions that offer new and improved language constructs. Enabling new standards is accomplished by setting the appropriate compiler flag. We have shown in the previous recipe how this can be done, either on a per-target basis or globally. With its 3.1 version, CMake introduced a platform- and compiler-independent mechanism for setting the language standard for C++ and C: setting the <LANG>_STANDARD
property for targets.
For the following example, we will require a C++ compiler compliant with the C++14 standard or later. The code for this recipe defines a polymorphic hierarchy of animals. We use std::unique_ptr
for the base class in the hierarchy:
std::unique_ptr<Animal> cat = Cat("Simon"); std::unique_ptr<Animal> dog = Dog("Marlowe);
Instead of explicitly using constructors for the various subtypes, we use an implementation of the factory method. The factory is implemented using C++11Â variadic templates. It holds a map of creation functions for each object in the inheritance hierarchy:Â
typedef std::function<std::unique_ptr<Animal>(const std::string &)> CreateAnimal;
It dispatches them based on a preassigned tag, so that creation of objects will look like the following:
std::unique_ptr<Animal> simon = farm.create("CAT", "Simon"); std::unique_ptr<Animal> marlowe = farm.create("DOG", "Marlowe");
The tags and creation functions are registered to the factory prior to its use:
Factory<CreateAnimal> farm; farm.subscribe("CAT", [](const std::string & n) { return std::make_unique<Cat>(n); }); farm.subscribe("DOG", [](const std::string & n) { return std::make_unique<Dog>(n); });
We are defining the creation functions using C++11Â lambda functions. Notice the use of std::make_unique
to avoid introducing the naked new
operator. This helper was introduced in C++14.
Note
This functionality of CMake was added in version 3.1 and is ever-evolving. Later versions of CMake have added better and better support for later versions of the C++ standard and different compilers. We recommend that you check whether your compiler of choice is supported on the documentation webpage:Â https://cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html#supported-compilers.
We will construct the CMakeLists.txt
 step by step and show how to require a certain standard (in this case C++14):
- We state the minimum required CMake version, project name, and language:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-09 LANGUAGES CXX)
- We request all library symbols to be exported on Windows:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
- We need to add a target for the library. This will compile the sources into a shared library:
add_library(animals SHARED Animal.cpp Animal.hpp Cat.cpp Cat.hpp Dog.cpp Dog.hpp Factory.hpp )
- Â We now set the
CXX_STANDARD
,CXX_EXTENSIONS
, andCXX_STANDARD_REQUIRED
 properties for the target. We also set thePOSITION_INDEPENDENT_CODE
property, to avoid issues when building the DSO with some compilers:
set_target_properties(animals PROPERTIES CXX_STANDARD 14 CXX_EXTENSIONS OFF CXX_STANDARD_REQUIRED ON POSITION_INDEPENDENT_CODE 1 )
- Then, we add a new target for the
animal-farm
 executable and set its properties:
add_executable(animal-farm animal-farm.cpp) set_target_properties(animal-farm PROPERTIES CXX_STANDARD 14 CXX_EXTENSIONS OFF CXX_STANDARD_REQUIRED ON )
- Â Finally, we link the executable to the library:
target_link_libraries(animal-farm animals)
- Let us also check what our example cat and dog have to say:
$ mkdir -p build $ cd build $ cmake .. $ cmake --build . $ ./animal-farm I'm Simon the cat! I'm Marlowe the dog!
In steps 4 and 5, we set a number of properties for the animals
and animal-farm
targets:
CXX_STANDARD
mandates the standard that we would like to have.CXX_EXTENSIONS
tells CMake to only use compiler flags that will enable the ISO C++ standard, without compiler-specific extensions.CXX_STANDARD_REQUIRED
specifies that the version of the standard chosen is required. If this version is not available, CMake will stop configuration with an error. When this property is set toOFF
, CMake will look for next latest version of the standard, until a proper flag has been set. This means to first look for C++14, then C++11, then C++98.
Note
At the time of writing, there is no Fortran_STANDARD
 property available yet, but the standard can be set using target_compile_options
; see https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-09.
CMake offers an even finer level of control over the language standard by introducing the concept of compile features. These are features introduced by the language standard, such as variadic templates and lambdas in C++11, and automatic return type deduction in C++14. You can ask for certain features to be available for specific targets with the target_compile_features()
command and CMake will automatically set the correct compiler flag for the standard. It is also possible to have CMake generate compatibility headers for optional compiler features.
Note
We recommend reading the online documentation for cmake-compile-features
to get a complete overview of how CMake can handle compile features and language standards: https://cmake.org/cmake/help/latest/manual/cmake-compile-features.7.html.
Note
The code for this recipe is available at https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-01/recipe-10 and has a C++ example. The recipe is valid with CMake version 3.5 (and higher) and has been tested on GNU/Linux, macOS, and Windows.
We have used if-elseif-endif
constructs in previous recipes of this chapter. CMake also offers language facilities for creating loops: foreach-endforeach
and while-endwhile
. Both can be combined with break
 for breaking from the enclosing loop early. This recipe will show you how to use foreach
 to loop over a list of source files. We will apply such a loop to lower the compiler optimization for a set of source files without introducing a new target.
We will reuse the geometry example introduced in Recipe 8, Controlling compiler flags. Our goal will be to fine-tune the compiler optimization for some of the sources by collecting them into a list.
 These are the detailed steps to follow in CMakeLists.txt
:
- As in Recipe 8, Controlling compiler flags, we specify the minimum required version of CMake, project name, and language, and declare the
geometry
library target:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR) project(recipe-10 LANGUAGES CXX) add_library(geometry STATIC geometry_circle.cpp geometry_circle.hpp geometry_polygon.cpp geometry_polygon.hpp geometry_rhombus.cpp geometry_rhombus.hpp geometry_square.cpp geometry_square.hpp )
- We decide to compile the library with the
-O3
compiler optimization level. This is set as aPRIVATE
compiler option on the target:
target_compile_options(geometry PRIVATE -O3 )
- Then, we generate a list of source files to be compiled with lower optimization:
list( APPEND sources_with_lower_optimization geometry_circle.cpp geometry_rhombus.cpp )
- We loop over these source files to tune their optimization level down to
-O2
. This is done using their source file properties:
message(STATUS "Setting source properties using IN LISTS syntax:") foreach(_source IN LISTS sources_with_lower_optimization) set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS -O2) message(STATUS "Appending -O2 flag for ${_source}") endforeach()
- To make sure source properties were set, we loop once again and print the
COMPILE_FLAGS
property on each of the sources:
message(STATUS "Querying sources properties using plain syntax:") foreach(_source ${sources_with_lower_optimization}) get_source_file_property(_flags ${_source} COMPILE_FLAGS) message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}") endforeach()
- Finally, we add theÂ
compute-areas
 executable target and link it against thegeometry
library:
add_executable(compute-areas compute-areas.cpp) target_link_libraries(compute-areas geometry)
$ mkdir -p build $ cd build $ cmake .. ... -- Setting source properties using IN LISTS syntax: -- Appending -O2 flag for geometry_circle.cpp -- Appending -O2 flag for geometry_rhombus.cpp -- Querying sources properties using plain syntax: -- Source geometry_circle.cpp has the following extra COMPILE_FLAGS: -O2 -- Source geometry_rhombus.cpp has the following extra COMPILE_FLAGS: -O2
- Finally, also check the build step with
VERBOSE=1
. You will see that the-O2
flag gets appended to the-O3
flag but the last optimization level flag (in this case-O2
) "wins":
$ cmake --build . -- VERBOSE=1
The foreach-endforeach
syntax can be used to express the repetition of certain tasks over a list of variables. In our case, we used it to manipulate, set, and get the compiler flags of specific files in the project. This CMake snippet introduced two additional new commands:
set_source_files_properties(file PROPERTIES property value)
, which sets the property to the passed value for the given file. Much like targets, files also have properties in CMake. This allows for extremely fine-grained control over the build system generation. The list of available properties for source files can be found here: https://cmake.org/cmake/help/v3.5/manual/cmake-properties.7.html#source-file-properties.get_source_file_property(VAR file property)
, which retrieves the value of the desired property for the given file and stores it in the CMakeÂVAR
 variable.
The foreach()
 construct can be used in four different ways:
foreach(loop_var arg1 arg2 ...)
:Â Where a loop variable and an explicit list of items are provided. This form was used when printing the compiler flag sets for the items inÂsources_with_lower_optimization
. Note that if the list of items is in a variable, it has to be explicitly expanded; that is,${sources_with_lower_optimization}
has to be passed as an argument.- As a loop over integer numbers by specifying a range, such asÂ
foreach(loop_var RANGE total)
 or alternativelyforeach(loop_var RANGE start stop [step])
. - As a loop over list-valued variables, such asÂ
foreach(loop_var IN LISTS [list1 [...]])
. The arguments are interpreted as lists and their contents automatically expanded accordingly. - As a loop over items, such asÂ
foreach(loop_var INÂ ITEMS [item1 [...]])
. The contents of the arguments are not expanded.