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
Published

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 GreeterApp.

Let's create a root folder named GreeterApp. Inside it, we'll create a src directory.

GreeterApp/
└─ src/

We'll start with a single source file for a "Hello, World!" program.

GreeterApp/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.

GreeterApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.16) 
project(GreeterApp)

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.0.3 released in June 2025, which can emulate behaviors as far back as version 3.0.5, released in 2016.

If there's 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. But for now, something like 3.16 is a reasonable starting point.

The project() Command

This command officially starts the project. It sets the project's name and enables support for programming languages.

project(GreeterApp) sets the name of our project to "GreeterApp". 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.

GreeterApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GreeterApp)

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. Here, we've named it GreeterApp. By convention, the primary executable of our project often matches the project name, but it doesn't have to.
  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 provides a greeting function, and 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.

GreeterApp/
├─ 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

GreeterApp
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:

GreeterApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GreeterApp)

# Create a library target named "Greeter"
add_library(Greeter src/Greeter.cpp) 

# Add our executable target like before
add_executable(GreeterApp src/main.cpp)

# Link the "Greeter" library to our executable
target_link_libraries(GreeterApp Greeter)

Let's break down the new additions.

add_library()

The add_library(Greeter src/Greeter.cpp) command works just like add_executable(). It creates a new library target named Greeter and specifies that it's built from src/Greeter.cpp. 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.

target_link_libraries()

The target_link_libraries(GreeterApp Greeter) command connects the library to the executable. It tells CMake, that the GreeterApp target depends on the Greeter target. This single command does several things for us automatically:

  • It ensures Greeter is built before GreeterApp.
  • It tells the linker to link the compiled Greeter library with the GreeterApp executable.
  • As we'll see later, it can also transitively pass on 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:

GreeterApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GreeterApp)

add_library(Greeter src/Greeter.cpp)

# Tell our Greeter library where its public headers are
target_include_directories(Greeter PUBLIC 
  ${PROJECT_SOURCE_DIR}/include 
) 

add_executable(GreeterApp src/main.cpp)

target_link_libraries(GreeterApp Greeter)

The target_include_directories() command adds include search paths to a target. Let's break down the three arguments we provided here.

1. The Greeter Argument

The include directory we want to provide relates to our Greeter library, so we provide this target as the first argument.

2. 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 (Greeter) 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 Greeter, 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.

3. 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 source directory.

If the header files for our Greeter library were instead located in /greeter/include within our project directory, then our expression would look like this:

target_include_directories(Greeter PUBLIC
  ${PROJECT_SOURCE_DIR}/greeter/include 
)

Specifying C++ Standards and Compiler Flags

The final piece of our basic puzzle is telling CMake which version of the C++ standard we want to use. You should always do this explicitly.

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:

GreeterApp/CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GreeterApp)

add_library(Greeter src/Greeter.cpp)
target_compile_features(Greeter PUBLIC cxx_std_20) 
target_include_directories(Greeter PUBLIC
  ${PROJECT_SOURCE_DIR}/include
)

add_executable(GreeterApp src/main.cpp)
target_compile_features(GreeterApp PUBLIC cxx_std_20) 
target_link_libraries(GreeterApp Greeter)

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 transitive. However, it's generally good practice to explicitly set the standard on all your executable and library targets to make the requirements clear.

Summary

We now have a complete, modern, and portable 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

GreeterApp
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 12

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 have been reviewed for accuracy