Programming/Tools/CMake

From Thalesians Wiki

Overview

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.

CMake is part of Kitware's collection of commercially supported open-source platforms for software development.

Whereas Make is a build system, which drives the compiler and other build tools to build your code, CMake is a generator of build systems. CMake can produce Makefiles, Ninja build files, KDEvelop or Xcode projects, and Visual Studio solutions from the same starting point—the same CMakeLists.txt file. So if you have a platform-independent project, CMake is a way to make it build system-independent as well.

According to Lars Knoll's Technical vision for Qt 6,

CMake is by far the most widely used build system in the C++ world, and better integration with it is sorely needed.

This tutorial

In this tutorial we demonstrate how to set up and build an (admittedly very simple) project using CMake on an Ubuntu 20.04.1 LTS system. The tutorial should be applicable to other Linux systems, possibly with minor modifications.

Preparation

First, we need to install CMake:

$ sudo apt update
...
$ sudo apt install cmake

Since we'll be using CMake to generate Makefiles and build them using Make, we need to install Make:

$ sudo apt install make

Since we'll be using CMake with gccGNU project C and C++ compiler,— we also need to install g++:

$ sudo apt install g++

Basic project

We'll start working from the home directory, so we

$ cd

to it. We then create a directory for the project

$ mkdir my_project

cd into it with

$ cd my_project

Now let us create a directory for source files

$ mkdir src

and the file CMakeLists.txt:

$ touch CMakeLists.txt

CMakeLists.txt contains a set of directives and instructions describing the project's source files and targets (executable, library, or both).

cmake_minimum_required(VERSION 3.5)
project(cmake_test_project)

While you can build the project from the current directory (.../my_project/), CMake will generate some files that will pollute it. Therefore it is a good idea to

$ mkdir build
$ cd build/
$ cmake ..

We should see something like this:

-- The C compiler identification is GNU 9.3.0
-- The CXX compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build

Under .../my_project/build/ you will see a pretty large Makefile containing something like this:

# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.16

# Default target executed when no arguments are given to make.
default_target: all

.PHONY : default_target

# Allow only one "make -f Makefile2" at a time, but pass parallelism.
.NOTPARALLEL:


#=============================================================================
# Special targets provided by cmake.

# Disable implicit rules so canonical targets will work.
.SUFFIXES:


# Remove some rules from gmake that .SUFFIXES does not remove.
SUFFIXES =

.SUFFIXES: .hpux_make_needs_suffix_list


# Suppress display of executed commands.
$(VERBOSE).SILENT:


# A target that is always out of date.
cmake_force:

.PHONY : cmake_force

#=============================================================================
# Set environment variables for the build.

# The shell in which to execute make rules.
SHELL = /bin/sh

# The CMake executable.
CMAKE_COMMAND = /usr/bin/cmake

# The command to remove a file.
RM = /usr/bin/cmake -E remove -f

# Escaping for special characters.
EQUALS = =

# The top-level source directory on which CMake was run.
CMAKE_SOURCE_DIR = /home/ubuntu/my_project

# The top-level build directory on which CMake was run.
CMAKE_BINARY_DIR = /home/ubuntu/my_project/build

#=============================================================================
# Targets provided globally by CMake.

# Special rule for the target rebuild_cache
rebuild_cache:
        @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Running                                                                                                              CMake to regenerate build system..."
        /usr/bin/cmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : rebuild_cache

# Special rule for the target rebuild_cache
rebuild_cache/fast: rebuild_cache

.PHONY : rebuild_cache/fast

# Special rule for the target edit_cache
edit_cache:
        @$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "No inter                                                                                                             active CMake dialog available..."
        /usr/bin/cmake -E echo No\ interactive\ CMake\ dialog\ available.
.PHONY : edit_cache

# Special rule for the target edit_cache
edit_cache/fast: edit_cache

.PHONY : edit_cache/fast

# The main all target
all: cmake_check_build_system
        $(CMAKE_COMMAND) -E cmake_progress_start /home/ubuntu/my_project/build/C                                                                                                             MakeFiles /home/ubuntu/my_project/build/CMakeFiles/progress.marks
        $(MAKE) -f CMakeFiles/Makefile2 all
        $(CMAKE_COMMAND) -E cmake_progress_start /home/ubuntu/my_project/build/C                                                                                                             MakeFiles 0
.PHONY : all

# The main clean target
clean:
        $(MAKE) -f CMakeFiles/Makefile2 clean
.PHONY : clean

# The main clean target
clean/fast: clean

.PHONY : clean/fast

# Prepare targets for installation.
preinstall: all
        $(MAKE) -f CMakeFiles/Makefile2 preinstall
.PHONY : preinstall

# Prepare targets for installation.
preinstall/fast:
        $(MAKE) -f CMakeFiles/Makefile2 preinstall
.PHONY : preinstall/fast

# clear depends
depend:
        $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-bui                                                                                                             ld-system CMakeFiles/Makefile.cmake 1
.PHONY : depend

# Help Target
help:
        @echo "The following are some of the valid targets for this Makefile:"
        @echo "... all (the default if no target is provided)"
        @echo "... clean"
        @echo "... depend"
        @echo "... rebuild_cache"
        @echo "... edit_cache"
.PHONY : help



#=============================================================================
# Special targets to cleanup operation of make.

# Special rule to run CMake to check the build system integrity.
# No rule that depends on this can have commands that come from listfiles
# because they might be regenerated.
cmake_check_build_system:
        $(CMAKE_COMMAND) -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR) --check-bui                                                                                                             ld-system CMakeFiles/Makefile.cmake 0
.PHONY : cmake_check_build_system

This Makefile is rather complex, but we needn't worry: CMake has generated it for us. However, this Makefile (at this stage) doesn't do anything useful. That is because we haven't written any code in our project. Let's add it.

First, let us modify .../my_project/CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)
project(cmake_test_project)

message(STATUS "*** Building my_project from ${PROJECT_SOURCE_DIR} ***")
add_subdirectory(${PROJECT_SOURCE_DIR}/src)

Make sure that we are under .../my_project/build and run

$ cmake ..

The following error message will appear:

-- *** Building my_project from /home/ubuntu/my_project ***
CMake Error at CMakeLists.txt:5 (add_subdirectory):
  The source directory

    /home/ubuntu/my_project/src

  does not contain a CMakeLists.txt file.


-- Configuring incomplete, errors occurred!
See also "/home/ubuntu/my_project/build/CMakeFiles/CMakeOutput.log".
See also "/home/ubuntu/my_project/build/CMakeFiles/CMakeError.log".

As the error suggests, the directory .../my_project/src/, which we have added to .../my_project/CMakeLists.txt must contain (another) CMakeLists.txt file. CMake uses a hierarchical configuration system. For now, let us simply

$ touch ../src/CMakeLists.txt

(assuming we are still under .../my_project/build/). Thus we create an empty child CMakeLists.txt file that does nothing.

Let's try building again:

$ cmake ..

This time the build has been successful:

-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build

Adding an executable

However, this build has been trivial since the project contains no code. Let us add some code to it:

$ touch ../src/main.c
$ nano ../src/main.c

and let's write the following content to .../my_project/src/main.c:

#include <stdio.h>

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

In order to build this file, we need to modify .../my_project/src/CMakeLists.txt:

add_executable(bin_main main.c)

This tells CMake to build an executable, bin_main out of the source files or, in this case, source file main.c.

Having made sure we are still under .../my_project/build, we again run

$ cmake ..

This has generated a Makefile. Now let's run make:

$ make

The output is

Scanning dependencies of target bin_main
[ 50%] Building C object src/CMakeFiles/bin_main.dir/main.c.o
[100%] Linking C executable bin_main
[100%] Built target bin_main

bin_main will be under .../my_project/build/src/ (since the directory structure under .../my_project/build/ mimics that above).

We can run it (assuming we are under .../my_project/build/):

$ src/bin_main
Hello, World!

Header-only libraries

Let us modify .../my_project/build/src/main.c as follows:

#include "sphere.h"

int main(void)
{
    float radius, vol;

    printf("Input the radius of the sphere: ");
    radius = get_value();
    printf("Surface area = ");
    put_value(surface_area(radius));
    vol = volume(radius);
    printf("Volume of sphere = ");
    put_value(vol);

    return EXIT_SUCCESS;
}

From the directory .../my_project/build/, let us run

$ cmake ..
-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build

and then

$ make
Scanning dependencies of target bin_main
[ 50%] Building C object src/CMakeFiles/bin_main.dir/main.c.o
/home/ubuntu/my_project/src/main.c:1:10: fatal error: sphere.h: No such file or directory
    1 | #include "sphere.h"
      |          ^~~~~~~~~~
