(An Advanced/Optional) Introduction to CMake

Last update: 16 Aug 2024 [History] [Edit]

This page provides an exhaustive description of CMake:

  • How one can use it to build code completely independent of ATLAS
  • how we make use of it for building ATLAS software
  • how one can use the features provided by the ATLAS CMake code to build code against ATLAS analysis (or offline) releases.

Getting Started

Acquiring CMake

Practically all linux distributions provide a native package for CMake. On such systems your best bet is usually just to use the native cmake executable. Keep in mind though that the ATLAS CMake code requires at least CMake version 3.2 these days. If your platform’s native CMake version is older than this, you should install a newer version by hand.

You can always find the sources and various binaries for the latest version(s) of CMake on their main download page: cmake.org/download/. The CMake binaries are very self-contained, so in most cases just downloading an appropriate binary is perfectly acceptable. But on UNIX-like systems building CMake is also very simple. You can do it like:

Note - CMake will be available in most package managers or even included by default

Configure - Build - Install (Optional - UNIX)

CMake follows the same general idea as practically all UNIX projects. Very often you build a software package like:

tar -xvzf SomeSoftware-1.2.3.tar.gz
cd SomeSoftware-1.2.3/
./configure --prefix=/usr/local/SomeSoftware/1.2.3
make
[sudo] make install

Even the build of CMake itself follows the very same procedure.

This is exactly the same for CMake based projects. In general you take 3 steps to install them:

  • Configure the build
  • Build
  • Install the products

As a reminder, CMake itself is really only used in the first step of all of this. During the project’s configuration it generates a configuration for the underlying build system (GNU Make in most cases) that takes care of the second and third steps. All in all, in general you build a CMake based project like:

tar -xvzf SomeSoftware-1.2.3.tar.gz
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/usr/local/SomeSoftware/1.2.3 ../SomeSoftware-1.2.3/
make
[sudo] make install

Separate Source and Build Directories

As you could see from the previous example already, the vast majority of CMake based projects can be “built out of source”. Such that the build products are kept completely separately from the source files/directories. It’s worth to point out however that different projects may have different preferences for being built in- or out-of-source.

To execute a default in-source build, you’d do the following:

tar -xvzf SomeSoftware-1.2.3.tar.gz
cd SomeSoftware-1.2.3
cmake
make

In ATLAS we only officially support out-of-source builds of the software at the moment. In-source builds should in principle work, but the clear recommendation is that you should always use out-of-source builds with the ATLAS code.

CMake 101 - Examples

Hello World! – Lets try a few examples

Create a separate directory away from your ATLAS analyses in a new lxplus session or even your local machine. As always, let’s start with a Hello World example. Set up a directory like tutorial/helloworld/source for this. And then write a simple .cxx file in it. Let’s call the source file helloworld.cxx.

// System include(s):
#include <iostream>

int main() {

   // Greet the world:
   std::cout << "Hello World!" << std::endl;

   // Return gracefully:
   return 0;
}

Building this source file is of course extremely simple in this case with your compiler. You can just call g++ -o helloworld helloworld.cxx or clang++ -o helloworld helloworld.cxx. You can just try doing this as a first step.

Now, let’s write a configuration for CMake that does practically the same. Every CMake project must have a file called CMakeLists.txt in its root directory. In this case let’s put a file into the same directory (tutorial/helloworld/source) as the source file. (Strictly speaking it’s not mandatory to put the project’s main CMakeLists.txt file in the root directory of the project. But it’s a widely accepted convention to organise code like this.)

The first (technical) thing that every CMakeLists.txt file has to declare is a minimum CMake version that’s required to interpret it. Expressed like:

cmake_minimum_required( VERSION 3.14 )

Next, one has to declare a name for the project that we’re configuring/building. This is done like:

project( HelloWorld )

You always have to include these two statements in your configuration file.

Next, let’s declare to CMake that it should build an executable. This can be done with the add_executable function. Like this:

add_executable( helloworld helloworld.cxx )
# Mandatory setting for minimal CMake requirement:
cmake_minimum_required( VERSION 3.14 )

# Create a project:
project( HelloWorld )

# Declare the executable:
add_executable( helloworld helloworld.cxx )

Now, create a build directory called tutorial/helloworld/build, and execute:

cd tutorial/helloworld/build/
cmake ../source
make

You should now have an executable called helloworld in your build directory. Go ahead, and try running it.

Library and Executable

Let’s now start building something a bit more complicated. A small project that builds a shared or static library, and an executable that makes use of this library. Let’s not make the source of it too complicated. You should just create 3 simple source files in a directory layout like:

  • tutorial/fullexample/source/lib/mylibrary.h
  • tutorial/fullexample/source/lib/mylibrary.cxx
  • tutorial/fullexample/source/app/myexecutable.cxx

You could write your example code for this if you like, or just take the simple implementation from here.

#ifndef FULLEXAMPLE_MYLIBRARY_H
#define FULLEXAMPLE_MYLIBRARY_H

/// Simple function
void simpleFunction();

#endif // FULLEXAMPLE_MYLIBRARY_H
// System include(s):
#include <iostream>

// Local include(s):
#include "mylibrary.h"

