PolarSPARC



Bhaskar S 10/09/2020


Overview

For smaller C/C++ projects (typically utility programs), one can perform the build by using the compiler (gcc/g++) directly.

For larger projects, one would have to create a Makefile, which defines a set of tasks to build the various project artifacts (libraries and executables - also referred to as the TARGETs), and use the make command to build the project (using the Makefile). When crafting the Makefile, one needs to keep in mind the various compiler/platform specific (Linux, MacOS, Win32) dependencies in order to generate the build in a compiler/platform agnostic way.

This is where CMake comes to the rescue. It is an open source META build tool for managing the build process of a C/C++ project in an compiler/platform independent manner. The META here means CMake is the tool for generating the Makefile.

Using CMake, one can generate a build environment for the specific compiler/platform, that can compile source files, create static or dynamic libraries, and build executables.

Installation

The installation is on a Ubuntu 20.04 LTS based Linux desktop.

To install the CMake, execute the following commands:

$ sudo apt update

$ sudo apt install cmake

Once the installation completes, execute the following command to check the version:

$ cmake --version

The following would be a typical output:

Output.1

cmake version 3.16.3

CMake suite maintained and supported by Kitware (kitware.com/cmake).

Hands-on with CMake

We will assume the user-id is alice with the home directory located at /home/alice.

We will create a simple project directory called cmake (under /home/alice) with a directory structure as shown in the illustration below:

Project Structure
Figure.1

CMake expects a configuration text file called CMakeLists.txt at the root of the project. It is an empty text file at this point in the demonstration.

Let us create a very simple C++ program called greet.cpp in the src directory as shown in the listing below:

greet.cpp
#include <iostream>

using namespace std;

int main() {
    cout << "CMake is COOL !!!" << endl;    
}

The CMakeLists.txt file defines a series of CMake specific commands which are evaluated in the order in which they appear in the file. Every CMake command has the following format:

    command(arg-1 arg-2 arg-3 ...)

where arg-1, arg-2, arg-3, etc are the arguments for the command.

The command cmake_minimum_required is used to set the minimum required version of cmake for the project.

    Ex: cmake_minimum_required(VERSION 3.16)

In the above example, we set the minimum cmake version for the project to 3.16.

The command set allows one to set a variable (including CMake specific built-in variables and platform environment variables) to the specified value.

    Ex: set(CMAKE_CXX_STANDARD 17)

In the above example, we set the C++ standard to version 17 to enable the C++17 extensions using the CMake built-in variable CMAKE_CXX_STANDARD.

    Ex: set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)

One can reference a CMake variable using the format ${VAR}, where the variable name is VAR. In the above example, we set the CMake built-in variable CMAKE_SOURCE_DIR to point to the appropriate source location (/home/alice/cmake/src). By default, CMAKE_SOURCE_DIR points to the project root (/home/alice/cmake).

The command project is used to set the project name and store the value in the CMake specific variable CMAKE_PROJECT_NAME.

    Ex: project(cmake)

In the above example, we set the project name to cmake.

The command add_executable is used to add an executable to the project that is to be built from the source files listed in the command arguments.

    Ex: add_executable(greet ${CMAKE_SOURCE_DIR}/greet.cpp)

In the above example, we set the executable name to greet to be generated by compiling the source file greet.cpp located at /home/alice/cmake/src.

Let us update the CMakeLists.txt file to have the contents as shown in the listing below:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

project(cmake)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)

add_executable(greet ${CMAKE_SOURCE_DIR}/greet.cpp)

Open a terminal and assuming we are in the project root (/home/alice/cmake), execute the following commands:

$ cd build

$ cmake ..

The following would be a typical output:

Output.2

-- 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/alice/cmake/build

The following illustration shows the contents of the /home/alice/cmake/build directory after the above command:

Build Structure
Figure.2

CMake caches variables and settings, for the project, in the file CMakeCache.txt. The file Makefile is generated by CMake for the project.

To build the project, execute the following command in the directory /home/alice/cmake/build:

$ make

The following would be a typical output:

Output.3

Scanning dependencies of target greet
[ 50%] Building CXX object CMakeFiles/greet.dir/src/greet.cpp.o
[100%] Linking CXX executable greet
[100%] Built target greet

