Programming/Tools/CMake

From Thalesians Wiki
< Programming
Revision as of 22:59, 22 December 2020 by Admin (talk | contribs)

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.

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 with gccGNU project C and C++ compiler,— we also need to install gcc:

$ 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).

project(cmake_test_project)
cmake_minimum_required(VERSION 3.0)

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 CXX compiler identification is GNU 9.3.0
-- 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

However, this Makefile 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:

project(cmake_test_project)
cmake_minimum_required(VERSION 3.0)

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:6 (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 has 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;
}

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.

Adding a library

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;
}

Let us also 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);
}

These source files share an include file, "sphere.h". Let us create a directory for include files:

$ mkdir .../my_project/build/include/

And let us add .../my_project/build/include/sphere.h with the following contents:

#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);

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

project(cmake_test_project)
cmake_minimum_required(VERSION 3.0)

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

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