CMake: An Essential Tutorial

Previously in this blog, we have talked about Yocto and how it is an extremely popular linux build system that allows you to generate a linux distribution of your choice. Yocto contains a tool called devtool that allows you to generate a recipe on the source tree that is provided to it. It currently supports GNU Autotools, CMake, Qmake, and Plain makefile.

Let us take a closer look at CMake.

CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. The suite of CMake tools were created by Kitware in response to the need for a powerful, cross-platform build environment for open-source projects such as ITK and VTK.

Installing CMake

The best place to grab CMake and install it is the CMake download page. This is especially true if you are interested in installing a particular release version. Also, one of the outputs of CMake is a Makefile that we will use the make tool on. It is a good idea to install that as well if not already installed!

If you are on Linux, you can also install CMake using a package manager like apt. For example, you can do the below on an Ubuntu machine.

$ sudo apt update
$ sudo apt install cmake make

Similarly on a Mac, you can install CMake using HomeBrew.

$ brew install cmake make

Creating your first CMake project

Let us now create an absolutely minimal but working CMake project called hello.

Anywhere on your PC, create a folder called hello and navigate into it.

$ mkdir hello
$ cd hello

Now, create a simple hello.c as below in this folder.

#include <stdio.h>

int main(void)
{
printf("Hello, World!\r\n");
return 0;
}

In the same folder, create a file named CMakeLists.txt and add the below content inside it.

cmake_minimum_required(VERSION 2.8.12)
project(hello)
add_executable(hello hello.c)

The CMake looks for a file named CMakeLists.txt to do its thing – always make sure that a file with this name is present in your source tree on which you run CMake.

This minimal CMakeLists.txt file does three things as below.

  • We have simply specified that the minimum version of CMake that should be used is 2.8.12
  • The name of the project is hello
  • We want an executable named hello which is built using the file hello.c.

NOTE: Source trees that have multiple directories can make use of CMake by either having just a top-level CMakeLists.txt or multiple such files inside each sub-directory. The name should remain the same.

It is considered a good practice to have the CMake output in a separate directory from the source tree. So, create a new folder called build (or anything you want!) inside the hello/ folder and navigate into it.

$ mkdir build
$ cd build

All that is remaining now is to do your first ever CMake build!

Remember – you have to provide the path to the source tree that contains the CMakeLists.txt. Since, we are now in the build/ folder, the argument shall be the previous folder i.e. ..

$ cmake ..

You should now see output messages that look like the below:

-- The C compiler identification is AppleClang 12.0.0.12000032
-- The CXX compiler identification is AppleClang 12.0.0.12000032
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Library/Developer/CommandLineTools/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Library/Developer/CommandLineTools/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/hello/build

NOTE: The output will look different depending on the OS you are on and/or the compiler version you have installed on your OS.

Previously, build/ was empty. Now, it has content that looks like this.

$ ls
CMakeCache.txt CMakeFiles Makefile cmake_install.cmake

The Makefile is the one that we will use the make tool on to build our hello application.

$ make

You should see the below output now.

[ 50%] Building C object CMakeFiles/hello.dir/hello.c.o
[100%] Linking C executable hello
[100%] Built target hello

You should now see the hello binary as the output as indicated above. Simply, execute the binary as below.

$ ./hello

You should now see a Hello, World! printed on your terminal.

Congratulations! You have successfully created and built your first CMake package! Let us now move on to something that reflects the most common use-cases of CMake. 

Working with a typical source-tree

A typical source tree consists of components that have to be built mandatorily, some are optional and may have dependencies on other components. Also, often the act of including components is governed by a global configuration file. Let us work with such a source tree and see how CMake makes life simple for such cases.

Say, we want to build a simple calculator library that provides functions as below.

  • Always Build
    • Hello World
  • Optional Build
    • Mandatory if parent build is enabled – Feature Set 0
      • Add
      • Subtract
      • Multiply
      • Divide
    • Optional if parent build is enabled – Feature Set 1
      • Percentage
      • Factorial
      • Power
      • Inverse

Simply put, we have the below possibilities here.

  • Build #1 – Only Hello World is built
  • Build #2 – Only Hello World + Feature Set 0 is built
  • Build #3 – Hello World + Feature Set 0 + Feature Set 1 is built

