Skip to main content
A first CMake project
"Modern CMake" course developed by Fergus Cooper and the Oxford Research Software Engineering group

"Modern CMake" course developed by Fergus Cooper and the Oxford Research Software Engineering group

Creative Commons License

A first CMake project

Getting Started

The course repository contains a number of checkpoints, each of which is a step through this course. Each checkpoint contains a small C++ project along with a CMakeLists.txt file.
Checkpoint 0 is a simple "hello, world" program written in C++. Let's use CMake to build it.
cd checkpoint_0

CMakeLists.txt

The CMakeLists.txt file is where you write the definitions of your targets and configuration, and is processed by CMake to generate the build files. It is written in the CMake language, which is a bit like a scripting language.
Let's look at the sample CMakeLists.txt line by line.

CMake has changed a lot

Often the first line of a CMakeLists.txt file describes the minimum version of CMake required, this is because CMake has changed a lot over the years, and some features are only available in newer versions.
cmake_minimum_required(VERSION 3.24...3.31)
This tells CMake which versions we support. The first number (3.24) is the minimum version needed to configure the project. The second (3.31) is the most recent version whose behaviour we have checked against: CMake changes its defaults over time through policies, and this upper bound stops a newer CMake from silently changing how our project builds.

Define a project

Then we define a project by giving it a name and specifying the languages used.
project(ModernCMakeCourse VERSION 1.0.0 DESCRIPTION "Modern CMake course" LANGUAGES CXX)
We have a project called ModernCMakeCourse, at version 1.0.0, written in C++. Giving the project a VERSION and DESCRIPTION is good practice: CMake exposes them as variables such as PROJECT_VERSION, and reuses them later when we install or package the project.

Configure the compiler

Often we want to target a particular version of the C++ standard, and we can do that like so:
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF)
We're using the C++20 language standard. On its own, CMAKE_CXX_STANDARD is only a request: if the compiler doesn't support it, CMake will quietly fall back to an older standard. Setting CMAKE_CXX_STANDARD_REQUIRED to ON turns that into a hard error instead. Setting CMAKE_CXX_EXTENSIONS to OFF asks for standard C++ (e.g. -std=c++20) rather than compiler-specific extensions (e.g. -std=gnu++20), which keeps the build portable across compilers.
These variables set the standard project-wide. Once you have defined a target, you can also request a standard on a per-target basis, which is often clearer and, for a library, propagates the requirement to anything that links it:
target_compile_features(main_executable PUBLIC cxx_std_20)
We use the project-wide variables here for simplicity.

Tell it what to build

Now we can tell CMake what to build. We do this with the add_executable command:
add_executable(main_executable main.cpp)
This defines a target called main_executable, built from the source code in main.cpp. A target is the unit CMake works in: something to be built, together with everything known about how to build it. We will have a lot more to say about targets later in the course.

Using CMake

That is our CMakeLists.txt, and now we can use CMake to configure our project. This is the first step in the compilation, during which CMake uses our CMakeLists.txt file to generate the build files. If we are using a Linux distribution like Ubuntu, it is likely that we will be using Make, so in this case CMake will generate a Makefile that contains instructions to build our project.
It's typical to build "out of tree", keeping the generated files in a separate directory. This keeps them out of your source folder and lets you keep several builds side by side, for instance for different compilers or configurations. We point CMake at the source directory with -S and at a build directory with -B, which it creates for us:
# from checkpoint_0 cmake -S . -B build_dir
[...] -- Build files have been written to: <...>/checkpoint_0/build_dir
Adding -DCMAKE_EXPORT_COMPILE_COMMANDS=ON at configure time writes a compile_commands.json file into the build directory. Editors and language servers such as clangd read this file to understand exactly how each source is compiled, giving you accurate navigation, completion, and diagnostics.
Try that now, and have a look inside the compile_commands.json that it generates:
# from checkpoint_0 cmake -S . -B build_dir -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
It should look something like:
[ { "directory": "<>/ModernCMakeCourse/checkpoint_0/build_dir", "command": "/usr/bin/c++ -std=c++20 -o CMakeFiles/main_executable.dir/main.cpp.o -c <>/ModernCMakeCourse/checkpoint_0/main.cpp", "file": "<>/ModernCMakeCourse/checkpoint_0/main.cpp", "output": "CMakeFiles/main_executable.dir/main.cpp.o" } ]

Build your project

CMake only generated the build files, it didn't compile anything yet. We ask CMake to drive the build for us:
cmake --build build_dir
[...] [100%] Built target main_executable
Using cmake --build rather than calling make (or ninja) directly means the same command works whatever build tool CMake generated for, which is convenient in scripts and on other people's machines.
We can then run the executable:
./build_dir/main_executable
Checkpoint 0 Hello, World!

CMake workflow

Verify that you can configure, compile and run the executable in Checkpoint 0.