H.3 CMake for developers

The following is a very short overview of some of the more commonly used commands to give you a rough idea of the CMake syntax. This is, however, but not a substitute for a full tutorial, which you should work through yourself before contributing.

CMake support in FHI-aims is organized in files called CMakeLists.txt. There is one CMakeLists.txt in the root directory, which contains most of the functionality, and one in every subdirectory, containing primarily a list of source files.

In any CMake project, the first command in the topmost CMakeLists.txt is usually

cmake_minimum_required(VERSION x.x.x)

which sets the minimum required version of CMake for the project and ensures compatibility with that version or higher. This is followed by

project(MyProject VERSION 1.0.0 LANGUAGES C)

which sets the project name, version, and any languages used in the project (more languages can be enabled later). The basic syntax for setting a variable is set(<var> <value>), like this:

set(LIBS mkl_sequential)

Variables can be referenced with the ${…} construct. For example,

set(LIBS ${LIBS} mkl_core)
message(${LIBS})

prints “mkl_sequential mkl_core” to the screen. This is a very basic usage of set, which can actually take several more arguments. The full functionality of the set or any other CMake command can be seen by either viewing the online CMake manual or using the --help argument to cmake:

cmake --help set

There is only one variable type in the CMake language, which is the string type. Even if some variables may be treated as numbers or booleans, they are still stored as strings.

Every statement is a command that takes a list of string arguments and has no return value. Thus, all CMake commands are of the form command_name(arg1 ...). No command can be used directly as input for another command. Even control flow statements are commands:

if (USE_MPI)
  message("MPI parallelism enabled")
endif() # This is also a command. It takes no arguments.

A CMake-based buildsystem is organized as a set of high-level logical targets. Each target corresponds to an executable or library, or is a custom target containing custom commands. Dependencies between the targets are expressed in the buildsystem to determine the build order and the rules for regeneration in response to change. Executables and libraries are defined using the add_executable and add_library commands. Linking against libraries takes place via the target_link_libraries command:

add_library(mylib func_info.c mgga_c_scan.c xc_f03_lib_m.f90)
add_executable(myexe main.f90)
target_link_libraries(myexe mkl_intel_lp64 mkl_sequential mkl_core)

A library may be given by its full path, which is the standard practice, or by just the base name where the “-l” part is optional (both “-lmkl_core” and “mkl_core” are fine). In the latter case, directories to be linked against must be specified elsewhere. In addition to the standard locations, additional header directories may be specified using

include_directories(...)

include_directories accepts an additional [AFTER|BEFORE] argument, which determines whether to append or prepend to the current list of directories.

When do I need to reconfigure the build files?

If a cache variable is modified, for example by using a cache editor, or if there are any other changes to the build settings, like when adding/removing a source file, the build files need to be regenerated. Fortunately, CMake can detect this and regenerate the build files automatically whenever the build command is issued. Thus, there is never a need to manually run cmake on the build directory except for the very first time. However, depending on the extent of your changes to the CMake scripts, the build configuration might sometimes have to be reset manually. Because CMake is incapable of tracking all the files that are generated during configuration, there is no cmake clean command. If you need to reset the build configuration, simply run rm -rf on the build directory.

Why is my compilation taking so long?

CMake uses Make recursively when generating the build files, which has an adverse effect for projects with a large number of source files. There is no way around it except to switch to a different build system. When using a different generator (which generates build files for a different build system), the only change is in the initial CMake call. For example,

cmake -G Ninja -C initial_cache.cmake ~aims

chooses Ninja as the build system (otherwise the default is Make in Linux), initializes the cache using initial_cache.cmake, and specifies aims as the source directory. The generator cannot be changed without emptying the build directory first. For users, it does not make a big difference which generator is used, but for developers it is advisable to use Ninja instead of Make as it is faster, especially for small incremental builds.