The source code for this example is located here (https://github.com/sckulkarni246/ke-cmake-sample-apps) – do check it out!

Snapshot of the source code organization

This is how the source code is organised.

  • <Package root folder>/
    • CMakeLists.txt – This is where the CMake magic starts
    • g_config.h.in – This is the input to the CMake configurator that will be used to generate a global configuration header file
    • hello_world .c and .h – This is the source code that will be built always
    • calculator/
      • f0 .c and .h – Implementation of the feature set 0
      • f1 .c and .h – Implementation of the feature set 1
      • CMakeLists.txt – This file will be used by the top-level CMakeLists.txt if the optional components are needed. Note – the name is exactly the same at both levels.
    • << other supporting files like .gitignore, LICENSE, README, etc. >>

Understanding <Package Root>/CMakeLists.txt

This is how the top-level CMakeLists.txt file looks like – see the descriptions below to understand what is happening here.

See the numbered descriptions below!

Let’s see what is happening here.

  1. This is a the basic configuration info like minimum CMake version supported, project name and version.
  2. We add the hello_world.c to the variable called SRCS
  3. We create an option called BUILD_CALC and set it to OFF – this is how typically optional build components are configured. If you need this component, it has to be explicitly set to ON in the CMakeLists.txt file or passed as ON during CMake invocation in the terminal (see below)
  4. If the BUILD_CALC option is ON, we tell CMake to look for a subdirectory called calculator/ which should have a CMakeLists.txt file that tells CMake what to do
  5. This is the interesting part – auto-generating a global configuration file. We use the g_config.h.in as the input and tell CMake to generate a g_config.h as the output header file.
  6. The g_config.h is located in the build directory. However, it is needed for the source code to build successfully. Hence, we tell CMake to add the build directory to the path where headers are looked for.
  7. Our goal was to create a static library. We do that here using the add_library(...) command with the STATIC argument. We also provide the name of the library we want along with the path to the source code that is to be built (these filenames are inside the SRCS variable).

Note how in step 4, we pointed CMake to a subdirectory called calculator/ – let us now look at the calculator/CMakeLists.txt file.

Understanding <Package Root>/calculator/CMakeLists.txt

See the numbered descriptions below!

Let’s see what is happening in this lower-level file.

  1. We tell CMake to include the feature support for dependent options – we shall use this next
  2. We create dependent options called BUILD_CALC_F0 and BUILD_CALC_F1.
    Notice that both of them depend on BUILD_CALC.
    If BUILD_CALC is ON,
    BUILD_CALC_F0 is set to ON if BUILD_CALC is ON making it a mandatory build
    BUILD_CALC_F1 is set to OFF if BUILD_CALC is ON making it an optional build
  3. We create an empty variable called CALC_SRCS
  4. If BUILD_CALC_F0(F1) is ON, we add the f0.c(f1.c) to the CALC_SRCS variable
  5. Finally, we append the CALC_SRCS variable’s contents to the SRCS variable from the top-level CMake file – note the PARENT_SCOPE at the end that specifies we have to use SRCS as it exists in the parent scope

Build the package!

Let us now build the package.

Remember, we had three possibilities – let us realise all three of those now. But first, create a build/ directory inside the package root and navigate to it as below.

$ mkdir build
$ cd build

Build #1 – Only mandatory components

The only mandatory component is hello_world.c. We have already done the needful configuration in our top-level CMakeLists.txt for this. Simply execute the below while in the build/ directory.

$ rm -rf *
$ cmake ..

You should see output similar to the one you got for the hello project above.

Now, run make.

$ make

Notice that the only file built is the hello_world.c in the package root. This is because we have not enabled any optional component.

[ 50%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o
[100%] Linking C static library libkecmakesampleapps.a
[100%] Built target kecmakesampleapps

Build #2 – Mandatory components + feature set 0

We saw that the BUILD_CALC option needs to be set to ON for the feature set 0 to be built. We can do the edits in the CMakeLists.txt files but let us do it on the command-line instead as below.

We don’t need to specify the BUILD_CALC_F0 as it is set to ON automatically if BUILD_CALC is set to ON.

$ rm -rf *
$ cmake .. -DBUILD_CALC=ON

The cmake output is similar to the previous one. Now, run make.

$ make

Notice that the output now shows that along with hello_world.c, even the f0.c gets built.

[ 33%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o
[ 66%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f0.c.o
[100%] Linking C static library libkecmakesampleapps.a
[100%] Built target kecmakesampleapps

Build #3 – Build all components

In order to build all components i.e. hello_world.c, f0.c and f1.c, we need to set to ON all the options in our configuration. We do that as below.

$ rm -rf *
$ cmake .. -DBUILD_CALC=ON -DBUILD_CALC_F1=ON

Notice how we don’t need to specify anything for BUILD_CALC_F0 as it is enabled if BUILD_CALC is enabled.

The cmake output is similar to our previous outputs. Now, run make.

$ make

Notice that now all components are built – hello_world.c, f0.c and f1.c.

[ 25%] Building C object CMakeFiles/kecmakesampleapps.dir/hello_world.c.o
[ 50%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f0.c.o
[ 75%] Building C object CMakeFiles/kecmakesampleapps.dir/calculator/f1.c.o
[100%] Linking C static library libkecmakesampleapps.a
[100%] Built target kecmakesampleapps

Did you observe the contents of the g_config.h file during the three builds? What is your observation? Let us know in the comments below!

See you in the next post!

One thought on “CMake: An Essential Tutorial

Leave a Reply