Apache Mesos - CMake

Install CMake 3.7+

Linux

Install the latest version of CMake from CMake.org. A self-extracting tarball is available to make this process painless.

Currently, few of the common Linux flavors package a sufficient CMake version. Ubuntu versions 12.04 and 14.04 package CMake 2; Ubuntu 16.04 packages CMake 3.5. If you already installed cmake from packages, you may remove it via: apt-get purge cmake.

The standard CentOS package is CMake 2, and unfortunately even the cmake3 package in EPEL is only CMake 3.6, you may remove them via: yum remove cmake cmake3.

Mac OS X

HomeBrew’s CMake version is sufficient: brew install cmake.

Windows

Download and install the MSI from CMake.org.

NOTE: Windows needs CMake 3.8+, rather than 3.7+.

Quick Start

The most basic way to build with CMake, with no configuration, is fairly straightforward:

mkdir build
cd build
cmake ..
cmake --build .

The last step, cmake --build . can also take a --target command to build any particular target (e.g. mesos-tests, or tests to build mesos-tests, libprocess-tests, and stout-tests): cmake --build . --target tests. To send arbitrary flags to the native build system underneath (e.g. make), append the command with -- <flags to be passed>: cmake --build . -- -j4.

Also, cmake --build can be substituted by your build system of choice. For instance, the default CMake generator on Linux produces GNU Makefiles, so after configuring with cmake .., you can just run make tests in the build folder like usual. Similarly, if you configure with -G Ninja to use the Ninja generator, you can then run ninja tests to build the tests target with Ninja.

Supported options

See configuration options.

Examples

See CMake By Example.

Documentation

The CMake documentation is written as a reference module. The most commonly used sections are:

The wiki also has a set of useful variables.

Dependency graph

Like any build system, CMake has a dependency graph. The difference is that targets in CMake’s dependency graph are much richer compared to other build systems. CMake targets have the notion of ‘interfaces’, where build properties are saved as part of the target, and these properties can be inherited transitively within the graph.

For example, say there is a library mylib, and anything which links it must include its headers, located in mylib/include. When building the library, some private headers must also be included, but not when linking to it. When compiling the executable myprogram, mylib’s public headers must be included, but not its private headers. There is no manual step to add mylib/include to myprogram (and any other program which links to mylib), it is instead deduced from the public interface property of mylib. This is represented by the following code:

# A new library with a single source file (headers are found automatically).
add_library(mylib mylib.cpp)

# The folder of private headers, not exposed to consumers of `mylib`.
target_include_directories(mylib PRIVATE mylib/private)

# The folder of public headers, added to the compilation of any consumer.
target_include_directories(mylib PUBLIC mylib/include)

# A new exectuable with a single source file.
add_executable(myprogram main.cpp)

# The creation of the link dependency `myprogram` -> `mylib`.
target_link_libraries(myprogram mylib)

# There is no additional step to add `mylib/include` to `myprogram`.

This same notion applies to practically every build property: compile definitions via target_compile_definitions, include directories via target_include_directories, link libraries via target_link_libraries, compile options via target_compile_options, and compile features via target_compile_features.

All of these commands also take an optional argument of <INTERFACE|PUBLIC|PRIVATE>, which constrains their transitivity in the graph. That is, a PRIVATE include directory is recorded for the target, but not shared transitively to anything depending on the target, PUBLIC is used for both the target and dependencies on it, and INTERFACE is used only for dependencies.

Notably missing from this list are link directories. CMake explicitly prefers finding and using the absolute paths to libraries, obsoleting link directories.

Common mistakes

Booleans

CMake treats ON, OFF, TRUE, FALSE, 1, and 0 all as true/false booleans. Furthermore, variables of the form <target>-NOTFOUND are also treated as false (this is used for finding packages).

In Mesos, we prefer the boolean types TRUE and FALSE.

See if for more info.

Conditionals

