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.

Greg Filak
Updated

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 version 3.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:

  1. 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 executable GreeterApp.
  2. 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

Greeter
Select a file to view its content

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 before GreeterApp.
  • It tells the linker to link the compiled GreeterLib library with the GreeterApp 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

Greeter
Select a file to view its content

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.

Next Lesson
Lesson 11 of 51

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.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate