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 as Release or Debug
  • 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 to project()
  • 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, and GREATER_EQUAL: used to compare numeric values
  • A VERSION_XXX B such as VERSION_EQUAL: compare software versions

There are also some special operators:

  • "A" STREQUAL "B": true if both strings are equal
  • "STR" MATCHES "REGEX": true if REGEX matches STR
  • item IN_LIST myList: true if item is inside myList

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.

  • 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)