For historical reasons, CMake conditionals such as if and elseif automatically interpolate variable names. It is therefore dangerous to interpolate them manually, because if ${FOO} evaluates to BAR, and BAR is another variable name, then if (${FOO}) becomes if (BAR), and BAR is then evaluated again by the if. Stick to if (FOO) to check the value of ${FOO}. Do not use if (${FOO}).

Also see the CMake policies CMP0012 and CMP0054.

Definitions

When using add_definitions() (which should be used rarely, as it is for “global” compile definitions), the flags must be prefixed with -D to be treated as preprocessor definitions. However, when using target_compile_definitions() (which should be preferred, as it is for specific targets), the flags do not need the prefix.

Style

In general, wrap at 80 lines, and use a two-space indent. When wrapping arguments, put the command on a separate line and arguments on subsequent lines:

target_link_libraries(
  program PRIVATE
  alpha
  beta
  gamma)

Otherwise keep it together:

target_link_libraries(program PUBLIC library)

Always keep the trailing parenthesis with the last argument.

Use a single space between conditionals and their open parenthesis, e.g. if (FOO), but not for commands, e.g. add_executable(program).

CAPITALIZE the declaration and use of custom functions and macros (e.g. EXTERNAL and PATCH_CMD), and do not capitalize the use of CMake built-in (including modules) functions and macros. CAPITALIZE variables.

CMake anti-patterns

Because CMake handles much more of the grunt work for you than other build systems, there are unfortunately a lot of CMake anti-patterns you should look out for when writing new CMake code. These are some common problems that should be avoided when writing new CMake code:

Superfluous use of add_dependencies

When you’ve linked library a to library b with target_link_libraries(a b), the CMake graph is already updated with the dependency information. It is redundant to use add_dependencies(a b) to (re)specify the dependency. In fact, this command should rarely be used.

The exceptions to this are:

  1. Setting a dependency from an imported library to a target added via ExternalProject_Add.
  2. Setting a dependency on Mesos modules since no explicit linking is done.
  3. Setting a dependency between executables (e.g. the mesos-agent requiring the mesos-containerizer executable). In general, runtime dependencies need to be setup with add_dependency, but never link dependencies.

Neither of these commands should ever be used. The only appropriate command used to link libraries is target_link_libraries, which records the information in the CMake dependency graph. Furthermore, imported third-party libraries should have correct locations recorded in their respective targets, so the use of link_directories should never be necessary. The official documentation states:

Note that this command is rarely necessary. Library locations returned by find_package() and find_library() are absolute paths. Pass these absolute library file paths directly to the target_link_libraries() command. CMake will ensure the linker finds them.

The difference is that the former sets global (or directory level) side effects, and the latter sets specific target information stored in the graph.

Use of include_directories

This is similar to the above: the target_include_directories should always be preferred so that the include directory information remains localized to the appropriate targets.

Adding anything to endif ()

Old versions of CMake expected the style if (FOO) ... endif (FOO), where the endif contained the same expression as the if command. However, this is tortuously redundant, so leave the parentheses in endif () empty. This goes for other endings too, such as endforeach (), endwhile (), endmacro () and endfunction ().

Specifying header files superfluously

One of the distinct advantages of using CMake for C and C++ projects is that adding header files to the source list for a target is unnecessary. CMake is designed to parse the source files (.c, .cpp, etc.) and determine their required headers automatically. The exception to this is headers generated as part of the build (such as protobuf or the JNI headers).

Checking CMAKE_BUILD_TYPE

See the “Building debug or release configurations” example for more information. In short, not all generators respect the variable CMAKE_BUILD_TYPE at configuration time, and thus it must not be used in CMake logic. A usable alternative (where supported) is a generator expression such as $<$<CONFIG:Debug>:DEBUG_MODE>.

Remaining hacks

3RDPARTY_DEPENDENCIES

