Skip to main content

Building C++ with CMake

Now that we have seen how to compile C++ code manually, we will discuss build automation using CMake. Generally, compiling manually is bad because it's difficult for someone to download your repo and just build it. Building the repo should not be burdensome.

The main objectives of this tutorial are as follows:

  1. Describe the structure of a proper C++ repo
  2. Show how to use CMake to compile a C++ repo
  3. Demonstrate how to build a basic CMakeLists.txt

We will re-use the example from Building C++ Manually.

Setup

You can get the iowarp container to reduce dependency install times:

git clone https://github.com/iowarp/iowarp-install.git
docker pull iowarp/iowarp-user:latest
cd iowarp-install/docker/user
docker compose up -d # Only for recent dockers
docker-compose up -d # Only for older dockers

To connect to the container:

docker exec -it iowarp bash
note

Docker does not save state. So when you build tutorials or create your own code, the data gets lost when the container shuts down.

Go to the tutorial:

cd grc-tutorial
export GRC_TUTORIAL=${PWD}
cd ${GRC_TUTORIAL}/cpp/03-cpp-build-with-cmake

Next perform:

spack install boost
spack load boost

Basic C++ Repo Structure

Generally, a C++ repo will contain at least the following directories

  1. include: where public header files go
  2. src: where private source and header files go
  3. test: where unit tests go

Note: A unit test is just a program which validates the correctness of some component of your program.

In CMake, build configurations are stored in files called CMakeLists.txt. In each directory which has source code or contains a directory which has source code, there should be a file called CMakeLists.txt. The CMakeLists.txt is responsible for determining which source codes are used for building a library or executable, dependencies, etc.

Building a CMake Project

To build this project, do the following:

cd ${GRC_TUTORIAL}/cpp/03-cpp-build-with-cmake
mkdir build
cd build
# CMake will produce a Makefile (in this case)
cmake ../
# Use make to build
make -j8

CMake is a build system generator, so it doesn't always need to be a makefile which gets produced. It could also be something like ninja. But generally it is make on Linux systems.

Top-Level (or Root) CMakeLists.txt

First we will look at the CMakeLists.txt file in the project's root directory. Generally, the root CMake is responsible for the following:

  1. Finding packages (e.g., shared libraries) which are relevant to the build
  2. Defining user configuration options
  3. Setting variables global to the build
  4. Setting compiler flags (e.g., optimization)

In this section, we will describe our root CMakeLists.txt.

CMake Preamble

cmake_minimum_required (VERSION 3.10)
project(MyFirstCMake)
set(CMAKE_CXX_STANDARD 17)

Here we require a minimum of CMake 3.10. This will cause CMake to fail if the installed version is too old.

We also set the name of this project to be MyFirstCMake. The project function will set the name of this project and store it in the variable PROJECT_NAME. Calling from the top-level CMakeLists.txt also stores the project name in the variable CMAKE_PROJECT_NAME.

Global Compiler Flags

#------------------------------------------------------------------------------
# Compiler optimization
#------------------------------------------------------------------------------
add_compile_options("-fPIC")
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0")
elseif(CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -03")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -03")
endif()

CMake defines the following variables automatically:

  1. CMAKE_CXX_STANDARD: The C++ version. For now C++17.
  2. CMAKE_BUILD_TYPE: What mode to build your project in. Typically this indicates compiler optimization. Default is usually RelWithDebInfo
  3. CMAKE_CXX_FLAGS: Flags to pass to the compiler. By default, this will be equivalent to the CXX_FLAGS environment variable from the shell CMake gets executed in.

In this example, we define four CMAKE_BUILD_TYPES:

  1. Debug: no compiler optimization
  2. RelWithDebInfo: moderate compiler optimization
  3. Release: heavy compiler optimization
  4. Everything else: same as Release

These build types are very common in CMake projects.

When Building C++ Manually, we mentioned that the -fPIC flag was required when building a shared library. In CMake this flag can be added to all libraries as follows:

add_compile_options("-fPIC")

For each CMake build type, we also enable different levels of optimization. For example, with Debug we disabled compiler optimization as follows:

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0")

Setting CMAKE_CXX_FLAGS and add_compile_options are effectively the same thing. In this case, it would also be equivalent (and actually encouraged) to write:

add_compile_options("-O0")

However, in many C++ projects people will set CMAKE_CXX_FLAGS. The main difference between the two approaches is that CMAKE_CXX_FLAGS will apply globally, even if set in a lower-level CMakeLists.txt. This is partially due to historical reasons.

Build Options

option(BUILD_TESTING "Build testing kits" OFF)

The option command allows users to configure the build. In this case, we include a flag which indicates whether or not to build unit tests. In many cases, users won't want to take the time to test code unless there are potential portability issues. By default, this value is set to OFF. The alternative is to set it to ON.

CMake options are passed to CMake using the -D flag. To build this project with testing, do the following:

cd ${GRC_TUTORIAL}/cpp/03-cpp-build-with-cmake
mkdir build
cd build
# Enable testing
cmake ../ -DBUILD_TESTING=ON
# Build
make -j8

Output Directories

In this section, we will describe how to define where CMake should output executables and shared objects.

#------------------------------------------------------------------------------
# Setup CMake Output Directories
#------------------------------------------------------------------------------
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin CACHE PATH "Single Directory for all Executables.")
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin CACHE PATH "Single Directory for all Libraries")
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY
${CMAKE_BINARY_DIR}/bin CACHE PATH "Single Directory for all static libraries.")
  • CMAKE_RUNTIME_OUTPUT_DIRECTORY: output for executables
  • CMAKE_LIBRARY_OUTPUT_DIRECTORY: output for shared libraries
  • CMAKE_ARCHIVE_OUTPUT_DIRECTORY: output for static libraries (not important for us)

CMAKE_BINARY_DIR is automatically provided by CMake. This is the absolute path to the directory which contains the root CMake. In our case, this would be cd ${GRC_TUTORIAL}/cpp/03-cpp-build-with-cmake.

In this example, we output all executables and shared objects to the bin directory.

Locating Dependencies

#-----------------------------------------------------------------------------
# Dependencies common to all subdirectories
#-----------------------------------------------------------------------------
find_package(Boost COMPONENTS system filesystem REQUIRED)

find_package is used to locate packages installed on the system by parsing the environment variable CMAKE_PREFIX_PATH. CMAKE_PREFIX_PATH must contain the paths to .cmake (not .txt) files which actually load the package information. This variable is often set by spack when loading packages.

Enable Testing

#-----------------------------------------------------------------------------
# Enable Testing
#-----------------------------------------------------------------------------
include(CTest)
if(CMAKE_PROJECT_NAME STREQUAL MyFirstCMake AND BUILD_TESTING)
enable_testing()
endif()

This code will enable the ability to use a functionality called CTest. CTests are used for automating unit tests for C++ projects. In our case, this is only enabled when BUILD_TESTING is ON.

Directory Descent

#-----------------------------------------------------------------------------
# Source
#-----------------------------------------------------------------------------
add_subdirectory(src)

#-----------------------------------------------------------------------------
# Testing Sources
#-----------------------------------------------------------------------------
if(CMAKE_PROJECT_NAME STREQUAL MyFirstCMake AND BUILD_TESTING)
add_subdirectory(test)
endif()

There is no source code in the root directory for this project. In order to get to the source code, we must go into the src and test directories. add_subdirectory will tell CMake to go to a specific directory and execute the CMakeLists.txt in that subdirectory.

src/CMakeLists.txt

In this section, we will discuss src/CMakeLists.txt. This CMake file is responsible for defining how to build + install the source code in this repo.