CMake
CMake
February 2025
Contents
Contents 2
Author’s Introduction 23
1 Introduction to CMake 25
1.1 Why Do You Need CMake? . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.1.2 The Challenges of Traditional Build Systems . . . . . . . . . . . . . . 26
1.1.3 How CMake Solves These Challenges . . . . . . . . . . . . . . . . . . 28
1.1.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.2 CMake vs. Traditional Build Systems (Make, Autotools, Ninja, etc.) . . . . . . 31
1.2.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.2.2 Traditional Build Systems: Overview and Limitations . . . . . . . . . . 31
1.2.3 How CMake Compares to Traditional Build Systems . . . . . . . . . . 35
1.2.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.3 Installing CMake on Different Operating Systems (Windows, Linux, macOS) . 38
1.3.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.3.2 Installing CMake on Windows . . . . . . . . . . . . . . . . . . . . . . 38
1.3.3 Installing CMake on Linux . . . . . . . . . . . . . . . . . . . . . . . . 42
1.3.4 Installing CMake on macOS . . . . . . . . . . . . . . . . . . . . . . . 44
2
3
1.3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
1.4 Verifying CMake Installation and Running It . . . . . . . . . . . . . . . . . . . 48
1.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
1.4.2 Verifying CMake Installation . . . . . . . . . . . . . . . . . . . . . . . 48
1.4.3 Running CMake for the First Time . . . . . . . . . . . . . . . . . . . . 51
1.4.4 Troubleshooting Common Issues . . . . . . . . . . . . . . . . . . . . . 55
1.4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
1.5 Your First CMake Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.5.2 Setting Up a Simple C++ Project . . . . . . . . . . . . . . . . . . . . . 57
1.5.3 Creating the CMakeLists.txt File . . . . . . . . . . . . . . . . . . 59
1.5.4 Configuring the Build System with CMake . . . . . . . . . . . . . . . 60
1.5.5 Building the Project . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
1.5.6 Running the Executable . . . . . . . . . . . . . . . . . . . . . . . . . 63
1.5.7 Understanding the Build Process . . . . . . . . . . . . . . . . . . . . . 63
1.5.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2 Fundamentals of CMakeLists.txt 65
2.1 Understanding the Structure of CMakeLists.txt . . . . . . . . . . . . . . . . . 65
2.1.1 Introduction to CMakeLists.txt . . . . . . . . . . . . . . . . . . . . . . 65
2.1.2 Key Sections of a CMakeLists.txt File . . . . . . . . . . . . . . . . . . 66
2.1.3 Best Practices for Organizing CMakeLists.txt . . . . . . . . . . . . . . 72
2.1.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.2 Defining the Minimal CMake Project . . . . . . . . . . . . . . . . . . . . . . . 74
2.2.1 What Makes a CMake Project ”Minimal”? . . . . . . . . . . . . . . . 74
2.2.2 CMake File Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.2.3 CMakeLists.txt File for the Minimal Project . . . . . . . . . . . . . . . 75
2.2.4 Understanding the Minimal CMake Project . . . . . . . . . . . . . . . 79
4
Appendices 579
Appendix A: CMake Command Reference . . . . . . . . . . . . . . . . . . . . . . . 579
Appendix B: CMake Best Practices . . . . . . . . . . . . . . . . . . . . . . . . . . . 582
Appendix C: CMake Troubleshooting Guide . . . . . . . . . . . . . . . . . . . . . . 585
Appendix D: CMake Project Examples . . . . . . . . . . . . . . . . . . . . . . . . . 587
Appendix E: CMake Tools and Integrations . . . . . . . . . . . . . . . . . . . . . . 589
References 591
Author’s Introduction
One of the most challenging aspects of C++ programming is the compilation process. Unlike
many modern languages that come with built-in package managers and streamlined build
systems, C++ requires developers to have a deep understanding of compilation, linking,
dependency management, and platform-specific configurations. This complexity often
discourages students and even experienced developers, leading them to abandon C++ in favor of
languages with simpler build processes. However, while C++ may have a steep learning curve in
this regard, mastering the right tools can dramatically improve the development experience and
unlock the full potential of the language.
This is where CMake comes in. CMake is not just another build system; it is a powerful
meta-build tool that simplifies and standardizes the configuration, compilation, and linking
processes across multiple platforms and compilers. In large-scale projects, particularly those
targeting multiple operating systems (Windows, macOS, Linux) or architectures (x86, ARM,
embedded systems), managing build files manually can be overwhelming. CMake provides an
elegant solution by allowing developers to define their build processes in a
platform-independent manner, generating appropriate build scripts for a variety of compilers
and environments.
Many C++ programmers hesitate to learn CMake, thinking of it as an additional layer of
complexity rather than a solution. However, once you understand its workflow, CMake becomes
an indispensable tool that simplifies project setup, dependency management, and integration with
external libraries. Whether you are working on small projects or large-scale software systems,
23
24
using CMake can save you countless hours of manual configuration and troubleshooting.
In this book, I will guide you through the fundamentals of CMake, from basic setup to
advanced configurations. You will learn how to efficiently manage source files, handle
third-party dependencies, optimize compilation settings, and create robust cross-platform
builds. By the end of this journey, you will have a solid grasp of CMake, allowing you to focus
more on writing great C++ code rather than struggling with build issues.
I strongly encourage every C++ developer to invest time in mastering CMake. It is not just a
tool—it is an essential skill that will make your C++ development process more efficient,
scalable, and enjoyable.
Stay Connected
For more discussions and valuable content about Modern C++ Pointers, I invite you to follow
me on LinkedIn:
https://linkedin.com/in/aymanalheraki
You can also visit my personal website:
https://simplifycpp.org
Ayman Alheraki
Chapter 1
Introduction to CMake
1.1.1 Introduction
Software development, particularly in languages like C++, involves multiple stages, including
writing source code, compiling it into object files, linking those files to create an executable, and
finally deploying the application. As projects grow in complexity, managing these tasks
efficiently becomes increasingly difficult. While simple programs with a few source files can be
compiled manually using compiler commands, larger projects require automated build systems
to handle dependencies, multiple files, libraries, and different build configurations.
Historically, developers have relied on manual Makefiles, Autotools, and other
platform-specific build scripts to manage the compilation process. However, these traditional
methods come with significant limitations, particularly when it comes to cross-platform
compatibility, maintainability, and scalability.
CMake is a modern, flexible, and cross-platform build system generator that simplifies the
process of compiling, linking, and managing C++ projects. Instead of manually writing complex
25
26
and platform-dependent Makefiles or build scripts, developers define their project’s structure
in a simple and declarative manner using CMakeLists.txt, and CMake generates the appropriate
build system for the target platform.
This section explores the need for CMake by identifying the challenges associated with
traditional build systems and demonstrating how CMake provides a powerful solution.
One of the biggest challenges in software development is ensuring that a project can be
compiled and executed on multiple operating systems. Many projects need to support
Windows, Linux, macOS, and even embedded platforms.
When using traditional build methods, developers often have to write separate build scripts
for each platform:
Each of these build scripts is tailored to the specific operating system and compiler used.
A Makefile that works on Linux with GCC might not work on Windows without
modifications, and a Visual Studio project file cannot be easily ported to Linux. This
fragmentation leads to increased maintenance overhead and makes cross-platform
development cumbersome.
Modern C++ projects often rely on multiple external libraries, such as Boost, OpenCV,
Qt, Eigen, GLFW, and SQLite. Managing these dependencies manually presents several
challenges:
• Locating the library: Developers must specify the correct paths for headers and
compiled binaries.
• Handling different versions: Different systems may have different versions of a
library installed, leading to potential compatibility issues.
• Static vs. dynamic linking: Some projects require static linking, while others need
shared libraries, leading to different linking options.
• Managing transitive dependencies: A library may depend on other libraries,
complicating the linking process.
app: $(OBJS)
$(CC) $(CFLAGS) -o app $(OBJS)
28
Every time a new source file is added, it must be explicitly listed in OBJS, making
maintenance error-prone. Large projects with hundreds of source files require a more
dynamic and automated approach to handling build configurations.
A project developed on Linux using Makefiles and GCC may not compile on Windows
without modification. Differences in compilers, library locations, and system APIs
require additional effort to ensure cross-platform compatibility.
1. Cross-Platform Compatibility
This allows developers to write once and build anywhere, eliminating the need for
maintaining multiple build scripts for different platforms.
CMake provides built-in functionality for locating and integrating third-party libraries.
Instead of manually specifying library paths, developers can use:
This streamlines dependency management and reduces the risk of version mismatches or
missing dependencies.
CMake detects the compiler, available system libraries, and hardware capabilities
automatically. This allows developers to write portable build configurations without
worrying about platform-specific details.
For example, CMake can check for the presence of certain libraries and enable features
accordingly:
find_package(OpenGL REQUIRED)
find_package(Boost 1.71 REQUIRED)
30
If the required libraries are not found, CMake can provide meaningful error messages,
guiding users to install the necessary dependencies.
1.1.4 Conclusion
CMake addresses the limitations of traditional build systems by providing a cross-platform,
maintainable, and scalable approach to building C++ projects. It simplifies dependency
management, multi-platform support, and automatic configuration detection, making it the
preferred choice for modern C++ development.
With CMake, developers can focus on writing code instead of dealing with the intricacies of
manually managing builds, dependencies, and platform-specific configurations.
31
1.2.1 Introduction
The process of building software from source code involves compiling, linking, and organizing
dependencies to create an executable or library. As software projects grow in complexity,
managing the build process manually becomes inefficient and error-prone.
Traditionally, developers relied on build systems such as Make, Autotools, and Ninja to
automate the compilation process. However, these systems come with limitations in
cross-platform support, maintainability, and flexibility.
CMake was developed to overcome these limitations by providing a higher-level build
system generator that abstracts platform-specific complexities. Unlike traditional build systems
that require developers to write platform-specific scripts, CMake allows them to define their
projects once and generate the appropriate build files for multiple platforms and compilers.
This section provides an in-depth comparison between CMake and traditional build systems,
highlighting their strengths, weaknesses, and use cases.
Make is one of the earliest and most widely used build systems. It is primarily used in
Unix-like operating systems and relies on Makefiles to define build rules and
dependencies.
32
CC = g++
CFLAGS = -Wall -O2
OBJ = main.o module.o
app: $(OBJ)
$(CC) $(CFLAGS) -o app $(OBJ)
%.o: %.cpp
$(CC) $(CFLAGS) -c $< -o $@
make
This compiles the source files and links them into an executable.
Advantages of Make
Disadvantages of Make
• Manual dependency tracking: Developers must explicitly list source files and
dependencies.
3. Libtool – Handles shared and static library creation across different platforms.
./configure
make
make install
Advantages of Autotools
Disadvantages of Autotools
3. Ninja
Ninja is a build system optimized for speed and efficiency. Unlike Make and Autotools,
which handle dependency resolution and build configuration, Ninja is designed purely for
executing build tasks as quickly as possible.
How Ninja Works
Ninja relies on a build.ninja file, which describes how source files should be
compiled and linked. However, developers do not write these files manually—they are
typically generated by higher-level tools like CMake or Meson.
35
rule compile
command = g++ -c $in -o $out
build main.o: compile main.cpp
Advantages of Ninja
Disadvantages of Ninja
cmake_minimum_required(VERSION 3.16)
project(MyApp)
add_executable(MyApp main.cpp)
cmake -S . -B build
cmake --build build
CMake automatically detects the compiler, generates the appropriate build system
(Makefiles, Ninja, Visual Studio), and compiles the project efficiently.
1.2.4 Conclusion
While traditional build systems like Make, Autotools, and Ninja have been widely used, they
come with limitations in portability, dependency management, and maintainability.
CMake provides a modern, flexible, and cross-platform solution that simplifies the build
process by generating native build files for different systems. With its ability to handle
dependencies, detect system configurations, and support multiple build backends, CMake
has become the industry standard for managing C++ projects.
38
1.3.1 Introduction
CMake is a powerful and flexible build system generator that plays a crucial role in simplifying
the process of building C++ projects across different platforms. The installation process is
straightforward, but due to the variety of operating systems and user preferences, there are
multiple methods to install it. This section will provide a detailed guide on how to install CMake
on the most widely used operating systems: Windows, Linux, and macOS.
The installation process for CMake can involve precompiled binary installers, package
managers, or even manual compilation from source. The method chosen depends on the
user's specific needs, such as ensuring the latest version, ease of use, or whether the user prefers
a command-line or graphical interface for installation.
Regardless of the installation method, once CMake is installed, it will allow you to generate
build files for various platforms, manage dependencies, and enable a seamless integration with a
variety of development tools. This section will walk you through each installation method for
different operating systems, and provide verification steps to ensure CMake is installed and
functioning correctly.
One of the easiest and most common methods for installing CMake on Windows is by
39
using the official CMake installer provided by Kitware, the creators of CMake. This
method ensures that you have the latest stable version of CMake, and it allows for an easy
installation process with a graphical user interface (GUI).
1. After the download is complete, double-click the .msi file to launch the
CMake installation wizard.
2. During the installation, you will be presented with different options. The key
option is to add CMake to the system PATH. This is a critical step because it
allows you to run CMake from the command line (Command Prompt or
PowerShell) without needing to specify its full path. You can select the option
”Add CMake to the system PATH for all users”.
3. Proceed with the default installation settings and click Next through the
installation wizard until the installation is complete.
cmake --version
If CMake has been installed correctly, you should see the version of CMake
displayed in the terminal, similar to:
If you see this output, CMake has been installed and is ready to use.
This will automatically install CMake and add it to your system’s PATH so that you
can use it from the command line.
cmake --version
cmake --version
• Ubuntu/Debian-based Distributions
If you're using a Debian-based distribution such as Ubuntu, CMake can easily be
installed using the APT package manager:
cmake --version
• Fedora-based Distributions
For Fedora or similar distributions, the DNF package manager is used:
cmake --version
Verify it using:
cmake --version
wget
,→ https://github.com/Kitware/CMake/releases/latest/download/cmake-3
./bootstrap
make -j$(nproc)
cmake --version
Homebrew is the most popular package manager for macOS, and it simplifies software
installation.
cmake --version
cmake --version
1. Open the .dmg file and drag the CMake.app into the /Applications folder.
2. To enable command-line usage, open CMake.app, go to Tools > How to
Install For Command Line Use, and follow the steps provided.
cmake --version
1.3.5 Conclusion
The installation of CMake varies depending on the operating system being used, but regardless
of the method, CMake provides the necessary tools to streamline and automate the build process
for C++ projects. Whether you use an installer, a package manager, or build from source, the
goal remains the same: to ensure that CMake is set up properly so that you can manage and
configure your project builds across various platforms.
47
Once CMake is installed successfully, you can begin using it to generate platform-specific build
files, configure your project, and manage complex builds in a consistent and efficient manner. In
the next section, we will explore CMake’s basic commands and structure, which will lay the
groundwork for mastering CMake in the context of real-world projects.
48
1.4.1 Introduction
After you have installed CMake on your system, it is essential to ensure that the installation was
successful and that CMake is functioning properly. This process is vital because any issues with
the installation or configuration of CMake could lead to problems when building C++ projects.
Verifying CMake’s installation helps to confirm that the required binaries, environment variables,
and necessary configuration files have been set up correctly.
CMake is a powerful build system generator, and if installed and configured properly, it should
work seamlessly across various platforms like Windows, Linux, and macOS. This section walks
you through how to verify your CMake installation and offers guidance on how to run it for the
first time.
1. On Windows:
cmake --version
• If CMake has been installed successfully, you will see the version number of
CMake. For example:
This indicates that CMake is installed and ready to use. If you encounter an error
message such as ”command not found” or ”CMake is not recognized as an internal
or external command,” this suggests that either the installation has failed or the
system’s PATH environment variable is not set correctly.
2. On Linux/macOS:
• Open a Terminal window.
• Run the following command:
cmake --version
• If CMake is correctly installed, you should see an output similar to the one on
Windows:
If you see an error indicating that cmake is not found, you may need to recheck the
installation process and ensure that the cmake binary is properly linked to your
system’s PATH.
If you receive an error message indicating that the cmake command cannot be found,
here are some troubleshooting steps:
• Ensure CMake is Added to PATH: When you install CMake, it is important to add
the CMake executable to your system’s PATH environment variable. If this step was
missed during installation, you can manually add CMake to your PATH:
– On Windows: You can add CMake to the PATH through the Environment
Variables settings. To do this, go to System Properties > Advanced >
Environment Variables, then edit the System PATH and add the directory
where CMake is installed (e.g., C:\Program Files\CMake\bin).
– On Linux/macOS: Open the shell configuration file (.bashrc or .zshrc,
depending on your shell) and add the following line to include CMake in your
PATH:
export PATH="/path/to/cmake/bin:$PATH"
• Verify Installation Location: Ensure that CMake was installed in the correct
directory. If you installed it via a package manager, it may have been installed in a
non-standard directory, especially on Linux or macOS. Verify that the installation
path is valid and contains the cmake binary.
• Reinstall CMake: If none of the above solutions work, you may need to reinstall
CMake. Be sure to follow the installation instructions carefully to avoid errors, and
make sure to include the option to add CMake to the system PATH during the
installation.
51
Before you can run CMake, it is best to set up a basic C++ project. This allows you to test
CMake’s functionality by creating a minimal project and using CMake to generate the
necessary build files. The following example demonstrates a very simple C++ program
and how to use CMake with it.
1. Create a Project Directory: First, create a directory to house the project files. Open
a terminal or command prompt and create a new directory:
mkdir MyTestProject
cd MyTestProject
2. Create a Simple C++ Source File: Inside the MyTestProject directory, create
a simple C++ source file named main.cpp:
// main.cpp
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
52
# CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
project(MyTestProject)
add_executable(MyTestProject main.cpp)
In this example:
• cmake minimum required(VERSION 3.0) specifies that the project
requires at least version 3.0 of CMake.
• project(MyTestProject) defines the project name as
MyTestProject.
• add executable(MyTestProject main.cpp) tells CMake to
generate an executable named MyTestProject from the main.cpp source
file.
This configuration file is very basic, but it covers the essential elements of a typical
CMake project.
1. Create a Build Directory: To keep the build files separate from the source code, it
is common practice to create a separate build directory. Create a new directory inside
your project folder called build:
53
mkdir build
cd build
2. Run CMake to Configure the Project: Inside the build directory, run the
following CMake command to configure your project:
cmake ..
This command tells CMake to look for the CMakeLists.txt file in the parent
directory (..) and generate the build system files based on the configuration in that
file. If everything is set up correctly, CMake will display output showing the
configuration process, where it detects the system’s compilers, checks for required
tools, and prepares the necessary build files.
The output will look something like:
If there are any errors during this process, CMake will output detailed messages that
can help you diagnose the issue. Common issues include missing dependencies,
incorrect file paths, or unsupported compilers.
Once the configuration step is complete, you can now build the project. CMake has
generated the necessary build files, so you can use the appropriate build tool to compile
the code.
make
This command will invoke the build system (Make) to compile the main.cpp file
and generate the executable MyTestProject.
2. On Windows (Using Visual Studio Project Files): If you are using Windows and
CMake generated Visual Studio project files, you can open the .sln file generated
by CMake and build the project directly in Visual Studio. Alternatively, you can use
the MSBuild command to build from the command line:
MSBuild MyTestProject.sln
Once the build completes successfully, you can run the executable generated by CMake.
./MyTestProject
Hello, CMake!
• On Windows: If you are using Visual Studio or the command line, you can simply
run the MyTestProject.exe executable.
While running CMake for the first time, there are a few common issues that you might
encounter:
• Missing or Incorrect Compiler: CMake requires a working C++ compiler to build your
project. If CMake cannot detect a valid compiler, it will show an error. Make sure that a
C++ compiler (like GCC, Clang, or MSVC) is properly installed and accessible. On
Linux/macOS, you can check the installed compiler version using gcc --version or
clang --version. On Windows, make sure the Visual Studio build tools are
installed correctly.
• Permissions Issues: On Linux and macOS, you may encounter permission-related errors
if you do not have write access to certain directories. Ensure that you are running CMake
with the appropriate permissions or try running with sudo if necessary.
• CMake Cache Conflicts: CMake caches configuration data to avoid reprocessing the
same information multiple times. However, if you make changes to the project structure or
the CMakeLists.txt file, the cached configuration may cause issues. You can delete
the CMakeCache.txt file in your build directory and rerun the CMake command to
clear the cache.
56
1.4.5 Conclusion
Verifying CMake installation and running it for the first time is a crucial step in setting up your
development environment. By checking the CMake version and running it on a simple C++
project, you can ensure that everything is working as expected. If you encounter issues,
troubleshooting steps such as checking the system PATH, verifying the compiler installation, or
clearing the CMake cache can help resolve common problems. Once CMake is verified and
working, you can proceed to more advanced topics, such as configuring larger projects and
utilizing CMake’s advanced features to improve your build system and project management
workflow.
57
1.5.1 Introduction
The previous sections provided an understanding of the importance of CMake, how it differs
from traditional build systems, and how to install it on various operating systems. Now, it’s time
to take the next step and create your very first CMake project. This hands-on guide will walk
you through the entire process, from setting up a simple C++ program to configuring the
necessary build files, and ultimately compiling and running the project. By the end of this
section, you will be comfortable with the fundamental aspects of working with CMake, which
will form the foundation for tackling more advanced CMake features in subsequent sections.
Creating a project with CMake is straightforward and involves defining how your source code is
compiled and linked, specifying compiler options, and ensuring that CMake generates the
correct build system files for your chosen platform. With CMake, the complexity of build
system generation is abstracted away, which saves time and reduces the possibility of errors in
managing project configurations.
1. Project Structure
The basic structure for your project will include the source code and a CMake
configuration file. Start by creating the project directory on your local machine. The
directory should look like this:
58
MyFirstCMakeProject/
CMakeLists.txt
main.cpp
• CMakeLists.txt: This file is the heart of CMake. It contains instructions that CMake
uses to configure the build process. It defines things like which source files to
compile, which compiler options to use, and how to organize the output.
• main.cpp: This is your source code, containing the C++ code that will be compiled
into an executable.
Let’s start by writing the code for the C++ program. Create the main.cpp file inside the
MyFirstCMakeProject directory. Here is a simple “Hello World” program to begin
with:
// main.cpp
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
This is a basic C++ program that prints ”Hello, CMake!” to the console. It contains the
minimal code needed to ensure the program compiles successfully and serves as a simple
test case for understanding how CMake is used to build a C++ project.
59
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
This is the simplest form of a CMakeLists.txt file, but as your projects become more
complex, this file will grow to include other configurations, such as libraries, dependencies,
compiler flags, etc.
build
mkdir build
cd build
This will be the directory where all the build-related files will be placed, such as Makefiles
or Visual Studio project files.
cmake ..
This command tells CMake to look for the CMakeLists.txt file in the parent
directory (..) and configure the project according to the specifications in that file.
Upon running the command, CMake will inspect the environment and attempt to detect
which compiler and tools to use for building the project. After processing the
CMakeLists.txt file, it will generate the necessary files for the build system. On a
typical system, this could include Makefiles, Visual Studio project files, or Ninja files,
depending on your platform.
At this point, CMake has successfully configured the project and written the necessary
build files in the build directory. These build files describe the process CMake will use
to compile your project.
If the build system is configured to use Make, which is common on Linux and macOS
systems, you can compile the project by running the following command:
make
This will invoke the make utility, which reads the generated Makefile and begins the
process of compiling your project. Make will compile the main.cpp source file into an
object file and link it to create the final executable.
The output will indicate the progress of the build process, and once the build is complete,
you should see something like:
This means the build was successful, and the MyFirstCMakeProject executable has
been created.
If you’re on a Windows system and CMake generated Visual Studio project files, you have
the option to either open the generated .sln solution file in Visual Studio and build the
project through the IDE or use the command line.
To build the project from the command line, use MSBuild as follows:
MSBuild MyFirstCMakeProject.sln
This command tells MSBuild to use the Visual Studio build system to compile and link
the project. After the build completes, you will have an executable ready to run.
63
1. Running on Linux/macOS
On Linux and macOS, the executable is typically located in the build directory. To run
the executable, use the following command:
./MyFirstCMakeProject
This will execute the program, and you should see the output:
Hello, CMake!
This confirms that the build was successful, and your program has run as expected.
2. Running on Windows
If you are using Visual Studio, you can run the executable directly from the IDE by
pressing the ”Start” button. Alternatively, after building the project, you can navigate to
the Debug or Release folder (depending on your build configuration) and double-click
the executable MyFirstCMakeProject.exe to run it.
1. CMake Configuration: When you run cmake .., CMake reads the
CMakeLists.txt file in the parent directory. It checks for the system’s environment,
64
such as the available compiler and toolchain, and configures the project according to the
options specified in the CMakeLists.txt file.
2. Build Generation: CMake generates build files that describe how to compile and link the
project. This could be Makefiles, Visual Studio project files, or Ninja files, depending on
your platform and configuration.
3. Compilation: When you run make or MSBuild, the build system compiles your source
code files into object files and links them to form the final executable.
4. Execution: Once the executable is built, you can run it and see the output. If all the steps
are followed correctly, you should see your program’s output displayed on the terminal or
IDE.
1.5.8 Conclusion
In this section, you learned how to create a basic CMake project, write the necessary C++ code,
configure the project using the CMakeLists.txt file, and then build and run the project. You
should now understand the basic workflow involved in working with CMake and be able to
create simple projects.
This foundational knowledge will serve as a springboard for diving into more advanced CMake
features, such as managing external libraries, building multi-target projects, handling
dependencies, and customizing build options. Understanding the basics of how CMake
configures and generates build files is crucial to unlocking the full potential of CMake in larger,
more complex projects.
Chapter 2
Fundamentals of CMakeLists.txt
65
66
beauty of CMake lies in its flexibility and portability; once a project is set up correctly, the same
CMakeLists.txt file can generate build files for different platforms without any
modification.
A simple project might only have one CMakeLists.txt file located at the root of the project
directory. However, for larger projects with multiple modules or libraries, each directory might
have its own CMakeLists.txt file that CMake will read recursively.
Here is an example of the simplest CMakeLists.txt file, which declares a minimum version
of CMake, defines a project, and specifies an executable target:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_executable(MyExecutable main.cpp)
This minimal setup creates a project called MyProject and an executable named
MyExecutable, built from the source file main.cpp.
Every CMakeLists.txt file begins with the declaration of the minimum version of
CMake required to process it. This is essential because different versions of CMake may
support different sets of features, syntax, or functionality. By specifying the minimum
67
version, you ensure that the build system will only be configured using a version of
CMake that is compatible with your project's requirements.
For example:
cmake_minimum_required(VERSION 3.10)
This line tells CMake that the project requires at least version 3.10 of CMake to work
correctly. If the user tries to configure the project with an older version of CMake, an error
will be generated.
2. Project Declaration
For instance, the following line declares a project named MyProject that uses the C++
language:
In this case:
• VERSION 1.0 declares the project version (although version numbers are often
omitted for smaller projects).
• LANGUAGES CXX specifies that the project uses the C++ language. This is optional
in CMake 3.0 and later, as CMake automatically assumes C and C++.
68
3. Build Configuration
This section defines various settings and configuration variables that affect the overall
build process. The settings in this section can include the programming language
standards, compiler flags, and whether to use debug or release builds.
For example:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_BUILD_TYPE Debug)
• set(CMAKE CXX STANDARD 17) ensures that the C++17 standard is used for
compiling C++ code. This is equivalent to passing the -std=c++17 flag to the
compiler.
• set(CMAKE BUILD TYPE Debug) specifies that the project should be built in
the Debug configuration, which will include debugging information and disable
optimizations.
For example:
add_executable(MyExecutable src/main.cpp)
In this case, MyExecutable is the name of the executable, and src/main.cpp is the
source file that will be compiled into it.
For example:
Here, MyLibrary is a shared library built from the src/my library.cpp file.
Once the targets (executables or libraries) are defined, you can modify their properties,
link them with other libraries, and define include directories. CMake provides commands
like target include directories(), target link libraries(), and
target compile options() to achieve this.
Example:
70
target_include_directories(MyExecutable PRIVATE
,→ ${PROJECT_SOURCE_DIR}/include)
This command tells CMake to include the include directory, which is located at the
root of the project (${PROJECT SOURCE DIR} is a CMake variable that holds the root
project directory).
• Linking Libraries: The target link libraries() command links the target
with other libraries. In this case, we are linking MyExecutable with
MyLibrary.
Example:
This makes sure that MyExecutable will be linked with MyLibrary when it is built.
• Compiler Options: You can also set compiler-specific options for individual targets
using target compile options().
Example:
For example, to find and link the Boost library, you would use the find package()
command as follows:
find_package(Boost REQUIRED)
target_link_libraries(MyExecutable Boost::Boost)
In this example, find package(Boost REQUIRED) searches for the Boost library
and ensures it is found. If Boost is not found, CMake will stop with an error. The
target link libraries() command then links Boost::Boost to
MyExecutable.
For large projects, you may want to break the project into smaller, manageable
submodules. CMake allows you to include other CMakeLists.txt files from
subdirectories by using the add subdirectory() command.
Example:
add_subdirectory(lib)
add_subdirectory(app)
In this example, CMake will process the CMakeLists.txt files in the lib and app
subdirectories. Each of these directories can have its own targets and build configuration,
making it easier to modularize the build process. The main CMakeLists.txt file
remains clean and high-level, delegating the detailed configuration to these subdirectories.
While not a functional part of the build process, comments are extremely important for
documenting the CMakeLists.txt file. CMake allows single-line comments using the
# symbol, and multiline comments can be handled by using an if() block with
endif().
For example:
if(FALSE)
# This block is not executed, but useful for documentation
endif()
Comments can clarify the purpose of certain sections or describe why specific options are
used, helping future developers (or yourself) understand the rationale behind the
configuration.
• Avoid Hardcoding Paths: Instead of hardcoding paths, use variables and CMake’s
built-in path handling functions to make your project portable. This ensures your build
configuration works across different environments and operating systems.
73
• Use Variables Wisely: Define and use variables to store file paths, flags, and other
project-specific settings. This makes the configuration more flexible and easier to
maintain.
• Modularize Large Projects: For large projects, break them into smaller subprojects and
use add subdirectory() to include these submodules in the build process. This
keeps your CMakeLists.txt files clean and modular.
• Write Clear and Descriptive Comments: It’s important to explain complex sections of
the CMakeLists.txt file. A well-commented file makes it easier to understand the
build process, especially when dealing with large or complex projects.
2.1.4 Conclusion
The CMakeLists.txt file is an essential part of every CMake-based project. Understanding
its structure and commands gives you the power to manage and customize the build process for
your C++ projects. By organizing the file into well-defined sections—such as setting up the
minimum CMake version, declaring the project, defining targets, handling dependencies, and
configuring the build—you ensure that your project is flexible, maintainable, and portable.
With the knowledge from this section, you should be able to write and understand the basic
structure of a CMakeLists.txt file, setting you on the path toward becoming proficient in
CMake and mastering your C++ build system.
74
This structure is sufficient to compile and link a simple C++ program. Once these basic elements
are understood, you can gradually extend the configuration to handle more complex tasks like
linking external libraries, defining multiple targets, or creating shared/static libraries.
Let’s begin by examining the key components and how to define them in a minimal CMake
project.
75
/MyMinimalProject
CMakeLists.txt
main.cpp
• CMakeLists.txt: The configuration file used by CMake to define the build instructions.
cmake_minimum_required(VERSION 3.10)
cmake_minimum_required(VERSION 3.10)
This command sets the minimum version of CMake required to process the project. It
ensures that the CMake version being used is at least 3.10, which is necessary for certain
features that might be used in the configuration. This line is essential because it
guarantees that your CMake file will not break or behave unexpectedly on older versions
of CMake that do not support newer commands or features.
The minimum required version should be chosen carefully based on the features your
project needs. It is a good practice to specify a version that is compatible with the features
you plan to use, but also widely available across different environments.
2. project()
The project() command defines the name, version, and language of the project. In
this case, MyMinimalProject is the name of the project, and 1.0 is the version
number. The LANGUAGES CXX argument specifies that the project is written in C++
(CMake defaults to C and C++ if no languages are specified, but explicitly stating it can
prevent potential confusion).
This command also sets some project-wide variables, such as PROJECT NAME (which
holds the name of the project) and PROJECT VERSION (which holds the version
number). These variables can be used later in the build process or in the project
documentation.
set(CMAKE_CXX_STANDARD 17)
The set() command is used here to define the C++ standard version for the project. In
this case, we are specifying that the project should be compiled using the C++17 standard.
This is equivalent to passing the -std=c++17 flag to the C++ compiler. By setting this
in the CMake configuration, we ensure that the C++17 features are enabled across the
project.
You can change this value to 11, 14, 20, etc., depending on the version of C++ you wish
to use in your project. This is an important setting because CMake will automatically
propagate the standard across all targets in the project, reducing the need for repetitive
compiler flags.
4. add executable()
add_executable(MyMinimalExecutable main.cpp)
The add executable() command defines an executable target that will be built from
the provided source files. In this case, we are creating an executable named
MyMinimalExecutable from the main.cpp source file.
This is the key command that ties together your source files and defines the primary output
of the build process—an executable program.
Once you have the CMakeLists.txt file set up and the source files in place, you can
now build your project using CMake. Here are the steps to build a minimal CMake project
from the command line:
mkdir build
cd build
2. Run CMake: Run CMake from the build directory, specifying the path to the root
directory of the project (where the CMakeLists.txt file is located):
cmake ..
This command will generate the necessary build system files (such as Makefiles or
Visual Studio project files) based on the configuration in the CMakeLists.txt
file.
3. Build the Project: Once the build files have been generated, you can build the
project using the appropriate build tool. If you're using Makefiles, for example, you
can use the make command:
make
4. Run the Executable: After the build process completes, you can run the executable:
79
./MyMinimalExecutable
This should output the result of your main.cpp program, which, in this case, might
simply be a ”Hello, World!” message or any other code you include in the main.cpp
file.
• Simplicity: The project is small and simple, containing only one executable target and
one source file. This minimal structure is useful for learning and testing the most basic
functionality of CMake.
• Portability: Once you have a minimal CMake project set up, it is portable. By adjusting
only the CMakeLists.txt file, you can generate build files for different platforms,
such as Linux, Windows, and macOS, without having to modify your source code or
project structure.
• Extensibility: Although this project is minimal, it serves as a foundation for extending the
project as you add more features. For instance, you can later add more source files, link
external libraries, define multiple targets, or introduce more advanced CMake features like
custom build commands or conditional logic.
80
1. Adding More Source Files: If your project grows and you need to organize your code
into multiple files, you can simply add more source files to the add executable()
command.
Example:
3. Building Libraries: If your project requires shared or static libraries, you can use the
add library() command to define libraries in addition to executables.
4. Organizing Source Files: For larger projects, consider organizing source files into
directories. You can then use add subdirectory() to manage different parts of the
project.
2.2.6 Conclusion
In this section, we defined the minimal CMake project, which includes the essential components
necessary to build a simple C++ program. By creating a CMakeLists.txt file with the
minimum required CMake version, project declaration, executable definition, and compiler
settings, you have established a basic build system that is portable, extensible, and easy to
maintain.
81
Understanding this minimal structure serves as a foundation for more complex projects. Once
you have mastered this, you can start adding features like multiple targets, external
dependencies, custom build commands, and other advanced CMake functionality. This is just
the first step toward building more sophisticated and scalable CMake projects.
82
2. Syntax
83
cmake_minimum_required(VERSION <version>)
3. Example
cmake_minimum_required(VERSION 3.10)
In this example, the project will require at least CMake version 3.10. If CMake is run with
an older version, an error will occur, and the build process will not proceed. This is
important to ensure that your CMakeLists.txt file uses only features and commands that
are supported by the specified version or later.
• Compatibility: It guarantees that your CMakeLists.txt file will run on systems with
a version of CMake that supports all the commands used in the script. If the
minimum version is not specified, CMake will assume that any version of CMake is
valid, which could lead to compatibility issues.
• Error Prevention: By specifying the minimum required version, you prevent
unexpected errors related to incompatible features and behaviors that may be
introduced in future versions of CMake.
• Clarity: It provides clear documentation about the version of CMake needed to
build the project, which helps anyone working with the project (especially in a team
or open-source context) understand which version of CMake is compatible with the
project.
84
2.3.2 project
1. Purpose
The project command is used to define the project's name, version, and the
programming languages that the project uses. This command essentially declares the
project's identity and tells CMake how to configure the build process accordingly. It is
typically one of the first commands in a CMakeLists.txt file after the
cmake minimum required command.
2. Syntax
• <name>: The name of the project. This is the primary identifier for the project and
is often used to define the output executable or library names.
3. Example
In this example, we define a project named MyProject, with a version of 1.0, and we
specify that the project uses the C++ programming language (CXX).
• Project Name: The name specified in the project command is stored in the
PROJECT NAME variable, and this name is used throughout the project
configuration process. For example, the output executables and libraries will often
take the project name as part of their default names.
• Project Version: The version is stored in the PROJECT VERSION variable and can
be used for version-specific logic in the CMakeLists.txt file, such as selecting
different compiler flags, dependencies, or features based on the version of the
project.
The add executable command is used to define an executable target for your project.
This is the primary command for specifying the compilation of a source file or set of
source files into an executable program. It ties together the source code and tells CMake to
generate the corresponding binary after compilation.
This command can be thought of as the key step in creating an application or a runnable
program. It is typically followed by additional configuration to link libraries or specify
86
2. Syntax
• <name>: The name of the executable that will be generated. This name will be the
resulting file’s name (on Linux or macOS, the executable will not have an extension,
but on Windows, it will have a .exe extension).
• [source1] [source2] ...: A list of source files that will be compiled into
the executable. These can be C++ source files (.cpp), header files (.h), or other
files needed for the build process.
3. Example
In this example, CMake will compile the source files main.cpp and utils.cpp into
an executable called MyApp. After running cmake and make (or an equivalent build
tool), an executable file named MyApp will be generated.
• Target Name: The <name> parameter in add executable defines the name of
the output executable. It is best to use a name that clearly represents the program’s
purpose.
• Source Files: The source files listed in add executable are compiled together
to produce the final binary. It is important to ensure that the correct set of files is
included for the project to build successfully.
87
• Multiple Source Files: You can list multiple source files within the
add executable command, and CMake will handle their compilation. For larger
projects, it is also common to organize source files into directories and use CMake
variables or file(GLOB ...) to automatically collect source files.
• Dependencies: After defining an executable, you will often link it to other libraries
or dependencies using the target link libraries() command. This ensures
that the executable has access to the necessary functionality provided by external
libraries.
cmake_minimum_required(VERSION 3.10)
set(CMAKE_CXX_STANDARD 17)
Explanation:
3. set(CMAKE CXX STANDARD 17): Specifies that C++17 should be used for
compiling the project.
This project can be built by creating a separate build directory, running CMake, and then
building the executable.
2.3.5 Conclusion
In this section, we examined three essential CMake commands that form the backbone of a basic
CMake project: cmake minimum required, project, and add executable.
• project defines the project’s name, version, and programming language(s), establishing
the identity of the project.
Mastering these commands is critical for any developer working with CMake. They help
structure your project and ensure that the build process is compatible with different CMake
versions and setups, making your project easier to manage and maintain.
These commands form the foundation on which more advanced CMake features—such as library
management, external dependencies, and complex configurations—are built. By understanding
these fundamental commands, you can begin creating simple CMake projects, and as your needs
grow, you can expand the configuration to accommodate more complex requirements.
89
2. Syntax
• <type>: The type of the variable (e.g., STRING, PATH, BOOL, FILEPATH).
• [FORCE]: Optional. Forces the variable to be set even if it has already been defined
in the cache.
3. Example
• User-defined configurations: If you want to allow the user to set certain variables
during configuration (e.g., through the CMake GUI or via the command line), you
can use CACHE variables. These are ideal for settings that are configurable, such as
the installation directory, path to external dependencies, or build options.
• Controlling build options: You can use CACHE variables to allow users to toggle
features (e.g., whether to enable a particular module or build type) during the CMake
configuration phase.
cmake -DMY_PROJECT_PATH="/new/path/to/project" ..
3. If you want to force a value to be set for a cache variable, even if it has already been
set, you can use the FORCE option:
2. Syntax
set(<variable> $ENV{<env_variable>})
3. Example
set(MY_LIBRARY_PATH $ENV{LIBRARY_PATH})
In this example, the CMake variable MY LIBRARY PATH will be set to the value of the
environment variable LIBRARY PATH. The value of LIBRARY PATH is typically set by
the system and contains directories where libraries are located.
• Portable builds: ENV variables help make your build system more portable across
different machines by automatically picking up paths and settings defined in the
environment.
• PATH: Contains directories for executable binaries. You can use this to find tools
like compilers.
• HOME: Represents the user's home directory and can be used for paths to
configuration files, data directories, etc.
LOCAL variables are used to define variables that exist only within the scope of the
current directory or block (such as a function() or macro()). These variables are
not visible outside of the scope in which they are defined, ensuring that they do not
interfere with other parts of the build configuration.
LOCAL variables are the default type of variable in CMake. When you use the set()
command without explicitly specifying a variable type, it creates a LOCAL variable. These
variables are temporary and are discarded once the scope in which they are defined ends.
2. Syntax
94
set(<variable> <value>)
3. Example
In this example, MY LOCAL VAR is a LOCAL variable. It is available only within the
current scope (e.g., within the CMakeLists.txt file or a specific function or block)
and cannot be accessed outside of it.
• Temporary values: Use LOCAL variables when you need to store temporary values
that will only be used within a specific part of the CMakeLists.txt file, such as
within a loop or function.
• Avoiding conflicts: Since LOCAL variables are confined to the scope in which they
are created, they help avoid conflicts with other variables defined in different parts of
the project. This is especially useful when writing functions or macros that need to
operate without altering the global state.
• Scope control: LOCAL variables provide better control over where a variable is
accessible. They don’t ”leak” into other parts of the project, which can help keep the
build configuration clean and organized.
95
• CACHE variables are used for persistent values that need to be available across multiple
CMake runs and can be modified by the user.
• ENV variables are used to access system or environment variables from the host operating
system.
• LOCAL variables are temporary and only exist within the scope in which they are defined,
making them ideal for internal values that don’t need to persist beyond the current
configuration step.
2.4.5 Conclusion
Understanding the different variable types in CMake—CACHE, ENV, and LOCAL—is essential
for effective project configuration and build management. By selecting the right variable type for
the job, you can control the scope, persistence, and visibility of configuration values in a way
that helps keep your build system clean and maintainable.
• Use CACHE for user-configurable settings that should persist across multiple CMake runs.
96
• Use ENV for environment-specific values that need to be accessed during the build
process.
• Use LOCAL for temporary values that are needed only in a specific scope.
By mastering these variable types, you can ensure that your CMake configuration is both flexible
and efficient.
97
• Debugging: Printing variable values, checking paths, or verifying conditions during the
configuration phase.
• Status Updates: Informing users or developers about the progress or state of the build
configuration.
• Warnings and Errors: Alerting users to issues that need attention or stopping the
configuration process if critical errors occur.
The message() command can output messages at different levels of severity, allowing you to
categorize the output based on its importance.
98
message([<mode>] <message>)
• <mode>
: Optional. Defines the severity level of the message. It can be one of the following:
– AUTHOR WARNING: A warning message intended only for the author (developer),
not the user. It is similar to WARNING but can be filtered out by users in a
non-interactive setup.
– SEND ERROR: Prints an error message and halts the configuration process. This is
used to indicate a critical issue that prevents further configuration.
– DEPRECATION: Used to warn the user about the use of deprecated features or
practices in the CMake configuration.
• <message>: The text or string that you want to print. This can include variables, paths,
or any other information you want to display.
99
This will print the message "This is a simple message" in the standard output
during the configuration phase.
By default, message() uses the STATUS mode, which is suitable for informational
messages that are not critical to the build process.
This will display the message "Configuring project..." in the output, typically
with a green color to denote that it's informational.
You can use WARNING to display a warning message. This is helpful when you want to
inform users of a potential issue that does not block the build process but might require
attention.
This will print a yellow-colored warning message that informs users about a possible
issue, but it won't stop the build configuration.
100
If you encounter a situation that must be addressed before proceeding with the
configuration, you can use SEND ERROR to display an error message. This will not stop
the configuration immediately but will mark the build as having an error, preventing the
generation of makefiles or build files.
This will print the error message and continue with the configuration process, but the error
will be recorded, and no build files will be generated.
If the configuration cannot proceed due to a critical error, you can use FATAL ERROR.
This will immediately stop the configuration process and prevent any further steps.
When this message is encountered, CMake will stop immediately, and no build files will
be generated. This is useful for situations where proceeding without resolving the error
would lead to a broken or incomplete build.
This will display a message indicating that a certain feature is deprecated, helping guide
users or developers toward better practices.
Using ${} allows you to reference the value of a variable and incorporate it into the
message.
You can use message() to debug variable values during the configuration process. This
is particularly useful when you want to track the values of important variables at different
points in the CMakeLists.txt file.
One way to control verbosity is by setting the CMAKE VERBOSE MAKEFILE variable.
When set to TRUE, it enables more detailed output during the build process, which can be
helpful for debugging the build steps themselves. However, this does not directly control
message() output, but it can help control the level of detail you get from the build
process.
set(CMAKE_VERBOSE_MAKEFILE TRUE)
Another way to control output visibility is by setting the CMAKE MESSAGE LOG LEVEL
variable. This determines the threshold of message severity that is displayed. You can
choose to show only errors, warnings, or detailed status messages.
set(CMAKE_MESSAGE_LOG_LEVEL "WARNING")
This would display only warnings and errors, suppressing informational messages.
Chapter 3
CMake is a powerful tool that automates the process of building and managing complex
projects. However, before the actual build process takes place, CMake goes through a few
preliminary steps: configure, generate, and build. These three distinct phases are
essential to the CMake workflow and understanding their roles is crucial for effectively
using CMake to manage your C++ projects. In this section, we will delve into each of
these steps and explore what they involve, how they relate to each other, and why they are
important.
104
105
2. Generate: After the configuration step, CMake generates the build system files.
These files are specific to the generator you selected (such as Makefiles, Visual
Studio project files, or Xcode project files). The generated files are used by the build
tools to carry out the actual compilation and linking of the project.
3. Build: This step is where the actual compilation and linking of your project take
place. It involves invoking a build tool (like make, ninja, or the native build
system for IDEs such as Visual Studio or Xcode) to perform the build based on the
files generated in the previous step.
The configure step is the initial phase of working with CMake, and it is where CMake sets
up everything needed to generate the build files. During configuration, CMake performs
the following tasks:
(e.g., gcc, clang, or MSVC), system libraries, required tools, and other software
dependencies. If a required dependency is missing, CMake will either notify you or
attempt to download or build it.
cmake <path-to-source>
For example:
cmake ../my_project
This will trigger CMake to process the CMakeLists.txt files in the specified
directory and configure the project for the current system. Once complete, CMake
will have generated the necessary build system files for the next step.
Once the configuration step is complete, the next phase is the generate step. In this phase,
CMake generates the files required by the build system. The generation process is
determined by the generator you select, which could be a build tool or IDE-specific file
format.
CMake supports several types of generators, such as:
• Makefiles: This is the most common generator for Linux and macOS environments.
It produces a Makefile that can be used with the make tool to compile and link
the project.
• Ninja: A small, fast build system that is an alternative to make. If you specify -G
Ninja, CMake will generate build.ninja files for use with the ninja build
tool.
• IDE-Specific Generators: These are used to generate project files for various IDEs
like Visual Studio, Xcode, or CodeBlocks. For example, on Windows, running
cmake -G "Visual Studio 16 2019" .. will generate Visual Studio
project files that you can open directly in the IDE.
• Unix Makefiles: These are the default generator on many Unix-like systems,
producing a set of Makefile scripts that can be used to invoke make.
The generation step is invoked automatically as part of the configuration phase when you
run the CMake command. For example:
This command will configure and then generate Makefile build files in the build
directory.
After the configuration and generation phases are complete, you move to the build step.
This is the phase in which the actual compilation, linking, and final build of your project
occur.
During this step, the build tool (such as make, ninja, or Visual Studio) uses the files
generated in the previous phase to build the project. The build tool will execute the
instructions specified in the generated files to compile the source code, link the object files
into executables, and create libraries as defined in the CMakeLists.txt file.
make
• With Ninja: If you used the Ninja generator, you would use the ninja tool
to build the project:
109
ninja
• With IDEs (e.g., Visual Studio): If you generated project files for an IDE like
Visual Studio, you can build the project directly from within the IDE interface
or use the command line:
msbuild MyProject.sln
make install
This will install the project if you have set up the installation rules in your
CMakeLists.txt file using commands like install().
While the configure, generate, and build steps are distinct, they are interdependent and
occur in sequence:
1. Configure: Set up the project, inspect the system, define variables, and check
dependencies.
2. Generate: Create the necessary build files (such as Makefile, ninja, or
IDE-specific files) based on the configuration.
3. Build: Use the generated build files to compile and link the project into executables
or libraries.
110
It’s important to note that the configure step often only needs to be run once unless you
make changes to the configuration (such as adding new source files, changing build
options, or modifying dependencies). The generate step is run after configuration to
generate the appropriate build system files, and the build step can be run multiple times
during the development cycle, especially when making incremental changes to the project.
If you need to modify your build configuration (for example, to change compiler flags or
enable/disable features), you can re-run the configure and generate steps. When this
happens, CMake will read the configuration files again, update the cache, and regenerate
the build system files.
Sometimes, changes to the CMakeLists.txt files or other source files will require
cleaning the build directory before re-running the configuration. CMake supports
incremental builds, but certain changes might require a fresh configuration.
cmake ../my_project
This will reconfigure the project. If the configuration or generator has changed, CMake
will regenerate the necessary files.
3.1.8 Conclusion
Understanding the three key steps of the CMake workflow—configure, generate, and
build—is critical for efficiently managing and building C++ projects with CMake. These
steps are interdependent and serve distinct purposes in the overall process:
By following this workflow, you can efficiently manage complex builds, handle
dependencies, and customize your project setup according to your system and
development environment.
112
CMake supports a variety of generators that allow you to configure and generate build
files for different platforms, build systems, and Integrated Development Environments
(IDEs). These generators define the type of build system that CMake will create for your
project. Understanding how to run cmake with different generators—such as Ninja,
Makefile, and Visual Studio—is essential for customizing your build process to suit your
development environment.
In this section, we will explore how to use CMake with different generators and how they
affect the project setup and build process. We'll walk through the specifics of working
with each of these popular build systems, explaining their strengths and providing
practical examples.
When you run CMake, one of the key options you specify is the generator. The generator
determines what kind of build files CMake will produce. Each generator corresponds to a
specific build system or IDE, and selecting the right one ensures that CMake can interact
seamlessly with your development environment.
Some of the most common generators include:
These generators offer flexibility in terms of performance, platform compatibility, and user
preference. Let’s dive into how to configure CMake to use each of these generators and
the scenarios in which they are most useful.
Ninja is a small, fast build system with a focus on performance. It is often used in
environments where speed is important and works especially well for large projects. Ninja
operates by processing small build files that contain just enough information to trigger the
necessary build steps, making it significantly faster than traditional build systems in many
cases.
• Fast: Ninja is known for its speed in incremental builds. It minimizes the work
done by only rebuilding parts of the project that have changed.
• Minimalistic: Unlike other build systems that generate large build files, Ninja
generates concise and efficient build files, resulting in reduced I/O and faster
execution.
• Cross-Platform: Ninja can be used on multiple platforms (Linux, macOS, and
Windows), making it a great choice for cross-platform projects.
This command tells CMake to generate Ninja build files in the build directory, based
on the source code in the specified directory. After configuration, you can then use
Ninja to build the project:
ninja
mkdir build
cd build
cmake -G Ninja ../my_app
ninja
This sequence of commands will configure the project, generate the Ninja build files,
and then execute the build process.
After CMake generates the necessary Makefiles, you can build the project using the
make command:
make
mkdir build
cd build
cmake -G "Unix Makefiles" ../my_app
make
This will configure the project, generate the Makefile, and compile the project using
the make tool.
116
1. Visual Studio is one of the most widely used IDEs for C++ development,
particularly on Windows. CMake provides generators for multiple versions of Visual
Studio, which allow you to create Visual Studio project files (e.g., .sln,
.vcxproj) for your project. This is particularly useful for developers who prefer
the Visual Studio environment for building and debugging C++ projects.
2. Why Choose Visual Studio?
• IDE Support: Visual Studio is a powerful IDE that provides an extensive set of
features such as debugging, profiling, and an intuitive graphical interface for
project management.
• Native Windows Development: Visual Studio is the standard development
environment for C++ on Windows, making it ideal for targeting
Windows-specific APIs and libraries.
• Advanced Features: Visual Studio provides features like IntelliSense, a visual
debugger, and integrated testing tools that improve productivity.
This will create .sln files that can be opened directly in Visual Studio. After the
project is generated, you can open the .sln file in Visual Studio and build the
project from the IDE.
4. Example: Using Visual Studio with a Project
117
Let’s say you have a project located in C:\projects\my app. To configure and
generate Visual Studio project files, use:
mkdir build
cd build
cmake -G "Visual Studio 16 2019" C:\projects\my_app
This will generate a Visual Studio solution file (e.g., my app.sln) in the build
directory. You can then open this .sln file in Visual Studio and build the project.
Choosing the correct generator depends on your specific requirements and development
environment. Here are some guidelines to help you decide which generator to use:
• Ninja: Ideal for fast, efficient builds, especially in large projects. It’s a good choice
if you prioritize build speed and want a cross-platform solution.
• Makefile: Best for traditional Unix-based systems (Linux, macOS). It’s the default
for many open-source projects and is widely supported.
• Visual Studio: Perfect for Windows-based development using the Visual Studio IDE.
If you need to work in a Microsoft-centric development environment, generating
Visual Studio project files is the way to go.
Additionally, consider the complexity of your project. For simple, small projects, any
generator will work fine. For large projects with complex dependencies or custom build
steps, you might prefer Ninja or Makefile due to their simplicity and speed. Visual Studio
is best suited for projects that benefit from deep IDE integration, such as debugging or
visual design tools.
118
3.2.6 Conclusion
• Ninja offers fast and efficient builds, making it ideal for large projects.
• Makefile is the traditional choice for Unix-based systems and offers flexibility and
control over the build process.
• Visual Studio is a powerful IDE for Windows development and integrates
seamlessly with CMake for generating project files.
By selecting the appropriate generator, you can streamline the build process and integrate
CMake more effectively into your existing workflow. Whether you are developing on a
Linux, macOS, or Windows platform, CMake provides the tools you need to manage and
build your C++ projects efficiently.
119
When working with CMake, understanding how to effectively manage source files and
define output executables is a critical part of setting up your build process. CMake
simplifies this task by providing clear, intuitive mechanisms to specify the organization of
source files and control where the final executables and libraries are placed. This section
will explain how to manage source files and control output executables in a CMake
project, covering basic commands like add executable(), add library(), and
the use of source groups.
Source files are the building blocks of your project—they contain the code that will be
compiled into the final executable or library. CMake offers a variety of ways to manage
these files, from defining them explicitly to leveraging wildcard patterns and automatic file
discovery. Whether your project has a simple structure or a more complex, multi-directory
setup, CMake provides tools to help organize and include source files efficiently.
CMake supports different types of source files for various build targets:
The key here is to specify the source files correctly to ensure that they are compiled into
the appropriate output.
120
The most fundamental operation in CMake is defining the source files that will be
compiled into an executable. This is achieved using the add executable() command.
The general syntax is:
• <source1>, <source2>, ... <sourceN>: The list of source files (e.g., .cpp
files) that will be compiled to create the executable.
main.cpp
foo.cpp
foo.h
This tells CMake to create an executable named my app from the source files
main.cpp and foo.cpp. After running the build process, the output will be an
executable file named my app (or my app.exe on Windows).
121
For larger projects with multiple directories, you might want to automate the process of
collecting source files. CMake provides the file() and aux source directory()
commands to facilitate this.
aux_source_directory(src SOURCES)
add_executable(my_app ${SOURCES})
This command will search the src directory for all .cpp files and add them to the
SOURCES variable. The executable my app will then be built from all the source
files found in the src directory.
2. Using file(GLOB ...)
Alternatively, you can use file(GLOB ...) to collect all files matching a
specific pattern, such as all .cpp files in a given directory:
122
The file(GLOB ...) command is useful when you have files in a directory that
follow a specific naming pattern. However, it’s generally recommended to avoid
overusing this command, as it can make the build system harder to maintain when
files are added or removed.
Once you have specified the source files for your project, you need to define where the
resulting executable should be placed. CMake provides various commands and options to
control this.
This sets the output directory for the executable my app to a subdirectory called
bin inside the build directory.
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
This will result in my app and my app2 being placed in the bin directory inside
your build folder, making it easier to manage the outputs of your project.
Many projects may require the creation of multiple executables from different sets of
source files. In CMake, each executable or library you define is treated as a target, and
you can manage them separately.
Each target is built independently, and CMake will ensure that all source files are
compiled correctly and linked into the appropriate executables.
my_project/
CMakeLists.txt
bin/
CMakeLists.txt
my_app.cpp
my_app2.cpp
lib/
libfoo.cpp
add_subdirectory(bin)
add_executable(my_app my_app.cpp)
add_executable(my_app2 my_app2.cpp)
This modular structure helps keep the build process organized, especially when
dealing with large projects.
In addition to executables, CMake also makes it easy to define libraries. Libraries are
reusable collections of code that can be linked with other projects or executables.
target_link_libraries(my_app my_lib)
This links the static or shared library my lib to the executable my app.
3.3.7 Conclusion
Managing source files and output executables in CMake is an essential part of setting up a
successful build system. By using commands like add executable(),
add library(), and target link libraries(), you can define which source
files to compile, where to place your output executables, and how to manage libraries
within your project. Additionally, CMake offers powerful tools like file() and
126
Once you’ve configured your CMake project and generated the appropriate build files, the
next step is to actually build and install your project. These tasks are essential to the
process of turning your source code into a usable application or library. CMake simplifies
the build and install process with the cmake --build and cmake --install
commands, respectively.
In this section, we’ll delve into how to run these commands, what they do, and how they
fit into the broader CMake workflow.
The cmake --build command is used to compile and link the project, essentially
performing the build step. This command simplifies the process by abstracting away the
complexities of interacting directly with the build system (such as make, ninja, or
MSBuild), and it ensures consistency across different platforms and environments.
• [options]: These are optional flags that you can pass to modify the build
process (such as building a specific target or specifying the number of parallel
jobs).
2. Running cmake --build Without Arguments
In its simplest form, you can run cmake --build without any additional
arguments to build the default target (typically the project’s primary executable or
library):
This command will invoke the correct build system for your platform (e.g., make,
ninja, or MSBuild) and will build the default target. If you are in the build
directory, you can simply run:
cmake --build .
CMake will handle determining the correct tool and invoking it with the necessary
arguments to perform the build.
3. Specifying Build Targets
You can specify which target you want to build by using the --target option.
This is useful if you want to build a specific target, such as a particular executable or
library, rather than the default target.
For example, to build a specific executable (e.g., my app), you would run:
This command will only build the my app target, skipping the other targets in the
project. This is particularly useful in larger projects with multiple components, as it
allows you to build only the necessary parts of the project.
129
This tells the build system to use 4 processors to compile the project in parallel.
This removes the object files and other intermediate files, but leaves the
CMake-generated files (such as Makefiles or Visual Studio project files) intact.
After building the project, the next step is typically to install it. The cmake
--install command allows you to copy the built files (executables, libraries, headers,
etc.) to their final locations on the system, making them ready for use or distribution. This
step is crucial when you want to deploy your project or make it available for other
software to link to.
130
cmake -DCMAKE_INSTALL_PREFIX=/path/to/install/directory
,→ <path-to-source>
Alternatively, you can specify the installation directory during the cmake
--install command itself:
131
This will copy the necessary files (such as executables, libraries, and headers) from
the build directory to the installation directory defined by
CMAKE INSTALL PREFIX.
This installs only the my app executable, not any other components.
CMake allows you to define custom installation rules for specific files or directories using
the install() command within your CMakeLists.txt. This command provides
flexibility in deciding what gets installed and where.
For example, if you want to install an executable and a library, you can define the
following in your CMakeLists.txt:
3.4.4 Conclusion
The cmake --build and cmake --install commands are key components of the
CMake build process.
133
• cmake --build compiles and links the project, invoking the underlying build
system (such as make, ninja, or MSBuild) to produce the final executables,
libraries, or other targets.
• cmake --install copies the built project files to a specified installation
directory, making them ready for use or distribution.
By understanding how these commands work and how to configure them for your needs,
you can effectively manage the build and installation of your CMake-based projects.
Whether you're working on a small application or a large library, CMake provides a
flexible and consistent way to build and install your software across multiple platforms.
134
CMake provides a powerful mechanism for controlling the behavior of the build process
through various configuration options, one of the most crucial being
CMAKE BUILD TYPE. This variable determines the type of build configuration you want
to generate, such as a debug build, release build, or a custom build type. This section will
explore how to effectively control build options using CMAKE BUILD TYPE, the impact
of different build types, and how to customize and fine-tune the configuration of your
builds.
The CMAKE BUILD TYPE variable is one of the primary ways to control how your
project is built. It specifies the type of build configuration that CMake should use when
generating the build system. Typically, this is a setting you configure before you generate
your build files, and it can affect several important aspects of the build, such as
optimization levels, debugging information, and compiler flags.
CMake supports several common build types, each of which comes with different
optimizations and debugging settings. The most commonly used build types are:
135
• Debug: This build type generates debugging information and disables optimizations
to help with debugging. It's suitable when you need to inspect your code in a
debugger or need detailed information about your program’s state during execution.
• MinSizeRel: This build type is focused on minimizing the size of the compiled
binary while still providing optimizations. It’s often used for embedded systems or
other scenarios where small binaries are essential.
To set the build type in CMake, you can define CMAKE BUILD TYPE during the
configuration phase. This is typically done from the command line when you run the
cmake command to generate the build files.
136
1. Basic Example
For example, to configure the build for a Debug build type, you can run:
This command tells CMake to configure the project for debugging, meaning it will
generate the appropriate build system with debugging flags and without
optimizations.
Similarly, to generate a Release build type, you would use:
This will configure the project for release, ensuring that compiler optimizations are
enabled and debugging symbols are removed.
Each of these will configure CMake to use the corresponding set of compiler flags
and options.
Setting the CMAKE BUILD TYPE variable not only determines the compiler flags for
optimization and debugging but also influences several other aspects of the build:
137
• Compiler Options: The compiler flags vary between build types, such as
optimizations (-O3), debugging symbols (-g), and additional runtime checks (e.g.,
-fsanitize=address for sanitizers).
• Linker Flags: Depending on the build type, CMake may add different linker options.
For example, a release build might include additional flags to strip debugging
information or reduce the size of the final executable.
• Debugging Symbols: In the Debug build type, CMake ensures that debugging
symbols are included, which are necessary for debugging tools like gdb or lldb.
In contrast, in Release builds, debugging symbols are typically omitted to
improve performance and reduce the size of the binary.
• Optimization: Release builds enable higher levels of optimization, leading to faster
execution times, while debug builds avoid optimization to make stepping through
code easier in a debugger. RelWithDebInfo provides a middle ground by
optimizing the code while still including some debug information.
• Conditional Code: Some code in the project might only be included in specific
build types. For example, CMake allows conditional inclusion of certain code
depending on the build type, using constructs like if(CMAKE BUILD TYPE
MATCHES "Debug").
specify the build type during the configuration step with Visual Studio because the IDE
handles it dynamically.
When generating build files for Visual Studio, you do not need to specify the build type at
the configuration step:
After running this command, you can open the generated .sln file in Visual Studio and
select the build configuration (Debug, Release, etc.) from the IDE’s build settings.
Similarly, with Xcode, you can specify the configuration directly within Xcode once the
project has been generated.
CMake also allows you to customize build types by defining your own custom
configurations. For example, you may want to create a special configuration that combines
optimizations and additional warnings or debugging checks for a particular use case.
To do this, you can define custom build types by adding to the CMAKE CXX FLAGS
variable in your CMakeLists.txt. For example:
Then, when running the configuration, you can specify this custom type:
139
This approach provides flexibility, especially in complex projects or when dealing with
specialized requirements like performance profiling or testing.
• CMAKE CXX FLAGS DEBUG: This variable allows you to add or modify flags
specifically for the Debug build type.
• CMAKE CXX FLAGS RELEASE: This variable lets you adjust flags for the Release
build type.
• CMAKE CXX FLAGS RELWITHDEBINFO: Adjusts flags for the RelWithDebInfo
build type.
• CMAKE CXX FLAGS MINSIZEREL: Used to modify flags for the MinSizeRel
build type.
For example, you might want to modify the compiler flags to add more strict debugging or
security checks for the Debug build:
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG}
,→ -fsanitize=address")
This will enable AddressSanitizer in the debug configuration, helping to catch memory
errors.
140
3.5.7 Conclusion
CMAKE BUILD TYPE is a powerful tool in CMake for controlling the type of build you
generate, whether you're working on a development/debugging phase or preparing for a
production release. By setting CMAKE BUILD TYPE, you can adjust compiler and linker
flags, optimization levels, and debugging information to match the needs of your project.
For single-configuration generators, setting the CMAKE BUILD TYPE is crucial for
customizing the build behavior. For multi-configuration generators like Visual Studio and
Xcode, this step is less critical, as these IDEs let you select the build configuration during
the actual build process. By combining CMAKE BUILD TYPE with other CMake features
like custom flags and multi-stage builds, you can fine-tune your project’s build process
and make it more efficient and easier to maintain.
Chapter 4
In CMake, when building C++ projects, libraries play an essential role in modularizing the
code, making it reusable, and simplifying the build process. These libraries can be either
static libraries or shared libraries, and understanding the differences between them is
key to managing dependencies and ensuring the correct build setup for your project. This
section dives into the key differences between static and shared libraries, their use cases,
and how they affect the build process.
4.1.1 Definition
141
142
copies the object code from the static library directly into the executable. As a result,
the executable does not need the static library at runtime because all the necessary
code is included within the executable itself.
• Shared Libraries: Also known as dynamic libraries or DLLs (Dynamic Link
Libraries) on Windows, shared libraries contain code that is linked at runtime
rather than during the compile time. The executable or other libraries that use the
shared library will dynamically load it during execution. This means the executable
depends on the shared library being present in the system at runtime to function
correctly.
• Static Libraries:
– The static library is compiled from the source code into object files. These
object files are then bundled together into a single library file, typically with a
.a extension on Unix-like systems and .lib on Windows.
– During linking, the entire library is copied into the executable. This process
results in larger executable files but does not require the library to be available
during runtime.
– Static libraries are commonly used for applications that require all dependencies
to be packaged into a single executable.
• Shared Libraries:
– A shared library is compiled and linked in a way that only the symbol
information (function names, data structures, etc.) is placed in the executable.
The actual code for these functions is placed in the shared library itself.
– The executable is not directly linked to the code but to the dynamic library file.
The linking to the library happens at runtime when the program is executed.
143
1. Static Libraries
Advantages:
Disadvantages:
• Larger Executables: The final executable file size is larger because it includes
all the code from the static libraries.
• No Shared Memory: Each running instance of the application gets its own
copy of the static library code, leading to higher memory usage when the
program is running.
• Updates Require Rebuilding: If the library code is updated, all executables
that use it must be recompiled and redistributed.
2. Shared Libraries
Advantages:
• Shared Memory: Multiple running instances of a program can share the same
loaded version of a shared library, which reduces overall memory usage.
• Easier Updates: When a shared library is updated, all executables that depend
on it will benefit from the update immediately without needing recompilation.
Disadvantages:
• Static Libraries:
– Ideal for standalone applications that do not need to rely on external libraries
at runtime.
– Useful in embedded systems, where minimizing external dependencies is
crucial.
– Used when you want to distribute a single file containing all necessary
components, such as in a proprietary application or for performance-sensitive
applications.
• Shared Libraries:
146
In CMake, you can specify whether to create a static or shared library using the
add library() command. Here’s how you would do that:
• Static Library:
• Shared Library:
You can also specify different build types (Release, Debug) and link libraries conditionally
based on the type of build being performed.
4.1.7 Conclusion
Choosing between static and shared libraries depends on the specific requirements of your
project. Static libraries are useful for reducing external dependencies, making the build
147
simpler, and ensuring that the application is completely self-contained. Shared libraries,
on the other hand, are more efficient in terms of memory and disk space, especially when
the same code is used across multiple applications or processes.
In CMake, it is straightforward to configure either type of library, but it is important to
understand the implications of your choice to make informed decisions about the
architecture of your project.
148
In this section, we will explore how to create a static library in CMake. A static library is
a collection of object files bundled together into a single file. This file can be linked into
executables at compile-time, resulting in larger executables but ensuring that no external
dependencies are required at runtime.
Creating a static library in CMake is simple, but understanding the process and
configuration is crucial to ensure it integrates seamlessly into your build system. This
section will cover the add library() command in CMake, the steps to create a static
library, and the typical workflow involved in using static libraries within a C++ project.
In CMake, the add library() command is used to create libraries, and the STATIC
keyword specifies that the library will be a static library.
Where:
• <library name>: The name you want to give to your library (e.g., MyLib).
• STATIC: Specifies that the library is a static library.
• <source files>: The list of source files to include in the library, such as .cpp
files.
For example, to create a static library MyLib from the source files my lib.cpp and
my lib utils.cpp, you would write:
Let’s walk through an example to create a static library MyLib in a CMake project:
MyProject/
CMakeLists.txt
src/
CMakeLists.txt
my_lib.cpp
my_lib_utils.cpp
main.cpp
150
cmake_minimum_required(VERSION 3.10)
project(MyProject)
4. src/my lib.cpp: This is the main source file for the static library.
// my_lib.cpp
#include "my_lib.h"
void MyLib::doSomething() {
// Implement some functionality here
}
5. src/my lib utils.cpp: Another source file that adds utility functions to the
library.
151
// my_lib_utils.cpp
#include "my_lib_utils.h"
6. main.cpp: The main executable that links to the static library MyLib.
#include <iostream>
#include "my_lib.h"
#include "my_lib_utils.h"
int main() {
MyLib lib;
lib.doSomething();
MyLibUtils utils;
std::cout << "Sum: " << utils.add(3, 4) << std::endl;
return 0;
}
7. Building the Project: After configuring CMake, you can build the project:
mkdir build
cd build
cmake ..
make
This process will compile the static library and link it to the main executable.
152
Once the static library is created, you need to link it to the executable that depends on it.
In CMake, this is done using the target link libraries() command.
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # Link the static library
This will ensure that MyApp links against the static library MyLib during the linking
phase, incorporating the object files from MyLib into the final executable.
When creating a static library, you often want to make sure that the header files used by
the library are accessible to other parts of the project. The
target include directories() command is used to specify the include
directories for the library.
• PUBLIC: This keyword makes the include directory available both to the static
library and to any target that links against the library (like MyApp).
• PRIVATE: If the include directory is only needed internally by the static library, you
can use PRIVATE instead.
153
If your static library has dependencies on other libraries (e.g., third-party libraries), you
need to link those libraries within your CMake configuration.
For example, if MyLib depends on another library OtherLib, you would modify the
CMake configuration as follows:
This ensures that when MyLib is linked to an executable or another library, the
OtherLib will also be included as a dependency.
Instead of hardcoding the list of source files, you can use CMake variables to collect them,
especially if you have a large number of files or want to avoid manual updates.
For example:
set(SOURCES
my_lib.cpp
my_lib_utils.cpp
)
This approach is useful when organizing larger projects with multiple source files.
If you want to install the static library for use outside the current project, you can use the
install() command. This is often done to create a CMake package for others to use.
For example:
This will install the MyLib static library to the lib directory and the headers to the
include directory.
• Larger Executables: The executable becomes larger since it includes all the
necessary object files from the static library.
155
• No Shared Memory: Each running instance of the program gets its own copy of the
library code, leading to higher memory consumption.
• Rebuild Required: If the static library is updated, you need to rebuild and
redistribute all executables that depend on it.
4.2.11 Conclusion
Creating a static library in CMake is a straightforward process that involves using the
add library() command with the STATIC keyword. Static libraries are a great
choice when you want self-contained executables and don’t mind the larger file sizes.
They are particularly useful for smaller applications or when you want to avoid runtime
dependencies. By following the steps in this section, you’ll be able to create, link, and
manage static libraries effectively in your C++ projects with CMake.
156
In this section, we will explore the process of creating a shared library in CMake, which
is also referred to as a dynamic library. Shared libraries differ from static libraries in that
they are linked at runtime, rather than being statically included within the executable.
This allows for smaller executables, easier updates, and shared code across different
applications, but it also introduces some challenges, such as dependency management at
runtime.
This section will guide you through the creation of shared libraries using CMake, explain
the key differences between static and shared libraries, and provide best practices for
managing and linking shared libraries in CMake projects.
A shared library (also called a dynamic library) is a collection of object files that are
linked at runtime. When an executable or another shared library is linked to a shared
library, the actual code is not included in the executable. Instead, a reference is created,
and the code from the shared library is loaded when the application is run.
• On Unix-like systems (Linux, macOS), shared libraries typically have .so (Shared
Object) extensions.
The key advantage of using shared libraries is that they can be shared between multiple
programs or processes, which helps reduce memory usage and the overall size of
executables. Additionally, shared libraries can be updated independently without requiring
157
the applications that use them to be recompiled, as long as the interface remains
compatible.
In CMake, shared libraries are created using the add library() command with the
SHARED keyword.
Where:
• <library name>: The name of the shared library you want to create (e.g.,
MyLib).
For example, to create a shared library MyLib from the source files my lib.cpp and
my lib utils.cpp, you would use:
MyProject/
CMakeLists.txt
src/
CMakeLists.txt
my_lib.cpp
my_lib_utils.cpp
main.cpp
cmake_minimum_required(VERSION 3.10)
project(MyProject)
4. src/my lib.cpp: This is the main source file for the shared library.
// my_lib.cpp
#include "my_lib.h"
void MyLib::doSomething() {
// Implement some functionality here
}
5. src/my lib utils.cpp: Another source file that adds utility functions to the
library.
// my_lib_utils.cpp
#include "my_lib_utils.h"
6. main.cpp: The main executable that will link to the shared library MyLib.
#include <iostream>
#include "my_lib.h"
#include "my_lib_utils.h"
160
int main() {
MyLib lib;
lib.doSomething();
MyLibUtils utils;
std::cout << "Sum: " << utils.add(3, 4) << std::endl;
return 0;
}
7. Building the Project: After configuring the CMake project, you can build it using
the following commands:
mkdir build
cd build
cmake ..
make
This process will compile the shared library MyLib, create the corresponding shared
object (.so or .dll), and link it to the executable MyApp.
Once the shared library is created, you need to link it to the executable that depends on it.
This is done using the target link libraries() command in CMake.
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # Link the shared library
This command tells CMake to link the MyApp executable with the MyLib shared library
during the linking phase.
Since shared libraries are loaded at runtime, you must ensure that the shared library is
available to the executable when it runs. This is typically managed using RPATH
(runtime library search path), which tells the system where to look for shared libraries.
You can configure RPATH in CMake with the following commands:
• $ORIGIN means that the library will be searched for in the directory where the
executable is located.
• Alternatively, you can specify an absolute path or relative path to the shared library.
When installing the shared library, CMake can also set the proper install paths for the
shared library and the executable:
This ensures that the shared library is placed in the correct lib directory, and the
executable is placed in the bin directory.
162
As with static libraries, you can use target include directories() to specify
the include directories for the shared library:
The PUBLIC keyword indicates that this include directory is necessary not only for the
library itself but also for any executable or library that links to it.
Shared libraries often require versioning to ensure that applications can link to a specific
version of the library. To manage versioned shared libraries in CMake, you can use the
following syntax:
set_target_properties(MyLib PROPERTIES
VERSION 1.0.0
SOVERSION 1
)
• SOVERSION: Specifies the API version of the shared library. This version is used
to ensure compatibility between the library and the applications that use it.
163
When building the shared library, CMake will automatically append the version and
SOVERSION to the library filename (e.g., libMyLib.so.1.0.0 or
libMyLib.so.1), which can help avoid version conflicts.
• Smaller Executables: The executable remains small because the shared library code
is not embedded within the executable.
• Memory Efficiency: Multiple running applications can share the same instance of
the shared library in memory, reducing memory usage.
• Easier Updates: You can update the shared library independently of the applications
that use it, as long as the interface remains backward compatible.
• Slightly Slower Startup: Shared libraries are loaded at runtime, which can result in
a slight delay in application startup.
164
4.3.10 Conclusion
Creating a shared library in CMake is a powerful way to modularize your C++ projects,
reduce the size of executables, and promote code reuse across multiple applications. By
using the add library(MyLib SHARED) command, you can easily create shared
libraries, link them to executables, and manage dependencies efficiently.
However, as with any dynamic linking, shared libraries introduce challenges such as
dependency management and versioning, which need to be carefully managed to ensure
the stability of your application. With proper setup and configuration, shared libraries can
significantly enhance the maintainability and scalability of your C++ projects.
165
Linking libraries is a fundamental concept when building C++ projects using CMake. The
target link libraries() command in CMake is used to specify which libraries
(static or shared) an executable or another library should be linked with. Proper linking
ensures that all necessary code from external libraries (whether static or shared) is
incorporated into the final executable or library.
In this section, we will explore the target link libraries() command in detail,
how it works with static and shared libraries, and provide practical examples to
demonstrate how to manage and link libraries effectively in your CMake project.
When you create a library or executable in CMake, it typically relies on external libraries
to provide functionality that isn't part of the C++ standard library. These external libraries
could be static libraries (which are compiled into the executable at compile time) or
shared libraries (which are linked at runtime). The target link libraries()
command is used to link an executable or library to one or more of these external libraries.
target_link_libraries(<target> <libraries>)
Where:
• <target>: The name of the target (either an executable or another library) that
you want to link the libraries to.
166
• <libraries>: The libraries that the target should be linked with. This can be a
single library or a list of libraries.
For example:
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)
This command tells CMake to link the MyApp executable with the library MyLib.
When linking static libraries, CMake will ensure that the object files from the static library
are copied into the final executable during the linking phase. Static libraries are included
in the executable at compile-time, making the executable self-contained.
Here’s how to link a static library to an executable using
target link libraries():
# Create an executable
add_executable(MyApp main.cpp)
In this case, MyApp depends on MyLib, and the MyLib library will be included directly
into the final executable. The PRIVATE keyword indicates that MyLib is needed only for
MyApp and does not need to be propagated to other targets that depend on MyApp.
167
When linking shared libraries, the executable or library doesn’t include the shared library's
object files at compile time. Instead, a reference to the shared library is included, and the
actual code is loaded into memory when the program runs (at runtime). This means the
shared library must be available when the program starts.
# Create an executable
add_executable(MyApp main.cpp)
In this case, MyApp will not contain the code from MyLib directly, but it will rely on the
shared library at runtime. The shared library (MyLib.so, .dll, or .dylib) must be
found in the system’s library search paths when the executable is run.
The target link libraries() command can also be used to specify a target’s
dependencies on other libraries. These dependencies can either be private, public, or
interface:
• PRIVATE: The library is only required for the target itself. Other targets that link to
this target do not need to know about this library.
168
• PUBLIC: The library is required for the target, and any other target that links to this
target will also need to link to this library.
• INTERFACE: The library is not required for the target itself, but any target that
links to this target will need to link to the library.
# Create an executable
add_executable(MyApp main.cpp)
• MyLib is linked only for MyApp and won’t propagate to any target that depends on
MyApp (via the PRIVATE keyword).
• OtherLib is required both for MyApp and for any target that links against MyApp
(via the PUBLIC keyword).
169
You can link multiple libraries to a target at once. In this case, simply list the libraries
separated by spaces:
# Create an executable
add_executable(MyApp main.cpp)
In this example, MyApp is linked with three libraries: MyLib, OtherLib, and
AnotherLib.
In addition to linking libraries that are part of your project, you may need to link to system
libraries or third-party libraries installed on your system (such as pthread, zlib, or
boost). These libraries can be linked in the same way:
You can also use find package() to find installed libraries, such as Boost, and then
link them to your target:
find_package(Boost REQUIRED)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Boost::Boost)
170
In this case, CMake will search for the Boost library on your system and link it to MyApp.
When a target is linked to another target that itself has dependencies, CMake will
automatically propagate those dependencies, provided the correct visibility is specified.
For instance, if a shared library OtherLib is linked to MyLib, and MyLib is linked to
MyApp, the dependencies of MyLib will be propagated to MyApp if the PUBLIC or
INTERFACE keyword is used:
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib) # MyApp will also depend
,→ on OtherLib
Here, since MyLib is linked to OtherLib using PUBLIC, MyApp will also implicitly
depend on OtherLib when it links to MyLib.
In some cases, your libraries may not be located in standard directories (such as
/usr/lib or /lib). You can specify custom library paths using the
link directories() command:
171
link_directories(/path/to/custom/libs)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)
This command tells CMake to look for libraries in the specified directory when linking.
However, it’s usually better practice to use find package() for third-party libraries or
to specify the full path in target link libraries() to avoid issues with finding
libraries across different systems.
Here are some best practices to keep in mind when linking libraries with
target link libraries():
• Use find package() for system libraries: For commonly used external
libraries (such as Boost, OpenSSL, or zlib), use CMake’s find package()
functionality to automatically locate and link these libraries.
172
4.4.10 Conclusion
When working with libraries in CMake, controlling symbol visibility is crucial for
managing dependencies, improving build efficiency, and ensuring modularity. The
PUBLIC, PRIVATE, and INTERFACE keywords help define how include directories,
compile options, and linking dependencies are propagated between different targets.
Understanding these keywords allows developers to:
This section explains the role of these keywords in CMake, how they affect compilation
and linking, and provides real-world examples.
The PUBLIC, PRIVATE, and INTERFACE keywords in CMake define how compile
options, include directories, and library dependencies are applied to a target and its
consumers.
These keywords are primarily used with:
• PRIVATE
– The setting applies only to the target itself.
– It does not propagate to other targets that depend on this target.
• PUBLIC
– The setting applies to the target itself and also to any targets that link to it.
– Useful for dependencies that must be available both during compilation of the
library and when used by consumers.
• INTERFACE
– The setting applies only to targets that link to this library.
– The target itself does not use the setting.
– Useful for header-only libraries or when exposing dependencies to consumers
without affecting the library itself.
The following table summarizes how these keywords behave:
how include directories are applied to the library and its consumers.
MyProject/
CMakeLists.txt
src/
CMakeLists.txt
include/
MyLib.h
my_lib.cpp
my_lib_utils.cpp
my_lib_utils.h
main.cpp
– MyLib uses headers from include/, but these headers are not exposed to
consumers.
– If another target links against MyLib, it won’t see this include directory.
target_include_directories(MyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR}/include)
– MyLib and any target linking to MyLib will use the include/ directory.
– If MyLib.h is part of the public API, it must be accessible to consumers.
• Using INTERFACE Include Directories
target_include_directories(MyLib INTERFACE
,→ ${CMAKE_CURRENT_SOURCE_DIR}/include)
Visibility Effect
PRIVATE MyLib needs OtherLib, but consumers of MyLib don’t need it.
PUBLIC MyLib and all targets linking to MyLib also need OtherLib.
INTERFACE MyLib itself doesn’t use OtherLib, but consumers must link to it.
The target compile options() command applies compiler flags to a target and
can use visibility keywords.
# MyLib and any target linking to MyLib will be compiled with -DDEBUG
target_compile_options(MyLib PUBLIC -DDEBUG)
Visibility Effect
PRIVATE -Wall applies only to MyLib.
PUBLIC -DDEBUG applies to MyLib and its consumers.
INTERFACE -O2 applies only to consumers of MyLib.
• Use PUBLIC when the dependency is required by both the target and its
consumers (e.g., a core utility library).
4.5.7 Conclusion
As C++ projects grow in complexity, they often transition from a single-file structure to a
multi-file structure. While small projects may have a single main.cpp file containing
all logic, larger projects require modular organization to improve maintainability,
reusability, and scalability.
CMake provides robust mechanisms to structure large-scale projects effectively. A
well-organized project:
180
181
This section explores how to design and implement a multi-file project structure,
including best practices and an example project setup using CMake.
single_file_project/
CMakeLists.txt
main.cpp
main.cpp:
#include <iostream>
int main() {
std::cout << "Hello, CMake!" << std::endl;
return 0;
}
This structure is suitable for small prototypes or simple scripts, but as the codebase
grows, this monolithic file becomes difficult to manage.
182
multi_file_project/
CMakeLists.txt # Root CMake file
src/ # Source code directory
CMakeLists.txt # CMake file for source files
main.cpp # Main application entry
MyLibrary.cpp # Implementation of MyLibrary
MyLibrary.h # Header for MyLibrary
Utilities.cpp # Additional source file
include/ # Header files
MyLibrary.h
Utilities.h
lib/ # External or custom libraries
ThirdPartyLib/
CMakeLists.txt
third_party.cpp
third_party.h
183
1. Root CMakeLists.txt
At the root of the project, CMakeLists.txt sets up the project name, minimum
required CMake version, and subdirectories for modular components.
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# Add subdirectories
add_subdirectory(src)
add_subdirectory(lib/ThirdPartyLib)
Explanation:
• add executable(MyApp ...) – Creates an executable called MyApp
from main.cpp, MyLibrary.cpp, and Utilities.cpp.
• target include directories() – Ensures that headers from
include/ are available.
• target link libraries() – Links MyApp with an external library
(ThirdPartyLib).
3. lib/ThirdPartyLib/CMakeLists.txt (External Libraries)
The lib/ThirdPartyLib/ directory may contain third-party libraries or
custom-built libraries. To build it separately:
target_include_directories(ThirdPartyLib PUBLIC
,→ ${CMAKE_CURRENT_SOURCE_DIR})
2. Faster Compilation
3. Code Reusability
5. Easier Collaboration
include/MyLibrary.h # Declarations
src/MyLibrary.cpp # Implementations
• Example:
5.1.6 Conclusion
As C++ projects grow, breaking them into subprojects (or modules) improves
modularity, maintainability, and scalability. Instead of managing a large monolithic
codebase, CMake allows projects to be structured into smaller, independent subprojects,
each with its own CMake configuration.
CMake achieves this using the add subdirectory() command, which enables:
This section explores how to structure and build multi-module C++ projects using
add subdirectory() in CMake.
Syntax:
188
Example Usage
add_subdirectory(lib/MyLibrary)
MyProject/
CMakeLists.txt # Root CMake file
src/ # Main application source
CMakeLists.txt
189
main.cpp
App.cpp
App.h
lib/ # Libraries (subprojects)
MyLibrary/
CMakeLists.txt
MyLibrary.cpp
MyLibrary.h
Utils/
CMakeLists.txt
Utils.cpp
Utils.h
build/ # Build directory (created by CMake)
1. Root CMakeLists.txt
The main CMakeLists.txt should include all subprojects using
add subdirectory().
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)
# Add subprojects
add_subdirectory(lib/MyLibrary)
add_subdirectory(lib/Utils)
add_subdirectory(src)
Key Points:
• add subdirectory(lib/MyLibrary) includes MyLibrary as a
subproject.
• add subdirectory(lib/Utils) includes Utils as a subproject.
• target link libraries(MyApp PRIVATE MyLibrary Utils)
links these subprojects to the main executable.
2. lib/MyLibrary/CMakeLists.txt (Defining a Subproject)
Each subproject should define its own library and specify include directories.
# Include headers
target_include_directories(MyApp PRIVATE
,→ ${PROJECT_SOURCE_DIR}/lib/MyLibrary)
target_include_directories(MyApp PRIVATE
,→ ${PROJECT_SOURCE_DIR}/lib/Utils)
# Link libraries
target_link_libraries(MyApp PRIVATE MyLibrary Utils)
2. Modular Compilation
192
5. Reusability
If a subproject is optional, you can exclude it from the default build using
EXCLUDE FROM ALL.
add_subdirectory(lib/ExperimentalFeature EXCLUDE_FROM_ALL)
cmake -DBUILD_EXPERIMENTAL=ON ..
193
5.2.8 Conclusion
As C++ projects grow in size and complexity, they often rely on external libraries for
additional functionality. Instead of manually managing and compiling dependencies,
CMake provides the find package() command, which enables projects to
automatically locate and use prebuilt libraries.
This section explores how to use find package() to locate external libraries,
configure dependencies, and integrate them into CMake projects effectively.
Basic Syntax
Example Usage
find_package(OpenSSL REQUIRED)
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# Find OpenSSL
find_package(OpenSSL REQUIRED)
196
# Create an executable
add_executable(MyApp main.cpp)
# Link OpenSSL
target_link_libraries(MyApp PRIVATE OpenSSL::SSL OpenSSL::Crypto)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE Boost::filesystem Boost::system)
find_package(OpenSSL QUIET)
if(NOT OpenSSL_FOUND)
message(FATAL_ERROR "OpenSSL not found! Please install it.")
endif()
find_package(OpenSSL REQUIRED)
find_package(OpenSSL QUIET)
if(NOT OpenSSL_FOUND)
message(FATAL_ERROR "OpenSSL not found! Please install it.")
endif()
cmake -DCMAKE_PREFIX_PATH=/path/to/custom/install ..
5.3.8 Conclusion
The find package() command is an essential tool in CMake for managing external
dependencies efficiently. By using it, projects can automatically detect installed
libraries, link dependencies correctly, and ensure compatibility across platforms.
By following best practices, CMake projects can integrate external libraries seamlessly,
improve modularity, and reduce manual configuration overhead.
200
Advantages of FetchContent
• Simplifies build setup – Avoids the need for users to manually install libraries.
• Direct integration with CMake targets – Enables easy linking of fetched libraries.
This section explores how to use FetchContent to fetch, configure, and integrate
dependencies in a CMake project.
201
include(FetchContent)
Basic Syntax
FetchContent_Declare(
<DependencyName>
GIT_REPOSITORY <URL>
GIT_TAG <TagOrBranch>
)
FetchContent_MakeAvailable(<DependencyName>)
fmt is a popular C++ formatting library. To include fmt in a CMake project dynamically,
use FetchContent:
cmake_minimum_required(VERSION 3.14)
project(MyProject)
include(FetchContent)
# Define an executable
add_executable(MyApp main.cpp)
Explanation
from GitHub.
2. FetchContent MakeAvailable(fmt) downloads and integrates fmt into
the build.
3. target link libraries(MyApp PRIVATE fmt) links the fmt library to
the executable.
This approach ensures that fmt is automatically downloaded and built if not already
available, eliminating the need for manual installation.
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.13.0
)
FetchContent_MakeAvailable(googletest)
add_executable(MyTests test.cpp)
Key Points
This ensures that GoogleTest is always available for unit testing, without requiring users
to install it manually.
find_package(fmt QUIET)
if(NOT fmt_FOUND)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.0.0
)
FetchContent_MakeAvailable(fmt)
endif()
This approach allows the project to use a system-installed version of the library if
available, reducing unnecessary downloads.
FetchContent_Declare(
mylib
URL https://example.com/mylib.tar.gz
206
URL_HASH SHA256=abcdef1234567890
)
FetchContent_MakeAvailable(mylib)
This downloads a tarball, verifies its integrity with SHA256, and extracts it.
FetchContent_Declare(
mylib
SOURCE_DIR ${CMAKE_SOURCE_DIR}/third_party/mylib
)
FetchContent_MakeAvailable(mylib)
This allows bundling dependencies within the project instead of downloading them
from external sources.
5.4.8 Conclusion
• Ensuring that external dependencies are built before the main project.
This section explores how to use ExternalProject Add to integrate and manage
external dependencies effectively.
include(ExternalProject)
Basic Syntax
ExternalProject_Add(
<ProjectName>
GIT_REPOSITORY <URL>
GIT_TAG <TagOrBranch>
PREFIX <Directory>
CMAKE_ARGS <Arguments>
BUILD_COMMAND <BuildCommand>
INSTALL_COMMAND <InstallCommand>
)
• GIT REPOSITORY – URL of the Git repository (or URL for tarballs).
• PREFIX – The directory where the project will be downloaded and built.
cmake_minimum_required(VERSION 3.14)
project(MyProject)
include(ExternalProject)
# Define an executable
add_executable(MyApp main.cpp)
Explanation
211
ExternalProject Add provides full control over the build and installation process.
If a library needs specific configuration options, pass them using CMAKE ARGS.
ExternalProject_Add(
mylib
GIT_REPOSITORY https://github.com/example/mylib.git
GIT_TAG master
PREFIX ${CMAKE_BINARY_DIR}/mylib
CONFIGURE_COMMAND ./configure
,→ --prefix=${CMAKE_BINARY_DIR}/mylib/install
BUILD_COMMAND make -j4
INSTALL_COMMAND make install
)
Key Differences
This flexibility makes ExternalProject Add ideal for integrating projects that do
not use CMake.
If multiple external projects depend on each other, use DEPENDS to ensure correct build
order.
ExternalProject_Add(
mylib
GIT_REPOSITORY https://github.com/example/mylib.git
PREFIX ${CMAKE_BINARY_DIR}/mylib
)
ExternalProject_Add(
myapp
GIT_REPOSITORY https://github.com/example/myapp.git
PREFIX ${CMAKE_BINARY_DIR}/myapp
DEPENDS mylib
)
• Example:
ExternalProject_Add(myproj CMAKE_ARGS
,→ -DCMAKE_BUILD_TYPE=Release)
5.5.9 Conclusion
6.1.1 Introduction
When developing a C++ project with CMake, it's common to rely on external libraries to
extend functionality and avoid reinventing the wheel. Managing these dependencies
efficiently is crucial for maintainability and portability. CMake provides the
find package() command as a robust mechanism to locate and integrate installed
libraries on a system. This section delves into how find package() works, its usage
patterns, and best practices.
216
217
paths, and compile definitions required to use the library in a C++ project.
Basic Syntax
Arguments Explanation
• REQUIRED – If specified, CMake will terminate with an error if the package is not
found.
Suppose we want to use Eigen3, a popular C++ linear algebra library, in our CMake
project.
218
find_package(Eigen3 REQUIRED)
• This command searches for Eigen3 and ensures it is found before proceeding.
• If Eigen3 is not installed, CMake generates an error and stops the configuration
process.
If our project needs Eigen3 version 3.3 or later, we specify it like this:
Config-Mode (CONFIG)
• Used when the library provides its own CMake configuration files
(<PackageName>Config.cmake or
<PackageName>-config.cmake).
• Typically found in installation directories like /usr/lib/cmake/ or custom
locations.
• More reliable than Module-Mode because it contains package-specific settings.
Example:
Module-Mode (MODULE)
Example:
find_package(OpenGL REQUIRED)
• CMake will look for FindOpenGL.cmake in its module path (usually inside
CMake’s installation directory).
While this method works for many libraries, it is less preferred than Config-Mode
because it may not always align with the latest versions of the library.
find_package(Eigen3 QUIET)
if (NOT Eigen3_FOUND)
message(FATAL_ERROR "Eigen3 was not found! Please install it.")
endif()
find_package(SDL2 QUIET)
if (SDL2_FOUND)
221
Alternatively, users can provide the path at the CMake command line:
cmake -DCMAKE_PREFIX_PATH=/custom/install/path ..
Project Structure
/my_project
CMakeLists.txt
main.cpp
222
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# Locate Eigen3
find_package(Eigen3 REQUIRED)
add_executable(my_project main.cpp)
main.cpp
#include <Eigen/Dense>
#include <iostream>
int main() {
Eigen::Matrix2d mat;
mat << 1, 2,
3, 4;
std::cout << "Matrix:\n" << mat << std::endl;
return 0;
}
Build Instructions
cmake -B build
cmake --build build
223
6.1.9 Conclusion
6.2.1 Introduction
When working with CMake, managing custom libraries or dependencies within your own
projects becomes more seamless if you create and use Config.cmake files. These files
provide a standardized way to define how your library should be located, linked, and
configured within other CMake-based projects. In this section, we’ll discuss how to create
Config.cmake files for custom libraries, which simplifies the integration of your
library into other projects.
3. Define Targets
Create imported targets to represent the library, so users can link to it easily.
4. Set Variables
Define variables that CMake users can refer to in their own CMakeLists.txt.
5. Package-Specific Settings
Define specific options that may be needed to configure the package (e.g., build
settings, compile options).
Let’s consider a simple C++ library called MyLib. Below is an example of what the
MyLibConfig.cmake file might look like.
# MyLibConfig.cmake
IMPORTED_LOCATION "${MYLIB_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_INCLUDE_DIR}"
)
• Set Variables: MYLIB INCLUDE DIR and MYLIB LIBRARIES define the paths
to the library’s headers and compiled libraries, respectively.
• Define Targets: The add library() command creates an imported target
MyLib::MyLib, which can be used by other projects to link with your library.
• Set Properties: set target properties() assigns properties like the
location of the library and the include directories to the target.
• Version and Metadata: The version and MYLIB FOUND variable provide metadata
that can be checked by users.
For other CMake projects to find your library, the Config.cmake file should be
installed into a known CMake search directory. Common locations include:
The CMAKE PREFIX PATH variable is often used to specify the path where CMake
should search for the Config.cmake files.
227
cmake -DCMAKE_PREFIX_PATH=/path/to/install ..
If you are distributing the library, you might want to install the Config.cmake file into
the following location in your library’s installation process:
install(
FILES MyLibConfig.cmake
DESTINATION lib/cmake/MyLib
)
This installation ensures that when another project uses find package(MyLib
REQUIRED), CMake can locate and use the MyLibConfig.cmake file.
Once the Config.cmake file is created and installed, other projects can use the
find package() command to locate and link your library. For example, in another
project, you can include the following in your CMakeLists.txt:
find_package(MyLib REQUIRED)
add_executable(my_project main.cpp)
When find package() is called, CMake will search for the MyLibConfig.cmake
file. If the library is found, it will configure the project to use MyLib. This includes
228
setting include paths, linking to the correct library, and creating an imported target for the
library.
If your library is evolving and you want to support multiple versions, you can specify the
version in the Config.cmake file. Additionally, you can enforce version checks by
specifying a version requirement in the find package() call.
Example:
# In MyLibConfig.cmake
set(MYLIB_VERSION "1.0.0")
This approach ensures that only the correct version of the library is linked with the
consuming project.
Libraries often have optional components or features that can be enabled or disabled. You
can add configuration options in the Config.cmake file to manage these features.
229
For example, suppose MyLib has an optional feature for SSL support. You could add an
option in the Config.cmake file like this:
# In MyLibConfig.cmake
option(MYLIB_USE_SSL "Enable SSL support" ON)
if(MYLIB_USE_SSL)
target_compile_definitions(MyLib::MyLib INTERFACE USE_SSL)
endif()
When using the library, consumers can enable or disable SSL support by setting the
option:
find_package(MyLib REQUIRED)
This way, the Config.cmake file provides flexibility in how the library is configured.
Here are some best practices for creating and maintaining Config.cmake files for
custom libraries:
6.2.9 Conclusion
Creating Config.cmake files is an essential skill for developers managing custom C++
libraries. These files provide a robust and flexible interface for users of your library,
helping them integrate it seamlessly into their own CMake-based projects. By following
the outlined best practices, you can ensure that your library is easy to use, versioned
correctly, and well-suited for complex dependency management.
231
6.3.1 Introduction
Managing external dependencies is a crucial part of any C++ project, especially when
building modular and scalable applications. While methods like find package() or
manually linking libraries have been widely used, CMake offers an elegant solution for
dynamically fetching and managing dependencies during the build process:
FetchContent. This feature allows you to download, configure, and use external
dependencies directly from the source at the time of the build, without the need for
pre-installed packages or system-wide configurations. In this section, we will explore how
to use FetchContent effectively for fetching dependencies dynamically.
FetchContent_Declare(
<content_name>
GIT_REPOSITORY <repository_url>
GIT_TAG <commit_or_branch_or_tag> # Optional: specify a version
DOWNLOAD_NO_EXTRACT <ON|OFF> # Optional: whether to extract
,→ content
)
• DOWNLOAD NO EXTRACT: If set to ON, CMake will only download the content but
will not extract it. Useful when dealing with archives that don't need to be extracted.
Let’s take an example where we want to fetch a library called fmt, a popular C++
formatting library. Here is how we can declare and fetch it dynamically using
FetchContent.
CMakeLists.txt Example:
cmake_minimum_required(VERSION 3.14)
project(MyProject)
Explanation:
234
While Git repositories are commonly used with FetchContent, CMake also supports
fetching from other sources like tarballs or zip files. The general idea remains the same,
but the parameters change slightly.
FetchContent_Declare(
my_library
URL https://example.com/my_library.tar.gz
)
FetchContent_MakeAvailable(my_library)
• URL: Specifies the location of the tarball or zip file to be downloaded and extracted.
If your project has a local subdirectory containing source files for an external dependency,
you can use FetchContent to manage it similarly.
FetchContent_Declare(
my_local_lib
URL_FILEPATH ${CMAKE_CURRENT_SOURCE_DIR}/libs/my_local_lib.tar.gz
)
FetchContent_MakeAvailable(my_local_lib)
Example:
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.0.1 # Fetch a specific release version
)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 94e57d7a098ae3289e6a658801c5b5487ef0981a # Specific
,→ commit hash
)
This ensures that the content will always be retrieved at that exact state, guaranteeing
reproducibility.
You can fetch multiple dependencies in the same CMakeLists.txt file by calling
FetchContent Declare() multiple times and using
FetchContent MakeAvailable() for each one.
cmake_minimum_required(VERSION 3.14)
project(MyProject)
# Fetch fmt
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 8.0.1
)
FetchContent_MakeAvailable(fmt)
# Fetch spdlog
237
FetchContent_Declare(
spdlog
GIT_REPOSITORY https://github.com/gabime/spdlog.git
GIT_TAG v1.9.2
)
FetchContent_MakeAvailable(spdlog)
In this example, both fmt and spdlog are fetched dynamically, and we link them to our
project.
Benefits:
Limitations:
238
• Build Time: Fetching and building dependencies during the configuration process
can increase the overall build time, especially if the dependencies are large or
numerous.
• Use Version Control: Always specify a version (tag or commit hash) to ensure
reproducibility. Avoid using branches like master unless necessary.
• Check CMake Version: Ensure that you’re using a version of CMake that supports
FetchContent (CMake 3.14 or newer).
6.3.10 Conclusion
6.4.1 Introduction
In this section, we’ll explore how to integrate pkg-config with CMake and use
find library() to locate libraries dynamically, enabling you to link external
dependencies seamlessly into your project. This integration is especially useful when
working with libraries that provide pkg-config files, such as GTK, OpenSSL, or
others commonly used in the open-source ecosystem.
• Library locations
• Version information
241
pkg-config works by reading .pc files, which are installed by the package manager
when a library is installed. These files contain the necessary information about the library
and are typically located in directories like /usr/lib/pkgconfig/ or
/usr/local/lib/pkgconfig/.
You can pass these flags to the compiler and linker manually, or automate the process with
CMake.
1. Enable pkg-config: First, ensure that CMake knows to look for pkg-config
by enabling the PkgConfig module.
2. Use find package(): You can then call find package() to find the
required library, relying on pkg-config to handle the search.
Example:
Let’s say we want to use the libpng library in our project. CMake has support for
pkg-config through its FindPkgConfig module, and we can use it like this:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# Enable pkg-config
find_package(PkgConfig REQUIRED)
Explanation:
243
While pkg-config is helpful for retrieving metadata, CMake also has the built-in
find library() function for locating libraries in specified directories. This command
searches for a library by name and returns the full path to the library file. It's particularly
useful when the library is installed but doesn't have a .pc file for pkg-config to find.
Where:
• <VAR>: The variable that will store the path to the found library.
• name: The name of the library (without any prefixes or extensions, e.g., m for
libm.so).
• [path1 path2 ...]: Optional search directories to look for the library.
244
Here’s how you can use find library() to locate the mathematics library libm:
find_library(MATH_LIB m)
if(MATH_LIB)
message(STATUS "Found libm at ${MATH_LIB}")
else()
message(STATUS "libm not found")
endif()
In this example:
• find library(MATH LIB m): Searches for libm (the math library).
In some cases, you might want to use both pkg-config and find library()
together. This is common when a library is available through pkg-config, but its
dependencies are not declared or need to be manually located.
Here’s an example of using both pkg-config and find library() for a project
that depends on the libpng library (via pkg-config) and the libm math library
(using find library()):
245
# Enable pkg-config
find_package(PkgConfig REQUIRED)
# Create executable
add_executable(my_project main.cpp)
# Link libraries
target_link_libraries(my_project PRIVATE ${LIBPNG_LIBRARIES}
,→ ${MATH_LIB})
Explanation:
• find library(MATH LIB m): Locates libm using the find library()
function.
• target link libraries(): Links both libpng and libm to the project.
This approach is useful for combining the strengths of both methods: pkg-config for
easily finding well-supported libraries and find library() for libraries that don’t
246
• Use find package() with PkgConfig when available: CMake has built-in
support for pkg-config via the PkgConfig module, so use
find package(PkgConfig REQUIRED) whenever possible to handle
external dependencies more elegantly.
• Set CMAKE PREFIX PATH for Custom Locations: If your libraries are installed in
non-standard locations, you can set the CMAKE PREFIX PATH to help
pkg-config or find library() locate the libraries.
cmake -DCMAKE_PREFIX_PATH=/path/to/custom/libs ..
• Check for Dependency Availability: Always verify that the required dependencies
are found before proceeding with the build. You can use find package() or
check the results of find library() to ensure everything is in place.
if(NOT LIBPNG_FOUND)
message(FATAL_ERROR "libpng not found!")
endif()
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBPNG REQUIRED libpng>=1.6)
247
6.4.7 Conclusion
6.5.1 Introduction
In this section, we’ll dive deep into how to integrate vcpkg and Conan with CMake,
providing you with powerful tools for dependency management, versioning, and
cross-platform builds. We will cover each tool’s installation, configuration, and usage
within CMake.
vcpkg
Conan
Conan is another popular C++ package manager that focuses on providing a
decentralized, user-centric way of managing dependencies. Unlike vcpkg, which uses a
central repository, Conan supports multiple repositories and gives developers the
flexibility to host and manage their own packages.
Key features of Conan include:
vcpkg simplifies the process of installing libraries and managing them for CMake-based
projects. Below is a detailed guide on how to use vcpkg in your CMake workflow.
Installing vcpkg
250
1. Clone the vcpkg repository: First, clone the vcpkg repository from GitHub:
2. Bootstrap the vcpkg build system: Inside the vcpkg directory, run the bootstrap
script to build the package manager:
On Windows:
.\bootstrap-vcpkg.bat
On Linux/macOS:
./bootstrap-vcpkg.sh
3. Install packages with vcpkg: Use vcpkg to install C++ libraries. For example, to
install the fmt library:
To make vcpkg work with your CMake project, you need to set the appropriate
environment variable and tell CMake where vcpkg is located.
1. Set the CMake Toolchain File: When invoking CMake, specify the vcpkg
toolchain file to let CMake know that you want it to use vcpkg for package
management:
251
cmake
,→ -DCMAKE_TOOLCHAIN_FILE=<path_to_vcpkg>/scripts/buildsystems/vcpkg.cma
,→ ..
Replace <path to vcpkg> with the actual path to your vcpkg installation.
2. Linking Installed Libraries: After installing a package using vcpkg, you can link
it to your project using find package() or target link libraries() as
you would with any other CMake-managed library.
For example, after installing fmt with vcpkg, you can use:
find_package(fmt REQUIRED)
target_link_libraries(my_project PRIVATE fmt::fmt)
Conan is another excellent option for C++ package management, providing an alternative
approach to managing dependencies with a focus on flexibility, versioning, and
decentralized package repositories. Let’s go over how to integrate Conan with your
CMake project.
Installing Conan
1. Install Conan: You can install Conan via pip (Python's package manager):
252
[requires]
fmt/8.0.1
[generators]
cmake
The --build=missing flag ensures that any missing packages are built from
source.
2. Link with CMake: After installing dependencies, you need to tell CMake to use the
Conan-generated configuration files. Conan generates CMake files that set up
environment variables and paths for the dependencies.
253
include_directories(${CMAKE_BINARY_DIR}/conan_includes)
Alternatively, if you are using the cmake generator, include the Conan CMake
module:
include(${CMAKE_BINARY_DIR}/conan.cmake)
3. Link the Libraries: With the dependencies installed and configured by Conan, you
can link the libraries to your project in the usual way:
Both vcpkg and Conan are popular and powerful tools for package management in C++.
However, they have different strengths and use cases:
• If you are working on a cross-platform C++ project and want a simple, streamlined
way to manage dependencies.
• If you need decentralized package management and control over your package
repository.
• If you need detailed version control and fine-grained control over the dependencies
of your project.
6.5.6 Conclusion
Both vcpkg and Conan provide excellent solutions for managing external dependencies
in CMake-based C++ projects. While vcpkg offers a simpler, centralized approach with
easy integration into CMake, Conan offers more flexibility, version control, and
decentralized package management. Depending on your project’s needs, either tool can
significantly simplify dependency management and ensure a more consistent, reproducible
build process.
By leveraging the power of these package managers, you can focus more on developing
your application and less on handling dependency configurations.
Chapter 7
CMake is a cross-platform tool that provides users with the ability to manage the build
process of software projects in a compiler-independent manner. One of the most
convenient ways to interact with CMake is through its graphical user interface (GUI),
which offers a user-friendly way to configure projects, set up build paths, and generate
platform-specific build files. This section focuses on how to use the CMake GUI on both
Windows and Linux platforms, giving you insights into its interface and functionality.
256
257
• Configure Projects: Set CMake variables, choose generators, and set specific paths
for building your project.
• Generate Build Files: Create platform-specific build files (e.g., Makefiles, Visual
Studio project files).
• Troubleshoot Configuration Errors: View error messages and warnings related to
the project configuration.
Before you can use the CMake GUI, you'll need to install it. Here are the installation steps
for both Windows and Linux.
For Fedora:
2. Verify Installation:
• After installation, you can open the CMake GUI by running the following
command in a terminal:
cmake-gui
This should launch the CMake GUI application, ready to configure and generate
build files.
Upon launching the CMake GUI, the interface will look slightly different on Windows and
Linux but functions similarly across both platforms. Below is a breakdown of the interface
elements:
1. Initial Configuration
259
When you first open the CMake GUI, you will be prompted to provide paths for both
your source directory (where the CMakeLists.txt file is located) and the binary
directory (where the build files will be generated).
• Source Directory: This is the location of the source code of your project,
typically the folder containing your CMakeLists.txt.
• Binary Directory: This is the folder where the generated build files will be
stored. It's common to create a separate build folder within your project’s
root directory for this purpose.
2. Setting CMake Variables
In the CMake GUI, the main window consists of several buttons and areas where you
can input information:
• CMake Cache Entries
: On the left side of the GUI, there is a list of variables that CMake uses for the
configuration. These variables can be set manually, or CMake can automatically
populate them based on the system configuration. For instance:
– CMAKE BUILD TYPE: Specifies the build type (e.g., Debug, Release).
– CMAKE INSTALL PREFIX: Defines the installation directory after
building the project.
– CMAKE CXX COMPILER: Specifies the C++ compiler to use.
• Edit Variables: You can double-click on any variable in the list to modify its
value. If you're unsure about a variable, you can click the Help button for a brief
description.
3. Generating Build Files
Once you have configured your project in the GUI, you can generate the appropriate
build files for your platform. The “Configure” and “Generate” buttons in the CMake
GUI allow you to:
260
1. Configure: CMake will analyze the source and binary directories, attempt to
detect your system’s properties, and populate the GUI with relevant values (e.g.,
compilers, libraries). If there are issues or warnings, they will be displayed in
the output window.
2. Generate: After configuration, you can press the Generate button to create the
build files. This step will generate files such as:
• Makefiles (on Linux/macOS)
• Visual Studio project files (on Windows)
• Ninja build files (if Ninja is selected as the generator)
4. Advanced Options
The CMake GUI also allows you to specify advanced settings:
• Choose a Generator
: CMake supports different generators for different platforms, such as:
– On Windows, you may choose Visual Studio or MinGW.
– On Linux, you may select Unix Makefiles or Ninja.
• CMake Log: The log section provides output messages that help debug or
verify configurations.
5. Building the Project
While the CMake GUI does not directly build the project, it helps set up the build
environment. After generating the build files, you will typically use your chosen
build system (e.g., make, Visual Studio) to actually compile and link your project.
For instance:
• On Linux, you would navigate to the build directory and run make or ninja
(depending on the generator you chose).
• On Windows, you would open the generated Visual Studio solution and build
from there.
261
Though the CMake GUI is very similar on both platforms, some Linux-specific behavior
should be noted:
• On Linux, the CMake GUI relies on the Qt framework for its interface, so having the
appropriate Qt libraries installed is essential.
• The Generate button will allow you to choose between different build systems, such
as Unix Makefiles or Ninja.
On Windows, the process is mostly the same as on Linux, with a few key differences:
• The most notable difference is the choice of Visual Studio generators, which allow
you to create Visual Studio project files directly from the GUI. Once you generate
these files, you can open and build them inside the Visual Studio IDE.
• If you're using MinGW or another alternative to Visual Studio, the CMake GUI will
configure for those environments as well.
7.1.6 Troubleshooting
The CMake GUI offers helpful error messages if something goes wrong during
configuration or generation:
• Missing Dependencies: If CMake can’t find required libraries or tools, it will show
warnings or errors. You can often resolve these by specifying the correct paths in the
GUI or ensuring that necessary software is installed.
262
7.1.7 Conclusion
Using the CMake GUI is an effective way to simplify the process of configuring and
building C++ projects. Whether you're working on Windows or Linux, the GUI provides
an intuitive interface that guides you through configuring your project, setting CMake
variables, and generating platform-specific build files. With its built-in error messages and
advanced options, it is an excellent tool for both beginners and advanced developers alike.
263
While the CMake GUI is a powerful tool for configuring and generating build files with a
graphical interface, many advanced users prefer the Command-Line Interface (CLI)
because of its flexibility, automation capabilities, and faster workflows. The CMake CLI
allows you to execute the same tasks that you would with the GUI, but through terminal
commands, making it easier to integrate CMake into scripts, continuous integration (CI)
pipelines, and large-scale automated builds.
In this section, we will walk through how to use CMake’s Command-Line Interface to
configure, generate, and build CMake projects. We’ll also cover important commands and
options that you can use to streamline your build process.
The CMake CLI is a text-based tool that uses commands to control how CMake configures
and generates build files. The basic syntax of the cmake command is as follows:
Where:
You can use the CMake CLI on both Windows and Linux (and other Unix-based systems),
and its commands are consistent across platforms, though the underlying build system
may differ.
264
Let’s go through a typical CMake CLI workflow, from configuring the project to
generating build files and building the project.
mkdir build
cd build
This step ensures that all generated files (such as Makefiles or Visual Studio project
files) are placed in the build directory rather than in the source directory.
cmake <path-to-source>
For example, if your project’s source code is in the parent directory, you would run:
cmake ..
– CMAKE BUILD TYPE: Specifies the build type, which typically defines
whether you want a Debug or Release build.
For example:
cmake -DCMAKE_BUILD_TYPE=Release ..
This sets the build type to Release, optimizing the code for performance.
– CMAKE GENERATOR: Defines the build system generator, such as Makefiles,
Ninja, or Visual Studio.
Example for Unix Makefiles:
cmake -G "Ninja" ..
Once you run the cmake command with the necessary options, CMake will generate
the build files in the build directory. The generated files are specific to the platform
and generator you’ve selected (e.g., Makefiles, Visual Studio solution files, etc.).
CMake provides a range of commands and options for advanced configurations. Here are
some key ones that you will likely encounter when using the CLI:
1. cmake --build
After configuring the project with CMake, you can invoke the build process directly
from the CLI using the cmake --build command. This command allows you to
build your project using the same configuration you generated earlier, without
needing to switch to a separate build tool or IDE.
For example, to build the project:
cmake --build .
This will use the default generator (e.g., Makefiles or Ninja) to build the project in
the current directory.
You can also specify the number of parallel jobs to run during the build process
(useful for speeding up builds, especially in large projects):
cmake -DCMAKE_INSTALL_PREFIX=/custom/install/path ..
cmake -DCMAKE_CXX_COMPILER=g++-9 ..
3. cmake --install
Once your project is built, you can use the cmake --install command to
install the project into the specified location. This is especially useful for projects
that require installation steps, such as libraries or applications that need to be copied
to a system-wide location.
For example:
cmake --install .
This will install the project using the installation paths defined during the
configuration process.
4. cmake --version
To check the installed version of CMake, you can use the following command:
cmake --version
This is useful when you want to confirm that you are using the correct version of
CMake, particularly in environments with multiple versions.
5. cmake --help
To view a list of available options and commands for CMake, you can use the help
command:
268
cmake --help
For more advanced workflows, the CMake CLI provides several additional features to
support complex build processes.
This reduces the need to manually set multiple flags and ensures consistency in how
your project is configured.
#!/bin/bash
Such automation is especially useful in CI/CD pipelines and for teams working on
large projects with many dependencies.
While the CMake CLI is powerful and flexible, there are a few common issues you might
encounter:
• Build Type Issues: If you’re not specifying the CMAKE BUILD TYPE, CMake
might default to an empty configuration, leading to unexpected results. Always
explicitly set the build type if needed.
270
• Generator Errors: If you specify an invalid generator (e.g., Visual Studio version
mismatch), CMake will return an error. Ensure that the chosen generator matches
your system’s available build tools.
7.2.6 Conclusion
The Command-Line Interface (CLI) provides CMake users with the flexibility to
configure, generate, and build projects directly from the terminal. This is especially useful
for automating builds, integrating with continuous integration systems, and streamlining
workflows. Mastery of the CMake CLI is a valuable skill for C++ developers, as it gives
them complete control over the build process while maintaining portability across
platforms. Through the combination of simple commands and powerful flags, the CLI
allows for highly customizable and efficient project management.
271
One of the most powerful features of CMake is its ability to generate build files for
different platforms, compilers, and build systems. This flexibility is enabled by CMake’s
use of generators, which control how the build process is handled once CMake has
configured the project. A generator specifies the type of build system (e.g., Makefiles,
Visual Studio project files, Ninja build files) that CMake will use to create the final output.
In this section, we will explore how to configure different generators in CMake, such as
-G "Ninja", -G "Unix Makefiles", and others. We’ll discuss when and why
you might choose one generator over another, and how to properly configure them using
the command-line interface (CLI).
A generator in CMake is a template that defines how build files will be created based on
the current platform and build tools. When you run the cmake command with a specific
generator, CMake uses that generator to produce the build system files that your chosen
platform requires.
For instance:
• On Linux, CMake may generate Makefile files that can be processed by the
make tool.
The -G option in the cmake command is used to specify which generator to use. You can
also choose from a wide variety of generators depending on your project’s needs.
Here are some of the most common CMake generators, how they work, and when you
might want to use them.
1. -G "Unix Makefiles"
The Unix Makefiles generator creates traditional Makefiles that can be
processed by the make build tool. This is one of the most widely used generators on
Linux and other Unix-like systems, including macOS.
Use Case:
• When working on Linux, BSD, or macOS systems with make as the build tool.
• For projects that do not require a specific IDE and prefer the command-line
interface.
Example:
This command will generate a Makefile that can later be used to build the project
with make:
make
Advantages:
Disadvantages:
• Slower compared to some other build tools like Ninja, especially for large
projects.
• Lacks advanced parallelization features found in other build systems.
2. -G "Ninja"
The Ninja generator is designed for fast builds and provides better parallelization
than traditional make-based builds. Ninja is often preferred for large projects where
speed is a priority. It also has a simpler, more efficient design than Make, which
leads to faster incremental builds.
Use Case:
Example:
cmake -G "Ninja" ..
Once the project is configured with Ninja, you can build it using:
ninja
Advantages:
Advantages:
• Provides seamless integration with Visual Studio.
• Supports advanced IDE features like debugging, code navigation, and profiling.
• Can be used for both C++ and C# projects in Visual Studio.
275
Disadvantages:
4. -G "Xcode"
The Xcode generator creates an Xcode project on macOS, enabling developers to
build their projects using the Xcode IDE. This is an ideal choice for macOS
developers who want to integrate with Xcode’s graphical interface, testing tools, and
simulator support for iOS/macOS applications.
Use Case:
• When working on macOS and targeting the Xcode IDE for development and
testing.
• Ideal for iOS and macOS applications, especially if you need to use the Xcode
interface for design, profiling, or app store submissions.
Example:
cmake -G "Xcode" ..
Advantages:
Disadvantages:
276
Advantages:
• Allows using make on Windows with the MinGW toolchain.
• Suitable for cross-platform C++ development targeting both Windows and
Unix-like systems.
Disadvantages:
• Requires MinGW to be installed and properly configured.
• Not as widely used as Visual Studio on Windows, so you may encounter
compatibility issues in certain environments.
• Toolchain: If you are using a specific build tool (e.g., Ninja, Make, or Visual
Studio), select the corresponding generator.
• Project Size: For larger projects, generators like Ninja are preferred due to their
faster incremental builds and parallelization features.
• IDE Preferences: If you prefer working in a specific IDE, such as Visual Studio or
Xcode, select the generator that integrates with that IDE.
• Linux/Unix Project: If you are on a Linux or Unix system and working with a
command-line environment, you would typically use -G "Unix Makefiles"
or -G "Ninja".
• Windows Developer: On Windows, if you are using Visual Studio, you would use
-G "Visual Studio 16 2019". If you are using a different build system like
MinGW, you would use -G "MinGW Makefiles".
In some cases, you may need to test or build your project with different generators. You
can simply run CMake with different -G flags, specifying a different generator for each
configuration. Keep in mind that some build systems (such as Visual Studio) may create
278
multiple configuration files (e.g., solution files, project files) for different build types
(Debug, Release).
Sometimes, you may run into issues while configuring CMake with a specific generator.
Some common issues include:
• Incorrect Generator Syntax: Make sure the generator string is entered correctly.
CMake is very specific about the exact names of the generators.
• Missing Tools: Some generators, like Ninja or MinGW, require the corresponding
tools to be installed. If CMake cannot find the necessary toolchain, it will generate
an error message.
• Generator Compatibility: Some generators might not work well on certain
platforms or with specific versions of CMake. Always refer to the CMake
documentation to verify compatibility.
7.3.6 Conclusion
CMake’s ability to support multiple generators makes it a highly flexible tool for
managing cross-platform and cross-toolchain projects. Whether you are targeting
traditional Makefiles, Ninja for speed, Visual Studio for IDE integration, or Xcode for
macOS/iOS development, CMake makes it easy to configure and generate the right build
files for your platform and toolchain. By understanding the various generator options and
when to use them, you can streamline your build process and improve productivity in your
C++ projects.
279
In this section, we’ll explore ccmake, how it works, how to manage project settings and
options through it, and how it compares to both the full CMake GUI and the
command-line interface.
ccmake stands for CMake curses interface, and it’s a command-line tool that provides
an interactive interface to configure CMake options for a project. The main purpose of
ccmake is to provide a text-based configuration interface that can be run inside a
terminal. This tool allows users to:
• Modify project settings such as build types, installation paths, and optional features.
• Save and apply changes interactively without needing to edit configuration files
manually.
Unlike the full graphical CMake GUI, ccmake works entirely in the terminal, making it
useful for remote development or environments where a GUI is impractical. It’s also often
preferred by advanced users who are comfortable with terminal-based workflows but still
want to take advantage of the interactive nature of CMake's configuration process.
280
ccmake --version
If the tool is not installed, you may need to install CMake via your system’s package
manager. For example:
Using ccmake is straightforward and involves running it in the build directory where
CMake has been previously configured. Here's the basic workflow:
1. Navigate to the Build Directory: It's a best practice to use an out-of-source build
directory (to keep source files clean). If you haven’t already created a build directory,
do so:
281
mkdir build
cd build
2. Run ccmake: Once you’re inside the build directory, run the ccmake command,
pointing to the project’s source directory (typically the parent directory containing
the CMakeLists.txt file).
ccmake ..
This command will start the interactive configuration interface, displaying the
current configuration options and settings for your project.
3. Navigate the ccmake Interface: The ccmake interface uses keyboard navigation.
Once the interface appears, you'll see a list of cache variables with their current
values. You can use the following keys to interact with ccmake:
A key feature of ccmake is the ability to view and modify the CMake cache variables.
These variables control the configuration of the project, such as the paths to dependencies,
compiler flags, or build types. The cache variables are saved in the CMakeCache.txt
file in your build directory.
In ccmake, the cache variables are displayed in a table-like structure, with each row
representing a variable, its current value, and its description. Some common types of
cache variables include:
You can change the values of these variables using the arrow keys to navigate to the
desired row, pressing Enter to edit the value, and typing the new value.
Example: Let’s say your project has an option to enable or disable a specific feature, and
it is defined as a Boolean cache variable, ENABLE FEATURE X. If the default value is
OFF, you can change it to ON using ccmake:
• Clear cached variables: If you want to reset the configuration to its initial state,
you can delete or clear the cache variables.
• Use advanced mode: By pressing the t key, you can switch to advanced mode,
which reveals additional configuration options that are typically hidden. This is
useful for more complex projects or when you need to fine-tune the build
configuration.
While both ccmake and the graphical CMake GUI serve similar purposes, they have
distinct use cases and benefits:
• ccmake is a text-based tool that works within the terminal, ideal for environments
without graphical interfaces. It’s best suited for users who prefer terminal-based
workflows but still want interactive configuration.
• CMake GUI provides a more visually intuitive interface, ideal for those who prefer
using a mouse and require a more user-friendly experience.
• CMake CLI is fully command-line based, offering the highest level of automation
and flexibility, particularly for integrating CMake into scripts or CI/CD systems.
mkdir build
cd build
ccmake ..
• Use the arrow keys to select CMAKE BUILD TYPE and change it from Debug
to Release.
• Enable or disable certain features by modifying Boolean cache variables (e.g.,
ENABLE TESTING).
6. Build the project: After generating the files, you can use the make or ninja
command (depending on your generator) to build the project.
7.4.7 Conclusion
One of the major benefits of using CMake is its ability to generate project files for various
integrated development environments (IDEs), including Visual Studio. Visual Studio is
one of the most popular IDEs for C++ development, especially on Windows, offering
powerful debugging, profiling, and code editing tools. With CMake's flexibility,
developers can easily integrate CMake into the Visual Studio workflow and take
advantage of the IDE’s features while maintaining a cross-platform build system.
In this section, we will explore how to use CMake with Visual Studio, covering topics like
generating Visual Studio project files, managing configurations, and leveraging the IDE's
full potential for development and debugging.
287
288
Before diving into the details, let's briefly discuss why integrating CMake with Visual
Studio is beneficial:
2. IDE Features: Visual Studio offers a rich set of features like IntelliSense,
auto-completion, refactoring tools, debugging, and performance profiling. Using
CMake with Visual Studio gives you access to all of these features while maintaining
a consistent build configuration.
5. Customizable Build Options: With CMake, you can specify detailed build options
and flags for your Visual Studio project, such as compiler options, preprocessor
definitions, and target-specific settings. This provides you with full control over how
your project is built.
289
CMake makes it easy to generate Visual Studio project files using the -G flag with the
cmake command. You need to specify the appropriate generator for the version of Visual
Studio you’re using. Each version of Visual Studio has a corresponding generator string.
For example:
These generator strings tell CMake to create the correct project files for the specified
version of Visual Studio. Let’s walk through the steps to generate Visual Studio project
files using CMake.
cd build
2. Run CMake to generate Visual Studio project files: In the build directory, run
the following CMake command to generate Visual Studio project files for a specific
version:
290
This will create a .sln (solution) file and other Visual Studio-specific project files
in the build directory. These files can now be opened directly in Visual Studio.
3. Open the Solution in Visual Studio: After CMake finishes generating the project
files, you can open the generated .sln file directly in Visual Studio by either
double-clicking the file or opening it from Visual Studio's File > Open >
Project/Solution menu.
Visual Studio will automatically recognize the CMake project and load it. If you
have multiple configurations (e.g., Debug, Release), Visual Studio will allow you to
choose between them before building.
Visual Studio supports multiple build configurations (e.g., Debug, Release, MinSizeRel,
RelWithDebInfo), and CMake allows you to define these configurations as part of the
build process. When you generate Visual Studio project files using CMake, it
automatically incorporates these configurations into the .sln file, giving you the
flexibility to build your project in different modes.
For example, here’s how you might define configurations in your CMakeLists.txt:
When you generate the Visual Studio project, you can choose the configuration from the
drop-down menu in the IDE. CMake ensures that the necessary flags and settings for each
configuration are set.
291
Additionally, CMake supports multi-config generators, which allow you to build multiple
configurations in one go (for instance, both Debug and Release configurations). When you
use Visual Studio with CMake, the build configurations are automatically handled for you,
making it easy to switch between different build modes.
Once you have generated the Visual Studio project files with CMake, building the project
becomes straightforward. Visual Studio handles the build process through the Build menu,
or you can use the toolbar to trigger builds.
1. Select the Build Configuration: In Visual Studio, you can select the build
configuration you want to use from the toolbar. Common configurations include:
2. Build the Project: After selecting your configuration, you can build the project by
clicking Build > Build Solution (or pressing Ctrl + Shift + B), which will
trigger the compilation process for your CMake-based project.
One of the main reasons for using Visual Studio is its advanced debugging tools. CMake
integrates seamlessly with Visual Studio's debugger, enabling you to set breakpoints,
inspect variables, and step through your code with ease.
1. Set Breakpoints: After opening the project in Visual Studio, you can navigate to
your code and set breakpoints by clicking in the margin next to the line numbers.
This is similar to debugging any other Visual Studio project.
2. Start Debugging: Once the breakpoints are set, press F5 (or select Debug > Start
Debugging) to run the program in debug mode. Visual Studio will automatically
stop at the breakpoints, allowing you to inspect the program state.
• Live code analysis with Visual Studio's IntelliSense and code suggestions.
• Memory debugging to analyze memory leaks or corrupted data.
• Performance profiling to help optimize your code by identifying bottlenecks.
Since you generated the Visual Studio project files using CMake, all these features
are available, making debugging and profiling your project much easier.
While CMake generates project files based on the CMakeLists.txt file, you might
need to tweak certain build settings for Visual Studio. You can do this directly within the
CMakeLists.txt file or by using CMake variables that influence the Visual Studio
build process.
293
1. Set Compiler Flags: CMake allows you to specify compiler flags that will be used
by Visual Studio. For instance, you can set flags to enable warnings or optimizations
specifically for Visual Studio:
if(MSVC)
add_compile_options(/W4) # Enable level 4 warnings for MSVC
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /O2") # Enable
,→ optimization for MSVC
endif()
2. Setting Up CMake Toolchain Files: For more advanced configurations, you may
use CMake's toolchain files to control various aspects of the build, such as the
compiler, linker, or additional build options. Toolchain files are particularly useful
when working with custom Visual Studio setups or when cross-compiling.
8.1.8 Conclusion
Integrating CMake with Visual Studio offers a powerful combination for C++
development on Windows. By generating Visual Studio project files through CMake,
developers can leverage the full range of Visual Studio’s IDE features, including advanced
debugging, profiling, and code navigation, while still maintaining a flexible,
cross-platform build system. Whether you are working on a small project or a large,
multi-platform application, CMake’s integration with Visual Studio provides a streamlined
workflow that enhances both productivity and code quality.
By understanding how to generate Visual Studio projects, configure build settings, and
debug using Visual Studio's tools, you can significantly enhance your CMake-powered
development process, making it easier to manage and build C++ projects within Visual
Studio.
295
This section will walk you through how to integrate CMake with CLion, explaining how
CLion handles CMake projects, the configuration options, and how you can leverage the
IDE’s features to streamline your development process. You’ll also see how CMake
simplifies managing cross-platform C++ projects in CLion.
Before diving into the steps, it’s important to understand why integrating CMake with
CLion is beneficial:
2. Intelligent CMake Support: CLion provides excellent support for CMake through
its built-in integration. It can automatically detect CMakeLists.txt files, handle
build configurations, and offer detailed CMake project navigation, making it easier
to work with large and complex projects.
296
5. Advanced IDE Features: CLion offers powerful features such as code completion,
code analysis, refactoring, and integrated unit testing support. When combined with
CMake, CLion allows you to fully utilize these features in your C++ projects,
enabling you to improve both productivity and code quality.
To begin using CMake with CLion, you'll first need to set up your project. Here are the
steps:
1. Install CLion: If you haven't installed CLion yet, go to the JetBrains CLion
download page and install the appropriate version for your operating system
(Windows, macOS, or Linux). You can download a free trial or purchase a full
license.
• Creating a New CMake Project: When starting a new project, you can create a
CMake-based project directly from within CLion. To do this:
– Open CLion.
– Go to File > New Project.
297
– Select C++ Executable under the project types and specify the language
standard and build system as CMake.
– CLion will generate a basic CMakeLists.txt file for you, where you
can add source files and other configurations.
• Opening an Existing CMake Project: If you already have a project with a
CMakeLists.txt file, you can open it directly in CLion:
– Open CLion and choose Open from the welcome screen.
– Browse to the directory containing your CMake project and select the
CMakeLists.txt file.
– CLion will automatically detect the project as a CMake-based project and
open it accordingly.
3. Verify CMake Integration: After opening or creating a CMake project, you should
see the CMake tool window on the right side of CLion. This window shows the
current CMake configurations and build targets for your project. You can access
additional options by clicking on the CMake tab or CMake GUI from this window.
Once your CMake project is open in CLion, it’s time to configure CMake settings. CLion
allows you to modify CMake variables and manage multiple build configurations, all from
within the IDE. Here’s how you can manage your project’s configuration in CLion:
• CLion detects and displays CMake variables in the CMake Tool Window. You
can modify these variables directly by clicking the gear icon in the CMake
window and selecting Edit Configurations. This opens a dialog where you can
298
• In CLion, you can define multiple build configurations (such as Debug, Release,
or custom configurations). You can create new configurations by going to File
> Settings > Build, Execution, Deployment > CMake.
• For each configuration, CLion will show options for specifying:
– CMake executable: The path to the CMake binary. Typically, CLion
auto-detects this, but you can modify it if necessary.
– CMake options: Extra flags you want to pass to CMake, such as
-DCMAKE INSTALL PREFIX=/custom/path.
– Build directory: Where the build files will be generated. By default, CLion
generates them in a folder called cmake-build-debug or
cmake-build-release, depending on the selected configuration.
• Once the configuration is set, you can select the build configuration from a
drop-down list in the toolbar and click the Build button to trigger the CMake
build.
3. Target Management:
• CMake projects often involve multiple targets (e.g., executables, libraries, etc.).
CLion lets you manage and select the target you want to build or run through its
Run/Debug Configurations. You can create new configurations for different
build targets in the Run/Debug Configurations dialog.
299
• You can also specify command-line arguments for the selected target and easily
debug specific targets by setting breakpoints and running the target in debug
mode.
CLion simplifies building and running CMake projects, allowing you to stay within the
IDE for the entire development lifecycle. Here's how the build process works in CLion:
• Once the project is configured, you can build it by clicking the Build button (a
hammer icon) in the toolbar. CLion will call CMake to generate the necessary
build files and invoke the build system (e.g., make, ninja, or MSBuild) to
compile the project.
• The Build Output tab in CLion will display the results of the build process,
showing any errors, warnings, or successful builds.
• You can run your application directly from within CLion by clicking the Run
button (a green arrow icon). CLion will use the appropriate build configuration
and launch the application.
• CLion will also allow you to specify runtime arguments, which can be useful for
testing different scenarios or running the program with specific configurations.
3. Automatic Rebuilds:
• If you modify your source files, CLion will automatically detect the changes
and offer to rebuild the project for you. You can also configure it to rebuild the
project on each launch or when necessary.
300
CLion’s debugging tools work seamlessly with CMake projects. When you set up your
project correctly, debugging becomes an integrated experience that takes full advantage of
CLion’s powerful debugger.
1. Set Breakpoints:
• To set a breakpoint, simply click in the left margin next to the line numbers in
your source code. The line will be highlighted, and a red dot will appear,
indicating that a breakpoint has been set.
2. Start Debugging:
• To start debugging, click the Debug button (a bug icon) in the toolbar or press
Shift + F9. CLion will compile your project (if necessary) and start the
application in debug mode.
• The Debugger tab will show the call stack, variables, and breakpoints, allowing
you to step through your code and inspect values.
• CLion offers the ability to watch variables and evaluate expressions in real time.
This is particularly useful for tracking the value of specific variables during
debugging or checking the result of complex expressions.
4. Remote Debugging:
CLion integrates well with CMake-based test frameworks, such as Google Test, Catch2,
and Boost.Test. You can run your tests directly within CLion, making the process of unit
testing seamless.
enable_testing()
add_subdirectory(tests)
add_executable(my_tests test_main.cpp)
target_link_libraries(my_tests gtest gtest_main)
add_test(NAME MyTests COMMAND my_tests)
2. Run Tests:
• Once your tests are configured, you can run them directly from CLion. CLion
will detect the tests in your project and display them in the Run tool window.
You can run all tests or select specific ones to execute.
• After running the tests, CLion provides a detailed view of the test results,
including pass/fail statuses, output, and logs.
8.2.7 Conclusion
Integrating CMake with CLion offers an efficient, user-friendly environment for C++
development. CLion’s built-in support for CMake allows you to easily manage, configure,
302
build, and debug CMake-based projects, while its intelligent features like code completion,
refactoring, and testing provide a significant productivity boost. Whether you're working
on a small C++ application or a large, cross-platform project, CLion offers the tools
necessary to streamline your development workflow.
By leveraging CMake and CLion together, you can focus more on coding and less on
configuring your build system, while also taking full advantage of the IDE's powerful
features to optimize your code and workflow.
303
To begin using CMake with Qt Creator, follow these steps to configure your project
within the IDE:
1. Install Qt Creator:
• First, ensure that Qt Creator is installed on your system. You can download the
installer from the Qt website and follow the installation instructions for your
platform (Windows, macOS, or Linux).
• Make sure to install the necessary Qt libraries if you plan to develop Qt-based
applications.
2. Create a New CMake Project:
• Step 1: Open Qt Creator and select New Project from the welcome screen or
File > New File or Project from the main menu.
• Step 2: In the New Project wizard, select Application under Projects and then
choose C++ Application or Qt Widgets Application, depending on your needs.
• Step 3: When asked to choose a build system, select CMake. Qt Creator will
automatically generate a basic CMakeLists.txt file for your project.
• Step 4: Choose the project location and name, and click Finish to create your
project.
This will create a new CMake-based project with an initial CMakeLists.txt that
includes the necessary setup for the application.
305
3. Open an Existing CMake Project: If you already have a CMake project (with an
existing CMakeLists.txt file), you can easily open it in Qt Creator:
Once the project is loaded, you can start configuring the CMake build system directly
within Qt Creator.
Qt Creator provides a variety of options for configuring your CMake-based project. Let’s
explore the key settings available in the IDE.
• CMake executable: This is the path to the CMake binary used to configure the
project. Qt Creator will auto-detect this, but you can change it if necessary.
306
2. Build Configurations
: Qt Creator supports multiple build configurations for different use cases (e.g.,
Debug, Release). For each build configuration, you can define CMake variables or
options to customize the build process.
• To switch between different build configurations, select the Build & Run tab,
then choose the desired configuration from the Build and Run configuration
lists.
• Qt Creator also allows you to add custom build configurations, which can be
especially useful for creating specialized configurations like unit tests or
cross-platform builds.
• Once the project is set up, you can build it by clicking the Build button (hammer
icon) in the toolbar or by selecting Build > Build Project from the main menu.
307
• Qt Creator will run CMake to generate the necessary build files and then call
the appropriate build system (e.g., make, ninja) to compile the project.
• After the project has been built, you can run it directly from Qt Creator by
clicking the Run button (green arrow icon).
• Qt Creator will launch the application within the IDE. If you’re working with a
Qt GUI application, the window will appear as expected.
1. Setting Breakpoints:
• To set a breakpoint, click in the left margin next to the line numbers in your
source files. A red dot will appear to indicate the breakpoint.
2. Starting Debugging:
• To start a debugging session, click the Debug button (bug icon) in the toolbar or
press Shift + F9.
308
• Qt Creator will build the project (if necessary) and run the application in debug
mode.
3. Debugger Interface:
• The Debugger tab in Qt Creator shows the call stack, local variables, and
allows you to inspect objects in the program.
• You can step through your code line-by-line, step into functions, step over lines,
and continue execution. You can also examine memory, variables, and the
program’s output.
4. Remote Debugging:
• Qt Creator supports remote debugging. This is useful if you’re working on an
embedded system or need to debug an application running on a different
machine. You can configure Qt Creator to connect to the remote system, set up
breakpoints, and debug as if you were working locally.
If your CMake project includes unit tests (e.g., using Google Test, Catch2, or
Boost.Test), you can run and manage these tests directly from Qt Creator.
enable_testing()
add_subdirectory(tests)
add_executable(test_example test_example.cpp)
309
2. Running Tests:
• Qt Creator detects the unit tests in the project and provides options to run them.
You can open the Test pane from the Projects view and see all the available
tests.
• Running tests within Qt Creator is as simple as selecting a test and clicking
Run. The test results are displayed directly in the IDE, with details on pass/fail
statuses.
• You can also debug individual tests by setting breakpoints in your test code and
running the tests in debug mode.
8.3.7 Conclusion
Integrating CMake with Qt Creator provides an efficient workflow for managing and
building C++ projects, whether you are developing Qt-based GUI applications or working
on other C++ projects. Qt Creator’s deep integration with CMake streamlines the project
setup, configuration, building, debugging, and testing processes, allowing developers to
focus on writing high-quality code while Qt Creator handles the rest.
By following the steps outlined in this section, you will be able to leverage the full power
of CMake and Qt Creator to simplify your development process, whether you’re
working on a small project or a large-scale C++ application.
310
This section will explore how to integrate CMake with Xcode on macOS to build and
manage C++ projects efficiently. We will cover how to generate Xcode project files using
CMake, how to configure and build the project within Xcode, and how to take advantage
of Xcode's features, such as debugging and performance analysis, while using CMake.
There are several reasons why using CMake with Xcode can benefit macOS developers:
1. Cross-Platform Development:
• CMake is a cross-platform build system, which means that once you configure
your project using CMake, you can generate build files for multiple platforms,
including macOS, Linux, and Windows. By using CMake with Xcode, you can
manage your macOS-specific build configurations while keeping your project
setup portable across different platforms.
CMake to generate Xcode projects means you can continue to leverage these
powerful tools while maintaining a flexible, portable build system.
To use CMake with Xcode, the first step is to ensure you have the necessary tools
installed on your macOS system.
1. Install Xcode:
xcode-select --install
2. Install CMake:
312
• Alternatively, you can download the official CMake installer from the CMake
website and install it manually.
• After installing
CMake
, you can verify the installation by running the following command in the
terminal:
cmake --version
Once CMake is installed, you can generate Xcode project files for your C++ project. This
involves configuring your CMakeLists.txt file to define how the project should be built
and then using CMake to generate the corresponding Xcode project.
CMakeLists.txt
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject)
• To generate the
Xcode
project files, navigate to your project directory in the terminal and run the
following
CMake
command:
cmake -G "Xcode"
• This command instructs CMake to generate an Xcode project for your current
project directory. After running this command, you will see an Xcode project
file (MyProject.xcodeproj) in the directory.
314
open MyProject.xcodeproj
target_link_libraries
target_link_libraries(MyProject MyLibrary)
Once you’ve generated the Xcode project files using CMake, you can build and run your
project directly from Xcode.
• In Xcode, click the Build button (the hammer icon) in the toolbar, or choose
Product > Build from the top menu to start the build process. Xcode will use
the build configurations generated by CMake to compile your project.
2. Run the Project
:
• After building your project, you can run it by clicking the Run button (the play
icon) in the toolbar or choosing Product > Run from the top menu.
• If you are working with a GUI-based application, the application window will
open. If it’s a console application, the terminal output will be displayed in the
Xcode debug console.
Xcode offers a rich set of debugging tools that can help you troubleshoot issues in your
C++ projects. When working with CMake, you can take full advantage of these
debugging features.
1. Setting Breakpoints:
• To set a breakpoint in your C++ code, click on the line number where you want
to stop the execution. A blue arrow will appear, indicating that a breakpoint is
set.
2. Starting a Debugging Session:
• To start debugging, click the Debug button (the bug icon) or select Product >
Debug from the menu. Xcode will build your project (if necessary) and start it
in debug mode.
• Execution will pause at your breakpoints, allowing you to inspect variables, step
through code, and evaluate expressions.
316
3. Inspecting Variables:
• While debugging, you can use the Variables View in Xcode to inspect the
values of your variables. This is especially helpful when tracking down memory
or logic issues.
• You can also use the Debug Area at the bottom of the screen to view detailed
information about the program’s execution and variables.
4. Stack Tracing:
5. Remote Debugging:
• Xcode also supports remote debugging. This can be useful if you're developing
for iOS or macOS devices, or working in a cross-platform environment. You
can set up your device as the target for debugging and follow the same
debugging steps.
• If your project depends on an external library (e.g., Boost , OpenCV ), you can
use
317
find_package
in your
CMakeLists.txt
find_package(OpenCV REQUIRED)
target_link_libraries(MyProject ${OpenCV_LIBS})
2. Using ExternalProject:
• For more complex cases where a library needs to be built from source, you can
use the ExternalProject module in CMake to download, build, and link
external projects directly within your Xcode project.
8.4.7 Conclusion
Integrating CMake with Xcode on macOS offers a seamless workflow for managing and
building C++ projects. By generating Xcode project files using CMake, you can leverage
the powerful features of Xcode, such as debugging, profiling, and unit testing, while still
using the flexible, cross-platform build system that CMake provides.
Whether you're working on a macOS-specific application or managing a multi-platform
project, CMake combined with Xcode allows you to streamline your development
process, manage dependencies efficiently, and take full advantage of Xcode’s debugging
and performance analysis tools.
By following the steps outlined in this section, you will be able to build, configure, and
debug your C++ projects in Xcode with the flexibility and power of CMake. This
318
combination is ideal for developers looking to maintain clean, portable, and efficient build
configurations on macOS.
319
Visual Studio Code (VS Code) is a lightweight yet powerful open-source code editor
developed by Microsoft. It is one of the most popular IDEs for a variety of programming
languages, including C++. With its rich set of features, extensions, and vast ecosystem,
VS Code provides a flexible and customizable development environment. When
combined with CMake, VS Code offers a robust solution for C++ project management,
especially for cross-platform development.
The CMake Tools extension for VS Code is a plugin that allows seamless integration of
CMake functionality directly within the VS Code environment. It simplifies tasks such as
configuring, building, and debugging CMake-based projects without leaving the editor. In
this section, we will explore how to set up VS Code for working with CMake and how to
use the CMake Tools extension effectively.
Before you can integrate CMake with VS Code, you need to install the necessary
components and configure the editor for C++ development. Here’s a step-by-step guide to
setting up VS Code.
1. Install VS Code:
• Download and install Visual Studio Code from the official website: VS Code
Download.
• In the Extensions view, search for the CMake Tools extension by Microsoft.
• Click Install on the extension to add CMake support to VS Code. This
extension provides various CMake-related features such as configuration, build,
and debugging, all accessible directly from the VS Code interface.
• Make sure that CMake is installed on your system. You can install it via
Homebrew (on macOS) or download the installer from the CMake website.
• If you are working on Linux, you can install CMake using your system’s
package manager (e.g., sudo apt install cmake on Ubuntu).
• On Windows, make sure that CMake is added to the system’s PATH during
installation so that it can be accessed from the terminal.
5. Install a Compiler:
• For Windows users, you need a compatible C++ compiler such as MSVC
(Microsoft Visual C++). The Build Tools for Visual Studio package includes
MSVC and related tools.
• For Linux/macOS users, ensure that a C++ compiler (like GCC or Clang) is
installed on the system.
Now that VS Code is set up, let’s create and configure a simple C++ project using CMake.
321
• Create a new folder for your project and add a new C++ source file, for example,
main.cpp
main.cpp
#include <iostream>
int main() {
std::cout << "Hello, CMake with VS Code!" << std::endl;
return 0;
}
CMakeLists.txt
file. This file will contain the build instructions for your project. Below is a
simple
CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(HelloWorld)
set(CMAKE_CXX_STANDARD 14)
add_executable(HelloWorld main.cpp)
• Open VS Code and navigate to your project directory. You can either use the
File > Open Folder menu option or the command line (code . in the project
folder) to open the project.
• After configuration, you can build the project directly within VS Code. In the
blue status bar, click on the Build button (or press Ctrl+Shift+P and type
CMake: Build).
• CMake Tools will use the selected build system (e.g., Unix Makefiles, Ninja,
323
or MSBuild) to compile the project. Once the build process completes, you’ll
see the output in the Terminal pane at the bottom.
6. Running the Executable:
• To run the executable, click on the green Run button in the status bar or use the
CMake: Run command from the Command Palette (Ctrl+Shift+P). This
will execute your program, and the output will appear in the Terminal pane.
VS Code provides integrated debugging capabilities, and with the CMake Tools
extension, you can debug your C++ projects seamlessly.
1. Setting Breakpoints:
• Open your main.cpp file and click on the left gutter next to the line numbers
to set breakpoints. A red dot will appear, indicating where execution will pause
during debugging.
2. Starting a Debugging Session:
• To start debugging, click the Run and Debug icon from the Activity Bar (or use
F5 to start debugging). VS Code will build the project first, then launch the
debugger.
• You will have full access to debugging features, such as stepping through the
code, inspecting variables, viewing call stacks, and more.
3. Debug Configuration:
• If you need to customize your debug configuration, you can modify the
launch.json file in the .vscode folder. The CMake Tools extension can
auto-generate a basic configuration for you. You can modify the configuration
to add specific options for debugging.
324
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug (GDB)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/HelloWorl
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"miDebuggerPath": "/usr/bin/gdb",
"preLaunchTask": "CMake: build"
}
]
325
The CMake Tools extension in VS Code provides several advanced features for efficient
CMake project management.
1. CMake Presets:
• CMake Presets allow you to configure different environments for building your
project. You can define multiple configurations and switch between them using
the extension. Presets can specify compiler options, build system types, and
more.
• With CMake Tools, you can easily switch between different build
configurations, such as Debug, Release, or custom configurations. You can
configure the active build configuration directly from the VS Code status bar.
3. Testing Integration:
• The extension supports integrating with CTest, the testing tool used by CMake.
If your project contains test cases, you can use the CMake Tools extension to
run and manage tests from within VS Code.
• The CMake Tools extension provides an interface for interacting with the
CMake Cache, allowing you to configure and modify cache entries easily. This
is particularly useful for managing project options and settings.
8.5.5 Conclusion
By integrating VS Code with CMake and using the CMake Tools extension, developers
can create a streamlined and efficient workflow for managing C++ projects. This
combination allows you to configure, build, and debug projects all within a lightweight yet
powerful editor, making it easier to develop, test, and maintain complex C++ projects.
The CMake Tools extension simplifies the process of interacting with CMake and
provides a native VS Code interface for common tasks like building, configuring, and
debugging. Whether you are working on a single-platform project or managing a
cross-platform codebase, VS Code and CMake provide a seamless experience for C++
development.
Chapter 9
In the process of developing C++ projects, ensuring the correctness of your codebase is
critical. Over time, as your project grows, keeping track of potential bugs and regressions
becomes increasingly complex. This is where testing frameworks come into play. While
testing is an essential practice throughout the software development lifecycle, integrating
it effectively with the build system is equally important for maintaining a smooth and
efficient development pipeline.
CTest is a testing tool provided by CMake, designed to automate the process of running
tests and generating reports. It allows developers to define and execute unit tests,
integration tests, and other quality assurance checks directly within the CMake build
process. By doing so, CTest integrates testing seamlessly with the project’s build,
327
328
providing a unified environment where both building and testing can be managed
efficiently.
CTest works in tandem with CTest-driven testing frameworks, such as Google Test or
Catch2, which provide the actual testing infrastructure. CTest itself doesn’t perform the
tests directly but coordinates their execution, manages results, and generates detailed
reports for the developers.
Integrating testing into the build system, especially in a larger C++ project, is essential for
several reasons:
the repository is verified against a set of automated tests. By triggering CTest as part
of the CI pipeline, developers can receive immediate feedback about the stability of
their codebase. This results in faster development cycles and more reliable software.
6. Scalability
CTest scales effortlessly with the size and complexity of your project. From small
C++ libraries to large multi-module applications, CTest can handle a diverse range of
test suites and workflows. Its ability to distribute tests and execute them in parallel
on multiple cores or machines further improves performance as the project grows.
• Test Discovery: CTest can automatically discover tests within the project, even if the
test cases are added or removed over time.
330
While other testing tools such as Google Test, Catch2, or Boost Test provide great testing
frameworks, they focus mainly on running and reporting tests. CTest, on the other hand, is
not a testing framework but a test driver that integrates these testing tools with the CMake
build system. It allows you to use different testing frameworks while leveraging CMake’s
configuration and build capabilities.
By using CTest, you achieve a streamlined development process where tests are
seamlessly integrated into your build cycle. This is a step above manually running tests
and makes it far easier to maintain a consistent and automated testing environment.
9.1.5 Conclusion
In CMake, testing is a fundamental part of the development workflow, and add test()
is the key command used to define and register tests with CTest. This command allows
you to associate specific tests with your project and makes it easy to automate test
execution through CTest. In this section, we will explore the syntax and usage of
add test(), provide practical examples, and cover best practices to effectively
incorporate tests into your CMake project.
The add test() command in CMake is used to register individual tests with CTest.
When you invoke add test(), you essentially tell CMake to treat a particular
executable or script as a test that should be run during the testing phase of your build
process.
• NAME: The name of the test. This is an identifier used by CTest to refer to the test.
• COMMAND: The executable or script to be executed for the test. This could be a
compiled test binary, a shell script, or any other executable that can be run from the
command line.
• ARGUMENTS: Optional arguments that are passed to the test executable when it is
run. These can be command-line arguments or test-specific parameters.
333
Once registered using add test(), the test can be run using the ctest command, or
as part of a larger suite of tests in a CI pipeline.
To begin, let’s look at a basic example of defining a unit test in a CMake project.
1. Write a simple test program: Create a small C++ file that outputs a success
message or fails deliberately.
// test_example.cpp
#include <iostream>
int main() {
std::cout << "Test Passed!" << std::endl;
return 0; // Return 0 indicates success
}
1. CMakeLists.txt for the test: In your CMakeLists.txt file, you will need to
compile the test executable and then register it using add test().
cmake_minimum_required(VERSION 3.10)
project(MyProject)
Once you have registered your test using add test(), you can run the tests using the
ctest command:
ctest
This command will search for all tests that were registered via add test(), execute
them, and report their results (whether they passed or failed).
You can also specify individual tests to run by using the -R flag with a regular expression:
ctest -R test_example
This will run only the tests whose names match test example.
While the basic usage of add test() is straightforward, there are several advanced
features and options you can use to control how tests are executed:
In this case, the test will be executed with the specified arguments.
Alternatively, you can organize tests into directories and use regular expressions to
run all tests from a specific group:
ctest -R unit_tests
By default, add test() assumes that an exit code of 0 indicates a successful test,
while a non-zero exit code indicates failure. You can customize this behavior for
specific tests, for example, by specifying that a particular test should pass with a
non-zero exit code:
This will mark the test as expected to fail, and ctest will not report it as a failure.
5. Testing with External Scripts
In addition to testing compiled C++ executables, you can use add test() to run
shell scripts, Python scripts, or any other executable. For example, you could use a
Python script to test specific functionality:
This is useful when integrating with scripting languages for tasks that are not easily
tested using compiled binaries.
To make the most of add test() and maintain a high-quality testing environment,
consider the following best practices:
1. Naming Conventions: Use descriptive and consistent names for your tests. Group
tests logically by functionality, and include the test's purpose in the name for easier
tracking.
337
9.2.6 Conclusion
The add test() command is the foundation for integrating automated testing into your
CMake projects. By using this command effectively, you can define and manage tests,
automate their execution, and integrate them into a continuous testing pipeline.
Understanding how to utilize its advanced features, such as passing arguments, setting
timeouts, and grouping tests, will help you build a robust and maintainable testing
infrastructure for your C++ project.
In the next sections, we’ll look at how to configure and run tests in more advanced
scenarios, including integrating tests with Continuous Integration systems, handling test
dependencies, and reporting detailed results.
338
After you have written and registered your tests using the add test() command in
CMake, the next step is to actually run those tests. The ctest command is the tool
provided by CMake to execute the tests that have been defined in your project. It
automates the process of running your tests, checking their results, and generating reports
that allow you to monitor the stability and correctness of your codebase.
In this section, we will cover how to use ctest, explore its various options for running
tests, discuss how to interpret test results, and look at ways to integrate ctest into your
build and Continuous Integration (CI) workflows.
ctest is a command-line tool that comes with CMake and is used to run tests that have
been registered with the add test() command. It works with CMake-generated
projects and allows you to run the tests in a controlled manner, check their status, and
generate reports. ctest can be run from the command line interface after the project has
been built, and it provides a range of options to customize how tests are run.
When you run ctest, it will search for all tests that were added via add test() and
execute them. It will then display a summary of the test results, indicating whether each
test passed, failed, or was skipped. Additionally, ctest can output detailed logs and
provide a number of command-line flags to refine your testing process.
The simplest way to run tests with ctest is by running the command without any
additional options:
339
ctest
By default, ctest will run all tests in the current directory and its subdirectories that
were defined via add test().
In a large project with many tests, you may not want to run all tests every time. ctest
allows you to execute a specific test or a subset of tests using several filtering options. The
most common method is by using the -R (regular expression) flag, which allows you to
specify a pattern that matches the names of tests.
For example, if you want to run only tests whose names contain the word ”unit”, you can
use:
ctest -R unit
This command will run only those tests whose names match the regular expression unit.
This is useful for running specific categories of tests, such as unit tests, integration tests, or
performance tests, without running the entire suite.
ctest -R test_addition
This would only run the test named test addition (assuming it’s been defined with
add test()).
In this output, Passed indicates that the test passed successfully. If the test fails, it will
be marked as Failed along with additional details.
However, ctest provides several flags to control the level of detail in the output. Some
useful flags for controlling output include:
• -V (Verbose): Displays detailed information about each test as it runs. This includes
the full output from each test, including any std::cout or std::cerr
statements.
ctest -V
ctest --output-on-failure
• -N (Dry Run): Performs a dry run without actually executing the tests. This will
only show which tests would be executed, without running them.
ctest -N
Sometimes, tests may hang or take longer to run than expected. ctest allows you to set
timeouts for the tests. You can define timeouts for individual tests using
set tests properties() with the TIMEOUT property in your
CMakeLists.txt. However, you can also limit the overall runtime for all tests through
ctest by setting the -T flag to control the maximum amount of time allocated for the
entire test run.
ctest -T 10
This will allow ctest to run for up to 10 seconds before terminating the test process.
ctest -j 4
You can also set -j to 4 or another number depending on the number of CPU cores
available on your machine.
The results of the test execution are returned through the exit code of ctest. The exit
code allows you to integrate ctest into CI/CD pipelines and other automated systems.
This makes it possible to use ctest within build systems or CI tools to monitor the
success or failure of tests programmatically.
Integrating ctest into Continuous Integration (CI) pipelines is an essential practice for
modern software development. By running tests automatically each time code is
committed or merged, teams can quickly identify regressions or other issues that may
affect the stability of the software.
Most CI systems (e.g., Jenkins, GitLab CI, Travis CI) provide built-in support for running
ctest commands as part of their build scripts. Here is an example of how you might use
ctest in a Jenkins pipeline:
343
ctest -j 4
This will execute the tests using 4 parallel jobs. If any tests fail, ctest will return a
non-zero exit code, which the CI system can use to mark the build as failed.
9.3.9 Conclusion
The ctest command is an indispensable tool for managing and running tests in
CMake-based projects. It provides a straightforward way to execute tests, customize their
execution, and report their results. Whether you are running a simple project or a complex
multi-module C++ application, ctest allows you to integrate automated testing into
your workflow, automate quality assurance tasks, and ensure that your project remains
stable and robust. By mastering ctest and utilizing its powerful features, you can
achieve faster feedback loops, reduce manual testing effort, and catch issues early in the
development process.
In the next section, we will explore advanced topics such as generating custom test reports,
integrating ctest with various CI/CD systems, and handling test dependencies to further
enhance your testing pipeline.
344
Unit testing is an essential practice in software development, ensuring that individual units
or components of the code work as expected. While ctest allows for running tests in
general, integrating a robust testing framework can provide greater flexibility, clarity, and
functionality. Google Test (often abbreviated as gtest) is one of the most popular C++
testing frameworks, known for its rich feature set, ease of use, and compatibility with
CMake and ctest.
In this section, we will explore how to integrate Google Test with your CMake project,
write unit tests, run them using ctest, and leverage the full potential of the Google Test
framework. By the end of this section, you’ll be able to write unit tests for your C++
project, use assertions to check conditions, and incorporate Google Test into your
CMake-based build system.
• Test Fixtures: Setup and teardown code that runs before and after each test,
allowing you to share code between tests.
345
• Test Organization: Tests can be grouped into test suites (test cases) and tests, and
they are executed automatically with meaningful test names.
• Mocking: Integration with Google Mock for creating mock objects in your tests.
• Rich Output: Detailed test results are printed, including failures, which makes
debugging easier.
To begin using Google Test in your CMake-based project, you need to integrate it into
your project’s build system. Fortunately, CMake provides excellent support for Google
Test, either by downloading it as a submodule or by installing it on your system.
Now, you’ve successfully included Google Test in your CMake project, and you can
start writing unit tests.
2. Alternatively, Install Google Test System-Wide
If you prefer, you can install Google Test on your system instead of including it as a
submodule. You can do this by following the instructions on the Google Test GitHub
page for your platform. After installation, you can link your tests to the installed
version of Google Test by modifying your CMakeLists.txt file to find and link
the library:
find_package(GTest REQUIRED)
add_executable(test_example test_example.cpp)
target_link_libraries(test_example GTest::GTest GTest::Main)
Once Google Test is integrated into your project, you can begin writing unit tests for your
code. Unit tests typically involve testing individual functions or classes to ensure that they
perform as expected under different conditions.
347
1. Include the Google Test Header: Each test file requires the Google Test header,
which provides the necessary functionality for writing and running tests.
#include <gtest/gtest.h>
1. Define Test Cases and Test Fixtures: In Google Test, tests are organized into test
suites (test cases) and individual tests. A test suite is a collection of related tests, and
each test case typically tests one specific unit of functionality. Test cases are defined
using the TEST macro.
TEST(AdditionTest, NegativeNumbers) {
EXPECT_EQ(-1 + -1, -2); // Testing negative numbers
}
You should use ASSERT * for conditions that should never fail, causing the test to
terminate early, while EXPECT * allows for continued execution even if the
assertion fails.
2. Test Fixtures: For tests that need setup and teardown operations (e.g., creating
objects, opening files), Google Test provides test fixtures. Test fixtures define
common setup and teardown code that is shared by multiple tests in the same test
case.
TEST_F(MathTest, Addition) {
EXPECT_EQ(a + b, 15);
}
TEST_F(MathTest, Subtraction) {
EXPECT_EQ(b - a, 5);
}
In the above example, MathTest is a test fixture with SetUp() code that initializes a
349
Once you have written your unit tests using Google Test, you can run them in the same
way as any other CMake-based tests.
1. Build the Test Executable: First, ensure that your tests are compiled by running the
following command:
cmake --build .
2. Run the Tests with CTest: After the tests are built, you can run them using ctest
or directly by running the executable. To run the tests using ctest, simply execute:
ctest
Or run the tests directly from the command line by executing the test executable
(e.g., ./test example).
If you wish to run only a specific test case or test, you can specify the name using the
-R flag:
ctest -R AdditionTest
Google Test also integrates seamlessly with Google Mock, another open-source library by
Google used for creating mock objects in unit tests. Google Mock allows you to mock
350
dependencies in your unit tests, making it easier to isolate the unit under test and verify
interactions with dependencies.
If you need to mock objects or functions, you can include Google Mock in the same way
as Google Test and use it for more advanced unit testing scenarios.
9.4.6 Conclusion
Unit testing with Google Test is an essential part of ensuring the correctness of your C++
code. By integrating Google Test with CMake and ctest, you can automate the process
of writing, running, and reporting unit tests. Google Test provides a wealth of features,
including assertions, test fixtures, and powerful mocking capabilities, making it an
excellent choice for testing complex C++ projects.
With Google Test and CMake, you can establish a strong testing foundation for your C++
projects, leading to higher code quality, easier debugging, and more reliable software. In
the next sections, we will explore how to enhance your testing workflow with Continuous
Integration and more advanced testing strategies.
351
One of the primary goals of running tests in a project is to gather valuable feedback on the
code's correctness, stability, and performance. Simply running tests isn't enough; you need
to be able to analyze the results effectively and generate comprehensive reports that
provide insights into the health of your project. In this section, we will explore how to
generate test reports using ctest and how to analyze the results to improve the quality of
your project.
By the end of this section, you will understand how to generate various types of test
reports, customize the reporting output, and interpret the results to take action on any
issues in your project.
Test reports are crucial for understanding the success or failure of individual tests, test
suites, or the entire test suite. Detailed reports help developers:
• Investigate the causes of failures by providing detailed logs and error messages.
• Track trends over time to see if code changes introduce regressions or new issues.
• Integrate testing results into Continuous Integration (CI) and build systems to
automate monitoring.
Effective test reports go beyond just indicating whether tests passed or failed—they
provide necessary context, detailed logs, and other diagnostic information that allow
teams to respond promptly to any issues.
352
By default, ctest provides a summary output with the results of all tests executed. The
simplest output looks like this:
While this provides basic information about whether the test passed or failed, more
detailed reports are often required to understand why tests fail, which tests failed, and how
to fix them.
To get more detailed output in the terminal, use the -V (verbose) flag:
ctest -V
This will show the complete output of each test, including any std::cout,
std::cerr, or other diagnostic information from the test run. This is useful for seeing
detailed logs, especially when tests fail and you need to troubleshoot the issue.
For more advanced reporting and integration with other tools, ctest can generate XML
reports. These reports can be processed and parsed by Continuous Integration (CI)
systems, test coverage tools, or other external applications that track the health of your
project.
• -T test: This specifies that we are running the tests and generating reports.
• --output-on-failure: This ensures that the output is shown only when a test
fails, keeping the report focused and concise.
• -D Experimental: This flag helps categorize the results as ”Experimental” for
easier tracking in a CI system.
After running the tests, ctest will generate a file called CTestTestfile.cmake that
contains the XML-formatted test results. The XML file contains detailed information
about each test, such as:
<?xml version="1.0"?>
<testsuite name="MyTestSuite" tests="1" failures="0" errors="0"
,→ skipped="0" timestamp="2023-02-04T12:00:00">
<testcase name="test_example" time="0.01">
<failure message="Test failed because of XYZ reason">Stack trace
,→ here</failure>
</testcase>
</testsuite>
354
This XML file can be integrated into build systems or CI tools like Jenkins, GitLab CI, or
Travis CI, which can automatically parse the results, track trends, and report on failures
and successes.
Most modern Continuous Integration (CI) tools (such as Jenkins, GitLab CI, Travis CI,
or CircleCI) can consume the XML reports generated by ctest. Integrating Google
Test and ctest into your CI pipeline allows you to automate the testing and reporting
process, ensuring that your codebase remains stable as you make changes.
Here's how to set up ctest reporting with popular CI tools:
• Jenkins Integration
To integrate ctest with Jenkins, you need to:
1. Set up a Jenkins job to build your CMake project.
2. In the
”Post-build Actions”
section, configure Jenkins to parse the test results:
– Choose ”Publish JUnit test result report”.
– Point Jenkins to the XML file generated by ctest (e.g.,
CTestTestfile.xml).
Jenkins will automatically parse the XML file, display the results on the Jenkins
dashboard, and notify you of any failing tests.
• GitLab CI Integration
For GitLab CI, you can use the JUnit format to parse ctest results. In your
.gitlab-ci.yml file, you can configure the ctest output to be saved as a
JUnit XML file:
355
test:
script:
- cmake --build .
- ctest -T test --output-on-failure -D Experimental
artifacts:
paths:
- CTestTestfile.xml
allow_failure: false
GitLab will then display the results in a well-formatted test report in the CI
dashboard.
• Travis CI Integration
For Travis CI, you can configure the .travis.yml file to run ctest and store
the results:
script:
- cmake --build .
- ctest --output-on-failure -T test -D Experimental
Travis will automatically display the test results in the build log, and you can use the
generated XML for further processing.
Once your test results are generated, it's time to analyze them and understand the health of
your project.
– Passed Tests: Indicate that the functionality works as expected. These tests are
a sign that your project is functioning correctly for the covered scenarios.
– Failed Tests: Indicate that the expected behavior was not met. You need to
investigate the failure reason, often by reviewing logs and error messages.
Common causes of test failures include logic errors, regression bugs, or
misconfigurations in your build or test environment.
– Skipped Tests: Tests may be skipped due to missing dependencies, incorrect
configurations, or platform-specific issues. If too many tests are skipped, it’s
essential to investigate why and fix the issues to ensure full test coverage.
– New failures: Are failing tests occurring more frequently after recent changes?
This may signal regressions.
– Test coverage: Are all areas of your codebase adequately tested? If certain
modules or functions lack tests, it’s time to write new tests.
– Test stability: Are tests passing consistently, or are there intermittent failures?
Intermittent failures may indicate issues with the environment or test setup.
By tracking these trends, you can ensure that your project remains in a stable state
and catch issues early before they escalate.
• Advanced Report Analysis with External Tools
While ctest provides a simple XML format, for more complex analysis, you can
integrate external tools like SonarQube for code quality and test coverage reports,
or Coveralls for tracking test coverage trends.
Additionally, advanced visualization tools like Allure can help you generate more
human-readable reports from the raw ctest XML files. These tools can enhance
357
your reporting system and make it easier for developers and stakeholders to analyze
the results.
9.5.6 Conclusion
Generating test reports and analyzing results is a crucial part of a successful testing
process. By using ctest, you can generate detailed XML reports that integrate with CI
systems and external tools, providing a comprehensive view of your project's health. By
effectively analyzing the test results, you can catch regressions early, improve test
coverage, and ensure your project remains stable as it evolves.
In the next section, we will explore best practices for managing and structuring tests in
large projects and dive deeper into automated testing strategies that help maintain code
quality over time.
Chapter 10
When developing C++ projects, it’s not enough to just build them—at some point, you’ll
want to package them for distribution, deployment, or sharing with others. This is where
CPack comes in. CPack is a powerful, flexible tool integrated into CMake that facilitates
the creation of installation packages for various platforms and formats. Whether you're
distributing your software as binaries, source code, or installers, CPack simplifies the
packaging process and ensures that your project can be shared with others with minimal
friction.
CPack is a packaging system that is tightly integrated with CMake, designed to create
distribution packages of your project. Once you’ve used CMake to configure and build
your project, CPack takes over and handles the creation of packages. These packages can
358
359
then be shared and installed on different systems, making it a crucial tool for developers
who need to provide easy access to their software.
CPack supports various packaging formats such as .zip, .tar.gz, .rpm, .deb,
.dmg, and .msi, among others. It can generate platform-specific installers that help
automate the process of installing software, ensuring that all necessary files are placed in
appropriate directories and that any necessary environment setup is done.
For many developers, packaging a project can be a tedious and error-prone process.
Manual creation of installers, configuration of paths, and ensuring that all dependencies
are bundled correctly can be time-consuming. This is especially true when your software
needs to be distributed across different platforms (Windows, Linux, macOS) and
packaging formats.
CPack streamlines this process by providing a standardized way of creating and managing
packages. Instead of having to manually configure packaging for each target platform,
CPack automates this, saving you significant time and effort. Furthermore, because CPack
is part of CMake, you don’t need to learn a new tool or set up a complex build system. It
integrates seamlessly with your existing CMake configuration files, which reduces
overhead and simplifies maintenance.
The role of CPack is to handle the final step in the CMake-based project build process:
creating distributable packages. After you’ve used CMake to configure your project and
run the build process (compiling your source code, linking libraries, etc.), CPack helps
create the final deliverables. CPack can package everything from binaries and
documentation to configuration files, ensuring your project is ready for deployment.
360
1. Configuration: First, CMake is run to configure the project. This step defines how
the project will be built, what source files to include, which dependencies to link,
and so on.
2. Build: Once the configuration is complete, you can use CMake to build the project.
This involves compiling source files, running tests, and creating any necessary
artifacts, such as libraries or executables.
3. Packaging: After the build process, CPack takes over. CPack will use the
instructions provided in your CMake configuration to package the project into a
distribution format of your choice (e.g., .deb, .rpm, .dmg, .zip, or .msi).
4. Installation: If you choose to distribute an installer package, CPack can also
generate an installer that makes the installation process easier for users. This
installer can automatically detect and configure the required paths and dependencies.
4. Ease of Use: Creating a package with CPack is simple. After configuring your
project with CMake, you can generate the package with a single command: cpack.
This simplicity makes it accessible to both novice and advanced developers.
5. Extensibility: CPack can be extended with custom scripts and commands to
accommodate more complex packaging scenarios. Whether you need to create
specialized installation routines or include custom build steps, CPack gives you the
freedom to extend its functionality.
6. Consistency: Because CPack is part of CMake’s ecosystem, it ensures consistency
across different projects. Once you’re familiar with CMake, you don’t have to learn
a new packaging system for each of your projects.
Getting started with CPack is straightforward if you already have a CMake-based project.
In fact, most of the setup involves adding a few additional lines to your existing
CMakeLists.txt file. In this chapter, we’ll walk through a detailed example of how to
configure and use CPack to package your project, including best practices for creating
installer packages and distribution archives.
362
Once you have configured your project with CMake and are ready to distribute it, CPack
provides a variety of options for packaging your software. These packages can be
generated in formats suitable for different platforms, making it easy for users to install
your software on various operating systems. In this section, we’ll cover how to use CPack
to create common installation package formats: .deb for Debian-based Linux
distributions, .rpm for Red Hat-based Linux distributions, .msi for Windows, and
.tar.gz for source code distribution and Linux environments.
1. Add the CPack module to your CMakeLists.txt: Make sure that the
CPACK GENERATOR is set to include .deb.
363
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name
,→ <[email protected]>")
set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6")
3. Generate the package: After configuring your project with CMake and
building it, run the following command to create the .deb package:
cpack -G DEB
This command will produce a .deb file that can be installed on any
Debian-based system using dpkg:
set(CPACK_GENERATOR "RPM")
2. Specify package metadata: Just like with .deb packages, you can specify
metadata for the .rpm package.
set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
3. Generate the package: Once you have configured your project, use the
following command to create the .rpm package:
cpack -G RPM
This will produce an .rpm package that can be installed with the rpm tool:
set(CPACK_GENERATOR "MSI")
2. Specify package metadata: For .msi packages, you can define various
installer properties, such as the installer’s title, the company’s name, and the
default installation directory.
set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
set(CPACK_MSI_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\your-project-name")
3. Generate the package: After running CMake and building the project, create
the .msi package by running the following:
cpack -G MSI
The output will be an installer .msi file that users can execute to install your
software. The installer will guide users through the installation process,
ensuring that files are placed in the correct directories and dependencies are
handled properly.
3. Packaging for Source Code: .tar.gz
In addition to binary packages, you might want to distribute the source code of your
project. The .tar.gz format is a common choice for source code distribution on
Linux and macOS systems. This format is widely recognized and allows users to
easily unpack the source code and build it manually.
366
set(CPACK_GENERATOR "TGZ")
2. Configure the package information: Similarly to the binary packages, you can
specify the project name, version, and description.
set(CPACK_PACKAGE_NAME "your-project-name")
set(CPACK_PACKAGE_VERSION "1.0.0")
set(CPACK_PACKAGE_DESCRIPTION "A brief description of your
,→ project")
3. Generate the package: After running CMake and building the project, you can
create the .tar.gz source package by running:
cpack -G TGZ
Users can then navigate into the extracted directory and build the project
manually.
CPack simplifies the process of creating installation packages for different platforms,
allowing you to focus on your project rather than worrying about packaging
complexities. By supporting multiple formats (.deb, .rpm, .msi, .tar.gz),
CPack ensures that your software can be distributed across various environments and
is easy to install for end users.
Each packaging format has its own use cases and advantages, so the format you
choose will depend on your target platform and the needs of your users. CPack’s
flexibility and ease of use make it an invaluable tool in the CMake ecosystem for
managing software distribution.
368
While CPack makes it easy to package your project, fine-tuning the packaging process
requires further customization. This is where CPackConfig.cmake comes into play.
This file provides an additional level of control over the packaging configuration, allowing
you to customize various aspects of the package creation process beyond the basic CMake
and CPack configuration. By configuring CPackConfig.cmake, you can define how
your package will behave during the packaging process, set custom values for the package
metadata, and handle complex packaging scenarios with ease.
When you invoke CPack via the cpack command, it first looks for
CPackConfig.cmake to determine any specific customizations for packaging. If this
file is not found, CPack will proceed with default settings defined in your
CMakeLists.txt.
1. Create CPackConfig.cmake:
In your project’s root directory (or build directory), create a file named
CPackConfig.cmake. Here’s an example of a basic CPackConfig.cmake
file:
# CPackConfig.cmake
include(CPack)
# CMakeLists.txt
project(YourProject)
3. Run CPack:
Once you've configured the CPackConfig.cmake file, you can run CPack as
usual:
cpack
This will generate the package according to the settings you specified in the
CPackConfig.cmake file.
371
There are many customizable options you can set in CPackConfig.cmake. Some of
the most commonly used options include:
1. Package Metadata
You can define several key attributes for your package, such as name, version,
description, vendor, and contact information. This metadata will be included in the
final package and can be useful for users or for distributing your software.
• CPACK PACKAGE NAME: The name of your package.
• CPACK PACKAGE VERSION: The version of your package.
• CPACK PACKAGE DESCRIPTION: A short description of your project.
• CPACK PACKAGE VENDOR: The vendor name or company.
• CPACK PACKAGE CONTACT: A contact email address.
Example:
set(CPACK_PACKAGE_NAME "MySoftware")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION "An awesome software package")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_CONTACT "[email protected]")
2. Package Generators
You can specify the format(s) in which you want your package to be generated.
CPack supports several formats, such as .deb, .rpm, .tar.gz, .zip, .msi,
and more. By defining the CPACK GENERATOR, you can generate one or multiple
package formats.
Example:
372
set(CPACK_GENERATOR "TGZ;DEB;RPM")
This will generate .tar.gz, .deb, and .rpm packages when CPack is invoked.
3. Installation Prefix
The installation prefix defines the root directory where your package will be installed
on the user’s system. This can be set to different values depending on the packaging
format. For instance, for Unix-based systems, it’s common to use /usr/local.
Example:
set(CPACK_INSTALL_PREFIX "/usr/local")
Example:
set(CPACK_MSI_PACKAGE_INSTALL_DIRECTORY "C:\\Program
,→ Files\\MySoftware")
5. Package Dependencies
For .deb and .rpm packages, you can specify package dependencies to ensure that
the package installer automatically installs the necessary libraries or packages before
the software can be installed.
Example:
373
Example:
set(CPACK_INSTALL_CMAKE_PROJECTS "path/to/your/project;ALL")
The CPackConfig.cmake file can be used for advanced customizations, such as:
• Custom post-installation scripts: You can write scripts that run after installation,
such as updating configuration files or setting environment variables.
When packaging software, one of the most significant challenges is ensuring compatibility
across different operating systems. This is especially true when working with C++
projects, which are often designed to run on multiple platforms. CPack, being part of the
CMake ecosystem, offers powerful features that enable you to create installation packages
that work seamlessly across Windows, Linux, macOS, and other Unix-like systems. In
this section, we will explore the best practices and techniques for supporting multiple
operating systems with CPack, ensuring that your project can be distributed and installed
with ease on different platforms.
Each operating system has its own package management system, installation directories,
system paths, and conventions. For instance:
• Windows uses .msi or .zip files, where installation often requires creating
registry entries or handling system-specific directories like Program Files.
• Linux uses package formats like .deb or .rpm, which rely on package managers
like dpkg or rpm. Installation paths and dependencies are managed differently
compared to Windows.
• macOS often uses .dmg or .pkg files for installation, with different conventions
for system paths and directory structures.
set(CPACK_GENERATOR "DEB;RPM;TGZ")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "/usr/local/MyProject")
else()
message(FATAL_ERROR "Unsupported platform")
endif()
In this example, we use if, elseif, and else to specify different package
formats and installation directories depending on the detected platform. CPack will
automatically adjust and generate the appropriate package for each platform during
the packaging process.
2. Platform-Specific Configuration Files
For complex projects, you may need to create additional configuration files that are
tailored to the specifics of each operating system. These files can help you set
platform-specific settings, include or exclude files, and define behavior that differs
across platforms.
# In CMakeLists.txt
if(WIN32)
set(CPACK_CONFIG_FILE "CPackConfigWindows.cmake")
elseif(APPLE)
set(CPACK_CONFIG_FILE "CPackConfigMac.cmake")
elseif(UNIX)
set(CPACK_CONFIG_FILE "CPackConfigLinux.cmake")
378
endif()
if(WIN32)
set(CPACK_PACKAGE_DEPENDS "msvcrt")
elseif(APPLE)
set(CPACK_PACKAGE_DEPENDS "libc++")
elseif(UNIX)
set(CPACK_PACKAGE_DEPENDS "libc6, libstdc++6")
endif()
This allows CPack to package the correct dependencies for each platform, ensuring
that users have everything they need when installing your software.
Each operating system has preferred package formats, so it’s essential to configure
CPack to generate appropriate packages for each platform. CPack supports several
popular formats for each operating system, and you can specify multiple formats in
the CPACK GENERATOR variable.
• Windows: .msi, .zip
• Linux: .deb, .rpm, .tar.gz
• macOS: .dmg, .pkg, .tar.gz
if(WIN32)
set(CPACK_GENERATOR "MSI;ZIP")
elseif(APPLE)
set(CPACK_GENERATOR "TGZ;DMG")
elseif(UNIX)
set(CPACK_GENERATOR "DEB;RPM;TGZ")
endif()
This approach allows you to generate different package formats for different
platforms while still using the same CMake project.
5. Cross-Compiling and Building for Multiple OSes
For some cases, you may want to cross-compile your project for different platforms.
CMake and CPack provide support for cross-compiling, allowing you to build and
package software for one platform from another. This can be useful when
developing for multiple operating systems but only having access to a single
development machine.
380
Supporting multiple operating systems for your CMake-based project is a powerful feature
that allows you to distribute your software to a wider audience. CPack simplifies this task
by providing robust configuration options for various platforms. By using conditional
logic, platform-specific configuration files, handling dependencies per OS, and generating
appropriate package formats, you can ensure that your project is well-packaged for a
variety of platforms.
To achieve full cross-platform compatibility, careful planning and testing are key. By
following the best practices discussed in this section, you can create a seamless packaging
381
process that works across all major operating systems, allowing your users to install and
use your software effortlessly, no matter their environment.
382
Once your project is packaged into a distributable format, the next step is ensuring that it
reaches the end users. Distribution is a critical phase in the lifecycle of a software project,
as it involves making the packaged software available to those who need it. CPack
simplifies this process by generating a variety of package formats that cater to different
operating systems, and it also integrates well with different distribution channels. In this
section, we will explore the key considerations and best practices for distributing your
packaged projects to end users.
The way you distribute your software largely depends on the target audience, the
platforms you are targeting, and the distribution channels available. There are several
common methods for distributing packaged projects:
3. App Stores and Package Managers: On macOS, Windows, and Linux, app stores
(e.g., the Mac App Store, Microsoft Store, Snap Store) provide a more structured and
383
1. Manual Distribution
For smaller-scale or internal projects, manual distribution may be the easiest and
most cost-effective method. After packaging the project using CPack, you can
upload the installation files to a file server, website, or cloud storage service like
Google Drive, Dropbox, or Amazon S3. Users can then download the relevant
installation files based on their operating system and manually install the software.
1. Package the Project: Use CPack to generate the installation files in the desired
formats (e.g., .zip, .msi, .deb, .rpm, .tar.gz).
2. Upload the Packages: Upload the generated files to your distribution platform.
This could be a personal website, an FTP server, or cloud storage.
3. Share the Download Links: Share the direct links to the package files with
your users via email, on your website, or through a public-facing repository.
4. Provide Installation Instructions: Depending on the format, provide
installation instructions for the users (e.g., running an .msi installer on
Windows, using dpkg or apt for Debian-based Linux distributions, or
extracting .tar.gz archives for macOS and Linux).
384
Distributing via repositories is a highly automated process and is ideal for ensuring
your users always have access to the latest versions. However, it often requires
meeting specific packaging standards and may involve submission processes that
take time to complete.
1. Prepare the .dmg File: Use CPack to generate a .dmg file for macOS.
386
2. Sign the Package: Code-signing is required by the Mac App Store for security
purposes. You’ll need a valid Apple Developer certificate to sign the .dmg file.
3. Submit to the Mac App Store: Upload the signed .dmg file to the App Store
for review and release.
Distributing through app stores provides high visibility and ensures that your
software follows platform-specific guidelines. However, the submission process can
be rigorous, and your software must meet stringent standards.
1. Configure Your CI/CD Pipeline: Set up your CI/CD pipeline to run CMake
and CPack whenever code is committed to your repository.
2. Build and Package the Project: Each time a change is pushed, the pipeline
will automatically compile the project, run tests, and generate the appropriate
installation packages using CPack.
3. Distribute the Packages
: Depending on your setup, the pipeline can:
• Upload the packages to a server or cloud storage.
• Push the packages to software repositories (e.g., APT, YUM, Snap Store).
• Submit the packages to app stores for review.
Automating the distribution process ensures that your users always receive the latest
387
version without manual intervention. This method is particularly useful for projects
that have frequent updates.
5. Digital Signing and Security Considerations
When distributing software, especially in professional environments, security is an
important consideration. Digital signing of your packages is crucial for ensuring the
authenticity of the software and protecting users from malicious tampering. Many
operating systems and distribution platforms require software to be digitally signed.
Signing Packages:
• Windows: Sign .msi and .exe packages using tools like SignTool to
ensure that users trust the source of the software.
• macOS: Sign .dmg and .pkg files using your Apple Developer certificate.
• Linux: Sign .deb and .rpm packages using GPG or other signing methods.
By signing your packages, you provide users with confidence that the software they
are installing is genuine and has not been altered.
Distributing your software effectively is key to reaching your target audience and ensuring
smooth installations. By using the appropriate distribution methods—manual distribution,
software repositories, app stores, or CI/CD automation—you can make it easy for your
users to obtain and install your project. Additionally, security practices such as digital
signing are crucial to protect both the software and its users.
Whether you are distributing through a public repository, an app store, or via automated
pipelines, CPack and CMake provide the tools you need to streamline the distribution
process and ensure that your packaged project reaches its users efficiently and securely.
Chapter 11
In this section, we will explore how to integrate CMake with GitHub Actions to automate
the process of building and testing C++ projects. GitHub Actions provides an easy-to-use
platform for Continuous Integration (CI) and Continuous Deployment (CD), which is
essential for modern software development, especially when collaborating in teams and
managing large codebases. By combining CMake's flexibility with GitHub Actions'
automation features, you can significantly improve your workflow, ensuring that your
code is always built and tested under different environments and conditions.
*
GitHub Actions is an automation tool integrated into GitHub that allows you to define
workflows to build, test, and deploy your code. A workflow is made up of one or more
388
389
jobs, which can run concurrently or sequentially. Each job consists of a series of steps,
where each step performs a specific action, such as setting up dependencies, building the
project, running tests, or deploying the application. These workflows are defined in YAML
files, stored in the .github/workflows directory of your repository.
CMake is a widely used build system for C++ projects that provides flexibility and
scalability. Integrating CMake with GitHub Actions can help automate the following
tasks:
• Automated Builds: Build your C++ projects on every push to your repository,
ensuring that the code is always in a buildable state.
• Cross-platform Testing: GitHub Actions allows you to run your tests across
different operating systems (Linux, macOS, and Windows) to ensure portability.
• Integration with External Services: You can easily connect to services like
coveralls, codecov, and others for code coverage reporting.
To get started, you need to create a GitHub Actions workflow configuration file for your
CMake project. This YAML file defines the steps for the entire build and test process.
Here’s a detailed guide on how to set up GitHub Actions for a CMake-based C++ project.
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
# Step 1: Checkout code
- name: Checkout code
uses: actions/checkout@v2
run: |
mkdir build
cd build
cmake ..
cmake --build .
• Run tests: Finally, we run the tests using ctest to ensure everything is
working as expected.
2. Configuring CMake for GitHub Actions
In order to ensure that CMake works correctly on GitHub Actions, there are a few
important considerations:
• Build Directory: In CI/CD pipelines, it is standard practice to create a separate
build directory to keep the source tree clean. This is done with the command
mkdir build followed by cd build to change into the directory before
running CMake.
• CMake Version: You can specify the required version of CMake that is
compatible with your project. It is crucial to use the version of CMake that
works best for your project’s requirements.
• Caching Build Dependencies: To speed up the build process, you can use
caching mechanisms provided by GitHub Actions. This is useful for
dependencies or CMake configurations that don’t change frequently.
Here is an example of how to enable CMake cache:
This cache step ensures that the build dependencies and CMake configuration are
393
One of the key benefits of using GitHub Actions with CMake is that it supports running
workflows on different operating systems and environments. GitHub Actions supports
ubuntu-latest, windows-latest, and macos-latest. You can run the same
workflow across all these environments to ensure cross-platform compatibility.
jobs:
build:
runs-on: ubuntu-latest
strategy:
394
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up CMake
uses: actions/setup-cmake@v1
with:
cmake-version: '3.21.2'
- name: Configure and Build
run: |
mkdir build
cd build
cmake ..
cmake --build .
- name: Run tests
run: |
cd build
ctest --output-on-failure
This example uses a matrix strategy, which allows you to run the same steps on multiple
operating systems concurrently.
While the example workflow above covers basic CI/CD tasks, you can extend your
workflow with more advanced features:
• Deploying to different platforms: For example, using GitHub Actions to build and
deploy your C++ application to cloud platforms, such as AWS, Azure, or Google
395
Cloud.
• Custom Docker images: Running your builds inside Docker containers to ensure
consistency across different development environments.
• Notification and Slack Integration: You can add steps to notify your team of build
status via Slack or other communication platforms.
11.1.6 Conclusion
Integrating CMake with GitHub Actions allows for a fully automated and streamlined
development workflow. From building and testing to deploying, this integration improves
efficiency and reduces human error in the development lifecycle. By using GitHub
Actions’ powerful CI/CD features, along with the flexibility and portability of CMake,
you can ensure that your C++ projects are always ready for production, no matter what
changes are made to the codebase.
396
In this section, we’ll delve into how to integrate CMake with GitLab CI/CD to automate
your build, test, and deployment processes for C++ projects. GitLab CI/CD is a powerful,
flexible, and scalable continuous integration tool that provides a range of features,
including pipelines, runners, and automatic deployment. Combining GitLab’s CI/CD
pipeline with CMake's versatility as a build system will enable you to manage and
maintain high-quality code with ease.
What is GitLab CI/CD? GitLab CI/CD is a feature of GitLab that helps automate the
process of software development. It allows you to automatically test, build, and deploy
your projects using pipelines and jobs. A GitLab CI/CD pipeline consists of stages, and
each stage has one or more jobs. Jobs can run on GitLab Runners, which are the machines
that execute the CI/CD jobs defined in the .gitlab-ci.yml file.
Why Use CMake with GitLab CI/CD? CMake provides a standardized and flexible
method for managing build configurations in C++ projects. By integrating CMake with
GitLab CI/CD, you gain the following benefits:
• Automated Builds: Automatically build and test your project with every code
change or merge request.
• Efficient Collaboration: By automating builds and tests, GitLab CI/CD ensures that
your project remains in a consistent and functional state for all contributors.
397
• Scalability: GitLab CI/CD is suitable for both small and large teams. As your
project grows, you can scale your CI/CD pipeline to support more complex
workflows.
Setting Up GitLab CI/CD for CMake Projects To use GitLab CI/CD with CMake,
you need to create a .gitlab-ci.yml file, which defines your CI pipeline. This
YAML file specifies the jobs, stages, and configuration for the build and test process.
stages:
- build
- test
paths:
- build/
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- build/
- .cache/
build_module_a:
stage: build
script:
- cmake -S module_a -B build/module_a
- cmake --build build/module_a
build_module_b:
stage: build
script:
- cmake -S module_b -B build/module_b
- cmake --build build/module_b
• Uploading Test Results: You can upload test results from your ctest run as
GitLab artifacts. This allows you to view the test results directly within GitLab.
Example of saving and uploading test results:
401
test:
stage: test
script:
- cd build
- ctest --output-on-failure
artifacts:
reports:
junit: build/test-results.xml
• Code Coverage: To ensure your tests cover enough of the code, you can use
tools like gcov (for GCC) or lcov to generate code coverage reports. You can
then upload these reports to GitLab to visualize the code coverage over time.
Example of generating and uploading code coverage:
test:
stage: test
script:
- cd build
- ctest --output-on-failure
- lcov --capture --directory . --output-file coverage.info
- lcov --remove coverage.info '/usr/*' --output-file
,→ coverage.info
- genhtml coverage.info --output-directory out
artifacts:
paths:
- out/
For example, you can create separate jobs for different platforms using Docker
images or GitLab runners:
build_linux:
stage: build
image: "ubuntu:20.04"
script:
- mkdir build
- cd build
- cmake -DCMAKE_BUILD_TYPE=Release ..
- cmake --build .
build_windows:
stage: build
image: "mcr.microsoft.com/windows/servercore:ltsc2019"
script:
- mkdir build
- cd build
- cmake -DCMAKE_BUILD_TYPE=Release ..
- cmake --build .
This setup defines separate jobs for building the project on Linux and Windows.
11.2.0.1 Conclusion
Integrating CMake with GitLab CI/CD automates and streamlines your development
pipeline. With GitLab’s powerful CI/CD tools, you can easily build and test your C++
projects on multiple platforms, ensure consistency, and provide valuable feedback to your
team in real-time. By leveraging caching, parallel execution, and integration with testing
tools, you can speed up the build process, improve collaboration, and maintain the
integrity of your project.
403
In this section, we will explore how to integrate CMake with Jenkins, one of the most
widely used open-source automation servers, to implement a robust CI/CD pipeline for
C++ projects. Jenkins allows for continuous integration and continuous delivery, enabling
you to automatically build, test, and deploy your software. Combining CMake's flexibility
and Jenkins' automation capabilities provides a powerful solution for managing and
maintaining C++ projects.
Integrating CMake with Jenkins provides several advantages for C++ projects:
• Automated Builds: Jenkins can automatically trigger builds when code changes are
pushed to a version control system like Git. This ensures that your project is always
up to date and functioning correctly.
404
• Cross-platform Builds: Jenkins allows you to configure jobs that run on different
operating systems (Linux, macOS, and Windows). By using CMake as your build
system, you can ensure that your code is portable and consistently built across all
platforms.
• Parallel Execution: Jenkins can run jobs concurrently, which speeds up build times,
especially for large projects with many components.
• Integration with Other Tools: Jenkins supports integration with other tools, such as
code quality analyzers, test frameworks, and deployment systems. You can easily
integrate tools like Clang, GCC, MSVC, or other compilers, as well as testing
frameworks like Google Test or Catch2.
• Scalability: Jenkins can be set up on a single machine or across multiple machines
(also known as Jenkins agents or slaves) to distribute build and test tasks, which is
particularly useful for larger teams or projects.
To get started with Jenkins and CMake, you need to configure Jenkins to use CMake as
the build system. This involves setting up Jenkins on a server, creating a Jenkins job or
pipeline for your CMake project, and ensuring that the required tools and dependencies
are installed on your Jenkins environment.
1. Install Jenkins: Download and install Jenkins from the official website
(https://www.jenkins.io/download/). Jenkins can be installed on
a variety of platforms, including Linux, macOS, and Windows.
405
• Make sure that your Jenkins server has the necessary build tools installed,
such as CMake, a C++ compiler (e.g., GCC, Clang, or MSVC), and any
required libraries.
• You can install CMake on your Jenkins server by using the package
manager for your operating system (e.g., apt on Ubuntu, brew on
macOS).
4. Build Steps:
• Add a build step to run a shell command (on Linux/macOS) or a batch
command (on Windows). The command will invoke CMake to configure
and build your project.
• Here’s an example shell script for Linux/macOS:
mkdir build
cd build
cmake ..
cmake --build .
• If you’re using Windows, you can use the following batch script:
mkdir build
cd build
cmake ..
cmake --build .
5. Post-build Actions:
• You can define post-build actions, such as publishing test results or
deploying the build artifacts.
• If you want to run tests, you can add a post-build step to execute the
407
ctest
ctest --output-on-failure
pipeline {
agent any
stages {
stage('Checkout') {
steps {
// Pull the latest code from the Git
,→ repository
git 'https://your-git-repository-url.git'
}
}
stage('Build') {
steps {
// Create build directory and configure with
,→ CMake
sh '''
mkdir build
cd build
cmake ..
cmake --build .
'''
}
}
stage('Test') {
steps {
// Run unit tests with CTest
sh '''
cd build
ctest --output-on-failure
'''
409
}
}
}
post {
success {
// Actions to take if the build succeeds, such as
,→ sending a notification
echo 'Build and tests completed successfully.'
}
failure {
// Actions to take if the build fails
echo 'Build or tests failed.'
}
}
}
Jenkins can also run jobs on multiple platforms using Jenkins agents or Docker
containers. This is especially useful if your project needs to be built on different
operating systems (Linux, macOS, and Windows).
• Jenkins Agents: You can set up agents (also known as slaves) that run on
different machines with different operating systems. This allows Jenkins to
execute builds on a variety of platforms.
• Docker Containers: You can use Docker to run builds in isolated environments
with specific dependencies and versions of CMake and the C++ compiler.
Jenkins can spin up containers automatically as part of the build process.
For example, a Jenkins job might look like this to run the build process inside a
Docker container:
pipeline {
agent {
docker { image 'ubuntu:20.04' }
}
stages {
stage('Build') {
steps {
sh 'mkdir build && cd build && cmake .. && cmake
,→ --build .'
}
}
}
}
This setup allows you to build your CMake-based project inside an Ubuntu Docker
container, ensuring consistency across all builds.
411
11.3.4 Conclusion
Integrating CMake with Jenkins creates a powerful CI/CD pipeline that automates the
building, testing, and deployment of C++ projects. Jenkins provides flexibility, scalability,
and a wide range of plugins, while CMake ensures a standardized and portable build
system. By setting up Jenkins to work with CMake, you can ensure that your project
remains in a consistent and functional state, allowing you to focus more on coding and
less on manual build processes.
412
In this section, we’ll explore how to integrate CMake with cloud platforms for automating
builds and continuous integration (CI). Cloud platforms such as AWS (Amazon Web
Services), Google Cloud Platform (GCP), and Microsoft Azure provide scalable,
cost-effective environments to run builds, tests, and deployments. By utilizing these
platforms, developers can offload the heavy lifting of building and testing, gain access to
scalable resources, and integrate CMake with various cloud-based CI/CD services.
The shift to cloud-based build environments is becoming increasingly popular for teams of
all sizes due to the flexibility, scalability, and ease of management offered by cloud
platforms. By automating CMake-based builds on the cloud, you can ensure that your C++
projects are always up-to-date, tested, and ready for deployment.
There are several compelling reasons to automate your builds on cloud platforms:
• Scalability: Cloud platforms allow you to scale up or down depending on the build
demand. You can handle increased build load during peak times without the need for
physical infrastructure.
• Cost Efficiency: With cloud services, you only pay for the resources you use. This
means you can run builds in parallel or use more powerful instances for short periods
without maintaining an expensive on-premise infrastructure.
• Global Availability: Cloud platforms offer data centers worldwide, allowing you to
run builds on different operating systems and regions to ensure your CMake projects
are cross-platform and globally distributed.
• Flexibility: Cloud platforms are highly customizable. You can define the resources
you need, automate provisioning with Infrastructure as Code (IaC), and integrate
various services and tools to enhance your CI/CD pipeline.
In the following sections, we'll explore how to configure cloud-based CI systems such as
AWS CodeBuild, Google Cloud Build, and Azure Pipelines to automate builds for
CMake-based C++ projects.
3. Build Environment:
• In the Environment section, choose a build image that supports your
CMake project. You can either use AWS-provided images or create a
custom Docker image.
• For a CMake-based C++ project, you can use an Amazon Linux or Ubuntu
image and install CMake, GCC, or other necessary compilers.
4. Build Specification:
• AWS CodeBuild uses a buildspec.yml file to define the build process.
Create a buildspec.yml file in the root of your repository. The
buildspec.yml file defines the steps involved in compiling your
CMake project, running tests, and storing artifacts.
Example of a buildspec.yml for a CMake-based project:
version: 0.2
phases:
install:
runtime-versions:
gcc: 10
cmake: 3.x
commands:
- echo Installing dependencies...
- yum install -y gcc-c++ make
pre_build:
commands:
- echo Preparing the build environment...
- mkdir build
- cd build
- cmake ..
build:
commands:
415
By using AWS CodeBuild, your CMake-based project will be built and tested in the
cloud with minimal setup. CodeBuild will handle scaling the build process
automatically based on the workload.
2. Automating Builds with Google Cloud Build
Google Cloud Build is a service that automates the building of code across multiple
platforms. It integrates well with Google Cloud’s ecosystem and can be used to build
CMake-based projects for C++ applications.
steps:
- name: 'gcr.io/cloud-builders/cmake'
id: 'Install dependencies'
args: ['--version']
- name: 'gcr.io/cloud-builders/cmake'
417
id: 'Configure'
args:
- '-Bbuild'
- '-H.'
- name: 'gcr.io/cloud-builders/cmake'
id: 'Build'
args:
- '--build'
- 'build/'
- name: 'gcr.io/cloud-builders/ctest'
id: 'Test'
args: ['--output-on-failure']
artifacts:
objects:
location: 'gs://YOUR_BUCKET_NAME/artifacts/'
paths:
- 'build/**'
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseCMake@1
inputs:
cmakeVersion: '3.x'
- script: |
mkdir build
cd build
cmake ..
cmake --build .
displayName: 'Build CMake Project'
- script: |
cd build
ctest --output-on-failure
displayName: 'Run Tests'
- publish: $(Build.ArtifactStagingDirectory)
artifact: drop
In this pipeline:
• trigger: Specifies that the pipeline is triggered on changes to the main
branch.
420
• pool: Defines the build agent (in this case, an Ubuntu-based agent).
• steps: Specifies the steps for building the CMake project and running tests.
4. Triggering Builds:
• Azure Pipelines will automatically trigger builds based on commits to the
repository. You can configure manual triggers or schedule builds at specific
times.
5. Monitoring and Logs:
• Logs for each build can be monitored in the Azure DevOps portal, and the
build summary provides detailed information about the success or failure of
each pipeline step.
11.4.2 Conclusion
Automating builds on cloud platforms such as AWS, Google Cloud, and Azure offers a
scalable, efficient, and cost-effective solution for managing CMake-based projects. By
integrating CMake with cloud-based CI/CD tools like AWS CodeBuild, Google Cloud
Build, and Azure Pipelines, you can take full advantage of the cloud's flexibility and
power to manage the build, test, and deployment workflows for your C++ projects.
Chapter 12
In this section, we will explore two powerful tools that can significantly speed up the
compilation process for C++ projects: ccache and distcc. These tools can be
integrated with CMake to optimize your build times, especially when working with large
codebases or frequent builds. By leveraging caching and distributed compilation, you can
greatly reduce the time it takes to build your project, making your development workflow
more efficient.
421
422
increases, which can significantly slow down development and testing cycles. To address
this issue, there are several optimization strategies you can use to speed up the build
process, including parallelization, incremental builds, and utilizing specialized tools for
caching and distributed compilation.
If these components remain unchanged, ccache can reuse the cached result,
significantly speeding up the build.
423
Benefits of ccache
1. Install ccache:
• On Linux:
2. Configure CMake to Use ccache: You can configure CMake to use ccache
by prepending the compiler commands with ccache. This can be done by
modifying your CMakeLists.txt or by setting environment variables.
To enable ccache for your CMake project, set the CXX and CC environment
variables to use ccache:
This ensures that ccache is used whenever CMake calls the compiler.
3. Verify ccache is Working: To check if ccache is working, you can monitor
the cache statistics by running the following command:
ccache -s
This will display the cache hit/miss statistics, showing how much time is saved
by reusing cached compilations.
2. distcc: Distributed Compilation
distcc is another tool designed to speed up compilation by distributing the work
across multiple machines. It allows you to distribute the compilation of source files
to different machines in a network, significantly reducing the overall build time. The
basic idea behind distcc is to offload the compilation of individual source files to
remote machines, which can be done in parallel. This is particularly useful when
building large projects with many files that can be compiled independently.
425
Benefits of distcc
• Significant speed-up for large projects: distcc can reduce build times
dramatically by distributing the compilation tasks across multiple machines.
• Scalability: You can scale your build system by adding more machines to the
distributed compilation pool, increasing the total available processing power.
• Transparency: distcc integrates seamlessly with the existing build system
and compilers (such as gcc and clang), requiring no changes to the code or
the build scripts.
• On Linux:
• On macOS:
distcc
distcc
compiler:
3. Configure CMake to Use distcc: Just as with ccache, you can configure
CMake to use distcc by setting the compiler to distcc gcc and distcc
g++.
You can add the following to your CMakeLists.txt:
Where <N> is the number of parallel jobs you want to run, and
<source file> is the file being compiled.
One of the most powerful ways to optimize your CMake-based build system is to combine
both ccache and distcc. ccache speeds up the build process by caching object files,
while distcc distributes the compilation workload across multiple machines. Together,
they can help you achieve faster builds by reducing both individual compilation time and
distributing the compilation across many machines.
To use both tools together, simply ensure that CMake is configured to use ccache and
distcc simultaneously. For example:
428
This setup allows you to benefit from both caching (for repeated builds) and distributed
compilation (for parallelizing the compilation process).
12.1.3 Conclusion
By integrating ccache and distcc into your CMake build system, you can
significantly speed up the compilation process for large C++ projects. ccache reduces
build times by caching object files and reusing them when possible, while distcc
distributes the compilation process across multiple machines to parallelize the work.
These tools can be easily integrated into existing CMake-based workflows with minimal
setup and can make a noticeable difference in build performance, especially for large,
complex projects.
429
In this section, we’ll explore Unity Builds, an optimization technique that can
significantly improve build performance for C++ projects. Unity Builds (also known as
Jack or Single Compilation Unit builds) offer a way to reduce compile time by grouping
multiple source files into a single file for compilation, thus reducing the overhead of
compiling individual files separately.
Unity Builds can be particularly useful in projects with a large number of source files,
where the cost of handling each file independently becomes a bottleneck in the build
process. While Unity Builds introduce some trade-offs (such as potential compilation
dependencies becoming more challenging to manage), they provide an effective way to
optimize CMake-based build systems and reduce compilation time.
In traditional C++ projects, each source file (.cpp) is compiled into an object file (.o or
.obj), and the compiler is invoked separately for each source file. This process can
become slow, particularly when dealing with projects that have a large number of source
files, where the overhead of invoking the compiler multiple times can add up quickly.
A Unity Build works by combining several source files into a single larger file before
invoking the compiler. By doing so, the compiler only needs to process one file, instead of
many smaller ones. This reduces the number of compiler invocations, minimizes the
overhead, and can lead to a significant reduction in build times. The Unity Build approach
works especially well for projects with many header files and dependencies, as it
minimizes the time spent on repeatedly processing them.
For example, instead of compiling each source file separately like this:
430
g++ -c file1.cpp
g++ -c file2.cpp
g++ -c file3.cpp
With Unity Builds, multiple source files are combined into a single large file (e.g.,
unity.cpp), and the compilation is done for this single file:
g++ -c unity.cpp
This can lead to a significant reduction in build time, particularly in large codebases.
1. Combining Source Files: You create a single ”unity file” (e.g., unity.cpp) that
includes multiple source files.
2. Compilation: This single unity file is compiled as if it were a regular source file.
3. Linking: Once compiled, the resulting object files are linked together to form the
final executable.
The key to Unity Builds is the mechanism for combining source files. Typically, you use a
preprocessor to concatenate multiple .cpp files into a single unity file. This can be done
automatically as part of the build process using CMake.
Despite their performance benefits, Unity Builds come with several trade-offs that should
be considered before implementing them:
432
• While compilation time is reduced, the link time might increase. With Unity
Builds, the linker may have to deal with larger object files that can take longer
to process, especially if the unity file is very large.
• Combining many source files into a single unity file can introduce compilation
dependencies that make it harder to track which source files depend on which
headers.
• It also may introduce issues with naming collisions, as multiple source files are
being compiled together. This can lead to conflicts in symbol names, which
must be resolved using techniques like #pragma once or #ifdef guards in
headers.
• Debugging can become more difficult when using Unity Builds, as it is harder
to isolate individual files and track errors to their source. Tools like gdb might
behave differently because of the combined nature of the source files.
• With Unity Builds, since multiple files are included in a single compilation unit,
the memory usage during the compilation process can be higher, as the compiler
has to hold larger amounts of data in memory at once.
To use Unity Builds in CMake, you will need to modify your CMakeLists.txt file to
automatically generate the unity files and compile them instead of individual source files.
433
CMake does not have native support for Unity Builds, but it’s easy to set up using a
custom function or macro.
Here’s how you can configure Unity Builds in CMake:
foreach(SOURCE ${SOURCES})
file(APPEND ${UNITY_FILE} "#include \"${SOURCE}\"\n")
endforeach()
This macro will create a unity.cpp file in the build directory by concatenating
the specified source files. You can call this macro in your CMakeLists.txt to
group the .cpp files you want to include in the unity build.
• Step 2: Apply the Unity Build Macro
Now, use the create unity build() macro to add your source files into a
single unity file:
file2.cpp
file3.cpp
)
In this example:
– We specify the list of source files we want to include in the Unity Build.
– The create unity build() macro combines them into a single
unity.cpp file, which will then be compiled by CMake as part of the build.
• Step 3: Optimize Unity Build Configuration
You can further optimize the Unity Build process by controlling the number of files
included in each unity file. For instance, grouping too many source files into a single
unity file can create excessively large object files that may lead to longer link times.
Instead, you can group files in smaller batches:
set(SOURCE_FILES_2
file3.cpp
file4.cpp
)
435
create_unity_build(${SOURCE_FILES_1})
create_unity_build(${SOURCE_FILES_2})
This method ensures that the unity files do not become too large, reducing potential
issues with link time or memory usage.
• Step 4: Control Unity Build Compilation in Debug/Release Modes
In some cases, you might want to enable Unity Builds only in certain build
configurations (e.g., in the release build but not in the debug build). You can
conditionally apply the Unity Build based on the build type:
This allows you to disable Unity Builds when debugging, where you may want to
retain faster link times and avoid issues with debugging large unity files.
12.2.6 Conclusion
Unity Builds are an effective optimization technique to speed up the compilation process
of large C++ projects. By grouping multiple source files into a single compilation unit,
Unity Builds reduce the overhead of invoking the compiler multiple times, leading to
faster builds. However, this technique comes with trade-offs, including the potential for
longer link times, increased memory usage, and debugging challenges.
Using CMake, Unity Builds can be easily implemented by generating unity files
dynamically and adjusting the number of files included in each unity build to optimize the
436
process. By carefully considering the size and complexity of the unity files and applying
conditional logic based on the build configuration, you can achieve faster build times
without compromising the quality of your project.
437
In this section, we will dive into Link Time Optimization (LTO), a powerful technique
that can reduce link times and improve the performance of your final executable. LTO
optimizes across translation units at the link stage, enabling the compiler to perform
interprocedural optimizations that are not possible during the individual compilation of
each source file. This section will explain how LTO works, how to enable it in CMake,
and the trade-offs involved in its use.
LLVM-based compilers like Clang) or a GCC intermediate file. Rather than producing
regular object files with machine code, the compiler produces these intermediate
representations, which can be optimized further by the linker.
When the linker runs, it takes the intermediate representations from all the object files and
applies optimizations, including:
These optimizations can result in better performance in terms of both execution speed and
binary size. Additionally, LTO can reduce link time by removing redundant code and
making the linking process more efficient.
1. Improved Performance:
• Function Inlining: Functions that are small or frequently called across multiple
source files can be inlined, eliminating the overhead of function calls.
• Dead Code Elimination: LTO can remove code that is never used, even if it
resides in different translation units.
• Better Instruction Scheduling: The compiler can optimize instruction
scheduling across the entire program, leading to better performance.
439
• While LTO can introduce an overhead in the linking phase, it can also speed up
the link process by eliminating unnecessary symbols and resolving function
calls across translation units more efficiently.
• The overall link time may be reduced because the linker can eliminate
redundant symbols and references, leading to fewer symbols to resolve.
4. Interprocedural Optimizations:
• LTO allows optimizations that require visibility into the entire program, such as
interprocedural constant propagation and cross-file inlining. These
optimizations are impossible with traditional, per-file compilation strategies.
5. Reduced Redundancy:
• By optimizing across translation units, LTO helps eliminate duplicate code that
may have been compiled separately in different translation units, leading to a
leaner program.
While LTO provides numerous benefits, there are trade-offs that need to be carefully
considered before enabling it:
• Compilation Time: The initial compilation with LTO can be slower because
the compiler produces intermediate representations (IR) instead of regular
object files.
• Link Time: While LTO can reduce redundant code and improve the linking
process, the link phase itself can become slower, especially in large projects
with many object files, as the linker has to process the entire program to apply
LTO optimizations.
2. Memory Usage:
• LTO requires the linker to keep all the intermediate representations in memory.
In large projects, this can lead to high memory usage during the linking process.
• Some linkers (such as gold and lld) are optimized for LTO and can reduce
the memory footprint during linking, but memory usage remains a
consideration.
3. Incompatibility with Some Debugging Tools:
• In some cases, LTO can interfere with debugging tools, as the compiler may
optimize away certain symbols or alter the structure of the binary in ways that
make debugging more difficult.
• Debugging with LTO-enabled binaries may be more challenging, as some
optimizations (e.g., inlining) can obscure the relationship between source code
and the compiled binary.
4. Compiler and Linker Support:
• LTO requires both the compiler and linker to support it. For example, if you're
using GCC or Clang, you need to ensure that both the compiler and the linker
support LTO.
• Some build systems and platforms may not fully support LTO or may have
specific quirks that make enabling it more difficult.
441
• The object files generated during the compilation stage may be larger than usual
because they contain intermediate representations, which can slow down the
overall build process if not handled efficiently.
This will enable LTO for both the C and C++ compilers, as well as during the linking
stage.
You might want to enable LTO only for release builds and leave it disabled for debug
builds (where faster compile times are often preferred). To enable LTO conditionally,
you can modify your CMakeLists.txt like this:
This ensures that LTO is only enabled when building the release version of your
project.
3. Step 3: Optimizing LTO with Linker Flags
When working with LTO, the linker needs to apply the optimizations to the entire
program, and this may require additional linker flags. If you are using GCC or Clang,
you can use the -fuse-ld=gold or -fuse-ld=lld linker flag to improve the
performance of the linking stage, as these linkers are optimized for LTO.
Alternatively, you can use the LLVM linker (lld), which is known to handle LTO
more efficiently in certain cases.
Not all compilers and platforms support LTO, so it’s important to check whether the
compiler supports LTO before enabling it. You can use CMake’s try compile()
function to check if LTO is supported on the current platform:
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-flto" HAS_LTO)
if(HAS_LTO)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -flto")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -flto")
else()
message(WARNING "LTO is not supported on this compiler")
endif()
This code snippet ensures that LTO is enabled only if the compiler supports it.
12.3.5 Conclusion
Link Time Optimization (LTO) is a powerful technique that can significantly reduce the
size of your executable and improve runtime performance by enabling optimizations that
span multiple translation units. While it introduces some trade-offs in terms of increased
memory usage and potentially slower link times, the performance benefits can be
substantial for large, complex C++ projects.
By enabling LTO in your CMake build system, you can easily leverage these
optimizations with minimal configuration. As with any optimization technique, it is
important to consider the specific needs of your project and evaluate the impact of LTO on
both build times and runtime performance. Experimenting with different linker options
and adjusting LTO settings based on the build type can help you achieve the best balance
between performance and build efficiency.
444
In this section, we will delve into the role of compilation flags in optimizing build
performance, specifically focusing on CMAKE CXX FLAGS RELEASE. These flags are
used to control the behavior of the C++ compiler when building in different configurations,
such as release or debug builds. By fine-tuning these flags, you can significantly impact
the performance of your C++ application, both during compilation and at runtime.
Understanding how to optimize compilation flags is crucial for achieving faster builds and
ensuring that your project runs efficiently in production environments.
Compilation flags are options passed to the compiler during the build process that control
various aspects of the compilation and optimization process. These flags can affect:
• Warnings: What kind of compiler warnings are emitted to help catch potential
issues in the code.
In CMake, these flags are typically defined in variables like CMAKE CXX FLAGS, which
445
affects all build types, or CMAKE CXX FLAGS RELEASE, which specifically influences
the compilation when the project is being built in release mode.
When building C++ projects for production (i.e., in release mode), the goal is to produce
the most optimized, efficient executable. By default, CMake applies certain flags to the
compiler, but these can be modified for better performance.
CMAKE CXX FLAGS RELEASE is a variable in CMake that holds the flags to be applied
when building in the release configuration. These flags can be used to instruct the
compiler to optimize code for performance, remove debugging information, and disable
certain features that are unnecessary for production builds. Customizing this variable
ensures that the release build of your application is as optimized as possible.
When configuring a C++ project for release, several compiler flags can be used to control
optimization behavior. These flags will vary depending on the compiler (GCC, Clang,
MSVC, etc.), but the following are common optimizations you can apply to improve the
performance of the release build.
Example:
LTO can significantly reduce the size of the generated binary and improve
performance by allowing more aggressive optimizations. However, as mentioned in
Section 3, LTO can increase link times and memory usage during the linking process,
so it is essential to test its impact on your build process.
3. Fast Math (-ffast-math)
The -ffast-math flag instructs the compiler to enable floating-point
optimizations that can lead to faster math operations at the cost of potentially less
strict conformance to the IEEE floating-point standard. This can be useful when
performance is a higher priority than absolute numerical accuracy.
Example:
447
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -ffast-math")
Use with caution: While this flag can make math-heavy programs run faster, it may
result in slightly less accurate results, especially in edge cases involving
floating-point precision.
4. Function Inlining (-finline-functions)
Inlining functions can speed up code execution by removing the overhead of
function calls. The -finline-functions flag enables the compiler to replace
small functions with their code at the call site. This is particularly effective for small
functions that are frequently called, as it can avoid the performance cost of function
calls.
Example:
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -finline-functions")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -funroll-loops")
448
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE}
,→ -fstrict-aliasing")
Caution: This flag assumes that the code adheres to the strict aliasing rule, so it can
lead to unexpected behavior if violated. It’s important to ensure that your codebase
correctly follows this rule if you decide to use this flag.
7. Optimization for Size (-Os)
While most release configurations prioritize performance, some situations demand
optimization for binary size, such as embedded systems or applications running on
resource-constrained devices. The -Os flag instructs the compiler to optimize for
size rather than performance.
Example:
This flag will reduce the size of the final binary by turning off some performance
optimizations that result in larger code, such as loop unrolling.
8. No Debug Information (-g0)
By default, the compiler generates debug information for release builds in some
setups. This can unnecessarily bloat the binary size. To reduce this, you can use the
-g0 flag to disable debug information for release builds.
449
Example:
If you do need to keep some level of debug information (for profiling, for example),
you can instead use -g with optimizations like -O3 and -flto to balance the
trade-offs.
When optimizing the CMAKE CXX FLAGS RELEASE variable, it is common to combine
several of the flags mentioned above. Here’s an example of how to combine flags for a
highly optimized release build:
12.4.5 Conclusion
In this section, we will explore how to use Ninja, a fast build system, to optimize the build
performance of your CMake-based C++ projects. Ninja is designed to be small, efficient,
and highly parallel, making it an excellent choice for speeding up build times, especially
in larger projects. We will cover the benefits of using Ninja, how to configure it within
CMake, and provide some tips on leveraging its strengths for faster builds.
Ninja is a small, low-level build system that was originally developed by Google. Unlike
traditional build systems like Make, Ninja focuses on being fast and optimized for parallel
builds. It reads a build description (usually a build.ninja file) that specifies the rules
for building the project and executes them efficiently, ensuring the shortest possible time
for building or rebuilding files that have changed.
Ninja works by focusing on the smallest possible operations to rebuild only the necessary
parts of the project, making it especially effective in large projects with complex
dependencies. It is designed to be used as a backend for higher-level build systems, such
as CMake, which generates the necessary build.ninja file based on your project
configuration.
The main reasons to use Ninja for building your CMake-based projects are:
1. Faster Builds:
452
• Ninja is optimized for speed. It minimizes overhead and executes tasks more
efficiently than traditional systems like Make, especially when dealing with
complex projects that require large numbers of files to be built.
• Ninja performs parallel builds more effectively by managing dependencies and
utilizing available CPU cores. This results in faster build times, especially on
multi-core systems.
2. Smaller Overhead:
• Ninja's minimalistic design means that it has less overhead compared to other
build systems. It doesn't have the complex features that may slow down other
build tools, such as Make or MSBuild.
• Ninja focuses solely on the job of building files, keeping the process as
streamlined and direct as possible.
3. Parallelism:
• Ninja’s parallel build capabilities are second to none. It determines the
dependencies between tasks and intelligently decides which tasks can run
concurrently. This is particularly useful when building large codebases, as
multiple processes can be executed simultaneously, fully utilizing modern
multi-core processors.
4. Incremental Builds:
• Ninja performs incremental builds extremely well. If only a small part of the
project has changed, Ninja will rebuild only the affected files, making the
rebuild process much faster than traditional systems.
5. Ease of Integration with CMake:
• Ninja is integrated directly with CMake, meaning it can be used with minimal
configuration. CMake has built-in support for generating Ninja build files,
allowing you to easily switch to Ninja without changing your project structure.
453
Using Ninja with CMake is relatively simple. Here’s a step-by-step guide to configuring
and using Ninja for faster builds.
– Windows:
* You can download the Ninja binary from the official Ninja website:
https://ninja-build.org/
* Alternatively, if you're using
Chocolatey
, you can install it with:
Navigate to your project’s root directory, and then run the following CMake
command:
cmake -G Ninja .
This command will configure your CMake project for a Release build and generate
the necessary build.ninja file for Ninja to use.
ninja
This will invoke Ninja to start the build process. Ninja will automatically determine
which files need to be rebuilt based on the dependencies specified in the
build.ninja file and execute the necessary commands. It will also execute tasks
in parallel when possible to speed up the build process.
To build a specific target, use:
455
ninja <target>
ninja my_target
ninja clean
ninja
This will remove all build artifacts and recompile everything from scratch, ensuring
a fresh build.
While Ninja’s default configuration is already optimized for speed, there are a few tips you
can follow to ensure you're getting the most out of your builds.
456
• Ninja automatically determines the number of jobs (parallel tasks) to run based
on the system’s CPU cores. However, you can manually specify the number of
parallel jobs with the -j flag:
ninja -j 8
This will instruct Ninja to run up to 8 tasks in parallel. You can adjust this number
based on the number of cores in your machine. For modern multi-core systems, this
can drastically reduce build time.
2. Enable Caching:
• CMake can generate a cache file that stores information about previously built
objects. This can speed up incremental builds by reusing previously compiled
object files instead of recompiling them.
• Ninja works excellently with Clang and GCC compilers, which are often much
faster than MSVC, especially when combined with the parallelism and
optimizations that Ninja brings.
For example, you can add extra flags or parameters that will be passed to Ninja
during the build process:
457
These flags will be included in the build instructions, optimizing your project further.
5. Use CMake Presets:
• With the introduction of CMake presets (CMake 3.19+), you can pre-define
build settings, including Ninja options. By creating a CMakePresets.json
file, you can easily switch between build configurations, making it even easier
to optimize your project for different platforms.
While Make has been the traditional choice for many developers, Ninja has significant
advantages when it comes to build performance. Here’s a quick comparison between
Ninja and Make:
12.5.6 Conclusion
Incorporating Ninja into your CMake workflow is one of the most effective ways to speed
up the build process. It provides a lightweight, parallelized, and incremental build system
that drastically reduces build times, especially for large projects. With its integration into
CMake, using Ninja is simple, and it provides an immediate improvement over traditional
systems like Make.
By configuring your CMake project to use Ninja and optimizing the compilation process
with appropriate flags, you can ensure faster, more efficient builds. Whether you're
developing a small application or a massive codebase, Ninja is an excellent tool to help
streamline your build process.
Chapter 13
In CMake, modules are an essential tool for managing dependencies, external libraries,
and various configurations within your build system. A custom module, like
FindMyLib.cmake, allows you to extend CMake's functionality by providing
user-defined logic for locating libraries, packages, or tools that aren't supported
out-of-the-box by CMake. This section dives into how you can write your own
FindMyLib.cmake module to simplify the management of external libraries in your
project.
459
460
the discovery and inclusion of the library into your build process. This ensures that your
build system remains flexible and portable across various systems and environments.
When dealing with external dependencies, CMake provides a set of predefined modules
(e.g., FindOpenSSL.cmake, FindBoost.cmake) that can automatically find
certain well-known libraries. However, when working with custom or lesser-known
libraries, writing your own module becomes necessary.
• The module will set up various variables that will contain the paths to the library
files once found. For example, it may set variables like
MyLib INCLUDE DIRS for the header files and MyLib LIBRARIES for the
binary files.
461
set(MyLib_INCLUDE_DIRS "")
set(MyLib_LIBRARIES "")
• The module will attempt to locate the library in common installation paths, such
as /usr/lib, /usr/local/lib, or any custom directories defined by the
user or the environment. This might involve using CMake's find path() and
find library() commands.
• If the library has a version requirement, the module will check whether the
version of the found library is appropriate for your project. This could be done
using find package() or version comparison logic.
if (NOT MyLib_LIBRARY)
message(FATAL_ERROR "MyLib not found")
endif()
• After successfully locating the library, you should define CMake variables that
contain information about the library. This includes its include directories,
libraries, and possibly its version.
set(MyLib_INCLUDE_DIRS ${MyLib_INCLUDE_DIR})
set(MyLib_LIBRARIES ${MyLib_LIBRARY})
• If the module can't find the library, it should handle this failure gracefully by
either providing informative warnings or terminating the process with an error
message.
if (NOT MyLib_INCLUDE_DIRS)
message(FATAL_ERROR "Could not find MyLib include directory")
endif()
if (NOT MyLib_LIBRARIES)
message(FATAL_ERROR "Could not find MyLib library")
endif()
• After the search is completed, the variables containing the results will be made
available to the rest of the project, which can then link against the found library
or include its headers.
mark_as_advanced(MyLib_INCLUDE_DIRS MyLib_LIBRARIES)
The mark as advanced() command ensures that these variables are not shown
by default in CMake's GUI or when listing all variables.
463
if (NOT MyLib_LIBRARY)
message(FATAL_ERROR "MyLib not found: No library files")
endif()
Once you have created your FindMyLib.cmake file, you can use it in your main
CMakeLists.txt file to locate and use MyLib. Here's how you can integrate it:
list(APPEND CMAKE_MODULE_PATH
,→ "${CMAKE_SOURCE_DIR}/cmake/Modules")
• You can now use the find package() command to locate MyLib in your
CMakeLists.txt. CMake will automatically search for
FindMyLib.cmake in the directories specified in CMAKE MODULE PATH.
465
find_package(MyLib REQUIRED)
• After successfully finding the library, you can link it to your targets using the
variables set by your module.
13.1.5 Conclusion
In CMake, the ability to create custom commands is a powerful feature that allows you to
extend the functionality of the build system. You can define new commands using
macro() and function() to encapsulate repeated or complex logic in your CMake
projects. By creating your own commands, you can make your CMake scripts more
modular, reusable, and easier to maintain. This section will explain how to define custom
commands in CMake using both macro() and function(), and outline the
differences between them.
CMake provides built-in commands for common tasks such as add executable(),
add library(), include directories(), and
target link libraries(). However, sometimes the built-in functionality is not
enough for your project's needs, and you need to define custom behavior that suits your
specific requirements.
Custom commands allow you to bundle a set of CMake commands into a single reusable
unit, which can be invoked by other parts of the CMake script. CMake provides two
primary ways to define custom commands: macros and functions. While they are similar,
they have distinct behaviors and use cases.
The macro() command in CMake defines a custom command that behaves like a
CMake script block, with an important distinction that the variables you modify inside the
467
macro affect the caller's environment. This means that any changes made to variables
within the macro will persist after the macro finishes executing, unless the variable was
explicitly set as CACHE or made private to the macro’s scope.
Syntax:
macro(<name> [arguments])
# Commands to execute
endmacro()
• [arguments]: An optional list of arguments that the macro will accept. These can be
used within the macro for more flexible behavior.
Here’s a simple example where we create a custom add warning() macro that adds a
compiler warning for a specific flag to the target:
In this example:
468
• The macro add warning() is defined to accept a target name and adds the
-Wall and -Wextra flags for compiler warnings.
• When we call add warning(MyApp), the macro adds these options to the
MyApp target.
The key thing to note about macros is that they affect the caller's environment, meaning
that target compile options() changes apply to the caller’s scope.
• Use macros when you want the custom command to directly modify variables or
target properties in the calling scope.
• Macros are particularly useful for tasks where you need to apply settings or options
to a set of targets, especially when those targets are created dynamically or within
loops.
The function() command in CMake, like macro(), defines a custom command, but
with one critical difference: the variables modified inside a function do not affect the
caller’s environment. This makes functions ideal when you want to encapsulate logic
without modifying the caller's state. Functions are typically used for more contained,
self-contained operations.
Syntax:
function(<name> [arguments])
# Commands to execute
endfunction()
469
In this example:
The key difference from the macro is that changes made inside the function do not persist
in the caller’s scope. This behavior ensures that functions don't unintentionally affect the
environment outside their scope, leading to more predictable and safer builds.
• Use functions when you want to encapsulate logic that should not modify the caller's
environment.
• Functions are ideal when you want to perform operations like adding compile
definitions, setting properties, or performing checks without affecting the calling
CMake script's state.
While both macro() and function() allow you to define custom CMake commands,
they differ in terms of scope and variable handling. Here’s a summary of the key
differences:
In most cases, if you need to modify variables or state outside of the custom command,
use macro(). If you want the custom logic to stay isolated without affecting the
environment, use function().
Here are some best practices when defining custom CMake commands:
471
1. Use functions for isolated logic: If your command doesn't need to modify the
caller's environment or global state, use a function to avoid unexpected side effects.
2. Keep macros small and focused: Since macros modify the calling environment,
they should be used sparingly and only when necessary. Avoid large macros that
could introduce unintended side effects.
3. Use set() with CACHE cautiously: If you intend to make a variable accessible
globally or persist across multiple CMake files, use set() with CACHE. However,
use this only when truly necessary.
4. Documentation and clarity: Be clear in naming your custom commands (e.g.,
add warning(), add definitions for target()) to make it obvious
what the command does. Also, consider documenting the arguments and expected
behavior of your custom commands.
13.2.6 Conclusion
Defining custom commands with macro() and function() is an essential skill for
advanced CMake users. It allows you to abstract repetitive or complex logic, making your
CMake scripts more modular and reusable. The choice between macro() and
function() depends on whether you want to modify the caller’s environment or keep
the changes isolated within the custom command itself.
By understanding the differences between macros and functions, and knowing when to
use each, you will be able to create highly flexible and efficient CMake scripts for
managing and building your C++ projects.
472
When working with CMake, environment variables can play a crucial role in determining
how the build system behaves. These variables can influence the configuration of
compilers, libraries, paths, and various other aspects of the project. Managing
environment variables within custom CMake modules becomes important when you need
to tailor the behavior of your build system to different environments, especially when
handling external dependencies, toolchains, or specific build configurations.
In this section, we will explore how to manage environment variables effectively within
CMake modules. This includes how to read, modify, and pass environment variables, as
well as ensuring that changes made to them are correctly reflected during the configuration
and build process.
In CMake, environment variables are variables that are typically defined outside the
CMake build system, often at the operating system level or within the user’s shell session.
These environment variables can affect how the CMake toolchain behaves or how certain
external dependencies are discovered.
For example:
• The PATH environment variable is often used to locate executable files (e.g.,
compilers, utilities).
• The CXX and CC variables specify which compilers should be used for C++ and C
code, respectively.
473
Managing these environment variables inside custom CMake modules allows your project
to be adaptable to different systems and setups.
CMake provides a way to read environment variables using the ENV{} syntax. This
allows you to query the environment of the current process (which may include
information set before running CMake). This can be especially useful when you need to
conditionally configure your build system based on system settings, or when integrating
external tools or libraries that rely on environment variables.
Syntax:
set(MY_VAR $ENV{MY_ENV_VARIABLE})
Example:
Suppose we want to read the HOME environment variable to configure a specific path in
the build system:
set(HOME_DIR $ENV{HOME})
message(STATUS "Home directory: ${HOME_DIR}")
474
This would print the value of the HOME environment variable on Unix-based systems. On
Windows, it could print the path to the user's home directory.
Unlike traditional shell scripts, changes to environment variables in CMake are typically
local to the CMake process and its subprocesses. Once the CMake process finishes, any
modifications to environment variables will not persist. However, changes to environment
variables can be passed down to external processes (such as compilers or custom build
steps) via CMake's set() and add custom command() commands.
set(ENV{MY_TOOL_PATH} "/path/to/tool")
This would set the MY TOOL PATH environment variable for any commands or processes
invoked after this line in the CMake script.
In this example, MY LIB PATH is set to /opt/mylib, and this value is passed to the
mylib tool during the post-build phase of the MyApp target.
CMake modules are often used to detect and configure external libraries or tools. To
handle environment variables within a module, you can check for their existence, set
defaults if needed, and modify them based on user input or system-specific settings.
Suppose you need to detect if an environment variable MYLIB PATH is set and, if not,
provide a default path:
In this example:
476
• If the MYLIB PATH environment variable is not set, it will be assigned a default
value (/default/path/to/mylib).
• If MYLIB PATH is set, the script will output the value to the user.
This example demonstrates how you can temporarily set an environment variable and then
use it in a custom command that is executed during the build process.
To make persistent changes to environment variables that apply across multiple CMake
runs, you can modify the CMakeCache.txt file or use set() with the CACHE option.
This allows you to store environment variable values across different configurations or
project runs.
477
This example sets the MYLIB PATH variable in the CMake cache, which can be modified
by the user through the CMake GUI or via command-line options. It ensures that the path
is preserved between builds, allowing for a consistent environment.
Managing environment variables inside CMake modules requires care to avoid conflicts,
ensure portability, and make the project environment predictable. Here are some best
practices for working with environment variables in CMake:
5. Use Cache Variables for User Configuration: If your project requires user-defined
paths or settings, consider using CMake’s CACHE to persist environment variables or
configuration settings. This allows users to customize their environment and avoid
having to reset values with every CMake run.
13.3.7 Conclusion
CMake is a powerful tool that allows for building and managing C++ projects across
different platforms. However, as projects grow in complexity, it is essential to structure
CMake scripts in a way that improves modularity, reduces duplication, and enhances the
ease of maintenance. One of the most important principles for managing complex projects
is reusability. By focusing on reusability, you ensure that the same logic can be applied
across multiple projects, configurations, or platforms without having to rewrite the code.
This section delves into strategies and best practices for improving the reusability of
CMake code. It covers various approaches, including modular design, use of functions,
macros, custom modules, and configuration files, all aimed at making your CMake scripts
more efficient and adaptable.
CMake scripts often grow large and complex, particularly when handling multiple
dependencies, toolchains, platforms, and configurations. Reusability in this context means
writing CMake code that can be easily reused across different parts of a project or even
across different projects. This can save time, reduce errors, and make the CMake build
system much more maintainable.
To achieve reusability, we can break down CMake scripts into smaller, self-contained
modules, write reusable functions or macros, and take advantage of existing libraries or
tools to manage common tasks.
480
The first step in improving the reusability of your CMake code is to modularize it. This
means breaking your CMake scripts into smaller, logical components (modules) that can
be easily reused in different parts of the project or in entirely different projects.
A CMake module is essentially a CMake script that encapsulates a specific task or set of
related tasks. These modules can be easily included into other CMake scripts using
include() or find package(). They can also be packaged as external CMake
modules for use across different projects.
1. Identify a specific task or set of tasks that can be encapsulated into a module.
3. Use include() or find package() to integrate the module into your project.
Let's say you frequently need to check for the presence of a particular library, such as
Boost. You can create a CMake module that encapsulates this logic and reuse it across
multiple projects.
# File: FindBoost.cmake
find_package(Boost REQUIRED)
if (Boost_FOUND)
message(STATUS "Boost found at ${Boost_INCLUDE_DIRS}")
else()
481
Then, in your main CMake script, you simply include this module:
# File: CMakeLists.txt
include(FindBoost)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp Boost::Boost)
By breaking out the logic into a separate module (FindBoost.cmake), you can now
reuse it across other projects without having to repeat the same code in each CMake script.
/cmake/modules/
/find_packages/
/utilities/
/platform_specific/
This structure makes it easy to find and manage reusable components for different tasks,
and it allows you to scale the reusability as the number of modules grows.
482
While CMake modules are an excellent way to modularize the configuration, you can also
improve the reusability of your code by defining functions and macros. These allow you
to encapsulate frequently used logic into reusable units that can be invoked with different
parameters.
# File: CMakeLists.txt
function(add_common_flags target)
target_compile_options(${target} PRIVATE -Wall -Wextra)
endfunction()
# Usage:
add_executable(MyApp main.cpp)
add_common_flags(MyApp)
By using a function, we can easily reuse the add common flags() logic across
different targets in the same or other projects, simply by calling the function with the
target name as an argument.
where you want to directly alter the calling CMake context. Macros are often used for
settings or changes that need to be applied globally across multiple parts of the build
system.
Here’s an example of a reusable macro that adds custom warning flags to multiple targets:
# File: CMakeLists.txt
macro(add_warning_flags target)
target_compile_options(${target} PRIVATE -Wall -Wextra
,→ -Wpedantic)
endmacro()
# Usage:
add_executable(MyApp main.cpp)
add_warning_flags(MyApp)
add_executable(AnotherApp another.cpp)
add_warning_flags(AnotherApp)
This macro applies the same set of compiler warning flags to multiple targets without
needing to rewrite the logic each time.
When working with external libraries or tools, it’s common to use the find package()
command in CMake. However, rather than writing the logic for finding a package or
library every time, you can use the find package() command within reusable CMake
modules.
For example, to reuse the logic for finding the Boost library, you could create a custom
module called FindBoost.cmake (as shown earlier), which will be automatically
484
included in different projects. The module would be reusable because it abstracts away the
complexity of finding the library and setting the appropriate flags.
# File: FindMyTool.cmake
find_package(MyTool REQUIRED)
if (MyTool_FOUND)
message(STATUS "MyTool found at ${MyTool_INCLUDE_DIR}")
else()
message(FATAL_ERROR "MyTool not found!")
endif()
Now, in any project, you can simply include FindMyTool.cmake to find and configure
MyTool:
# File: CMakeLists.txt
include(FindMyTool)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp MyTool::MyTool)
This modular approach enables the reuse of the same external dependency configuration
across different projects.
In addition to creating CMake modules, functions, and macros, another powerful tool for
improving reusability is CMake configuration files. These configuration files allow you
485
to store project settings, variables, and options in an external file that can be easily shared
and reused across different projects.
Suppose you have a set of common configuration options (e.g., compiler flags, library
paths, version numbers) that you want to share across multiple projects. You can create a
config.cmake file:
# File: config.cmake
set(MYLIBRARY_PATH "/path/to/mylibrary")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
Then, you can include this configuration file in your CMake script:
# File: CMakeLists.txt
include(config.cmake)
By using external configuration files, you ensure that these shared settings can be reused
and maintained in one central location.
To ensure your CMake code remains modular and reusable, consider the following best
practices:
486
1. Encapsulation: Break down complex CMake scripts into smaller, reusable modules
that encapsulate specific tasks or logic. This prevents your main
CMakeLists.txt file from becoming too cluttered.
2. Use Functions and Macros Effectively: Functions should be used for tasks that
need to operate within a local scope (e.g., modifying specific targets), while macros
should be used for tasks that need to modify the calling scope.
3. Avoid Hardcoding Paths and Settings: Use environment variables, cache variables,
or configuration files to manage paths and tool settings. This makes your CMake
scripts more flexible and adaptable to different environments.
4. Document Reusable Code: Provide clear documentation for any reusable modules,
functions, and macros you create. This will help others (and your future self)
understand their purpose and usage.
5. Testing Reusability: Regularly test the reusability of your modules, functions, and
macros in different projects and environments to ensure they remain adaptable and
functional.
13.4.7 Conclusion
Improving the reusability of CMake code is an essential skill for managing larger and
more complex projects. By modularizing your CMake scripts, leveraging functions and
macros, reusing find modules, and using configuration files, you can ensure that your build
system is flexible, maintainable, and scalable. These strategies will allow you to avoid
code duplication, simplify maintenance, and make your CMake configurations easier to
adapt to different project requirements or environments.
By following the best practices outlined in this section, you can create a build system that
grows with your project and remains efficient and easy to manage over time.
Chapter 14
487
488
CMake is a powerful tool designed to manage and automate the build process for C++
projects. One of its primary benefits is that it can generate native build files for various
operating systems, including Windows, Linux, and macOS. This allows developers to
write a single CMakeLists.txt file that will work across these platforms, reducing the need
for separate platform-specific build scripts.
However, differences between operating systems (such as file paths, compilers, libraries,
and system tools) require that some platform-specific logic be incorporated into the
CMake configuration. Writing a CMakeLists.txt that works for all platforms involves
detecting the operating system, setting platform-specific flags, and handling
platform-specific dependencies.
Regardless of the platform, the basic structure of a CMakeLists.txt file remains consistent.
A typical file includes:
cmake_minimum_required(VERSION 3.10)
add_executable(MyProject main.cpp)
While the basic structure stays the same, platform-specific code can be incorporated as
needed.
Certain libraries may be required for specific platforms. For example, on Linux, you
might need to link to pthread or on macOS, the CoreFoundation library
might be required. You can use find package() or
target link libraries() conditionally based on the operating system.
Example:
File paths are another area where platform-specific differences can occur. While
UNIX-based systems (Linux and macOS) use forward slashes (/) in file paths, Windows
uses backslashes (\). CMake abstracts most of these differences, but there are still places
where platform-specific handling may be needed.
For example, when specifying a list of include directories or source files, CMake
automatically translates paths between Windows and UNIX-style systems. However,
when using third-party tools or working with non-standard file systems, you may need to
ensure paths are written correctly.
Example:
492
For complex C++ projects, you may need to rely on libraries or tools that need to be
available across platforms. CMake provides find package() to locate and configure
third-party dependencies like Boost, OpenGL, and others. You can use
find package() with version control, and conditionally link to the appropriate
libraries for each platform.
Example:
By default, the option is set to ON, but users can override it at configuration time.
1. Use CMake's built-in platform detection: Rely on CMAKE SYSTEM NAME and
CMAKE CXX COMPILER ID to handle platform-specific logic.
2. Minimize platform-specific logic: Keep platform-specific code to a minimum to
maintain portability. Try to use cross-platform libraries whenever possible.
3. Keep paths platform-agnostic: Always rely on CMake’s path-handling features,
such as CMAKE CURRENT LIST DIR, to ensure proper path management.
4. Test on all target platforms: The only way to guarantee that your CMakeLists.txt
will work across all platforms is by testing it on all of them (Windows, Linux, and
macOS).
494
14.1.8 Conclusion
When developing a C++ application that targets multiple platforms, you may encounter
platform-specific libraries and APIs that are available only on certain operating systems.
For example, Windows may require Windows-specific libraries (such as the Windows API
or COM libraries), while Linux may use libraries such as pthread or X11, and macOS
may require frameworks like Cocoa or CoreFoundation. The goal is to write code
that can conditionally compile depending on the target platform, allowing you to manage
these differences efficiently.
Preprocessor directives like #ifdef WIN32 and #ifdef linux are used to
determine which platform the code is being compiled for, and to include or exclude
specific code snippets depending on the operating system.
496
The most common way to handle platform-specific code is through the use of
preprocessor directives that check for specific platform macros. These macros are
automatically defined by the compiler based on the platform it’s targeting. For example:
This allows you to write conditional code based on the platform, including
platform-specific libraries, headers, and functionality.
Example:
#ifdef _WIN32
// Code specific to Windows
#elif defined(__linux__)
// Code specific to Linux
#elif defined(__APPLE__)
// Code specific to macOS
#endif
In the following sections, we’ll look in more detail at how to use these preprocessor
checks to manage platform-specific library differences and ensure that your project builds
properly on each platform.
497
• Windows API: This is the native set of APIs that allow programs to interact
with the operating system for tasks like window management, file I/O, and
networking.
• Windows SDK Libraries: Libraries like Windows.h provide access to
system-level functionality.
• DirectX: A collection of APIs for handling multimedia, particularly game
development and graphical interfaces.
When you write cross-platform code that targets Windows, you may need to use
specific libraries or system calls available only on Windows. For example, the
Windows.h header must be included when working with the Windows API.
Example:
#ifdef _WIN32
#include <Windows.h>
// Use Windows-specific functionality
void win_specific_function() {
// Windows-specific API call, e.g., CreateFile
}
#endif
• POSIX Libraries: These provide a standard set of APIs for Unix-like operating
systems, including file I/O, networking, threading, etc.
• pthread Library: Used for multithreading support in Linux.
• X11: A protocol and set of libraries used for GUI applications in Unix-like
systems.
• libdl: Used for dynamically loading shared libraries.
When writing cross-platform code that targets Linux, you will often need to include
POSIX headers, pthread for multithreading, or X11 for GUI applications. These
libraries are not available on Windows, so conditional compilation is required.
Example:
#ifdef __linux__
#include <pthread.h>
// Linux-specific threading functionality
void linux_specific_function() {
// Use POSIX threading functions
pthread_t thread;
pthread_create(&thread, NULL, some_function, NULL);
}
#endif
Additionally, libraries like libm (math library) or libdl may need to be linked on
Linux to ensure the program works properly.
3. macOS: Working with macOS-Specific Frameworks
499
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
// macOS-specific functionality
void macos_specific_function() {
// Use macOS-specific framework APIs
CFStringRef myStr =
,→ CFStringCreateWithCString(kCFAllocatorDefault, "Hello,
,→ macOS", kCFStringEncodingUTF8);
}
#endif
CMake provides the ability to set compile definitions and flags for each platform. For
example:
• On Windows, you can set the WIN32 macro to ensure Windows-specific code is
included.
Example:
This ensures that each platform will correctly identify its own specific preprocessor
macros and conditionally compile the appropriate sections of code.
501
When dealing with external libraries or tools that are platform-specific, such as GUI
libraries or system APIs, CMake’s find package() and find library()
commands are invaluable. You can use these commands in conjunction with platform
checks to automatically link platform-specific libraries during the build process.
Example:
This ensures that the right libraries are found and linked depending on the platform the
user is building on.
14.2.6 Conclusion
libraries and headers are included for each target platform. By following best practices
and understanding how to manage platform-specific differences, you can create robust,
cross-platform C++ projects that compile and run seamlessly across Windows, Linux, and
macOS.
503
This section discusses the various challenges, techniques, and best practices for building
applications that can target multiple platforms seamlessly using CMake.
• Testing and Debugging: Ensuring the application works across all platforms can be
a challenge, as bugs may only manifest on certain systems. Testing must be done on
each supported platform to ensure consistency.
CMake can generate build systems for various platforms, including Visual Studio on
Windows, Makefiles on Linux, and Xcode projects on macOS. One of the keys to
multi-platform development is ensuring that CMake is configured correctly to work with
the platform-specific tools and dependencies.
cmake_minimum_required(VERSION 3.10)
add_definitions(-D_WINDOWS)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Platform-specific configurations for Linux
add_definitions(-D_LINUX)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# Platform-specific configurations for macOS
add_definitions(-D_MACOS)
endif()
Example:
• Toolchain File for Linux: For Linux, you typically use GCC or Clang as the
compiler. A toolchain file can be used to configure the appropriate settings.
Example:
CMake can then be configured to use the right toolchain based on the platform by
specifying the toolchain file during the CMake generation step.
cmake -DCMAKE_TOOLCHAIN_FILE=windows_toolchain.cmake ..
In cases where the library locations differ per platform, CMake provides the HINTS
or PATHS options, allowing you to specify custom paths.
508
Example:
CMake allows you to specify platform-specific flags or settings for compilers or linkers.
This includes using compiler-specific optimizations (e.g., /O2 for MSVC, -O3 for
509
To ensure that your multi-platform application is always built correctly across all
supported platforms, it’s a good idea to set up a Continuous Integration (CI) pipeline. CI
tools such as GitHub Actions, GitLab CI, or Jenkins can automatically build your
application for different platforms in virtualized environments.
By setting up different build configurations for each platform (e.g., Windows, Linux, and
macOS), CI ensures that changes to the codebase are tested in all environments, reducing
the chances of cross-platform bugs.
Example CI configuration:
jobs:
windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup CMake
uses: cschwarz/setup-cmake@v1
510
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup CMake
uses: cschwarz/setup-cmake@v1
- run: cmake ..
- run: cmake --build .
14.3.6 Conclusion
511
512
When CMake is run in an empty build directory, it attempts to automatically detect the
system’s default C and C++ compilers. The compiler selection process follows these steps:
cmake --version
1. Overview of GCC
GCC is the standard compiler for Linux and is available on Windows (via
MinGW/MSYS2) and macOS. It is widely used for open-source projects and
supports various C++ standards.
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_executable(MyProject main.cpp)
1. Overview of Clang
Clang is an alternative to GCC and is widely used for:
• macOS development (Xcode uses Clang by default).
• Linux (many distributions offer Clang as an alternative to GCC).
• Windows (Clang is supported in Visual Studio).
2. Setting Up CMake for Clang
To configure CMake to use Clang:
1. Overview of MSVC
MSVC is the standard compiler for Windows development and is integrated with
Visual Studio. It has strong support for C++ standards and Microsoft-specific
optimizations.
516
MSVC supports /MT and /MD flags for static and dynamic linking.
3. Configuring MSVC-Specific Compiler Flags
if(MSVC)
target_compile_options(MyProject PRIVATE /W4 /O2
,→ /permissive-)
endif()
15.1.7 Conclusion
CMake makes it easy to work with multiple compilers by automatically detecting and
configuring compiler settings. By understanding the differences between GCC, Clang,
and MSVC, and how to set compiler-specific flags in CMake, developers can ensure that
their projects compile and run correctly on all platforms.
518
Key Takeaways:
• Use CMAKE C COMPILER and CMAKE CXX COMPILER to specify the compiler.
• Use if(CMAKE CXX COMPILER ID STREQUAL "...") to apply
compiler-specific settings.
• Use GCC for open-source projects, Clang for performance and static analysis,
and MSVC for Windows development.
This knowledge is essential for writing cross-platform C++ projects that work
seamlessly across Windows, Linux, and macOS.
519
Compilation flags control how source code is translated into machine code by the
compiler. These flags influence optimization levels, debugging information, warning
levels, standard compliance, and more.
In this section, we explore how to configure and manage these flags for different compilers
like GCC, Clang, and MSVC.
CMake provides the CMAKE CXX FLAGS variable to set global compiler flags that apply
to all targets.
Note: This approach affects all C++ targets in the project. If different targets
require different flags, target compile options() is recommended.
CMake allows setting different compilation flags based on the build type (e.g., Debug,
Release, RelWithDebInfo).
By default, CMake provides these variables:
Example:
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
Different compilers require different flags for optimization, warnings, and features.
CMake provides CMAKE CXX COMPILER ID to detect the compiler and apply specific
flags.
2. GCC-Specific Flags
Example:
3. Clang-Specific Flags
Example:
4. MSVC-Specific Flags
Example:
if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /O2 /EHsc")
endif()
Instead of modifying CMAKE CXX FLAGS, a better practice is to set flags per target using
target compile options().
Example:
add_executable(MyApp main.cpp)
add_compile_definitions(MY_FEATURE_ENABLED)
Compilation flags control how code is compiled, but linker flags control how the final
binary is generated.
Set linker flags globally:
524
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-O1")
cmake_minimum_required(VERSION 3.10)
project(MyApp)
add_executable(MyApp main.cpp)
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build .
15.2.9 Conclusion
By managing compilation flags properly, you can ensure better performance, debugging,
and portability across different compilers and platforms.
526
Different C++ compilers support various flags and features that impact optimizations,
warnings, debugging, and compliance with C++ standards. However, not all compilers
support the same flags, and some flags may change between compiler versions.
To write portable and robust CMake configurations, it's crucial to check whether a
compiler supports specific flags before applying them. CMake provides the
check cxx compiler flag() function to test if a compiler supports a given flag.
This section covers:
If an unsupported flag is used, the compiler may issue warnings or errors, breaking the
build. Checking compiler support ensures: Portability – The same CMake project builds
across different compilers.
Robustness – No unexpected errors due to unsupported flags.
Better debugging and performance – Enables compiler-specific optimizations where
available.
CMake provides the check cxx compiler flag() function to test if a specific C++
compiler flag is supported.
Basic Syntax
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-fstack-protector-strong"
,→ STACK_PROTECTOR_SUPPORTED)
if(STACK_PROTECTOR_SUPPORTED)
message(STATUS "Compiler supports -fstack-protector-strong")
else()
message(STATUS "Compiler does NOT support
,→ -fstack-protector-strong")
endif()
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-march=native" SUPPORTS_MARCH_NATIVE)
if(SUPPORTS_MARCH_NATIVE)
target_compile_options(MyProject PRIVATE -march=native)
endif()
This ensures:
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-Wall" SUPPORTS_WALL)
check_cxx_compiler_flag("-Wextra" SUPPORTS_WEXTRA)
check_cxx_compiler_flag("-Wpedantic" SUPPORTS_WPEDANTIC)
add_executable(MyApp main.cpp)
529
if(SUPPORTS_WALL)
target_compile_options(MyApp PRIVATE -Wall)
endif()
if(SUPPORTS_WEXTRA)
target_compile_options(MyApp PRIVATE -Wextra)
endif()
if(SUPPORTS_WPEDANTIC)
target_compile_options(MyApp PRIVATE -Wpedantic)
endif()
Since not all compilers support the same flags, check cxx compiler flag() can be
combined with CMAKE CXX COMPILER ID to apply flags per compiler.
include(CheckCXXCompilerFlag)
This approach:
Instead of checking flags, you can check compiler support for a language feature (e.g.,
C++17, C++20).
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
include(CheckCXXSourceCompiles)
check_cxx_source_compiles("
#include <filesystem>
int main() { std::filesystem::path p; return 0; }
" SUPPORTS_FILESYSTEM)
if(SUPPORTS_FILESYSTEM)
message(STATUS "Compiler supports std::filesystem")
else()
message(WARNING "Compiler does NOT support std::filesystem,
,→ falling back to boost::filesystem")
endif()
This method is useful for checking language feature availability before using them
in the project.
Another way to check for compiler support is using try compile(), which compiles a
small test program.
532
Example:
try_compile(
COMPILE_SUCCESS
${CMAKE_BINARY_DIR}
SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/test_flag.cpp
)
if(COMPILE_SUCCESS)
message(STATUS "Test program compiled successfully")
else()
message(STATUS "Test program failed to compile")
endif()
While more flexible, try compile() is slower and requires a test source file.
15.3.8 Conclusion
Using check cxx compiler flag() ensures that only supported compiler flags
are applied, improving portability and reliability across different compilers.
Key Takeaways
Use check cxx compiler flag() to test flag support before adding it.
Combine with CMAKE CXX COMPILER ID to check flags per compiler.
Use CMAKE CXX STANDARD for standard compliance.
Use check cxx source compiles() for checking language features.
Avoid using unsupported flags to prevent build failures and compatibility issues.
By applying these techniques, you can create highly portable, compiler-agnostic C++
projects with CMake!
533
15.4.1 Introduction
Compiler errors and warnings provide crucial feedback when building C++ projects.
Different compilers—GCC, Clang, and MSVC—have their own ways of reporting
issues, and sometimes a warning in one compiler may be an error in another. Managing
these warnings and errors consistently across multiple platforms ensures:
Better code quality
Easier debugging and troubleshooting
Greater portability across compilers
Compiler errors indicate issues that prevent compilation (e.g., syntax errors, type
mismatches). Compiler warnings indicate potential problems but do not stop compilation
(e.g., unused variables, implicit conversions).
Examples:
534
Most compilers allow configuring how warnings are handled—they can be ignored,
enabled selectively, or elevated to errors.
To write cleaner, more robust code, enable warnings in CMake for different compilers.
2. MSVC Warnings
MSVC has a different warning flag system:
535
if(MSVC)
target_compile_options(MyApp PRIVATE /W4)
endif()
2. MSVC
if(MSVC)
target_compile_options(MyApp PRIVATE /WX)
endif()
Example:
536
#ifdef _MSC_VER
#pragma warning(disable : 4996) // Disable MSVC-specific warning
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
int main() {
int unusedVar = 42; // Warning suppressed with
,→ -Wno-unused-variable
return 0;
}
if(MSVC)
target_compile_options(MyApp PRIVATE /wd4996) # Disable
,→ deprecated function warnings
endif()
Example:
int main() {
printf("Hello, World!\n"); // MSVC would normally warn about
,→ unsafe `printf`
return 0;
}
Example:
add_executable(MyApp main.cpp)
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)
add_executable(MyApp main.cpp)
main.cpp
#include <iostream>
#ifdef _MSC_VER
#pragma warning(disable : 4996) // Suppress MSVC deprecated
,→ warnings
540
int main() {
int unusedVar = 42; // Warning suppressed
std::cout << "Hello, World!" << std::endl;
return 0;
}
Result:
15.4.9 Conclusion
Properly managing compiler warnings and errors ensures clean, portable, and
maintainable code.
Key Takeaways
By following these best practices, you can ensure better code quality and
cross-platform compatibility in your CMake projects!
Chapter 16
(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*
CMake is a powerful build system generator, but users frequently encounter error
messages when configuring, generating, or building a project. These errors can stem from
misconfigured scripts, missing dependencies, incorrect variable settings, or version
mismatches. Understanding how to interpret and resolve these error messages is crucial
for efficient troubleshooting.
542
543
CMake error messages typically follow a consistent structure that helps identify the cause
and location of the issue. A typical error message includes:
1. Configuration Errors
Errors occurring during the CMake configuration phase (cmake ..) usually
result from incorrect syntax, missing files, or invalid variables.
2. Missing Dependencies
Missing packages, libraries, or toolchains can lead to errors.
Fix: Install a valid C++ compiler and ensure it is in the system’s PATH.
• Example: Package Not Found
Fix: Install Eigen3 and set CMAKE PREFIX PATH to its installation directory.
3. Generator Errors
CMake requires a generator to create build files. If a required generator is missing or
incompatible, errors occur.
• Example: Unsupported Generator
Fix:
Ensure
NinjaX
is installed, or use
cmake --help
Fix:
Ensure the target links against the correct library with
target_link_libraries()
Fix:
Replace with
target_include_directories()
cmake .. --trace
cmake .. --debug-output
4. Verify Dependencies
Ensure dependencies are installed and correctly detected using:
16.1.4 Summary
(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*
16.2.1 Introduction
CMake provides the message() command, which is a powerful tool for debugging
CMake scripts. It allows developers to print messages during the configuration phase,
helping them inspect variable values, check execution flow, and diagnose issues. Among
the different modes of message(), STATUS and DEBUG are particularly useful for
troubleshooting and debugging.
This section explores how to effectively use message(STATUS) and
message(DEBUG), when to use them, and best practices for debugging CMake scripts.
The message() function in CMake takes a message type and a string to print. The
syntax is as follows:
The <mode> defines how the message is displayed. The most commonly used modes
are:
549
Mode Description
STATUS Prints general status messages during configuration.
WARNING Issues a warning but allows execution to continue.
FATAL ERROR Stops configuration immediately with an error.
DEBUG Prints debugging information, but only when
--log-level=DEBUG is used.
VERBOSE Prints messages only when verbose mode is enabled
(--log-level=VERBOSE).
This section focuses on STATUS and DEBUG, which are particularly useful for debugging.
The STATUS mode prints messages to standard output during the configuration phase. It
is used for:
Output:
if(DEFINED MY_VAR)
message(STATUS "MY_VAR is defined")
else()
message(STATUS "MY_VAR is NOT defined")
endif()
If MY VAR is undefined, the second message will appear in the output, helping
identify conditional execution issues.
Output:
This helps confirm that the correct compiler and standard are being used.
552
While STATUS messages are always printed, DEBUG messages are only shown when the
CMake log level is explicitly set to DEBUG.
cmake .. --log-level=DEBUG
set(PROJECT_NAME "MyApp")
message(STATUS "Hello")
message(STATUS "Path is: /usr/local")
Use:
16.2.6 Summary
• Using them effectively helps diagnose variable issues, execution flow, dependency
problems, and toolchain configurations.
• Following best practices ensures readable and maintainable debugging output.
556
(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*
16.3.1 Introduction
CMake relies heavily on environment variables and build settings to configure and
generate a project correctly. Incorrectly set environment variables or misconfigured build
settings can lead to errors, missing dependencies, or unexpected behavior.
• Inspect and verify build settings such as compiler flags, linker settings, and build
types.
By mastering these techniques, developers can troubleshoot complex build problems more
effectively.
-- PATH: /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
This helps verify that necessary paths (such as compiler directories) are set.
2. Setting Environment Variables in CMake
Use set(ENV{VAR NAME} VALUE) to modify environment variables during
CMake execution.
set(ENV{PATH} "/custom/path:$ENV{PATH}")
message(STATUS "Updated PATH: $ENV{PATH}")
However, note that changes only affect the current CMake run and do not persist
after execution.
3. Important Environment Variables for CMake
Some common environment variables that influence CMake behavior:
558
export CMAKE_PREFIX_PATH="/custom/install/path"
or in CMakeLists.txt:
set(CMAKE_PREFIX_PATH "/custom/install/path")
This helps identify unexpected values that might interfere with the build.
Apart from environment variables, CMake relies on various build settings that control
compiler options, build types, and linking behavior. Misconfigurations here can lead to
compilation failures, performance issues, or incorrect binary generation.
Typical values:
• Debug → Includes debug symbols (-g).
• Release → Enables optimizations (-O3).
• RelWithDebInfo → Optimized build with debug info.
• MinSizeRel → Optimization for minimal size.
If CMAKE BUILD TYPE is missing, explicitly set it:
cmake -DCMAKE_BUILD_TYPE=Release ..
or inside CMakeLists.txt:
560
Expected Output:
cmake -DCMAKE_CXX_COMPILER=/usr/bin/clang++
,→ -DCMAKE_C_COMPILER=/usr/bin/clang ..
To modify flags:
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-rpath,/custom/lib")
Or use:
16.3.5 Summary
Tracking environment variables and build settings is critical for debugging CMake
issues.
• Environment Variables:
• Build Settings:
• CMake Cache:
Mastering these techniques makes troubleshooting more systematic and efficient. In the
next section, we will explore advanced debugging tools such as cmake --trace and
cmake-gui.
564
(From Chapter 16: Troubleshooting and Debugging CMake Issues of the book **CMake:
The Comprehensive Guide to Managing and Building C++ Projects (From Basics to
Mastery))*
16.4.1 Introduction
CMake is a powerful tool for configuring and managing C++ projects, but developers
often encounter errors related to missing dependencies, incorrect configurations, or syntax
issues. Understanding common CMake errors and their solutions helps streamline
debugging and improve build reliability.
• Generator-related errors.
Configuration errors occur when running cmake .. and usually result from incorrect
syntax, missing files, or uninitialized variables.
Example
Cause
Solution
Fixed Code
Example
Cause
566
Solution
Fixed Code
These errors occur when find package() or find library() fails to locate
required dependencies.
Example
Cause
Solution
set(CMAKE_PREFIX_PATH "/custom/path/to/boost")
find_package(Boost REQUIRED)
Example
Cause
Solution
568
set(CMAKE_LIBRARY_PATH "/custom/path/to/lib")
find_library(MyLib NAMES MyLib)
Errors during the build phase (cmake --build .) often indicate incorrect compiler
settings or missing link libraries.
Example
Cause
Solution
1. Install a compiler :
cmake -DCMAKE_CXX_COMPILER=/usr/bin/g++ ..
Example
Cause
Solution
Example
Cause
Solution
cmake --help
cmake -G "Ninja" ..
Example
Cause
Solution
16.4.6 Summary
This book, CMake: The Comprehensive Guide to Managing and Building C++ Projects
(From Basics to Mastery), has taken you on a structured journey through the world of
CMake, equipping you with the knowledge and skills to efficiently manage and build C++
projects. We started with the basics of CMake, explaining its role as a cross-platform build
system generator, and then progressed into more advanced topics such as toolchain
configurations, dependency management, and performance optimizations.
573
574
By now, you should have a deep understanding of CMake and be capable of managing and
scaling projects efficiently, whether for small personal projects or enterprise-level software
development.
Even though this book provides a comprehensive foundation, mastering CMake requires
continuous learning and hands-on experience. Below are some advanced tips to further
refine your CMake skills:
Additional Resources
To stay up to date with the latest CMake features and best practices, consider referring to
the following resources:
1. Books
• Professional CMake: A Practical Guide – Craig Scott
• Mastering CMake – Ken Martin, Bill Hoffman
• Modern CMake for C++ – Rafal Swidzinski
2. Official Documentation
• CMake Official Documentation – The most authoritative and up-to-date source
for CMake features and commands.
• CMake Wiki – Contains additional guides and community discussions.
3. Useful Websites and Blogs
• CMake Discourse Forum – A community-driven forum for discussing CMake
issues.
• Kitware Blog – Articles on CMake, VTK, and related technologies.
• Modern CMake GitHub Guide – A collection of CMake examples covering
various use cases.
577
• CppCon YouTube Channel – Talks on modern C++ and CMake best practices.
By leveraging these resources, you can continue to improve your CMake expertise
and stay up to date with evolving best practices.
1. LLVM/Clang (GitHub)
4. OpenCV (GitHub)
6. GoogleTest (GitHub)
• A practical example of using CMake for testing frameworks with proper test
discovery mechanisms.
7. Qt Framework (GitHub)
By studying and contributing to these projects, you can gain practical experience
with real-world CMake configurations and improve your problem-solving skills.
Final Thoughts
CMake is an incredibly powerful tool for managing and building C++ projects across
multiple platforms. By mastering the techniques covered in this book, you will be able to
tackle complex build challenges, optimize workflows, and enhance project scalability.
Keep experimenting, stay updated with the latest CMake advancements, and contribute to
open-source projects to refine your expertise.
Happy coding with CMake!
Appendices
This appendix is a comprehensive list of the most commonly used CMake commands,
providing a quick reference for when you're working on CMake-based projects.
1. cmake
The main command to configure, generate, and build a project.
cmake <path-to-source>
Example:
cmake /path/to/project
2. add executable
Defines an executable target in a CMake project.
579
580
add_executable(MyApp main.cpp)
3. add library
Defines a library target (static or shared).
target_link_libraries(MyApp MyLibrary)
5. find package
Searches for and configures external dependencies, such as libraries or tools.
find_package(OpenCV REQUIRED)
6. include directories
Adds directories to the compiler's search path for include files.
include_directories(${CMAKE_SOURCE_DIR}/include)
7. set
Sets a variable or cache entry.
8. message
Displays messages during the configuration process.
9. install
Defines installation rules for files and targets.
enable_testing()
add_test(NAME MyTest COMMAND MyTestExecutable)
This appendix allows you to quickly locate command syntax and descriptions, which can
be especially helpful when debugging or experimenting with advanced CMake
configurations.
582
This appendix provides a collection of best practices for writing clean, efficient, and
maintainable CMakeLists.txt files. These best practices are essential for scaling
projects and ensuring that your build system is easy to manage in the long term.
add_subdirectory(src)
add_subdirectory(tests)
set(MY_LIBRARY_PATH ${CMAKE_SOURCE_DIR}/libs)
target_include_directories(MyApp PRIVATE
,→ ${CMAKE_SOURCE_DIR}/include)
target_link_libraries(MyApp PRIVATE MyLibrary)
583
find_package(OpenCV REQUIRED)
set(CMAKE_BUILD_TYPE "Release")
585
The troubleshooting guide in this appendix offers solutions to common issues and tips for
diagnosing problems during the CMake configuration or build process.
cmake_minimum_required(VERSION 3.10)
project(SimpleProject)
add_executable(MyApp main.cpp)
cmake_minimum_required(VERSION 3.10)
project(MultiTargetProject)
cmake_minimum_required(VERSION 3.10)
project(OpenCVExample)
find_package(OpenCV REQUIRED)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE ${OpenCV_LIBS})
These examples provide a foundation for building larger, more complex CMake
projects. You can adapt these templates as needed for your own projects.
589
This appendix lists various tools and IDE integrations that work seamlessly with CMake
to streamline your development process.
1. CMake GUI
A graphical interface that simplifies the configuration process for projects. It allows
you to set variables and configure your project without using the command line.
3. CLion
A powerful IDE for C++ development, CLion has native CMake support, which
makes it a great choice for managing large C++ projects with CMake.
4. Ninja
A small build system with a focus on speed, Ninja can be used with CMake to speed
up builds and optimize the build process.
Final Thoughts
The appendices in this book offer essential reference material and troubleshooting advice
to support your ongoing CMake journey. By utilizing these resources, you can navigate
590
common pitfalls, write more efficient CMakeLists.txt files, and manage complex
builds with ease.
References
Books
591
592
• Key Topics: CMake internals, writing custom CMake modules, advanced topics
in toolchain file configurations, and understanding the build process at a low
level.
• Why It’s Useful: This is an excellent resource for developers who need to write
highly customized CMake files or troubleshoot complex build systems.
• Key Topics: Modern CMake syntax, integration with external tools like CI/CD
pipelines, writing clean and reusable CMake code, and utilizing new CMake
features.
• Why It’s Useful: It offers a practical, example-driven approach to modern
CMake, which is great for developers looking to move away from legacy
CMake practices.
• Why It’s Useful: For any question or issue related to CMake, the official
documentation is the go-to resource. It will help you understand the tool's core
functionality and how to use it in various scenarios.
2. CMake GitLab Repository
This repository contains the source code of CMake itself. It’s a valuable resource if
you need to see how CMake is implemented or want to report a bug, contribute to the
development of CMake, or explore the change history.
• Key Features: CMake source code, bug tracking, feature requests, and
contributions.
• Why It’s Useful: Developers who want to go beyond using CMake and
contribute to its development or learn about its internal mechanics will find this
a great resource.
3. CMake Wiki
The CMake Wiki is a community-driven resource that supplements the official
documentation with additional guides, how-tos, and solutions to common problems.
• Key Features: Community-contributed tutorials, tips and tricks, case studies,
and frequently asked questions.
• Why It’s Useful: The Wiki often contains solutions to real-world CMake
problems, contributed by experienced users and experts from the community.
It’s a great place to find practical solutions.
4. CMake Discourse
CMake’s official forum is an excellent place to discuss issues, ask for help, and find
discussions around CMake features. It’s a valuable resource for troubleshooting and
learning from others' experiences.
• Key Features: Discussion threads on CMake topics, feature requests, bug
reports, and user-provided examples.
594
• Why It’s Useful: The CMake Discourse forum allows you to connect with the
community, ask specific questions, and read about other users' experiences and
solutions to problems.
1. Modern CMake
This website is dedicated to modern CMake best practices and provides an excellent
summary of the most current CMake features, syntax, and patterns.
• Key Features: Quick reference, guides on how to use CMake in modern C++
development, and examples of best practices.
• Why It’s Useful: This website is a great starting point for developers who want
to learn modern CMake practices in a structured and concise way.
2. Kitware Blog
Kitware, the company behind CMake, regularly publishes blog posts on new
features, best practices, case studies, and tutorials. The blog provides insights into
how CMake is used in various industries, including scientific computing, game
development, and more.
• Key Features: Updates on CMake features, case studies, tutorials, and advice
from CMake experts.
• Why It’s Useful: For developers looking to stay up-to-date with new features in
CMake or explore case studies of CMake used in complex environments, the
Kitware blog is a valuable resource.
tips, and tricks from the community. It’s an excellent place to find clean,
maintainable examples of how to structure and organize CMake files.
• Key Features: Best practices for writing reusable CMake code, guidelines for
organizing large projects, and solutions to common problems.
• Why It’s Useful: This repository is a useful guide for writing clean and efficient
CMake code. It contains advice on how to structure your build system to make
your projects more scalable and maintainable.
4. CMake Examples
This GitHub repository provides a collection of CMake examples covering various
use cases, from simple projects to more complex configurations.
• Key Features: Practical examples, explanations of CMake techniques, and
diverse use cases.
• Why It’s Useful: If you are learning CMake through hands-on examples, this
repository is a goldmine for realistic configurations. You can find examples on
advanced topics like cross-compiling, multi-platform support, and complex
dependency management.
1. LLVM/Clang
LLVM is a highly modular project that uses CMake as its build system. It
demonstrates complex, cross-platform CMake configurations in a large-scale
codebase.
• Why It’s Useful: By studying LLVM’s CMake setup, you can learn how to
handle large codebases, manage external dependencies, and optimize build
processes using CMake.
596
2. Boost Libraries
Boost is one of the most widely-used C++ libraries, and its CMake setup provides
examples of managing complex dependencies, creating libraries, and structuring
large C++ projects.
• Why It’s Useful: Boost’s CMake files provide a clear example of handling
external libraries, setting up multi-platform builds, and supporting various build
configurations.
3. OpenCV
OpenCV is a popular open-source computer vision library that uses CMake. The
OpenCV project is a great example of how to manage cross-platform builds, link to
external libraries, and organize large projects with CMake.
• Why It’s Useful: Studying OpenCV’s CMake configuration will give you
practical examples of using CMake with external dependencies and handling
complex C++ codebases.