Until Mesos on Windows is stable, we keep some dependencies in an external repository, 3rdparty. When all dependencies are bundled with Mesos, this extra repository will no longer be necessary. Until then, the CMake variable 3RDPARTY_DEPENDENCIES points by default to this URL, but it can also point to the on-disk location of a local clone of the repo. With this option you can avoid pulling from GitHub for every clean build. Note that this must be an absolute path with forward slashes, e.g. -D3RDPARTY_DEPENDENCIES=C:/3rdparty, otherwise it will fail on Windows.

EXTERNAL

The CMake function EXTERNAL defines a few variables that make it easy for us to track the directory structure of a dependency. In particular, if our library’s name is boost, we invoke:

EXTERNAL(boost ${BOOST_VERSION} ${CMAKE_CURRENT_BINARY_DIR})

Which will define the following variables as side-effects in the current scope:

The implementation is in 3rdparty/cmake/External.cmake.

This is not to be confused with the CMake module ExternalProject, from which we use ExternalProject_Add to download, extract, configure, and build our dependencies.

CMAKE_NOOP

This is a CMake variable we define in 3rdparty/CMakeLists.txt so that we can cancel steps of ExternalProject. ExternalProject’s default behavior is to attempt to configure, build, and install a project using CMake. So when one of these steps must be skipped, we use set it to CMAKE_NOOP so that nothing is run instead.

CMAKE_FORWARD_ARGS

The CMAKE_FORWARD_ARGS variable defined in 3rdparty/CMakeLists.txt is sent as the CMAKE_ARGS argument to the ExternalProject_Add macro (along with any per-project arguments), and is used when the external project is configured as a CMake project. If either the CONFIGURE_COMMAND or BUILD_COMMAND arguments of ExternalProject_Add are used, then the CMAKE_ARGS argument will be ignored. This variable ensures that compilation configurations are properly propagated to third-party dependencies, such as compiler flags.

CMAKE_SSL_FORWARD_ARGS

The CMAKE_SSL_FORWARD_ARGS variable defined in 3rdparty/CMakeLists.txt is like CMAKE_FORWARD_ARGS, but only used for specific external projects that find and link against OpenSSL.

LIBRARY_LINKAGE

This variable is a shortcut used in 3rdparty/CMakeLists.txt. It is set to SHARED when BUILD_SHARED_LIBS is true, and otherwise it is set to STATIC. The SHARED and STATIC keywords are used to declare how a library should be built; however, if left out then the type is deduced automatically from BUILD_SHARED_LIBS.

MAKE_INCLUDE_DIR

This function works around a CMake issue with setting include directories of imported libraries built with ExternalProject_Add. We have to call this for each IMPORTED third-party dependency which has set INTERFACE_INCLUDE_DIRECTORIES, just to make CMake happy. An example is Glog:

MAKE_INCLUDE_DIR(glog)

GET_BYPRODUCTS

This function works around a CMake issue with the Ninja generator where it does not understand imported libraries, and instead needs BUILD_BYPRODUCTS explicitly set. This simply allows us to use ExternalProject_Add and Ninja. For Glog, it looks like this:

GET_BYPRODUCTS(glog)

Also see the CMake policy CMP0058.

PATCH_CMD

The CMake function PATCH_CMD generates a patch command given a patch file. If the path is not absolute, it’s resolved to the current source directory. It stores the command in the variable name supplied. This is used to easily patch third-party dependencies. For Glog, it looks like this:

PATCH_CMD(GLOG_PATCH_CMD glog-${GLOG_VERSION}.patch)
ExternalProject_Add(
  ${GLOG_TARGET}
  ...
  PATCH_COMMAND     ${GLOG_PATCH_CMD})

The implementation is in 3rdparty/cmake/PatchCommand.cmake.

Windows patch.exe

While using patch on Linux is straightforward, doing the same on Windows takes a bit of work. PATH_CMD encapsulates this:

As such, PATCH_CMD lets us apply patches as we do on Linux, without requiring an administrative prompt.

Note that on Windows, the patch file must have CRLF line endings. A file with LF line endings will cause the error: “Assertion failed, hunk, file patch.c, line 343”. For this reason, it is required to checkout the Mesos repo with git config core.autocrlf true.