CMake Cookbook

4.4 (16 reviews total)
By Radovan Bast , Roberto Di Remigio
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. From a Simple Executable to Libraries

About this book

CMake is cross-platform, open-source software for managing the build process in a portable fashion. This book features a collection of recipes and building blocks with tips and techniques for working with CMake, CTest, CPack, and CDash.

CMake Cookbook includes real-world examples in the form of recipes that cover different ways to structure, configure, build, and test small- to large-scale code projects. You will learn to use CMake's command-line tools and master modern CMake practices for configuring, building, and testing binaries and libraries. With this book, you will be able to work with external libraries and structure your own projects in a modular and reusable way. You will be well-equipped to generate native build scripts for Linux, MacOS, and Windows, simplify and refactor projects using CMake, and port projects to CMake.

Publication date:
September 2018
Publisher
Packt
Pages
606
ISBN
9781788470711

 

Chapter 1. From a Simple Executable to Libraries

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
 

Introduction


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.

 

Compiling a single source file into an executable


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.

Getting ready

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;
}

How to do it

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:

  1. Open a text file with your favorite editor. The name of this file will be CMakeLists.txt.
  2. 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)
  1. The second line declares the name of the project (recipe-01) and the supported language (CXX stands for C++):
project(recipe-01 LANGUAGES CXX)
  1. We instruct CMake to create a new target: the executable hello-world. This executable is generated by compiling and linking the source file hello-world.cpp. CMake will use default settings for the compiler and build automation tools selected:
add_executable(hello-world hello-world.cpp)
  1. Save the file in the same directory as the source file hello-world.cpp. Remember that it can only be namedCMakeLists.txt.
  2. 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
  1. If everything went well, the configuration for the project has been generated in the build directory. We can now compile the executable: 
$ 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

How it works

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

 The CMake language is case insensitive, but the arguments are case sensitive.

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 that make 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 .

There is more

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 (or ALL_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 the CMakeCache.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 (or RUN_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.
 

Switching generators


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.

Getting ready

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.

How to do it

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-GCLI switch.

  1. First, we configure the project using the following:
$ 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
  1. In the second step, we build the project:
$ cmake --build .

[2/2] Linking CXX executable hello-world

How it works

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 and rules.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.

See also

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.

 

Building and linking static and shared libraries


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.

Getting ready

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;
}

How to do it

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:

  1. 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
  )
  1. The creation of the target for the hello-world executable is unmodified:
add_executable(hello-world hello-world.cpp) 
  1. Finally, tell CMake that the library target has to be linked into the executable target:
target_link_libraries(hello-world message)
  1. 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

How it works

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 to add_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 prefix lib in front and the appropriate extension as a suffix. The library extension is determined based on the second argument, STATIC or SHARED, and the operating system.
  • target_link_libraries(hello-world message): Links the library into the executable. This command will also guarantee that the hello-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 the hello-world executable.

After successful compilation, the build directory will contain the libmessage.a static library (on GNU/Linux) and thehello-worldexecutable. 

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 to add_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:

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.

There is more

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:

  1. On GNU/Linux and macOS, it will work, regardless of the chosen compiler.
  2. 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.

 

Controlling compilation with conditionals


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.

How to do it

Let us start with the same source code as for the previous recipe. We want to be able to toggle between two behaviors:

  1. Build Message.hpp and Message.cpp into a library, static or shared, and then link the resulting library into the hello-world executable.
  2. Build Message.hpp, Message.cpp, and hello-world.cpp into a single executable, without producing the library.

Let us construct CMakeLists.txt to achieve this:

  1. 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)
  1. 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}")
  1. Set the BUILD_SHARED_LIBS global variable, defined in CMake, to OFF. Calling add_library and omitting the second argument will build a static library:
set(BUILD_SHARED_LIBS OFF)
  1. We then introduce a variable, _sources, listing Message.hpp and Message.cpp:
list(APPEND _sources Message.hpp Message.cpp)
  1. We then introduce an if-else statement based on the value of USE_LIBRARY. If the logical toggle is true, Message.hpp and Message.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()
  1. We can again build with the same set of commands. Since USE_LIBRARY is set to OFF, the hello-world executable will be compiled from all sources. This can be verified by running the objdump -x command on GNU/Linux.

How it works

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

When introducing the _sources variable, we have indicated to readers of this code that this is a local variable that should not be used outside the current scope by prefixing it with an underscore.

 

Presenting options to the user


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.

How to do it

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:

  1. 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 be OFF:
option(USE_LIBRARY "Compile sources into a library" OFF)
  1. Now, we can switch the generation of the library by passing the information to CMake via its-DCLI 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.

How it works

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, either ON or OFF.

There is more

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 manual page for any module can also be accessed from the command line using cmake --help-module <name-of-module>. For example, cmake --help-option CMakeDependentOption will print the manual page for the module just discussed.

 

Specifying the compiler


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 to do it

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:

  1. By using the -D option in the CLI, for example:
$ cmake -D CMAKE_CXX_COMPILER=clang++ ..
  1. By exporting the environment variables CXX for the C++ compiler, CC for the C compiler, and FC 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.

How it works

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.

There is more

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 to TRUE 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 to TRUE 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 be CC for the C language, CXX for the C++ language, and G77 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 for CMAKE_<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.

 

