Modern CMake (>=3.0.0) and guidelines
Table of Contents
- 1. Daniel Pfeifer’s video
- 2. Variables scope
- 3. Custom commands (function/macro)
- 4. Generator expressions
- 5. Don't edit CMAKECXXFLAGS in cmake project
- 6. Managing source file lists
- 7. Properties on targets vs. variables
- 8. Propagate properties on target or not, that is the question (PRIVATE|INTERFACE|PUBLIC)
- 9. Export your targets into a config file for your users
- 10. Use exported target when looking for dependencies
- 11. ctest and cdash and how to use it with gcov and valgrind
- 12. static analysis with cmake
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}")