Skip to content

Integrating with CMake

In the previous guide, we went through how to set-up the project files for a simple calculator app. We went through how to build and run both the application and the tests by hand, invoking the compiler manually.

In this guide, we are going to see how to automate the build and run process with CMake and Make. After completing this guide, you will be able to build complex C++ applications with multiple source files and multiple test suites.

Pre-requisites

To complete this guide, you must have a working installation of CMake. CMake is a widely used, powerful build system. Check the guides in the official CMake website to learn more about how to install and configure CMake in your machine.

Setup

This guide starts from the last point of the first guide (Writing your first test). If you haven't gone through the first guide, its recommended you go through that content first.

After completing the first guide, the directory structure should look like this:

Directory structure
/calculator
├ /src
│ ├ main.cpp
│ ├ calculator.cpp
│ └ calculator.h
├ /test
│ └ calculator.test.cpp
├ /lib
│ └ cest
└ /build
  ├ calculator_test
  └ calculator

Creating the Makefile

Let's start by creating a simple Makefile recipe to automate the build, test, run and clean processes. We will place the Makefile at the root directory of the project. After creating the file, the directory structure should look like this:

Directory structure
/calculator
├ /src
│ ├ main.cpp
│ ├ calculator.cpp
│ └ calculator.h
├ /test
│ └ calculator.test.cpp
├ /lib
│ └ cest
├ /build
│ ├ calculator_test
│ └ calculator
└ Makefile

With a simple clean rule, we can dispose of the compiled files from the previous guide.

Rule to clean build files
clean:
  @rm -rf build/*

.PHONY: clean # (1)
  1. The clean build target is set as a PHONY build target, because we want to indicate Make that this target should not follow the dependency resolution mechanisms of Make. Check the Make documentation to know more.

Next, we will add a rule to build all the source files in the project. Since we will be using CMake to run the compilation process, the build rule will just invoke CMake and compile the resulting Makefile generated by CMake:

Rule to build the project
build:
  cd build
  cmake .. # (1)
  make # (2)

clean:
  @rm -rf build/*

.PHONY: clean build
  1. CMake is invoked in the project parent directory, where the CMakeLists.txt file will reside. We will go through its contents in next sections.
  2. After CMake has completed preparing the project, we will have a Makefile available in the build directory. Invoking this Makefile will build the project.

Finally, we will add two extra rules to execute the application and the tests, respectively. This will simplify the build and run process a lot from now on, as we will be able to run all tests with a single command:

Rules to run the application and tests
run: build # (1)
  ./build/app

test: build
  @find $(pwd)/build -name "test_*" -type f -executable -print0 | xargs -0 -I % sh -c % # (2)

build:
  cd build
  cmake ..
  make

clean:
  @rm -rf build/*

.PHONY: clean build run test
  1. Both the run and test targets depend on the build target, as the project must be compiled prior to running the application or the tests.
  2. To run all the tests, we look for all executable files in the build folder named test_*. Then, found files are executed.

CMake rules to build the application

To be able to use CMake to build our project, we must create a set of rules CMake will follow to create the required dependency graph and be able to build all executables in the project. This rules are defined in CMakeLists.txt files.

CMake is a complex and powerful language, check CMake's official website to learn more about it and find available bibliography.

In this guide, we will create a very simple CMakeLists.txt file that will let us get started. We will create the file in the root directory of the project. After creating it, the directory structure should look like this:

Directory structure
/calculator
├ /src
│ ├ main.cpp
│ ├ calculator.cpp
│ └ calculator.h
├ /test
│ └ calculator.test.cpp
├ /lib
│ └ cest
├ /build
│ ├ calculator_test
│ └ calculator
├ CMakeLists.txt
└ Makefile

Let's start by building the minimum set of rules to compile the calculator application into an executable. The generated binaries will be split in two parts: on one hand, the business logic will be inside a static library (libcalculator.a).

On the other hand, the entrypoint of the application (main.cpp) will be compiled independently to generate the application's executable, linking against the static library.

This will allow us to create each test suite as an independent executable file, linking to the static library to be able to access all the business logic from the test suite.

Basic rules to generate the library and application
cmake_minimum_required(VERSION 3.5)
project(Calculator)

set(CALCULATOR_SOURCES src/calculator.cpp)
add_library(calculator ${CALCULATOR_SOURCES})

add_executable(app src/main.cpp)
target_link_libraries(app calculator)

After creating the CMake rules, test the whole set by cleaning and building the project. To do so, simply run:

make clean build

Afterwards, the directory structure should be like:

Directory structure
/calculator
├ /src
│ ├ main.cpp
│ ├ calculator.cpp
│ └ calculator.h
├ /test
│ └ calculator.test.cpp
├ /lib
│ └ cest
├ /build
│ ├ ... Many files generated by CMake
│ ├ libcalculator.a
│ └ app
├ Makefile
└ CMakeLists.txt

Both the static library containing the business logic (libcalculator.a) and the application (app) have been compiled.

CMake rules to build the tests

Now that all the application's business logic is being compiled into a static library, creating new test suites is as simple as linking each test suite file with the static library. This will generate a new executable with the test suite.

The CMake rules to include the test suite from the previous example (calculator.test.cpp) would be like the following:

Rules to build all the project's binaries
cmake_minimum_required(VERSION 3.5)
project(Calculator)

set(CALCULATOR_SOURCES src/calculator.cpp)
add_library(calculator ${CALCULATOR_SOURCES})

add_executable(app src/main.cpp)
target_link_libraries(app calculator)

add_executable(test_calculator test/calculator.test.cpp)
target_link_libraries(test_calculator calculator)

After running the compilation process, the test_calculator test suite runnable file will be compiled.

Next reading

After completing this guide, you have seen how to integrate Cest Framework with CMake and automate the compilation of test suites and the application.

CMake is a complex topic, and there is enormous flexibility on what you can achieve with it.

In the next section, you will learn how to use Cest Framework to test C code, adding powerful semantics to C testing.