compilation terminated.
make[2]: *** [src/CMakeFiles/bin_main.dir/build.make:63: src/CMakeFiles/bin_main.dir/main.c.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:94: src/CMakeFiles/bin_main.dir/all] Error 2
make: *** [Makefile:84: all] Error 2

We are getting this error because we haven't yet created sphere.h. Let us first

$ mkdir .../my_project/include

and underneath it create sphere.h with the following contents:

#ifndef SPHERE_H_INCLUDED
#define SPHERE_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>

#define PI 3.14159265

float get_value(void)
{
    float x;

    scanf("%f", &x);
    return x;
}

void put_value(float x)
{
    printf("%f\n", x);
}

static float my_pow(float x, int n);

float surface_area(float r)
{
    return 4. * PI * my_pow(r, 2);
}

float volume(float r)
{
    return 4. * PI * my_pow(r, 3) / 3.;
}

static float my_pow(float x, int n)
{
    if (n < 0)
        return 1. / my_pow(x, -n);
    else if (n == 0)
        return 1;
    else
        return x * my_pow(x, n - 1);
}

#endif /* SPHERE_H_INCLUDED */

We need to tell the compiler about this file (actually, the directory that contains it) by modifying .../my_project/CMakeLists.txt as follows:

cmake_minimum_required(VERSION 3.5)
project(cmake_test_project)

message(STATUS "*** Building my_project from ${PROJECT_SOURCE_DIR} ***")
include_directories(${PROJECT_SOURCE_DIR}/include)
add_subdirectory(${PROJECT_SOURCE_DIR}/src)

(Note that we have added include_directories.)

Make sure that we are under .../my_project/build/ and run

$ cmake ..
-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build
$ make
Scanning dependencies of target bin_main
[ 50%] Building C object src/CMakeFiles/bin_main.dir/main.c.o
[100%] Linking C executable bin_main
[100%] Built target bin_main

Looks like our build has been successful. Let's test the executable:

$ src/bin_main
Input the radius of the sphere: 3.5
Surface area = 153.938034
Volume of sphere = 179.594376

sphere.h constitutes a "header-only" library. A library is called header-only if the full definitions of all macros, functions, and classes comprising the library are visible to the compiler in a header file form. Header-only libraries do not need to be separately compiled, packaged, and installed in order to be used. All that is required is to point the compiler at the location of the headers, and then #include the header files into the application source. Another advantage is that the compiler's optimizer can do a much better job when all the library's source code is available.

The disadvantages include:

  • brittleness—most changes to the library will require recompilation of all compilation units using that library;
  • longer compilation times—the compilation unit must see the implementation of all components in the included files, rather than just their interfaces;
  • code-bloat (this may be disputed)—the necessary use of inline statements in non-class functions can lead to code bloat by over-inlining.

Nonetheless, the header-only form is popular because it avoids the (often much more serious) problem of packaging.

For C++ templates, including the definitions in the header is the only way to compile, since the compiler needs to know the full definition of the templates in order to instantiate.

Adding a static library

A library is a collection of items that you can call from your program. You can save much time by reusing work that someone else has already done and be more confident that it has fewer bugs: since other people may be using the same library, you will benefit from their usage, bug detection, and bug fixes.

We have mentioned some reasons why header-only libraries may not be a good idea in some situations.

The next most straightforward way of using a library function is to have the object files from the library linked directly into your executable, just as with those you have compiled yourself. When linked like this the library is called a static library, because the library will remain unchanged unless the program is recompiled. The final result is a simple executable with no dependencies.

The static library under Linux is nothing more than an archive of object files.

Let us modify .../my_project/build/include/sphere.h so it contains declarations rather than definitions:

#ifndef SPHERE_H_INCLUDED
#define SPHERE_H_INCLUDED

#include <stdio.h>
#include <stdlib.h>

#define PI 3.14159265

float get_value(void);
void put_value(float x);
float surface_area(float r);
float volume(float r);

#endif /* SPHERE_H_INCLUDED */

Let us add .../my_project/build/src/geometry.c with the following contents:

#include "sphere.h"

static float my_pow(float x, int n);

float surface_area(float r)
{
    return 4. * PI * my_pow(r, 2);
}

float volume(float r)
{
    return 4. * PI * my_pow(r, 3) / 3.;
}

static float my_pow(float x, int n)
{
    if (n < 0)
        return 1. / my_pow(x, -n);
    else if (n == 0)
        return 1;
    else
        return x * my_pow(x, n - 1);
}

And let us add .../my_project/build/src/simple_io.c:

#include "sphere.h"