The following illustration shows the contents of the /home/alice/cmake/build directory after the above command:

Make Project
Figure.3

Now, execute the following command in the directory /home/alice/cmake/build:

$ ./greet

The following would be a typical output:

Output.4

CMake is COOL !!!

COOL !!! We were able to build and test the binary generated from this simple project.

How do we configure CMake to build executables in the directory /home/alice/cmake/bin ???

Let us modify the CMakeLists.txt file to have the contents as shown in the listing below:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

project(cmake)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/../bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

add_executable(greet ${CMAKE_SOURCE_DIR}/greet.cpp)

In the above file CMakeLists.txt, we set the CMake built-in variable CMAKE_BINARY_DIR to point to the appropriate location (/home/alice/cmake/bin) for the executables. By default, CMAKE_BINARY_DIR points to the same location as CMAKE_SOURCE_DIR.

Also, we set the CMake built-in variable CMAKE_RUNTIME_OUTPUT_DIRECTORY to point to the location identified by CMAKE_BINARY_DIR (/home/alice/cmake/bin).

Now, execute the following commands in the directory /home/alice/cmake/build:

$ make clean

$ cmake ..

The following would be a typical output:

Output.5

-- Configuring done
-- Generating done
-- Build files have been written to: /home/alice/cmake/build

Re-build the project by executing the following command in the directory /home/alice/cmake/build:

$ make

The following would be a typical output:

Output.6

[ 50%] Building CXX object CMakeFiles/greet.dir/src/greet.cpp.o
[100%] Linking CXX executable ../bin/greet
[100%] Built target greet

The executable is now built and stored in the directory /home/alice/cmake/bin.

We demonstrated building a simple C++ project with a single source file. A typical C++ project will have multiple source and header files, with different project TARGETs, such as libraries and executables.

In the next example, we will demonstrate a simple C++ application that uses a simple mortgage calculator library. The source code for the mortgage calculator involves a header file and a source file, which is compiled into a library. The main program uses the mortgage calculator library to link and build an executable.

The following header file calculator.h (in the include directory) defines the function prototype for mortgage_amount as shown in the listing below:

calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

double mortgage_amount(double loan, double rate, int years);

#endif //CALCULATOR_H

The following source file calculator.cpp (in the src directory) includes the above listed header file calculator.h and implements the function mortgage_amount as shown in the listing below:

calculator.cpp
#include <cmath>
#include "../include/calculator.h"

using namespace std;

double mortgage_amount(double loan, double rate, int years) {
    double c = rate / (12 * 100);
    int n = years * 12;
    return loan * pow((1 + c), n) * c / (pow((1 + c), n) - 1);
}

The following main source file mortgage.cpp (in the src directory) includes the above listed header file calculator.h and invokes the function mortgage_amount as shown in the listing below:

mortgage.cpp
#include <iostream>

#include "../include/calculator.h"

using namespace std;

int main() {
    double loan {250'000.00};
    double rate {3.5};
    int years {30};

    cout << "Loan: " << loan << ", Rate: " << rate << ", Years: " << years << endl;
    cout << "===> Mortgage amount: " << mortgage_amount(loan, rate, years) << endl;
}

We will need to add few more commands to the CMakeLists.txt file to build the calculator library and then link it to build the mortgage executable.

The command add_library is used to add a library TARGET to the project that is to be built from the source file(s) listed in the command arguments. The keyword STATIC or SHARED may be specified to indicate the type of library to be created.

    Ex: add_library(calculator STATIC ${CMAKE_SOURCE_DIR}/calculator.cpp)

In the above example, we desire a static library with the name calculator to be generated by compiling the source file calculator.cpp located at /home/alice/cmake/src.

The command target_link_libraries is used to link the specified libraries in order to build the named TARGET executable. NOTE that the named TARGET executable must be created by the command add_executable prior to this command.

    Ex: target_link_libraries(mortgage calculator)

In the above example, we build the executable mortgage by linking it with the static library for calculator.

Once again, we modify the CMakeLists.txt file to have the contents as shown in the listing below:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

project(cmake)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/../bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

add_executable(greet ${CMAKE_SOURCE_DIR}/greet.cpp)

SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../lib)

