7 Tips for Great CMake Scripts. How to Make Your DevOps Life Easier | by Branislav Holländer | Towards Data Science

CMake has become the de facto standard tool for C++build automation, testing, and packaging. It is widely used for multi-platform development and supports generating build files for most C++ compilers. Since it was introduced 21 years ago, it came a long way and added support for many great features, gaining popularity among C++ developers.
In this post, I would like to introduce you to some tips that might help you write better CMake scripts. While you may already be familiar with some of them, I am sure you will find some useful ones as well.
Functions like add_definitions, include_directories, link_libraries, and others are still around in modern CMake due to backward compatibility. However, whenever possible, you should prefer using their counterparts target_compile_definitions, target_include_directories, target_sources, or target_link_libraries instead.
Since the functions without target_ do not specify a scope, they will leak to all targets specified in the directory. For instance, if you want to build an executable target that additionally depends on a library defined in a subdirectory, you could specify it like this:
project(main_project)add_executable(main_project main.cpp)add_subdirectory(dependency_lib)include_directories(include_files)
Unfortunately, since the directories specified in include_directories are appended to the list of all targets in the current CMakeLists.txt file, they will also be appended to dependency_lib. Thus, we might end up with a scenario of using the wrong include files in dependency_lib. The same also applies to the other commands. Especially with commands such as add_definitions or add_compile_options, this may cause some hard-to-find compilation errors.
On the other hand, using target_include_directories or target_link_libraries, we explicitly specify the target we want to use, avoiding any leakage problems. Furthermore, these commands let us optionally specify the scope to achieve the desired inheritance behavior (see next tip).
In modern CMake, target commands let you specify the command scope using INTERFACE, PRIVATE and PUBLIC keywords. This is useful if you want to carry over a dependency from a child target to the parent.
Let us consider, for example, the following code:
project(main_project)add_executable(main_project main.cpp)add_subdirectory(dependency_lib)target_link_libraries(main_project PRIVATE dependency_lib)
In the subdirectory dependency_lib, we have the following CMakeLists.txt:
project(dependency_lib)add_library(dependency_lib SHARED library.cpp)add_subdirectory(sub_dependency_lib)target_link_libraries(dependency_lib PRIVATE sub_dependency_lib)
Clearly, we have a hierarchy of dependencies. main_project depends on dependency_lib, which in turn depends on sub_dependency_lib. Written like this, sub_dependency_lib is not visible to main_project. This means that main_project won’t be able to use any functions of sub_dependency_lib directly. If you substitute the keyword PRIVATE with PUBLIC, you will also be able to use sub_dependency_lib in main_project. This is useful if dependency_lib does not completely hide the dependency, which is often the case. The INTERFACE keyword is less frequently used and designates a dependency used in “upstream” targets without being used in the target where it is declared (in this example, sub_dependency_lib would be visible to main_project but wouldn’t be used in dependency_lib itself).
The default steps you should perform before configuring any CMake build are creating a build subdirectory and then running CMake. However, sometimes you may forget to create the additional directory and run CMake in the root. This “in-source build” pollutes your project and creates many changes in git.
If you want to disable in-source builds, put the following lines in your root CMakeLists.txt file:
if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})message(FATAL_ERROR “In-source build detected!”)endif()
This simple script compares the source directory and build directory and throws an error if they are equal.
It is recommended to specify the C++ standard for every CMake project with the following definition:
project(main_project LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)
This ensures that the standard will be enforced given compiler support and turns off non-standard C++ features. Avoid setting the C++ standard by adding compile flags. This is not cross-platform compatible since every compiler uses different flags.
If you require more fine-tuned control, you may specify concrete C++ features to be supported by the compiler as well:
target_compile_features(dependency_lib    PUBLIC        cxx_nullptr    PRIVATE        cxx_variadic_templates        cxx_lambdas)
In this case, we require that dependency_lib is compiled with support for variadic templates, nullptr, and lambdas, while upstream targets should also be compiled with nullptr support.
With modern CMake, you aren’t restricted only to C or C++. CMake also supports other languages such as Objective-C or Fortran. One of the more frequently used languages in the C++ world is CUDA, NVIDIA’s GPGPU programming language. Setting up CUDA was traditionally rather difficult. With CMake, you can add CUDA as a programming language in a CMake project like this:
cmake_minimum_required(VERSION 3.8 FATAL_ERROR)project(cmake_and_cuda LANGUAGES CXX CUDA)
Then you can add new CUDA kernels just as easily as C++ source files:
add_executable(cmake_and_cuda kernel.cu kernel.h)
To select a specific CUDA architecture(s), you can add the following to the cmake command:
cmake -DCMAKE_CUDA_FLAGS=”-arch=sm_75” .
As with any other programming languages, CMake also advocates the Do Not Repeat Yourself (DRY) principle. It is always good to put code that you need more than twice into a separate function or macro. A macro in CMake is defined as follows:
macro(foo arg)  <commands>endmacro()
A function, on the other hand, is defined as:
function(foo arg)  <commands>endfunction()
Although these two concepts are very similar, they differ in the scope: a function defines its own scope, while a macro behaves like if you pasted in the code where it is used. For example, a return() statement in a macro, rather than returning control from the macro, returns the enclosing scope's control.
If your project depends on a custom target, such as an external repository, you can use the ExternalProject module. The ExternalProject_Add command allows you to download and use a git repo like this:
ExternalProject_Add(  Eigen  GIT_REPOSITORY "https://gitlab.com/libeigen/eigen.git"  GIT_TAG "${EXPECTED_EIGEN_VERSION}"  SOURCE_DIR eigen  BINARY_DIR eigen-build  CMAKE_ARGS    -DBUILD_TESTING:BOOL=OFF    -DBUILD_SHARED_LIBS:BOOL=ON)
In this example, we use the popular Eigen3 matrix-vector library by specifying the repository URL, a specific git tag to checkout, a source directory, and a binary directory as in an ordinary CMake project, as well as CMake arguments. Besides git, the command also supports Subversion, CVS, Mercurial, and an ordinary download. The added project is assumed to be a CMake project by default (although it doesn’t need to be). You can specify a separate CMake generator and a separate build command as well.
After specifying the external project, it can be used as another CMake target, i.e., your target can depend on it, and the ExternalProject itself can depend on other targets as well. You can find more information about the ExternalProject module in the CMake documentation.
Modern CMake is an extensive and extensible system for defining an entire software build. It supports many different platforms and languages and allows you to build a hierarchy of interdependent targets. One last tip: treat your CMake code as a part of your codebase and maintain it well. This will make the build process transparent to all developers on the project.
CMake documentation: https://cmake.org/cmake/help/v3.20/
CMake targets explained: https://leimao.github.io/blog/CMake-Public-Private-Interface/
Target transitivity explained: https://pspdfkit.com/blog/2018/modern-cmake-tips/