Learning to Learn: CMake
Table of Contents
CMake is the de-facto C++ build system used by an overwhelming number of C++ projects. Even if you personally favor more modern alternatives such as Meson, Bazel, or Pants, if you ever pull in a 3rd party dependency, there is a good chance that it uses CMake so knowing enough about CMake to understand it is worth knowing.
How to get started #
I recommend new users to CMake start with the following resources:
- Mastering CMake is a high level overview of how to use CMake developed and maintained by the CMake Developers.
- Modern CMake Is focuses on a useful subset of CMake for a number of common tasks.
Using CMake to build something #
If a package cmake 3.15 or newer (most people), the following sequence will build and install a cmake project
cmake -S ./path/to/sourcedir -B ./path/to/builddir
cmake --build -j $(nproc)
cmake --install
Here ./path/to/sourcedir is the directory where you source files (specifically the “toplevel” CMakeLists.txt is stored).
and ./path/to/builddir is a directory (which may not exist) where you want to store build artifacts prior to installation.
You can customize the build by passing flags to the first cmake command. I commonly use the following
cmake -S ./path/to/sourcedir -B ./path/to/builddir \
-G Ninja \
-DCMAKE_INSTALL_PREFIX=$(pwd)/.local \
-DBUILD_SHARED_LIBS=ON \
-DBUILD_TESTING=ON \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
cmake --build -j $(nproc)
cmake --install
To prefer the Ninja build generator, and ccache for faster development builds with shared libraries
installed to my user’s prefix instaed of the the / or /usr prefix which may require admin privledges
and to enable LLVM based tooliing (i.e. clangd for completions in my editor). These preferences
are encoded into my m build tool.
CMake also respects GNU style envionment variables (i.e. CXX, CC, CFLAGS,
CXXFLAGS, and LDLIBS) to pick up common defaults.
Key Commands #
Required boilerplate that must appear at the top of your top-level cmake
cmake_minimum_requiredproject
Basics
add_libraryandadd_executablecreate build objectstarget_link_librariesandPUBLICvsPRIVATEto consume dependancies (including their header file flags) and link librariestarget_include_directoriesto set include paths not provided by link librariestarget_compile_featuresa portable way to set the C++ standard- The most essential cmake variables
$CMAKE_CURRENT_SOURCE_DIRECTORYand$CMAKE_CURRENT_BINARY_DIRECTORY - Basic generator expressions such as
$<BUILD_INTERFACE:>vs$<INSTALL_INTERFACE:>for use withtarget_include_directories add_subdirectoryfor code organization
Adding 3rd party dependencies. Do yourself a favor and put all dependencies imports at the top level so the scope is right for the whole project.
find_packageto import dependancies built using CMake and certain system dependancies such as threads, MPI, Python, and OpenMP.find_package(PkgConfig)andpkg_search_modulesto import dependencies described by pkg-configFetchContentdownload and provide dependencies as part of the build, but consider instead third party tools such asspack,connan, andvcpackage.
I’ve written a guide on CMake package dependencies here.
- How do you currently handle dependencies and builds in your system? Is this portable to other machines where you would like to use this software? If not, what do you need to change and why?
Build Options
optionto add build optionsiffor basic control flowconfigure_fileto add a “configure file” which is typically header which has#definesfor certain build system variables set with#cmakedefineor#cmakedefine01target_add_sourcesto add additional source files to a library after the fact for example to compile in an optional plugin.
Installation
include(GNUInstallDirs)and its associated variables likeCMAKE_INSTALL_PREFIX,CMAKE_INSTALL_INCLUDEDIR, andCMAKE_INSTALL_LIBDIRinstall(TARGETS)to install your libraries and their export filesinstall(DIRECTORY)to install your header files and data filesinclude(CMakePackageConfigHelpers)andconfigure_package_config_fileandwrite_basic_package_version_fileto make your package importable by others
See this project for an example on how to properly expose a CMake dependency
Testing
include(CTest),BUILD_TESTING, andadd_testinclude(GoogleTest)andgtest_discover_testsfor high quality integration of googletest based tests
CMake Magic Variables to reconfigure builds
CMAKE_BUILD_TYPEautomatically configure optimizations like-O2and-Og -gwith settings likeReleaseorDebugthere are also other settings forRelWithDebInfoandMinSizeRelfor small build artifactsBUILD_SHARED_LIBSto choose between shared and static linkingINTERPROCEDURAL_OPTIMIZATIONproperty enables IPO for faster optimizations
Enabling 3rd Party technologies #
CMake provides most of what you want on its own, but a few tools are worth knowing about:
spacka dependencies manager for HPC softwareconnanandvcpackagea dependency managers more common in enterpriseccacheandsccachecan be provided toCMAKE_<Lang>_COMPILER_LAUNCHERto dramatically speed up re-builds buildsninjais a much faster project builder thanMake. You can enable it with-G Ninjapassed to cmakeccmakea terminal user interface for setting cmake build optionsclang-tidyandinclude-what-you-usefor extra static analysis can be used by setting the<LANG>_CLANG_TIDYand<LANG>_INCLUDE_WHAT_YOU_USEDoxygenfor automatic documentation form the header-files of your project. Can be used automatically from CMake withfind_package(Doxygen)
Important Concepts #
PUBLIC,INTERFACEvsPRIVATEdependency and configurationsSHARED,STATIC,INTERFACEvsIMPORTEDlibraries. The later you will use directly only seldom but is used forfind_packageinternally.- that
target_*functions accumulate from everywhere they are used allowing code that adds specific features to be spread out - Don’t use globbing to add files to a build. List them explicitly for best performance
Debugging CMake #
CMake can be obtuse at times. A few key commands can help:
--traceputs cmake into trace mode to print all calls made in a CMake build system.CMAKE_FIND_DEBUG_MODEprints out extra information when finding a packages that can be used to track down how a variable was set. Newer versions have a flag--debug-find-pkg=which can enable this for specific packages.message(WARNING ...)print a warning message to the console with the specified message can be used for tracing and viewing values of a variable at a point in the code
Advanced Topics #
for,function, andmacrofor advanced control flowexecute_processfor when you just need to run a script- CUDA and other accelerator language support
Where to learn more #
Changelog #
- 2023-02-09 linked to other cmake resources added section on debugging
- 2023-01-09 Added basic usage example
- 2022-11-15 Created