Writing a CMakeLists File
Creating the bare minimum CMakeLists.txt
file and build up to a project with an executable and a library, learning the fundamental commands along the way.
We've explored the build pipeline, wrestled with the command line, and installed the necessary tools. Now, we're going to leave the world of manual compilation behind and write our first CMakeLists.txt
file.
We'll start with the simplest possible project - a single "Hello, World" executable. Then, we'll progressively build on it, adding a library, linking them together, and specifying the C++ standards.
By the end of this lesson, you'll have a solid, working CMake project and an understanding of the fundamental commands that make it all possible.
Project Setup and Source Tree
First, let's create a sensible directory structure for our project, following the conventions we discussed in the previous chapter. We'll call our project Greeter
.
Let's create a root folder named Greeter
. Inside it, we'll create a src
directory.
Greeter/
└─ src/
We'll start with a single source file for a "Hello, World!" program.
Greeter/src/main.cpp
#include <iostream>
int main() {
std::cout << "Hello, CMake!";
return 0;
}
That's our project for now. A single source file in a src
directory. Next, we need to tell CMake how to build it. We do that by creating the special CMakeLists.txt
file in the root of our project.
The Minimum CMakeLists.txt
Every CMake project is defined by a text file named CMakeLists.txt
. This file must be in the root directory of your project.
Let's create this file and add the two fundamental commands that every CMakeLists.txt
file should have.
Greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
That's it. This is the bare minimum required for a valid CMake project. Let's break down what these two lines do.
Setting the CMake Version
The the very first line in your top-level CMakeLists.txt
should be the cmake_minimum_required()
command. It does two things:
- it checks the version of CMake that is installed on the system, to ensure it meets the requirements of our
CMakeLists.txt
file - it sets CMake's "policies" to emulate the behavior of that version
Let's describe both of these aspects in a little more detail
Version Check
Firstly, cmake_minimum_required()
tells CMake what minimum version is needed to build your project. Like most software, CMake gets updated over time, and some of those updates include new commands.
If a developer tries to build your project with an older version of CMake that didn't include some of the commands you're using in your CMakeLists.txt
, this version check will help CMake understand why it doesn't recognize those commands, and explain to the user what the problem is.
Policy Setting
More importantly, cmake_minimum_required()
also sets CMake's "policies" to match the behavior of the version you specify. In addition to new commands, CMake updates sometimes change the behavior of existing commands. The cmake_minimum_required()
line ensures that your project will build predictably, even with newer versions of CMake, because that newer version can emulate the behavior of previous versions.
What Version Should I Use?
Setting the minimum required version to the latest release is not recommended, unless your project genuinely uses those bleeding edge features and behaviours.
Requiring an extremely recent version when it's not needed forces people to update their development environments to use your project, which can be quite difficult to do in some contexts. Perhaps they're using a version of CMake that is integrated with their IDE or other tools, and they don't even have the option to update it.
We're setting our cmake_minimum_required()
to be 3.16
in these examples. This is chosen somewhat arbitrarily, but the main goal is to select a version that is:
- Old enough to be compatible with the tools that most developers will have installed
- New enough to have access to modern features and likely to be supported in future CMake releases for quite some time. At the time of writing, the latest version of CMake is
4.1.0
released in August 2025, which can emulate behaviors as far back as version3.0.5
, released in 2016.
If there is a feature in a newer version of CMake that is useful for solving a problem we have, we'd consider bumping our cmake_minimum_required()
up to that version. We'll introduce some of those features later in the course but, for now, something like 3.16
is a reasonable baseline.
The project()
Command
This command officially starts the project. It sets the project's name and enables support for programming languages.
project(Greeter)
sets the name of our project to "Greeter". This name is also stored in the PROJECT_NAME
variable, which we'll see how to use later. By default, this command also enables support for C and C++.
With just these two commands, we have a valid, albeit empty, CMake project. It doesn't build anything yet, but we've laid the foundation.
Adding an Executable Target
Now let's tell CMake to actually build something. In CMake, anything that gets built - such as an executable or library - is called a target.
The command to create an executable is add_executable()
. Let's add it to our file.
Greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_executable(GreeterApp src/main.cpp)
The add_executable()
command takes two main arguments:
- The Target Name: The first argument is the name we want to give our executable target. In simple projects, the primary executable of our project often matches the project name, but it doesn't have to. Here, we've called our project
Greeter
and the executableGreeterApp
. - The Source Files: The remaining arguments are the source files needed to build that executable. We've provided a single source file for now -
src/main.cpp
.
And that's it! With these three lines, you have a complete, working CMake project that can build an executable.
We have described what we want - an executable named GreeterApp
built from main.cpp
- and CMake will figure out how to do it on any platform.
We'll run the build in the next lesson, but first, let's make our project a little more complex.
Using a Library
Real-world projects are rarely a single file. As we've discussed, good design involves breaking code into logical, reusable components. In C++, we do this with libraries.
This project is obviously too simple to warrant such separation, but let's run with the contrivance to learn the process. We'll refactor our project into a simple library called Greeter
that includes a header and source file, and provides a get_greeting()
function. Our main executable will then use this library.
Updating the Project Structure
First, let's create the files for our library. We'll follow the convention of putting the public header in an include
directory and the source file in src
.
Greeter/
├─ include/
│ └─ Greeter.h
└─ src/
├─ Greeter.cpp
└─ main.cpp
Here's the content for our new library files, and we'll also update main.cpp
to use it:
Files
Now, we need to update our CMakeLists.txt
to tell CMake about this new library and how it relates to our executable.
Adding a Library Target and Linking
We'll use two new commands: add_library()
to define our library target, and target_link_libraries()
to create the dependency. We can also add comments to our CMakeLists.txt
file using the #
token:
Greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
# Create a library target named "GreeterLib"
add_library(GreeterLib src/Greeter.cpp)
# Add our executable target like before
add_executable(GreeterApp src/main.cpp)
# Link the "GreeterLib" library to our executable
target_link_libraries(GreeterApp GreeterLib)
Let's break down the new additions.
add_library()
The add_library(GreeterLib src/Greeter.cpp)
command works just like add_executable()
. It creates a new library target, where we provide the name we want to use (GreeterLib
in this example) and the source files it is built from (a single file - src/Greeter.cpp
- in this example).
By default, this creates a static library (.a
or .lib
). We'll learn how to change this to a shared library later in the course.
In real-world projects, libraries are generally going to have more sensible names based on their specific purpose. They might have names such as "Renderer" or "PhysicsEngine".
In our contrived example, we'll call our target GreeterLib
, to distinguish it from the overall project (which we called Greeter
) and the executable target (which we called GreeterApp
).
target_link_libraries()
The target_link_libraries(GreeterApp GreeterLib)
command connects the library to the executable. It tells CMake, that the GreeterApp
target depends on the GreeterLib
target. This single command does several things for us automatically:
- It ensures
GreeterLib
is built beforeGreeterApp
. - It tells the linker to link the compiled
GreeterLib
library with theGreeterApp
executable. - As we'll see later, it can also transitively pass on configuration options and usage requirements, like include directories.
Adding Include Directories
We still have one problem. The main.cpp
file includes Greeter.h
, but the compiler doesn't know to look in our include
directory for this header file. Let's fix that:
Greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_library(GreeterLib src/Greeter.cpp)
# Tell our GreeterLib library where its public headers are
target_include_directories(GreeterLib PUBLIC
${PROJECT_SOURCE_DIR}/include
)
add_executable(GreeterApp src/main.cpp)
target_link_libraries(GreeterApp GreeterLib)
The target_include_directories()
command adds include search paths to a target. Let's break down the three arguments we provided here.
The GreeterLib
Argument
The include directory we want to provide relates to our GreeterLib
library, so we provide this target as the first argument.
Our GreeterApp
target also needs this include directory for the #include
directive in it's main.cpp
file, so you might think we need a second target_include_directories()
for that target.
We could add that, but one of the benefits of CMake is that we don't need to. As long as our library makes this include directory PUBLIC
and we then link to the library using target_link_libraries()
, CMake can take care of it for us. This management of interconnected targets is a major feature of CMake and something we've dedicated a chapter to later in the course.
The PUBLIC
Argument
The PUBLIC
keyword effectively makes this dependency known to any other target that might be interested. It means: "Anyone who links to me (GreeterLib
) also needs this include directory to compile."
In our case, the GreeterApp
also needs to know about this include directory. Because we made this relationship PUBLIC
, and we later we link GreeterApp
to GreeterLib
, then GreeterApp
automatically inherits the include directory, solving our compilation problem.
We'll fully explain the PUBLIC
keyword and the other options - PRIVATE
and INTERFACE
- later in the course.
The Include Directory
Finally, as our third argument, we need to provide the actual path to our /include
directory.
We do this using the ${PROJECT_SOURCE_DIR}/include
expression, which is our first example of using a variable within CMake. We'll cover variables in much more detail soon, but PROJECT_SOURCE_DIR
a one of the many variables that CMake automatically defines for us.
It's value is the path on our file system to our project directory - that is, the location where our CMakeLists.txt
file is. The ${PROJECT_SOURCE_DIR}/include
interpolates this variable into a string to generate the full path to our /include
folder within our project's directory.
If the header files for our GreeterLib
library were instead located in /greeter/include
within our project directory, then our expression would look like this:
target_include_directories(GreeterLib PUBLIC
${PROJECT_SOURCE_DIR}/greeter/include
)
If our library had multiple include directories, we can add additional arguments to our target_include_directories()
command:
target_include_directories(GreeterLib PUBLIC
${PROJECT_SOURCE_DIR}/include
${PROJECT_SOURCE_DIR}/headers
${PROJECT_SOURCE_DIR}/more-headers
)
Specifying C++ Standards
The final piece of our basic puzzle is telling CMake which version of the C++ standard we want to use.
Modern CMake provides a target-based way to do this with target_compile_features()
. We should do this for both of our library and executable targets. We'll use C++20 in this example, but not for any specific reason beyond it being a relatively modern and widely supported baseline:
Greeter/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(Greeter)
add_library(GreeterLib src/Greeter.cpp)
target_compile_features(GreeterLib PUBLIC cxx_std_20)
target_include_directories(GreeterLib PUBLIC
${PROJECT_SOURCE_DIR}/include
)
add_executable(GreeterApp src/main.cpp)
target_compile_features(GreeterApp PUBLIC cxx_std_20)
target_link_libraries(GreeterApp GreeterLib)
The command target_compile_features(MyTarget PUBLIC cxx_std_20)
tells CMake: "The target MyTarget
requires compiler features associated with the C++20 standard."
CMake will then automatically add the correct compiler flag (e.g., -std=c++20
for GCC/Clang or /std:c++20
for MSVC) when compiling that target.
Just like with target_include_directories()
, the PUBLIC
keyword makes this requirement propagate between targets. We'll fully explain this propagation process, the PUBLIC
keyword, target_compile_features()
and managing compiler flags later in the course.
Summary
We now have a complete, modern CMakeLists.txt
file documenting the full configuration of a simple project with both a library and an executable.
Let's review our final CMakeLists.txt
for this lesson:
Files
We've learned the five most fundamental commands for any CMake project:
cmake_minimum_required()
: Sets the required version and policies.project()
: Names the project.add_library()
: Creates a library target.add_executable()
: Creates an executable target.target_link_libraries()
: Creates a dependency between targets.
And we've seen how to add requirements like include paths and C++ standards using modern, target-based commands.
Building and Running CMake Projects
Learn the two-stage process of building a CMake project. This lesson covers configuring, generating, building, running, and troubleshooting common errors.