float get_value(void)
{
    float x;

    scanf("%f", &x);
    return x;
}

void put_value(float x)
{
    printf("%f\n", x);
}

If we now (making sure that we are under .../my_project/build) try to build the project, we'll get some errors:

$ cmake ..
-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build
$ make clean
$ make
Scanning dependencies of target bin_main
[ 50%] Building C object src/CMakeFiles/bin_main.dir/main.c.o
[100%] Linking C executable bin_main
/usr/bin/ld: CMakeFiles/bin_main.dir/main.c.o: in function `main':
main.c:(.text+0x1e): undefined reference to `get_value'
/usr/bin/ld: main.c:(.text+0x42): undefined reference to `surface_area'
/usr/bin/ld: main.c:(.text+0x47): undefined reference to `put_value'
/usr/bin/ld: main.c:(.text+0x53): undefined reference to `volume'
/usr/bin/ld: main.c:(.text+0x77): undefined reference to `put_value'
collect2: error: ld returned 1 exit status
make[2]: *** [src/CMakeFiles/bin_main.dir/build.make:84: src/bin_main] Error 1
make[1]: *** [CMakeFiles/Makefile2:94: src/CMakeFiles/bin_main.dir/all] Error 2
make: *** [Makefile:84: all] Error 2

That's because CMake doesn't know about the source files geometry.c and simple_io.c.

Let us edit .../my_project/src/CMakeLists.txt:

add_library(lib_sphere geometry.c simple_io.c)
add_executable(bin_main main.c)
target_link_libraries(bin_main lib_sphere)

If we now (making sure that we are under .../my_project/build) try to build the project, everything will work:

$ cmake ..
-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build
$ make
Scanning dependencies of target lib_sphere
[ 20%] Building C object src/CMakeFiles/lib_sphere.dir/geometry.c.o
[ 40%] Building C object src/CMakeFiles/lib_sphere.dir/simple_io.c.o
[ 60%] Linking C static library liblib_sphere.a
[ 60%] Built target lib_sphere
Scanning dependencies of target bin_main
[ 80%] Linking C executable bin_main
[100%] Built target bin_main
ubuntu@ip-172-31-24-17:~/my_project/build$

Notice that the resulting static library is called liblib_sphere.a. We shouldn't have named the library lib_sphere in CMakeLists.txt: the prefix lib is automatically prepended, the suffix .a is automatically appended.

Let's test the executable:

$ src/bin_main
Input the radius of the sphere: 3.5
Surface area = 153.938034
Volume of sphere = 179.594376

Adding a shared library

Static libraries, while reusable across multiple programs, are locked into a program at compile-time. Dynamic or shared libraries exist as separate files outside the executable file.

The code of a static library is locked into the executable file and cannot be modified without a re-compile. In contrast, a dynamic library can be modified without a need to recompile. If a dynamic library becomes corrupt, the executable file may no longer work. A static library, however, is untouchable because it lives inside the executable file. The upside of using a dynamic library is that multiple running applications can use the same library without the need for each to have its own copy.

Let us see how we can modify .../my_project/src/CMakeLists.txt to build a static library instead of a dynamic one:

add_library(sphere SHARED geometry.c simple_io.c)
add_executable(bin_main main.c)
target_link_libraries(bin_main sphere)

Now, from under .../my_project/build,

$ make clean
$ cmake ..
-- *** Building my_project from /home/ubuntu/my_project ***
-- Configuring done
-- Generating done
-- Build files have been written to: /home/ubuntu/my_project/build
$ make
Scanning dependencies of target sphere
[ 20%] Building C object src/CMakeFiles/sphere.dir/geometry.c.o
[ 40%] Building C object src/CMakeFiles/sphere.dir/simple_io.c.o
[ 60%] Linking C shared library libsphere.so
[ 60%] Built target sphere
Scanning dependencies of target bin_main
[ 80%] Building C object src/CMakeFiles/bin_main.dir/main.c.o
[100%] Linking C executable bin_main
[100%] Built target bin_main

Let's try running the executable:

$ src/bin_main
Input the radius of the sphere: 3.57
Surface area = 160.157135
Volume of sphere = 190.586975

If we now

$ rm src/libsphere.so

and try

$ src/bin_main

we'll get

src/bin_main: error while loading shared libraries: libsphere.so: cannot open shared object file: No such file or directory

The shared library file libsphere.so is now required for running bin_main; the code for functions such as surface_area is no longer included in the binary bin_main.