/// Implement the simple function
void simpleFunction() {

   // Greet the world:
   std::cout << "Hello World from simpleFunction!" << std::endl;

   // Return gracefully:
   return;
}
// Local include(s):
#include "mylibrary.h"

int main() {

   // Call the library's function:
   simpleFunction();

   // Return gracefully:
   return 0;
}

Now let’s try to build this project…

Building the Library

We start out with a tutorial/fullexample/source/CMakeLists.txt file like in the first example. Holding just:

# Manadtory setting for minimal CMake requirement:
cmake_minimum_required( VERSION 3.14 )

# Create a project:
project( FullExample )

Building a library can be done using the add_library function. It has a syntax very similar to add_executable. To build a default static library, you can just write:

add_library( MyLibrary lib/mylibrary.cxx )

Note that you only have to specify the source file that makes up your library. But it’s a very good habit to list both the header and source files that make up a component in the component’s declaration. As in this case when CMake generates an IDE project for something like XCode or VisualStudio, the header files would show up correctly in the IDE. So in this case you should preferably write:

add_library( MyLibrary lib/mylibrary.h lib/mylibrary.cxx )

Try building your project. It should produce a static library called libMyLibrary.a in your build directory. Now, you should try building a shared library instead. To do this, you just need to modify your library declaration to look like:

add_library( MyLibrary SHARED lib/mylibrary.h lib/mylibrary.cxx )

Building the Executable

As a first iteration let’s just try to build the executable like we did in the Hello World example. By just using:

add_executable( MyExecutable app/myexecutable.cxx )

As you should expect, this will not just work like this. If you try building your project with a configuration like this, you should see something like the following:

[ 50%] Built target MyLibrary
Scanning dependencies of target MyExecutable
[ 75%] Building CXX object CMakeFiles/MyExecutable.dir/app/myexecutable.cxx.o
/afs/cern.ch/user/a/aparker/public/SoftwareEssentials/NonATLASCMake/fullexample/source/app/myexecutable.cxx:2:10: fatal error: mylibrary.h: No such file or directory
 #include "mylibrary.h"
          ^~~~~~~~~~~~~
compilation terminated.
make[2]: *** [CMakeFiles/MyExecutable.dir/app/myexecutable.cxx.o] Error 1
make[1]: *** [CMakeFiles/MyExecutable.dir/all] Error 2
make: *** [all] Error 2

Let’s work around this issue for now by modifying the myexecutable.cxx source file to include the header of the library with the relative “../lib/mylibrary.h” path instead. Once you do that, the build will fail like:

Scanning dependencies of target MyExecutable
[ 25%] Building CXX object CMakeFiles/MyExecutable.dir/app/myexecutable.cxx.o
[ 50%] Linking CXX executable MyExecutable
Undefined symbols for architecture x86_64:
  "simpleFunction()", referenced from:
      _main in myexecutable.cxx.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make[2]: *** [MyExecutable] Error 1
make[1]: *** [CMakeFiles/MyExecutable.dir/all] Error 2
make: *** [all] Error 2

The problem here is of course that we never declared that the executable has to use the library that we are also building in the project. We do this by using the target_link_libraries command. By putting the following line after the declaration of the executable and library:

target_link_libraries( MyExecutable MyLibrary )

With this added, the project should finally build both the library and the executable.

# Mandatory setting for minimal CMake requirement:
cmake_minimum_required( VERSION 3.14 )

# Create a project:
project( FullExample )

# Only necessary on MacOS X to silence a warning:
set( CMAKE_MACOSX_RPATH ON )

# Build the library:
add_library( MyLibrary SHARED lib/mylibrary.h lib/mylibrary.cxx )

# Build the executable:
add_executable( MyExecutable app/myexecutable.cxx )
target_link_libraries( MyExecutable MyLibrary )

Component Properties

Let’s now come back to the first compilation problem that we bumped into. That the myexecutable.cxx source file tried to include the library’s header simply with “mylibrary.h”. Which would be very desirable, as that source file should not have to know where that header is exactly in the source tree. Or if it’s even inside of the same source tree, or in some external location. So let’s change the include statement back to #include "mylibrary.h" and try to make the build work.

The blunt approach for this may be to simply use the include_directories function. This function allows us to declare additional include paths for all of the components in the project. You may just add the following to the configuration to make it work:

include_directories( ${CMAKE_SOURCE_DIR}/lib )

Note the usage of ${CMAKE_SOURCE_DIR} variable in this expression. At this point it should be mentioned how CMake deals with variables in the configuration files. You can set a variable using the set function. After setting a variable, you can refer to it in consecutive lines with the ${varname} formalism.

Now, using include_directories will make this tiny project work correctly, but it’s not a technique that wold work nicely for large projects. We don’t want to declare all components to use this include path all the time. We just want components that need to link against the MyLibrary library to use this include path. CMake has a system of doing this. Using the target_include_directories function. Now in this case, the logical setup is that everything that links against MyLibrary should be seeing files under lib/`. We can do this by adding the following to the configuration after declaring the library:

target_include_directories( MyLibrary PUBLIC ${CMAKE_SOURCE_DIR}/lib )

After adding this, just by declaring that MyExecutable needs to link against MyLibrary, it will receive this include path automatically for its build. Also, if you would set up the build of another library, that needs to link against MyLibrary, that new library, and all of its clients, would see lib/ in their include paths.