Estimated reading time: 40 minutes
Building a simple C++ project with CMake
Introduction
Having grasped the background and motivation for adopting the CMake build system generator, let us get our hands dirty and do a hands-on exercise in building a simple C++ project with CMake. This will be CMake’s equivalent of the classic hello world project, wherein we build an executable from C++ source code that prints … well, “Hello World!” (what else?) to the screen.
We will perform this as the very first exercise during Week 1 of the class.
Project Structure
To help focus on the new CMake-related aspects, we shall use a somewhat simplified version of the hierarchical project structure presented in the previous lesson.
For this exercise, the following project structure is given:
./CMakeHelloWorld
├── LICENSE.txt
├── README.md
└── src
└── hello.cpp
The hello.cpp
file inside the src/
folder contains a simple C++ source code that prints “Hello World!” to the console.
#include <iostream>
int main() {
std::cout << "Hello World!" << '\n';
return 0;
}
CMake processes user-provided configuration files that are named CMakeLists.txt
(known as listfiles) in which we describe our build requirements at a high level.
Our task is to create and populate one or more CMakeLists.txt
files that reflects our project’s modular/hierarchical organisation.
The modern best practice is to always have a top-level CMakeLists.txt
file at the project root, with additional, separate nested/hierarchical CMakeLists.txt
files for each individual unit of logic/functionality i.e. resembling our project’s file organisation.
Therefore, in this case, we shall use the following structure:
CMakeHelloWorld/
├── CMakeLists.txt
├── LICENSE.txt
├── README.md
└── src
├── CMakeLists.txt
└── hello.cpp
Populating the top-level (root) CMakeLists.txt
cmake_minimum_required()
CMakeLists.txt
files are plain-text files written in the CMake-specific custom language syntax.
For every project, the top most CMakeLists.txt
must start by specifying a minimum CMake version using the cmake_minimum_required()
command.
Although upper, lower and mixed case commands are supported by CMake, the modern trend is to use lower case commands.
Yet another thing to note is that, like C++ itself, CMake
is whitespace-agnostic while processing CMakeLists.txt
files.
Therefore, we start off with the following:
cmake_minimum_required(VERSION 3.25)
Even the most recent versions of CMake installations can replicate the exact behaviour of older releases.
In this example, the minimum VERSION
is specified, and other options such as the maximum version the project is tested with have been omitted.
Owing to its long development history and strong emphasis on retaining backward compatibility, most CMake commands have multiple variants and sometimes have an overwhelming array of options. Naturally, it is not feasible to cover every aspect of the language here. However, we shall skim just enough of the language here to address our needs, and developers typically acquire proficiency in the language gradually over the years. Therefore, we encourage you not to get disheartened by the slightly verbose documentation at the outset.
The main caveat regarding CMake’s version used here is that the chosen minimum version should at least support C++17 language standards and compiler options. Recent versions of CMake have improved package detection, simplified dependency handling. While an older version suffices for this simple HelloWorld example, it does not hurt to use a CMake version as recent as possible.
project()
Next, we use the project()
command to set the project name.
This is required with every project and should be called soon after cmake_minimum_required()
.
This command can also be used to specify other project level information such as the language or version number.
project(hello_world
VERSION 0.0.1
LANGUAGES CXX
)
The first (required) argument to this command is the project’s name.
The name of the project can be any valid string.
It does not have to be the same string as the name of the main executable program to be built (although it is conventional/convenient to use the same string for them).
The VERSION field is optional, but is conventional to populate it.
Next, we specify the programming languages that the project uses.
The keyword for C++ projects is CXX
.
It is helpful to specify the list of languages used in the project, so that cmake does not spend time inspecting the system for the presence of the toolchains for every supported language (CUDA, fortran, C, Java etc).
Setting global options at the project root with set()
It was common practice to make liberal use of the set()
command to define various built-in, user-defined and environment variables at the top level CMakeLists.txt
.
This includes setting up the C++ language standard, compiler flags etc.
While it can be useful to do so, CMake provides a much more fine-grained control to set options for each logical/modular unit in our project, and we have a slight preference towards this approach.
Nevertheless, the top level CMakeLists.txt
file can be used to set various global conditions applicable to the whole project.
When CMake configures a project, the build tree closely resembles the source tree hierarchy in the build tree.
The path to the root of the build directory is available as the built-in variable CMAKE_BINARY_DIR
whose value can be accessed within CMakeLists.txt
by dereferencing it using the syntax ${VAR_NAME}
, in this case ${CMAKE_BINARY_DIR}
.
However, a common practice for implementing a logical separation is to have CMake place the generated executables within a separate bin/
subfolder at the root of the build directory.
This can be achieved by setting the value of the CMAKE_RUNTIME_OUTPUT_DIRECTORY
as follows
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
Parsing nested CMakeLists.txt
within the src/
directory
Until now, we have set certain high-level description of the project, but have not provided instructions on building the “hello_world” executable program from the source!
This is intentional.
The task of describing the build of each logical/modular project unit is delegated to nested/leaf CMakeLists.txt
that reflects the project’s hierarchy.
In this case, we wish to ‘hand-off’ control to src/CMakeLists.txt
for building the hello_world
executable.
This is achieved by using the add_subdirectory()
command.
add_subdirectory(src)
In the above line, we instruct CMake to process the CMakeLists.txt
located within the src/
directory.
These list files can be nested quite deeply, and at each depth level, we can have as many ‘sibling’ folders with one CMakeLists.txt
per folder that describes the logical program unit to be built.
After processing all nested list files, control returns to the top level CMakeLists.txt
.
Since the above line was the last in this file, the configuration process is completed.
We now focus on the build details that is to be described in src/CMakeLists.txt
.
Contents of src/CMakeLists.txt
Within this ‘leaf’ CMakeLists.txt
file, we describe how the hello_world
executable is to be built.
This is considered best practice, i.e. to have a CMakeLists.txt
as close to the relevant source files for each modular functionality within the project.
This file can see and use all the properties, variables, functions and macros defined at any CMakeLists.txt
in its parent scope (in this case, all definitions from the top-level CMakeLists.txt
file are visible here i.e. they are in the ‘scope’ of this leaf file).
Targets and usage requirements
We like CMake to build for us an executable called ‘hello_world’.
In this case, hello_world
is a CMake ‘target’ (or a CMake ‘executable target’ to be precise).
Targets (not variables) are the first-class citizens in modern CMake language.
Hence, the relationships between various constituents (i.e. executables, source files, header files, internal libraries, other third-party code/files, system dependencies etc.) is to be described in a target-centric way within each CMakeLists.txt
file.
The behaviour of targets, their features and their various properties can be set in three ways:
- only on/for/affecting itself (using the PRIVATE keyword)
- only on/for/affecting other targets that consume this current target (using the INTERFACE keyword)
- on/for/affecting itself as well as other targets that consume this current target (using the PUBLIC keyword)
We recognise that this is quite a confusing aspect for newcomers to CMake, but is one of the key things to understand how targets and their dependencies interact for a given build. The only way to get better comprehend this critical component is by practicing these ideas during the first few weeks of the course.
The following matrix may help slightly, and we may choose to refer to this at times throughout the course:
Keyword selection table (Who needs/uses a dependency?)
Other (consuming targets) | ||
---|---|---|
Current target | YES | NO |
YES | PUBLIC | PRIVATE |
NO | INTERFACE | N/A |
The add_executable()
command
We use the add_executable()
command to declare that we’d like CMake to build an executable program called ‘hello_world’ (executable target in CMake parlance).
add_executable(hello_world)
The target_sources()
command
Next, we tell CMake that the executable target hello_world
depends on one C++ source file, viz hello.cpp
.
This is done via the target_sources()
command.
target_sources(hello_world PRIVATE hello.cpp)
Referring to the usage requirements table, the keyword PRIVATE
conveys to CMake that the file hello.cpp
serves as a dependency only for the current target hello_world
, and is not propagated to any other targets that depend on hello_world
.
This is not much relevant here, since the hello_world
target is simply an executable program which does not depend on the file hello.cpp
after it has been built (i.e. at run-time).
In such circumstances, the keyword can be omitted.
However, the usage requirement keywords become crucial (and sometimes mandatory) in describing more intricate dependency graphs involving various libraries as projects evolve in complexity over time.
Hence, we strongly encourage students not to omit this keyword and think about the usage requirement for every target property/behaviour set in CMakeLists.txt
.
Setting the compile time properties of targets
We wish to ensure that the hello_world
target conforms to the C++17 standard.
One way to achieve this in CMake is through the target_compile_features()
command which can be used to set some pre-defined properties/behaviour/characteristics to follow for compiling targets.
We add the following line:
target_compile_features(hello_world PUBLIC cxx_std_17)
Here the keyword cxx_std_17
keyword gently suggests CMake to invoke a C++17 compatible compiler if detected on the target system.
One of PRIVATE or PUBLIC keyword is required here (the INTERFACE keyword does not directly apply to executable targets).
We may use either here, but we chose PUBLIC to be compatible with recommended best practices for new projects.
In the future, if we wish to split our project into a library the PUBLIC keyword will ensure that our library as well as any other executables or other libraries that depend on this shall acquire C++17 capabilities.
Setting individual properties on targets
While target_compile_features()
allows us to select a list of high-level behaviour, an alternate is to set individual properties/flags on each target through some pre-defined CMake variables.
This is achieved using the set_target_properties()
command.
While target_compile_features(hello_world PUBLIC cxx_std_17)
encourages the use of C++17 standards, it does not enforce this.
The built-in boolean CXX_STANDARD_REQUIRED
property can be set to True/ON
to enforce this.
Another useful thing to do to ensure wide platform and target portability of our project is to disable compiler extensions.
Various C++ compilers take liberties to enable compiler-specific extensions by default unless explicitly directed not to do so.
The built-in boolean property CXX_EXTENSIONS
can be set to False/Off
to disable these compiler extensions.
The set_target_properties()
command allows us to set multiple properties on targets within a single invocation like so:
set_target_properties(hello_world PROPERTIES CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF)
A starter template/scaffolding for the project has been provided with relevant prompts for filling in the required CMake commands in the two CMakeLists.txt
files.
We shall perform this during the first class.
We have now fully described our build, and are now ready to proceed to perform the build for our project, all whilst conforming to the desired C++ standard and implementing CMake best practices!
The procedure to build and run the executable is described in the next lesson.