Modern CMake (>=3.0.0) and guidelines

Table of Contents

1 Daniel Pfeifer’s video

2 Variables scope

There are different variable types that can be defined in cmake.

Normal variable

set(<variable> <value>... [PARENT_SCOPE])

Cache variable

set(<variable> <value>... CACHE <type> <docstring> [FORCE])

meant to provide user-settable values

Environment variable

set(ENV{<variable>} [<value>])
  • Normal variables are only defined in the current scope
  • Each new directory or function (not macro) creates a new scope

3 Custom commands (function/macro)

  • do not hesitate to define functions and macro to factorize code
  • function create new scope, text replacement in macro
  • argc argv, argn, …

Function example

function(bar arg1 arg2)
  message(STATUS "Arguments are: ${arg1} and ${arg2}")
  message(STATUS "Number of arguments: ${ARGC}")
  message(STATUS "First argument: ${ARGV0}")
  message(STATUS "Second argument: ${ARGV1}")
  message(STATUS "All arguments: ${ARGV}")
  message(STATUS "All optional arguments: ${ARGN}")
  set(list_var "${ARGV}")
  foreach(_var IN LISTS list_var)
    message(STATUS "${_var}")
  endforeach()
endfunction()

message(STATUS "--bar--")
bar(one two three four)
message(STATUS "list_var: ${list_var}")

Macro example

macro(bar arg1 arg2)
  message(STATUS "Arguments are: ${arg1} and ${arg2}")
  message(STATUS "Number of arguments: ${ARGC}")
  message(STATUS "First argument: ${ARGV0}")
  message(STATUS "Second argument: ${ARGV1}")
  message(STATUS "All arguments: ${ARGV}")
  message(STATUS "All optional arguments: ${ARGN}")
  set(list_var "${ARGV}")
  foreach(_var IN LISTS list_var)
    message(STATUS "${_var}")
  endforeach()
endmacro()

message(STATUS "--bar--")
bar(one two three four)
message(STATUS "list_var: ${list_var}")

Use macros if you want to change parameters (output parameters), function otherwise.

4 Generator expressions

To improve compact writting when properties are added under some conditions see cmake generator expressions.

target_compile_options(MyTarget PRIVATE "$<$<CONFIG:Debug>:--my-flag>")
target_compile_options(MyTarget PRIVATE "$<IF:$<CONFIG:Debug>,--my-flag,--my-other-flag>")

5 Don't edit CMAKECXXFLAGS in cmake project

It's difficult to maintain due to the different compilers flags and each compiler also evolved.

Prefer using targetcompileoptions and targetcompilefeatures, optionnaly with generator expressions.

target_compile_options(FOO PRIVATE -Werror)
target_compile_features(FOO PRIVATE cxx_std_11)

6 Managing source file lists

Avoid using file(GLOB …)