add_library(calculator STATIC ${CMAKE_SOURCE_DIR}/calculator.cpp)
add_executable(mortgage ${CMAKE_SOURCE_DIR}/mortgage.cpp)
target_link_libraries(mortgage calculator)

We set the CMake built-in variable CMAKE_ARCHIVE_OUTPUT_DIRECTORY to the appropriate directory location for building and storing static libraries (/home/alice/cmake/lib).

Now, execute the following commands in the directory /home/alice/cmake/build:

$ make clean

$ rm -rf *

$ cmake ..

The typical output would be similar to the one in Output.2 above.

Re-build the project by executing the following command in the directory /home/alice/cmake/build:

$ make

The following would be a typical output:

Output.7

[ 16%] Building CXX object CMakeFiles/calculator.dir/src/calculator.cpp.o
[ 33%] Linking CXX static library ../lib/libcalculator.a
[ 33%] Built target calculator
Scanning dependencies of target mortgage
[ 50%] Building CXX object CMakeFiles/mortgage.dir/src/mortgage.cpp.o
[ 66%] Linking CXX executable ../bin/mortgage
[ 66%] Built target mortgage
Scanning dependencies of target greet
[ 83%] Building CXX object CMakeFiles/greet.dir/src/greet.cpp.o
[100%] Linking CXX executable ../bin/greet
[100%] Built target greet

The following illustration shows the contents of the /home/alice/cmake/lib and the /home/alice/cmake/bin directories after the above command:

Library and Binary
Figure.4

From the above illustration, we see the library for calculator (called libcalculator.a) is located in /home/alice/cmake/lib and the executable for mortgage is located in /home/alice/cmake/bin.

Now, execute the following command in the directory /home/alice/cmake/bin:

$ ./mortgage

The following would be a typical output:

Output.8

Loan: 250000, Rate: 3.5, Years: 30
===> Mortgage amount: 1122.61

WALLA !!! We were able to build the necessary dependency (library) and test the binary generated from this simple project.

In the above, we demonstrated how-to build a library and an executable (linking the library) for a simple C++ project with a single header file and two source files.

In the next example, we will demonstrate the other capabilities of CMake such as conditional logic, looping, etc.

The following is the source for greet2.cpp (in the src directory) as shown in the listing below:

greet2.cpp
#include <iostream>
#include <string>

using namespace std;

int main() {
    string msg {"CMake is AWESOME !!!"};
    cout << msg << endl;
}

Similarly, the following is the source for greet3.cpp (in the src directory) as shown in the listing below:

greet3.cpp
#include <iostream>
#include <string>
#include <memory>

using namespace std;

int main() {
    unique_ptr sp = make_unique("CMake is WONDERFUL !!!");
    cout << *sp << endl;
}

In addition to the executable greet, we also want to build the executables greet2 and greet3 in the bin directory.

The CMake built-in variable CMAKE_SYSTEM_NAME indicates the OS platform. In our case, it will be set to Linux.

The CMake built-in variable CMAKE_CXX_COMPILER_ID indicates the compiler being used. In our case, it will be set to GNU.

The command message is used to display a message to the user.

    Ex: message("*** Build Platform: ${CMAKE_SYSTEM_NAME}")

In the above example, we display the name of the build platform on the terminal.

The command if is used to check the specified boolean condition and if evaluates to true, execute the command(s) in the if body. If the condition evaluates to false, the command(s) in the body of the command else are executed. This command MUST end with the mandatory command endif. For our example, we will enable all compiler warnings.

The following are the contents of the modified CMakeLists.txt file as shown in the listing below:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

project(cmake)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/../bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

message("*** Build Platform: ${CMAKE_SYSTEM_NAME}")
message("*** CXX Compiler: ${CMAKE_CXX_COMPILER_ID}")

if(${CMAKE_SYSTEM_NAME} MATCHES Linux AND ${CMAKE_CXX_COMPILER_ID} STREQUAL GNU)
    message("*** Enabling *ALL* compiler warnings")
    set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS} -Wall")
endif()

add_executable(greet ${CMAKE_SOURCE_DIR}/greet.cpp)
add_executable(greet2 ${CMAKE_SOURCE_DIR}/greet2.cpp)
add_executable(greet3 ${CMAKE_SOURCE_DIR}/greet3.cpp)

SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../lib)

add_library(calculator STATIC ${CMAKE_SOURCE_DIR}/calculator.cpp)
add_executable(mortgage ${CMAKE_SOURCE_DIR}/mortgage.cpp)
target_link_libraries(mortgage calculator)

Now, execute the following commands in the directory /home/alice/cmake/build:

$ make clean

$ rm -rf *

$ cmake ..

The following would be a typical output:

Output.9

-- 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/gcc
-- Check for working C compiler: /usr/bin/gcc -- 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/g++
-- Check for working CXX compiler: /usr/bin/g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
*** Build Platform: Linux
*** CXX Compiler: GNU
*** Enabling *ALL* compiler warnings
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alice/cmake/build

Notice the three messages (prefixed by ***) displayed on the terminal.

Re-build the project by executing the following command in the directory /home/alice/cmake/build:

$ make

The following would be a typical output:

Output.10

Scanning dependencies of target calculator
[ 10%] Building CXX object CMakeFiles/calculator.dir/src/calculator.cpp.o
[ 20%] Linking CXX static library ../lib/libcalculator.a
[ 20%] Built target calculator
Scanning dependencies of target mortgage
[ 30%] Building CXX object CMakeFiles/mortgage.dir/src/mortgage.cpp.o
[ 40%] Linking CXX executable ../bin/mortgage
[ 40%] Built target mortgage
Scanning dependencies of target greet3
[ 50%] Building CXX object CMakeFiles/greet3.dir/src/greet3.cpp.o
[ 60%] Linking CXX executable ../bin/greet3
[ 60%] Built target greet3
Scanning dependencies of target greet2
[ 70%] Building CXX object CMakeFiles/greet2.dir/src/greet2.cpp.o
[ 80%] Linking CXX executable ../bin/greet2
[ 80%] Built target greet2
Scanning dependencies of target greet
[ 90%] Building CXX object CMakeFiles/greet.dir/src/greet.cpp.o
[100%] Linking CXX executable ../bin/greet
[100%] Built target greet

One observation from the above CMakeLists.txt file - we seem to repeat the add_executable command for greet, greet2, and greet3 respectively. We could use looping to optimize it.

The command foreach is used to loop over a list of values (separated by semi-colons) and for each value in the list, execute the command(s) in the foreach body. This command MUST end with the mandatory command endforeach.

The following are the contents of the modified CMakeLists.txt file as shown in the listing below:

CMakeLists.txt
cmake_minimum_required(VERSION 3.16)

project(cmake)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/../bin)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})

message("*** Build Platform: ${CMAKE_SYSTEM_NAME}")
message("*** CXX Compiler: ${CMAKE_CXX_COMPILER_ID}")

if(${CMAKE_SYSTEM_NAME} MATCHES Linux AND ${CMAKE_CXX_COMPILER_ID} STREQUAL GNU)
    message("*** Enabling *ALL* compiler warnings")
    set(CMAKE_CXX_FLAGS  "${CMAKE_CXX_FLAGS} -Wall")
endif()

set(TARGETS "greet;greet2;greet3")

foreach(EXE ${TARGETS})
    message("*** Add Executable for: ${EXE}")
    add_executable(${EXE} ${CMAKE_SOURCE_DIR}/${EXE}.cpp)
endforeach()

SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../lib)

add_library(calculator STATIC ${CMAKE_SOURCE_DIR}/calculator.cpp)
add_executable(mortgage ${CMAKE_SOURCE_DIR}/mortgage.cpp)
target_link_libraries(mortgage calculator)

Now, execute the following commands in the directory /home/alice/cmake/build:

$ make clean

$ rm -rf *

$ cmake ..

The following would be a typical output:

Output.11

-- 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
*** Build Platform: Linux
*** CXX Compiler: GNU
*** Enabling *ALL* compiler warnings
*** Add Executable for: greet
*** Add Executable for: greet2
*** Add Executable for: greet3
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alice/cmake/build

Notice the three messages (prefixed by *** Add Executable) displayed on the terminal.

Re-build the project by executing the following command in the directory /home/alice/cmake/build:

$ make

The typical output would be similar to the one in Output.10 above.

References

CMake

CMake Commands

CMake Variables

Quick CMake Tutorial


© PolarSPARC