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

These tutorials are meant to be worked through inside the tutorial Dev Container, which gives you a ready-made C++ environment (compiler, CMake, debugger, MPI, Boost, spack, cpplint, and the clangd language server) with zero local setup beyond Docker and an editor. See the Dev Containers overview for background.

Prerequisites:

Clone the tutorial and open it in the container:

git clone https://github.com/grc-iit/grc-tutorial.git
cd grc-tutorial
code .

Then open the command palette (Ctrl+Shift+P / Cmd+Shift+P) and run "Dev Containers: Reopen in Container". VS Code builds the image, installs the C++ extensions inside the container, and reopens the project there. The container is defined in the tutorial's .devcontainer/ directory.

Once you are inside the container, the tutorial code is at the workspace root and the GRC_TUTORIAL environment variable is already defined for you (the Dev Container sets it to the workspace folder), so the cd ${GRC_TUTORIAL}/cpp/... commands in the following pages just work — no setup needed.

note

Docker does not save state outside your workspace. Files you create inside the mounted project directory are preserved on your host, but changes elsewhere in the container are lost when it is rebuilt.

Go to the tutorial:

cd ${GRC_TUTORIAL}/cpp/03-cpp-build-with-cmake

This example depends on Boost. The tutorial Dev Container already provides it, so you can build right away. On your own machine you can install it with spack (see Installing HPC Software):

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} -O3")
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3")
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.