Switching the build type


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:

  1. Debug for building your library or executable without optimization and with debug symbols,
  2. Release for building your library or executable with optimization and without debug symbols,
  3. RelWithDebInfo for building your library or executable with less aggressive optimizations and with debug symbols,
  4. MinSizeRel for building your library or executable with optimizations that do not increase object code size.

How to do it

In this recipe, we will show how the build type can be set for an example project:

  1. 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)
  1. 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 a CACHE 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}")
  1. 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}")
  1. 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
  1. 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

How it works

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.

There is more

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 Releaseand 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

When developing code with single-configuration generators, create separate build directories for the Release and Debug build types, both configuring the same source. With this, you can switch between the two without triggering a full reconfiguration and recompilation.

 

Controlling compiler flags


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.

Getting ready

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.

How to do it

Now that we have the sources in place, our goal will be to configure the project and experiment with compiler flags:

  1. We set the minimum required version of CMake:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
  1. We declare the name of the project and the language:
project(recipe-08 LANGUAGES CXX)
  1. Then, we print the current set of compiler flags. CMake will use these for all C++ targets:
message("C++ compiler flags: ${CMAKE_CXX_FLAGS}")
  1. 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()
  1. 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
  )
  1. We set compile options for this library target:
target_compile_options(geometry
  PRIVATE
    ${flags}
  )
  1. We then add a target for the compute-areas executable:
add_executable(compute-areas compute-areas.cpp)
  1. We also set compile options for the executable target:
target_compile_options(compute-areas
  PRIVATE
    "-fPIC"
  )
  1. Finally, we link the executable to the geometry library:
target_link_libraries(compute-areas geometry)

How it works

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 andgeometrytargets will use the-fPICflag. 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 the geometry target will not be inherited by the compute-areas, even though compute-areas will link against the geometry 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.

There is more

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.

 

Setting the standard for the language


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.

Getting ready

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.

How to do it

We will construct the CMakeLists.txt step by step and show how to require a certain standard (in this case C++14):

  1. We state the minimum required CMake version, project name, and language:
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

project(recipe-09 LANGUAGES CXX)
  1. We request all library symbols to be exported on Windows:
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
  1. 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
  )
  1.  We now set the CXX_STANDARD,CXX_EXTENSIONS, andCXX_STANDARD_REQUIRED properties for the target. We also set the POSITION_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
  )
  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
  )
  1.  Finally, we link the executable to the library:
target_link_libraries(animal-farm animals)
  1. 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!

How it works

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 to OFF, 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.

Note

If the language standard is a global property shared by all targets, you can set the CMAKE_<LANG>_STANDARD, CMAKE_<LANG>_EXTENSIONS, and CMAKE_<LANG>_STANDARD_REQUIRED variables to their desired values. The corresponding properties on all targets will be set with these values.

There is more

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.

 

Using control flow constructs


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.

Getting ready

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.

How to do it

 These are the detailed steps to follow in CMakeLists.txt:

  1. 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
  )
  1. We decide to compile the library with the -O3 compiler optimization level. This is set as a PRIVATE compiler option on the target:
target_compile_options(geometry
  PRIVATE
    -O3
  )
  1. 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
  )
  1. 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()
  1. 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()
  1. Finally, we add the compute-areas executable target and link it against the geometry library:
add_executable(compute-areas compute-areas.cpp)

target_link_libraries(compute-areas geometry)
  1. Let us verify that the flags were correctly set at the configure step:
$ 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
  1. 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

How it works

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.

Note

In CMake, lists are semicolon-separated groups of strings. A list can be created either by the list or the set commands. For example, both set(var a b c d e) and list(APPEND a b c d e) create the list a;b;c;d;e.

Note

To lower optimization for a set of files, it would probably be cleaner to collect them into a separate target (library) and set the optimization level explicitly for this target instead of appending a flag, but in this recipe our focus was on foreach-endforeach.

There is more

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.

About the Authors

  • Radovan Bast

    Radovan is working at the High Performance Computing Group at UiT - The Arctic University of Norway in Tromsø and leads the CodeRefinery project. He has a PhD in theoretical chemistry and as code developer is contributing to a number of quantum chemistry programs. He enjoys learning new programming languages and techniques, and to teach programming to students and researchers. He got in touch with CMake in 2008 and has ported a number of research codes and migrated a number of communities to CMake since.

    Browse publications by this author
  • Roberto Di Remigio

    Roberto is a postdoctoral fellow in theoretical chemistry at the UiT - The Arctic University of Norway in Tromsø, Norway and Virginia Tech, USA. He is currently working on stochastic methods and solvation models. He is a developer of the PCMSolver library and the Psi4 open source quantum chemistry program. He contributes or has contributed to the development of popular quantum chemistry codes and libraries: DIRAC, MRCPP, DALTON, LSDALTON, XCFun, and ReSpect. He usually programs in C++ and Fortran.

    Browse publications by this author

Latest Reviews

(16 reviews total)
ı never I could find a good source on Cmake...this book is awsome just buy there is everything you need..even more
good overview of cmake
感謝Packt的努力提供友好且吸引人的優惠 讓知識的交流更加容易

Recommended For You

Book Title
Unlock this full book FREE 10 day trial
Start Free Trial