file(GLOB SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
add_executable(foo ${SRC_FILES})

because if no CMakeLists.txt file changes when a source is added or removed then the generated build system cannot know when to ask CMake to regenerate.

Prefer directly define your target with a precise list of sources. Additional files can be added later.

add_executable(foo
  foo1.cpp
  foo2.cpp)
...
if (FOO_BOOST)
  target_sources(foo foo_boost.cpp)
endif()
if (WIN32)
  target_sources(foo foo_win.cpp)
endif()

This allows to stick to requirements and is less prone to errors than playing with custom variables. Custom variables can contain errors, empty strings or typos. Giving such a bazar to cmake commands leads to undefined behaviors and is hard to debug.

7 Properties on targets vs. variables

  • cmake <= 2.8.12: create custom variables, append list of sources, flags, link libraries and call a few time cmake commands addexecutable, addlibrary, targetlinklibraries, includedirectories, addcompileoptions
  • cmake >=3.0.0: targets and properties, call appropriate cmake commands to update properties of specific targets

Don't do that

find_package(Boost 1.55 COMPONENTS asio)
list(APPEND INCLUDE_DIRS ${BOOST_INCLUDE_DIRS})
list(APPEND LIBRARIES ${BOOST_LIBRARIES})

include_directories(${INCLUDE_DIRS})
link_libraries(${LIBRARIES})
  • avoid using command that operates at the directory level
- add_compile_options
- include_directories
- link_directories
- link_libraries
  • prefer
- target_compile_definitions
- target_compile_features
- target_compile_options
- target_include_directories
- target_link_libraries
- target_sources
- set_target_properties

Do that

find_package(Boost 1.55 COMPONENTS asio)
target_include_directories(MyTarget PUBLIC ${BOOST_INCLUDE_DIRS})
target_link_libraries(MyTarget PUBLIC ${BOOST_LIBRARIES})

8 Propagate properties on target or not, that is the question (PRIVATE|INTERFACE|PUBLIC)

target_link_libraries is used to add dependencies between targets. This is not only dependency in term of library linking but also in term of flags (compilation flags, definitions). Flags can be propagated transitively from targets to targets that are linked depending on the keyword used.

Keywords PRIVATE, INTERFACE, PUBLIC can be used in "target_" commands in order to inform cmake whether to propagate the given properties of a target (library) to its dependents or not.

  • PRIVATE : target properties only required at the build level
  • INTERFACE : only required when the target is used, when linked to it (header only library)
  • PUBLIC : required in both situation, to build the target and when another target depends on it
target_link_libraries(foo
  PUBLIC  bar
  PRIVATE cow)
  • cow is used internally by foo but is not exposed in foo's API so that dependents do not need it to use foo
  • bar is also used by foo and some bar's objects are used in foo's API so that we need to inform the dependent of foo that bar is also required to use foo

This can be used to transitively propagate required dependencies and allows to avoid having to manage different custom lists of dependencies by hand (cumbersome and difficult to maintain).

In the following example public headers are stored in include while some other private headers are located in src

target_include_directories(foo
  PUBLIC  ${CMAKE_CURRENT_SOURCE_DIR}/include
  PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)

8.1 headers only library

add_library(fooho INTERFACE)
target_compile_definitions(fooho INTERFACE FOO=1)
target_include_directories(fooho INTERFACE include)

Users only have to use for example

target_link_libraries(bar PUBLIC fooho)

to get appropriate flags from fooho.

9 Export your targets into a config file for your users

# define target information to export
install(TARGETS heat EXPORT heatTargets
        DESTINATION lib
        PUBLIC_HEADER DESTINATION include)
# rule to generate the file containing the targets information
install(EXPORT heatTargets
        FILE HeatTargets.cmake
        NAMESPACE heat::
        DESTINATION lib/cmake/heat)

# install the config file containing all information (targets + some variables like the version)
install(FILES "HeatConfig.cmake"
        DESTINATION lib/cmake/heat)

Minimalist Config file

include("${CMAKE_CURRENT_LIST_DIR}/HeatTargets.cmake")

10 Use exported target when looking for dependencies

Find modules vs. config files exported

  • find modules like FindFOO.cmake are full of guesses, it tries to determine where and how a dependeny has been installed
  • Config files are better like FOOConfig.cmake because it contains the exact properties of the installed project
  • and config files with targets and not just variables are the best (the list of targets exported should be documented somewhere)

In an utopic world users should just write that and inherit from the foo target "INTERFACE" properties

find_package(foo REQUIRED)
...
target_link_libraries(bar Foo::foo ...)

11 ctest and cdash and how to use it with gcov and valgrind

  • ctest script outside of the project, this can be project agnostic
  • filtering tests by name, naming convention with your project name as prefix (so that your project can be imported)
  • –output-on-failure
# CTest script

set(CTEST_SOURCE_DIRECTORY "/src")
set(CTEST_BINARY_DIRECTORY "/build")

# should ctest wipe the binary tree before running
set(CTEST_START_WITH_EMPTY_BINARY_DIRECTORY TRUE)

set(CTEST_CMAKE_GENERATOR "Unix Makefiles")

find_program(COVERAGE_COMMAND gcov)
find_program(MEMORYCHECK_COMMAND valgrind)
set(CTEST_COVERAGE_COMMAND "${COVERAGE_COMMAND}")
set(CTEST_MEMORYCHECK_COMMAND "${MEMORYCHECK_COMMAND}")

# define ctest steps
ctest_start("Experimental")
ctest_configure(OPTIONS "-DCMAKE_C_FLAGS=-g -O0 -Wall --coverage -DCMAKE_EXE_LINKER_FLAGS=--coverage -DCMAKE_C_COMPILER=gcc")
ctest_build()
ctest_test()
ctest_coverage()
ctest_memcheck()
ctest_submit()
ctest -S bench.cmake

12 static analysis with cmake

Tools like cppcheck, clang-tidy, cpplint, include-what-you-use (iwyu) can be used natively when invoking make after cmake configure

find_program(CPPCHECK cppcheck)
set(CMAKE_CXX_CPPCHECK "${CPPCHECK}"
  "--language=c"
  "--platform=unix64"
  "--enable=all"
  "--force"
  "--inline-suppr"
  )
...
set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY}")
set(CMAKE_CXX_CPPLINT "${CPPLINT}")
set(CMAKE_CXX_INCLUDE_WHAT_YOU_USE "${IWYI}")

Created: 2019-08-20 ar. 10:11

Validate