CMake
CMake 🏝️ is a cross-platform language and tool to build, and run programs. It's a higher-level tool that will generate a file for a lower-level build tool (such as a Makefile) according to your needs.
The syntax of a CMake file is similar to a C program, but it's usually considered harder to learn and to master 🪜.
💡 CMake automatically detects and handles dependencies, making it easier to use for large or complex projects.
Create a CMakeLists.txt
$ touch CMakeLists.txt
$ nano CMakeLists.txt # edit
Execute a target 🌴
$ mkdir build && cd build
$ cmake .. # path to folder with the CMakeLists.txt
$ make # execute the generated Makefile
$ ./my_program # execute our "my_program"
CLI Usage
Basics
CMake is commonly used as cmake ..
then make
but this is not recommended by the documentation.
$ cmake -B /path/to/build -S /path/to/sources
-- Configuring done
-- Generating done
-- Build files have been written to: xxx
$ cmake --build /path/to/build
ninja: no work to do.
➡️ cmake ..
is the same as cmake -B . -S ..
and make
is the same as cmake --build .
(if the generator is a Makefile).
💡 You can build only specific targets using -t target
.
🫧 To undo the previous build, you can use the command below, which is the same as make clean
or ninja clean
.
$ cmake --build /path/to/build -t clean
Cleaning... 0 files.
Log Level
You can set the verbosity of CMake to one of ERROR
, WARNING
, NOTICE
, STATUS
,
VERBOSE
, DEBUG
, or TRACE
.
$ cmake --log-level=ERROR ...
Generators
CMake files are compiled to a lower-level tool such as a Makefile
. This is a generator and CMake supports multiple of them.
CMake will try to find a suitable generator for your environment, but you can explicitly ask for a generator using:
$ cmake -G Ninja # Use ninja (build.ninja)
$ cmake -G /usr/bin/ninja # Use a custom generator
...
You can pass arguments to the build tool:
$ cmake [...] -- -j 4 # ex: "make -j 4"
Override Variables
You can override the value of some variables using -D
:
$ cmake -DVAR_NAME VAR_VALUE [...]
$ cmake -D VAR_NAME=VAR_VALUE [...]
Common pre-defined variables:
-
CMAKE_BUILD_TYPE
: the kind of build such asRelease
orDebug
-
CMAKE_SOURCE_DIR
: path to the folder with the top-level CMakeLists -
PROJECT_SOURCE_DIR
: path to the nearest parent folder with a CMakeLists that as a call toproject()
-
CMAKE_BINARY_DIR
: folder where we build all projects -
PROJECT_BINARY_DIR
: folder where we build the current project -
CMAKE_INSTALL_PREFIX
: folder to install the project (ex: make install) -
CMAKE_INSTALL_DATADIR
: folder to install non-executable files -
CMAKE_PREFIX_PATH
: location used to locate dependencies - ...
Basic Usage
Basic CMakeLists.txt
These two lines are the only required lines.
# cmake_minimum_required(VERSION A..B)
# cmake_minimum_required(VERSION A)
# cmake_minimum_required(VERSION A FATAL_ERROR)
cmake_minimum_required(VERSION 3.18)
project(your_project_name)
The project function is quite powerful:
project(your_project_name C CXX) # C and C++
project(your_project_name VERSION 1.0 LANGUAGES C CXX)
A target 📍 is like a build artifact, such as a library or an executable.
CMake will automatically detect the languages for each target from each source file extension.
Compiler
Common compiler-related configuration:
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 17)
You can set compile options for all or one target:
target_compile_options(targetName PRIVATE -Wall -Wextra -Wpedantic [...])
target_compile_features(targetName PRIVATE cxx_std_17)
# enable #DEFINE XXX etc. (same as -DXXX etc.)
target_compile_definitions(target PRIVATE XXX "YYY=ZZZ" [...])
Some options are passed according to the type of build selected. See CMAKE_CXX_FLAGS_DEBUG
and CMAKE_CXX_FLAGS_RELEASE
.
Build Executables
You can generate an executable targetName
using:
# build a binary
add_executable(targetName xxx.c xxx.h [...])
add_executable(targetName xxx.cpp xxx.hpp [...])
Build Libraries
You can generate a library .so
(shared) or .a
(static) or header-only:
add_library(targetName INTERFACE xxx.h [...]) # header-only
add_library(libA SHARED src/xxx.cpp inc/xxx.h) # .so
add_library(libA STATIC src/xxx.cpp inc/xxx.h) # .a
add_library(libA src/xxx.cpp inc/xxx.h) # see BUILD_SHARED_LIBS
It's up to each library to determine which of its headers are available to others. So, if you want to use #include "xxx.h"
in sources of another target, the library must allow it first.
# ex: include headers in "inc" (PRIVATE) "api" (PUBLIC)
target_include_directories(targetName
# included files are visible to ours and other targets (api)
PUBLIC api [...]
# included files are not visible to ours and other targets (internal)
PRIVATE internal [...]
# included files are visible to other targets but not ours
INTERFACE inc [...]
)
# include system libraries (same as -isystem xxx)
target_include_directories(target SYSTEM ...)
# include libraries (same as -Ixxx)
target_include_directories(target ...)
Sources
You can add sources after creating a target using target_sources
.
target_sources(targetName xxx.h [...])
target_sources(targetName PUBLIC xxx.h [...])
target_sources(targetName PRIVATE xxx.h [...])
target_sources(targetName INTERFACE xxx.h [...])
You should enter all sources manually, as using functions such as file(...)
, break some generators and some IDEs.
file(GLOB_RECURSE ALL_SOURCES src/*.c src/*.cpp)
If supported by your generator, use CONFIGURE_DEPENDS
to add a CMake hook checking if any glob pattern changed:
file(GLOB_RECURSE ALL_SOURCES CONFIGURE_DEPENDS src/*.c src/*.cpp)
Dependencies
All projects usually have external or internal dependencies 👨👩👧👦.
- An executable requiring a library
- A library requiring another library
- ...
To do this, you should only have to use:
target_link_libraries(targetA SCOPE targetB)
target_link_libraries(targetA SCOPE -lxxx)
target_link_libraries(targetA SCOPE -L/path/to/lib/)
target_link_libraries(targetA SCOPE /path/to/lib.a)
target_link_libraries(targetA SCOPE /path/to/lib.so)
The scope, which is optional, can be one of:
-
PRIVATE
: dependencies only required to build targetA -
PUBLIC
: dependencies required to build targetA and dependencies that require targetA -
INTERFACE
: dependencies required to build dependencies that require targetA but not targetA
➡️ It may be obvious, but we almost mostly use PRIVATE
.
➡️ The default scope is determined according to the target (a lib...).
⚠️ If you are exposing headers that depend on a library, then the library must be PUBLIC
.
External Libraries
For external libraries, e.g., the ones not directly within the project, we use a finder to find some information needed to import them.
# sudo apt-get install libxml2 libxml2-dev
find_package(LibXml2 REQUIRED)
The line below changes according to how the finder works. The finder documentation usually documents which variables are set.
target_link_libraries(libB PRIVATE ${LIBXML2_LIBRARIES})
By using find /usr -name "FindLibXml2.cmake"
(the format is Find<PKGNAME>.cmake
) I could find the finder. Online documentation.
Common examples
Using math.h
target_link_libraries(my_program PRIVATE m)
Using pthread.h
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
target_link_libraries(my_app PRIVATE Threads::Threads)
Using PkgConfig
find_package(PkgConfig REQUIRED)
pkg_check_modules(XXX REQUIRED IMPORTED_TARGET pkgName)
# Use "PkgConfig::XXX" instead of "ProjectName::"
Custom Finders
We can add folders to CMAKE_MODULE_PATH
where our finders will be located. There is no specific rule/syntax although most follow the same conventions/patterns.
Example: Custom FindLibXml2.cmake
# try to find the library
# store in "LIBXML2_LIBRARIES" the path of the library if found
# Look for "libxml2.so" or "libxml2.dll" in /usr/lib, /usr/local/lib...
# Add "PATHS /custom/path/" to function call to use a custom path)
find_library(LIBXML2_LIBRARIES NAMES xml2 libxml2 REQUIRED)
# find the include directory
# set the variable "LIBXML2_INCLUDE_DIR" with the path to it
# specify the header we want to find to ensure that we really have libxml headers
find_path(LIBXML2_INCLUDE_DIRS NAMES libxml2/libxml/parser.h REQUIRED)
# include another module called FindPackageHandleStandardArgs
include(FindPackageHandleStandardArgs)
# use it to check if we can find LibXml2
find_package_handle_standard_args(LibXml2 DEFAULT_MSG LIBXML2_LIBRARIES LIBXML2_INCLUDE_DIRS)
mark_as_advanced(LIBXML2_LIBRARIES LIBXML2_INCLUDE_DIRS)
Example: Custom FindLibXml2.cmake
using PkgConfig
# sudo apt-get install pkg-config
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBXML2 REQUIRED libxml-2.0)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LibXml2 DEFAULT_MSG LIBXML2_LIBRARIES LIBXML2_INCLUDE_DIRS)
Core Syntax
Logging
We commonly add logging messages using message
:
message(STATUS "message") # messages for users
message(WARNING "message") # warning messages
message(DEBUG "message") # debug messages
message(TRACE "message") # trace
message(FATAL_ERROR "message") # raise an error
Variables
You can use variables with ${VARIABLE_NAME}
.
set(VARIABLE_NAME)
set(VARIABLE_NAME VARIABLE_VALUE)
unset(VARIABLE_NAME)
Some examples:
set(MY_NUMBER 3.4)
set(MY_BOOL TRUE)
set(MY_OPTION ON)
set(MY_CONDITION YES)
set(MY_STR something) # risky, always quote strings
set(MY_STR "something")
Lists Functions
Lists are variables with comma-separated values (a;b;...
).
set(myList "a;b;...")
set(myList A B...)
You have many functions to operate on lists:
# list(OPERATION DESTINATION_VARIABLE ARGS)
list(LENGTH MY_LIST_LENGTH myList) # get the length
list(APPEND myList A B...) # add values
list(GET myList 0 item) # get value at 0
File Functions
There are many file utilities:
file(READ filename CONTENT_READ)
file(WRITE filename "CONTENT")
file(APPEND filename "CONTENT")
file(DOWNLOAD URL filename)
Refer to advanced strings for multi-line calls.
There are multiple useful functions too:
get_filename_component(VAR /path/to/file DIRECTORY) # get directory
get_filename_component(VAR /path/to/file NAME) # get filename
get_filename_component(VAR /path/to/file NAME_WE) # without the extension
Conditions and statements
Statements
The syntax is:
if (CONDITION)
elseif ()
else ()
endif ()
Conditions
TRUE
, ON
, YES
and non-zero numbers are all true. OFF
, NO
, FALSE
, zero, and empty strings are all false.
As always, you have basic utilities:
-
NOT CONDITION
-
CONDITION1 AND CONDITION2
-
CONDITION1 OR CONDITION2
And there are some common operators:
-
DEFINED VARIABLE
: true if a variable is defined -
TARGET VARIABLE
: true if a target is defined -
COMMAND VARIABLE
: true if a command/function/macro is defined -
TEST VARIABLE
: true if a test is defined -
EQUAL
,LESS
,LESS_EQUAL
,GREATER
, andGREATER_EQUAL
: used to compare numeric values -
A VERSION_XXX B
such asVERSION_EQUAL
: compare software versions
There are also some special operators:
-
"A" STREQUAL "B"
: true if both strings are equal -
"STR" MATCHES "REGEX"
: true ifREGEX
matchesSTR
-
item IN_LIST myList
: true ifitem
is insidemyList
Functions
The syntax to declare a function is as follows:
function(FUNCTION_NAME)
endfunction()
function(FUNCTION_NAME ARG0)
endfunction()
function(FUNCTION_NAME ARG0 ARG1)
endfunction()
To call a function:
function_name(ARG0 ARG1)
FUNCTION_NAME(ARG0 ARG1 ARGN)
There are some pre-defined variables:
-
${ARGC}
: count of arguments -
${ARGV}
: list of arguments -
${ARG0}
: the first argument -
${ARGN}
: additional arguments after the last expected one
You can't return a value, and variables created within a function are deleted, unless you use:
set(VARIABLE_NAME VARIABLE_VALUE PARENT_SCOPE)
Common Topics
Multi-modules project
It's common to have big projects made of smaller units that can be configured and manipulated separately.
You will create a CMakeLists.txt
inside each submodule.
add_library(libA SHARED src/libA.cpp)
target_include_directories(libA PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
The top-level CMakeLists.txt
will include them.
cmake_minimum_required(VERSION 3.18)
project(untitled2)
...
# include nested ./libA/CMakeLists.txt
add_subdirectory(libA)
Custom Output Directories
There are a few variables you can set:
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CMAKE_REPORTS_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/reports")
set(CMAKE_DOC_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/doc")
CMAKE MODULE PATH
The CMAKE_MODULE_PATH
variable determines where CMake will look for some files. We can add our own folder will our own scripts/modules.
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
Create Reusable CMake Files
We can create .cmake
files with anything we want from variables to functions. There are some predefined ones and we create ours by adding .cmakes
files in folders in CMAKE_MODULE_PATH.
include(moduleName)
include(folder/moduleName)
Prevent In-Source Builds
Don't use the same folder as your sources to compile.
if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)
message(FATAL_ERROR "In-source builds not allowed.")
endif()
Clone External Projects
You can use ExternalProject
or the newer and improved FetchContent
to clone, configure, and build an external project.
ExternalProject was operating at build time which was inconvenient, while FetchContent operates at the project configure stage.
Handle project without a CMakeLists.txt
This is an overly complex example that:
- Clone a project
- Add a header and a CMakeLists.txt inside
- Allow our target to include the header
It shows that we can patch a cloned project and use it inside include and link.
⚠️ project_name
must be a lowercase string.
set(DEPS_NAME "project_name")
set(DEPS_SOURCES "${DEPS_NAME}_SOURCE_DIR")
FetchContent_Declare(
${DEPS_NAME}
GIT_REPOSITORY "https://github.com/xxx/yyy.git"
GIT_TAG "xxx"
)
# Ensure we don't call the code when it's unneeded
FetchContent_GetProperties(${DEPS_NAME})
if(NOT ${DEPS_NAME}_POPULATED)
# Use a manual handler
FetchContent_Populate(${DEPS_NAME})
# Do custom stuff to initialize the project
# (ex: we are adding a Makefile and a header, that's stupid)
file(WRITE ${${DEPS_SOURCES}}/CMakeLists.txt
"cmake_minimum_required(VERSION ${CMAKE_VERSION})\n"
"project(${DEPS_NAME})\n"
"add_library(${DEPS_NAME} INTERFACE)\n"
"target_include_directories(${DEPS_NAME} INTERFACE \${CMAKE_CURRENT_SOURCE_DIR})\n"
)
file(WRITE ${${DEPS_SOURCES}}/xxx.h
"#pragma once\n"
"void xxx();\n"
)
endif ()
# Fall back to the default handler (load our CMakeLists.txt)
FetchContent_MakeAvailable(${DEPS_NAME})
add_executable(someTarget main.cpp)
target_include_directories(someTarget PRIVATE ${${DEPS_SOURCES}})
target_link_libraries(someTarget PRIVATE ${DEPS_NAME})
Testing
CMake provides a utility to execute tests called ctest
. It works with popular testing libraries such as gtest
below.
File: src/add.cpp
#include <iostream>
int add(int a, int b) {
return a + b;
}
File: tests/test_add.cpp
#include <gtest/gtest.h>
#include "../src/add.cpp"
TEST(AddTest, PositiveNumbers) {
EXPECT_EQ(add(3, 4), 7);
}
You first need to enable testing, and find your testing library:
enable_testing() # support "add_test"
find_package(GTest REQUIRED) # Fill GTest::GTest GTest::Main
Assuming the following target for your sources:
add_library(my_target src/add.cpp) # not necessarily a library
The way to create a testing target is the same as normal targets:
add_executable(my_testing_target tst/test_add.cpp)
target_link_libraries(my_testing_target PRIVATE my_target GTest::GTest GTest::Main)
Old Traditional Way
We were manually calling add_test
. We got the test results for each target instead of the results for each test.
add_test(NAME my_testing_target COMMAND my_testing_target)
New Modern Way
We let gtest discover the tests. We can see the result of each test.
include(GoogleTest) # import gtest_discover_tests
gtest_discover_tests(my_testing_target)
Run the tests
You can execute the binary manually or use ctest
:
$ cmake --build . # don't forget to build
$ ctest # use "-V" for verbose
Static Code Analysis
You can configure static code analyzers. There are multiple tools and multiple ways to do it, each with its own pros and cons.
We will use clang-tidy as a reference here.
The easiest way is to set CMAKE_CXX_CLANG_TIDY
:
# all targets - before any target declaration
find_program(CLANG_TIDY_PATH clang-tidy REQUIRED)
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_PATH};-checks=*")
# per target - after target declaration
find_program(CLANG_TIDY_PATH clang-tidy REQUIRED)
set_target_properties(targetName
PROPERTIES CXX_CLANG_TIDY
"${CLANG_TIDY_PATH};-checks=*"
)
Unfortunately, clang-tidy is now called after each build. There is a well-known script that handles this: cmake-scripts/tools.cmake.
Otherwise, you may use the previous common approach. As long as you provide the list of all sources to compile, it works well.
# tell clang-tidy which options to use (see: -p=.)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# list all sources to check - use a regex?
set(ALL_SOURCES "${CMAKE_SOURCE_DIR}/src/example.cpp")
# add a target clang-tidy
find_program(CLANG_TIDY_PATH clang-tidy REQUIRED)
set(CLANG_TIDY_ARGS "-p=.;-checks=*") # Specify your desired checks
add_custom_target(clang-tidy
COMMAND ${CLANG_TIDY_PATH} ${CLANG_TIDY_ARGS} ${ALL_SOURCES}
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
The main advantage is that you control when clang-tidy is called, and that you can redirect the output for instance to a file.
$ cmake --build . -t clang-tidy
CMake Custom Targets
We use custom targets when we need to:
- 🚀️ Execute a command
- 🗃️ Execute a script
- ...
A common use case is when we need to generate some files.
Declare a custom target
Each custom target is associated with a command, directory within the function call, or using add_custom_command
.
# ex: we need some header generated by a command
add_custom_target(customTargetName DEPENDS api/header.h)
# 💡 You can link this target to another using:
add_dependencies(targetName customTargetName)
Declare a custom command
A custom command is a wrapper for a command generating some files (OUTPUT
) often based on dependencies (DEPENDS
) using COMMAND
.
add_custom_command(
OUTPUT # list of generated files
api/libH.h
COMMAND # command + arguments to get the output
script/my_script.sh
${CMAKE_BINARY_DIR}/data/file1.txt
DEPENDS # execute the command each time they change
data/file2.txt
${CMAKE_BINARY_DIR}/data/file1.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
💡 All parameters aside from OUTPUT
and COMMAND
are optional.
✅️ If cleaning commands such as ninja clean
or make clean
are available, OUTPUT
files are deleted automatically.
Random notes
Advanced Strings
multi-line strings
The multi-line string below corresponds to one string with newlines.
message([==[
some text
some text
]==])
This is the same as:
message("some text\nsome text")
Or:
message("some text
some text")
Match Pattern in String
CMake supports regexes and capture groups.
string(REGEX MATCH "xxx=([^ ]+)" match input_string)
if (match)
# ${CMAKE_MATCH_1} for the first capture group
endif()
➡️ If you must MATCHALL
, match
will be a list.
CMake Advanced Compile Options
You can set compile options per file language:
target_compile_options(my_target PRIVATE
-Wall # Apply to all source files
$<$<COMPILE_LANGUAGE:CXX>:
-O3 # Apply to C++ files only
>
$<$<COMPILE_LANGUAGE:C>:
-O2 # Apply to C files only
>
)
Execute Process
Execute Process can be used to execute a command during the configuration step instead of the build step.
execute_process(
COMMAND "command" "arg1" "arg2" "arg3"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
RESULT_VARIABLE CMD_STATUS
OUTPUT_VARIABLE CMD_OUTPUT
)
if (NOT ${CMD_STATUS} EQUAL 0) # handle the error
endif()
Configure files
Usage: Generate Headers 🔥
You can generate headers from a template filled with CMake Variables:
$ cat configure.h.in
#cmakedefine XXX "${ZZZ}"
#cmakedefine YYY "@ZZZ@"
Both syntaxes can be used. Variables are replaced if they are defined, or the "define" line is commented out.
configure_file(configure.h.in configure.h)
Usage: Add files to the build 🔥
We may want to copy some files in the build, for instance, if they are not compiled but used by other targets.
configure_file(
data/file.txt
${CMAKE_BINARY_DIR}/data/file.txt COPYONLY
)
Usage: Generate Files 🔥
We often use it to generate headers, but it works for any file.
$ cat template.sh
XXX=@MY_VARIABLE@
set(MY_VARIABLE "SOME VALUE")
configure_file(template.sh script.sh @ONLY)
👻 To-do 👻
Stuff that I found, but never read/used yet.
- cmake-gui, ccmake
- ctest, cpack
- Contextual Logs
- cmake presets, CMakePresets.json, CMakeUserPresets.json (user-speficic override)
- ModernCppStarter
- Akagi201/learning-cmake and awesome-cmake
- target_precompile_headers
-
mark_as_advanced
(show in GUI editor?) - An Introduction to Modern CMake
- macros (unlike function, no need for PARENT_SCOPE)
-
cmake --install /path/ --prefix /path/
-
XXX-config.cmake
/XXXConfig.cmake
$<XXX:arg>
$<IF:xxx,yyy,zzz>
$<IF:xxx,yyy,>
option(MY_OPTION "XXX" ON)