I find out that a lot of people don’t know what CMake is at all. And all of these people go to other build tools that are often native on their own system and forget about the idea of working with other people or creating a cross-platform applications. This creates a huge gap in the end of what applications are available on different platforms.
In this article I aim to introduce and educate you on what CMake is and how it is releated to your work as a C or C++ developer. I will also explain how it can be used to configure projects, work with unit testing, cross-compile, and manage dependencies.
Table of Contents
- What is CMake?
- CMake for project configuration
- Using CMake for Unit Testing
- Using CMake for Cross-Compiling
- Using CMake for Dependency Management
What is CMake?
CMake is a cross-platform, open-source build system that simplifies the process of building software projects. It generates platform-specific build scripts for a wide range of build environments, including Make, Ninja, Visual Studio, Xcode, and many more. CMake was originally created by Kitware, but it is now a community-driven project with a large number of contributors.
CMake is designed to be easy to use and to provide a consistent build environment across different platforms and tools. With CMake, developers can define the build process in a platform-independent way, and CMake will generate the necessary build scripts for the target platform. This makes it easier to support multiple platforms and tools without having to manually configure and maintain build scripts for each platform.
Note: You can check out my article on how to install cmake.
CMake’s Core Features
CMake has a number of core features that make it a popular choice for building software projects. Let’s take a look at some of its key features.
One of CMake’s key features is its support for a wide range of platforms and tools. CMake can generate build scripts for a wide range of build environments, including Make, Ninja, Visual Studio, Xcode, and many more. This makes it easier to support multiple platforms and tools without having to manually configure and maintain build scripts for each platform.
This is essential for any project even if you don’t think that it will support multiple platforms at the beginning it might have to at some point. Having a standard apporach also allows other people to help you or work with you even if they’re on different operating systems.
CMake’s Modular Design
CMake is designed to be modular and extensible, allowing developers to add their own custom modules and functions to the build process. This makes it easier to customize the build process for a specific project or platform. CMake also has some built-in modules for the most common actions or libraries that you need. Even though they are not auto-included you can choose to enable them with one line of code. Examples of such modules are CTest and for managing dependencies through the ExternalProject setup.
CMake uses configuration files to define the build process. These configuration files are written in CMake’s own scripting language, which is designed to be simple and easy to understand. This makes it easier to define the build process in a platform-independent way. This language is not a turing-complete language though, as it does not have the full range of features required to meet the criteria of a Turing-complete language. While CMake does provide control flow structures such as conditionals, loops, and custom functions, it does not have the ability to perform arbitrary computations or support recursion, which are typically required for a language to be considered Turing-complete.
CMake is designed to be a build system generator, and it is intended to be used in conjunction with other programming languages such as C++, Python, or Java to build applications. While CMake can be used to perform complex build configurations, it is not intended to be a general-purpose programming language.
CMake provides support for managing dependencies, including libraries, header files, and other resources. CMake can automatically download and build dependencies, or it can be configured to use pre-built binaries. Newer version of CMake introduce the ExternalProject module but you can also copy dependencies into a subdirectory or use a more mainstream package manager like VCPKG.
CMake provides support for unit testing, allowing developers to define and run unit tests as part of the build process. CMake can be configured to use a variety of testing frameworks, including Google Test, Boost.Test, and Catch. This can all be done thrugh CMake’s own CTest solution for executing tests which integrates very nicely with common test frameworks.
CMake for project configuration
One of CMake’s key features is its ability to configure projects. CMake provides a number of tools and features to help developers define the build process and generate the necessary build scripts for the target platform.
The heart of the CMake build process is the CMakeLists.txt file. The CMakeLists.txt file is written in CMake’s own scripting language, which is designed to be simple and easy to understand.
In CMake, a CMakeLists.txt file is used to specify how a project should be built. The file contains a set of CMake commands and variables that define the project’s source files, dependencies, build options, and other configuration settings.
The CMakeLists.txt file is typically placed in the root directory of the project and is used by CMake to generate build files for the target platform. These build files can then be used to compile and link the project on the target platform.
The structure of a CMakeLists.txt file can vary depending on the project, but it usually includes the minimum version of CMake required, the project name, the source files, and any dependencies or libraries required by the project. CMakeLists.txt files can also include control flow structures such as conditionals, loops, functions, and macros to enable more complex build configurations.
Example CMakeLists.txt file
# Set the minimum version of CMake required
# Set the project name and version
project(MyProject VERSION 1.0)
# Set the source files for the project
# Add an executable target
# Set the include directories
target_include_directories(MyExecutable PUBLIC include)
# Set the compiler flags
target_compile_options(MyExecutable PRIVATE -Wall -Wextra)
In this example, the CMakeLists.txt file sets the minimum version of CMake required, sets the project name and version, and specifies the source files for the project. It then adds an executable target and sets the include directories and compiler flags.
The file is composed of CMake commands, which are called with arguments to configure the build process. These commands are interpreted by CMake and used to generate the build files for the project.
Variables and Commands
CMake uses a number of variables and commands to configure and build projects. Variables are considered to be strings, and they can be used to specify paths, compiler flags, and other configuration options. There are two types of variables in CMake: built-in variables and custom variables. Built-in variables are provided by CMake and can be used in any project. These variables are defined by CMake and are automatically set when a project is configured. Custom variables are defined by developers and can be used to store project-specific information.
CMake also provides a number of built-in commands that can be used to perform common tasks, such as setting variables and running commands. Built-in commands are defined by CMake and are available in any project.
In addition to variables and commands, CMake also provides a number of control flow features that allow developers to customize the build process. These features include conditionals, loops, custom functions, and macros.
Conditionals are used to specify different build configurations for different platforms or build types. CMake provides support for a number of conditionals, including the IF, ELSE, and ENDIF statements.
The IF statement allows developers to check whether a variable or expression evaluates to true or false. If the expression is true, CMake executes the code block following the IF statement. Otherwise, CMake skips the code block and moves on to the next statement.
For example, the following code checks whether the platform is Windows:
# Code to execute on Windows
# Code to execute on other platforms
CMake provides support for loops, including the FOREACH and WHILE loops. The FOREACH loop iterates over a list of values, assigning each value to a variable. The WHILE loop executes a code block repeatedly as long as a condition is true.
For example, the following code uses a FOREACH loop to iterate over a list of source files:
# Code to build each source file
Custom Functions and Macros
CMake allows developers to define custom functions and macros to simplify the build process. Functions and macros are similar, but macros allow developers to define more complex behavior.
Functions are defined using the FUNCTION statement, and macros are defined using the MACRO statement. Both functions and macros can take parameters, and they can be called just like built-in commands.
For example, the following code defines a custom function to build a library:
FUNCTION(BUILD_LIBRARY NAME SOURCES)
# Call the custom function
CMake provides a number of control flow features that allow developers to customize the build process. Conditionals, loops, custom functions, and macros can be used to create complex build configurations and simplify the build process. By using these features, developers can create efficient and effective build systems that meet their specific needs.
CMake provides a number of tools and features to help developers configure projects. Developers can specify the project name, version, and description, as well as the programming languages and libraries used in the project. CMake can also be configured to generate project files for different platforms and tools. When configuring a project for a different platform than the current one it is called cross-compiling. The project configuraton step is essentially CMake understanding your project structure in its internal format so that it can then continue with the phase of generating a project. At this step CMake will also cache some variables so that future reconfiguration is faster.
Generating Build Scripts
Once the project has been configured, CMake can generate build scripts for the target platform. CMake can generate build scripts for a wide range of build environments, including Make, Ninja, Visual Studio, Xcode, and many more. CMake generates the build scripts based on the configuration defined in the CMakeLists.txt file.
Customizing Build Configuration
CMake provides a number of tools and features to help developers customize the build configuration. Developers can specify the build type (debug, release, etc.), compiler flags, and other options. CMake can also be configured to use different compilers and tools for different platforms. Developers can even create whole pipelines by executing their own custom tools and scripts in other languages when the project is configured or when the project is built.
Some examples of what you can do is that you can specify that you want to run static analyzers on your build so that you get more messages when you make mistakes.
CMake allows developers to define build targets, which represent individual components of the project. A build target can be a library, executable, a test suite or a custom target. CMake provides a number of tools and features to help developers define and manage build targets. It also allows developers to define steps and order of building them.
Using CMake for Unit Testing
CMake provides support for unit testing, allowing developers to define and run unit tests as part of the build process. CMake can be configured to use a variety of testing frameworks, including Google Test, Boost.Test, and Catch. You can check out more on how to write unit tests.
Defining Test Suites
To define a test suite in CMake, developers use the add_test() function. This function takes two arguments: the name of the test suite and the command to run the test. The command can be a script or executable that performs the test. This command should execute the test application and based on the return value of that execution it will determine the success or failure of the test. If the command or program executed normally and returned a code of zero then it is a pass and if it returns a error code number then it is a fail.
Running Test Suites
CMake provides a number of tools and features to help developers run test suites. Developers can use the ctest command to run all the tests in the project, or they can use the ctest -R <regex> command to run tests that match a particular regular expression. Since tests are a separate executable you could also not use ctest at all and just directly execute your tests by hand of course.
Generating Test Reports
CMake provides built-in support for running tests as part of the build process. This includes the ability to generate test reports in various formats, such as XML or HTML, which can be used to track the results of tests and identify any issues or failures.
Generating test reports with CMake can be useful for tracking the results of tests across different builds or platforms, as well as identifying and fixing any issues or failures in the test suite.
There is also an integration with CDash. CDash is an open-source web-based system for collecting, organizing, and presenting software testing results. It provides a centralized platform for managing and analyzing testing data from multiple sources, including CMake’s built-in test reporting tool. CTest can generate test reports in a format that is compatible with CDash, which allows you to automatically upload your test results to a CDash server and view them in a web interface.
Using CMake for Cross-Compiling
CMake provides support for cross-compiling, which allows developers to build software for platforms that are different from the development platform. Cross-compiling is useful for embedded systems, where the target platform may have limited resources or a different architecture. Compiling for the web using WebAssembly is also considered cross-compiling since you are compiling your C/C++ code to a platform that has certain limitations.
CMake toolchain files are used to specify custom toolchains for cross-compiling or other specialized build environments. A toolchain file is a script that sets the CMake variables needed to build a project with a specific toolchain.
When CMake is invoked with a toolchain file, it uses the settings specified in the file to determine the compiler, linker, libraries, and other build tools needed to build the project. This enables CMake to generate build files that are specific to the target platform and environment.
Toolchain files are typically used when building for embedded systems or other non-standard environments, where the target platform may have different architecture or libraries than the development machine. They can also be used for cross-compiling to different operating systems or architectures.
When cross-compiling, developers need to ensure that the project uses the correct libraries for the target platform. CMake provides a number of tools and features to help developers manage platform-specific libraries, including the find_library() function and the target_link_libraries() function. I personally have made the mistake of linking to a windows lib file and then wondering why my linux build always fails.
Using CMake for Dependency Management
CMake provides support for managing dependencies, including libraries, header files, and other resources. CMake can automatically download and build dependencies, or it can be configured to use pre-built binaries. ExternalProject, add_subdirectory or using VCPKG or Conan can all be a way to manage dependencies with CMake.
CMake provides a number of built-in modules that help developers locate libraries and other dependencies. These modules can be used to find system libraries, third-party libraries, and other resources needed for the build process. One of the most popular ones for graphical applications is FindOpenGL which locates the opengl libraries on your system.
CMake can automatically build dependencies, using the ExternalProject_Add() function. This function allows developers to specify the dependency, the source code location, and the build configuration. You can also build dependencies using Fetch or add_subdirectory.
Using Pre-Built Binaries
CMake can be configured to use pre-built binaries for dependencies. Developers can specify the location of the pre-built binaries, and CMake will automatically include them in the build process. This is a bit harder to maintain for cross-platform development as you have to have the pre-built binaries for each platfrom you’re compiling for. But using this approach would save a lot of time in building since you rarely modify your dependencies (except when you want to fix bugs).
CMake is a powerful tool for configuring and building software projects. It provides support for a wide range of build environments, including cross-compiling and unit testing. CMake also provides tools for managing dependencies and customizing build configuration. By following best practices, developers can use CMake to build efficient and effective software projects.
If you want to become even more proficient in CMake you can check out my Udemy course on CMake.