CMake & Build Systems

Reviewed & published by Brayan K

By the end of this lesson you'll be able to write a CMakeLists.txt from scratch, build a project with two commands, split reusable code into a library, switch between Debug and Release builds, and pull in third-party libraries with find_package — the way every professional C++ project is built.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of CMakeLists.txt as a recipe and CMake as the head chef who reads it. The recipe lists ingredients (your source files) and the steps. CMake doesn't cook — it writes a precise, station-by-station prep list (the build/ folder) that the line cooks (your compiler, g++ or MSVC) actually follow. That's why building is two steps: first the chef plans the kitchen (cmake -B build — the configure step), then the cooks execute the plan (cmake --build build — the build step). Write the recipe once and the same kitchen runs on Linux, macOS, or Windows.

1. Your First CMakeLists.txt & the Build Flow

A CMakeLists.txt sits at the root of your project and declares the minimum CMake version, the project name and language, the C++ standard, and the program to build. You build with two commands: cmake -B build reads the recipe and writes build files into a fresh build/ folder (the configure step), then cmake --build build runs the compiler (the build step). Keeping generated files in build/ — an "out-of-source" build — means you can delete that folder any time without touching your code.

#include <iostream>
using namespace std;

// CMake config is NOT C++, so it cannot run in this editor.
// Read the commented CMakeLists.txt below, then run the program
// to see the exact terminal commands that build it.

// ============================================================
//  project/CMakeLists.txt   (the build recipe for your project)
// ============================================================
//
//  cmake_minimum_required(VERSION 3.20)   # oldest CMake allowed
//  project(Hello LANGUAGES CXX)           # name the project; CXX = C++
//
//  set(CMAKE_CXX_STANDARD 20)             # compile as C++20
//  set(CMAKE_CXX_STANDARD_REQUIRED ON)    # fail if C++20 unsupported
//
//  add_executable(hello src/main.cpp)     # build program "hello"
//
// project/src/main.cpp is THIS file.

int main() {
    cout << "Hello from a CMake project!" << endl;
    return 0;
}

// ============================================================
//  Build it in the terminal (two steps):
// ============================================================
//   $ cmake -B build          # 1) CONFIGURE: read CMakeLists.txt,
//                             #    write build files into ./build
//   $ cmake --build build     # 2) BUILD: run the compiler
//   $ ./build/hello           # 3) RUN the program
//
//  Expected terminal output:
//    Hello from a CMake project!

// ✅ Expected output:
//    Hello from a CMake project!

Every recipe opens with the same three lines: cmake_minimum_required, project(), and a target created by add_executable. A target is just a thing CMake knows how to build — usually a program or a library.

#include <iostream>
using namespace std;

// Every CMakeLists.txt starts with the same three lines.
// They are shown here as comments because CMake is not C++.

// ------------------------------------------------------------
//  cmake_minimum_required(VERSION 3.20)
//      Refuse to run on a CMake older than 3.20. This guarantees
//      the features you use below actually exist.
//
//  project(MyApp VERSION 1.0 LANGUAGES CXX)
//      Name = MyApp, version = 1.0, language = C++ (CXX).
//      Sets handy variables like PROJECT_NAME for you.
//
//  add_executable(myapp src/main.cpp)
//      Create a build TARGET called "myapp" from one source file.
//      A "target" is just a thing CMake knows how to build.
// ------------------------------------------------------------

int main() {
    // The C++ side is whatever main.cpp contains.
    string project = "MyApp";
    cout << "Building target from project: " << project << endl;
    // Expected output:  Building target from project: MyApp
    return 0;
}

// ✅ Expected output:
//    Building target from project: MyApp

Your turn. Complete the CMakeLists.txt in the comments by filling in the three ___ blanks using the // 👉 hints, then run the program to see the target it would build.

#include <iostream>
using namespace std;

// 🎯 YOUR TURN — complete the CMakeLists.txt written in the comments.
// (Edit the comment text, then run the C++ to see the goal output.)
//
//  cmake_minimum_required(VERSION 3.20)
//
//  # 1) Name the project "Greeter" with language C++:
//  project(___ LANGUAGES CXX)        // 👉 replace ___ with  Greeter
//
//  # 2) Build C++17:
//  set(CMAKE_CXX_STANDARD ___)       // 👉 replace ___ with  17
//
//  # 3) Build an executable target "greeter" from src/main.cpp:
//  add_executable(greeter ___)       // 👉 replace ___ with  src/main.cpp

int main() {
    cout << "greeter built with CMake!" << endl;
    return 0;
}

// ✅ Expected (after: cmake -B build && cmake --build build && ./build/greeter):
//    greeter built with CMake!

2. Libraries, Executables & Linking

add_executable() builds a runnable program; add_library() builds reusable code other targets link against (a STATIC library is bundled straight into the programs that use it). You join them with target_link_libraries(program PRIVATE the_lib), and you expose a library's headers with target_include_directories(the_lib PUBLIC include) so anything linking it can #include them. This is what makes testing clean: your test program links the same library as your app, so it exercises real code.

#include <iostream>
using namespace std;

// Real projects split reusable code into a LIBRARY that programs LINK against.

// ============================================================
//  CMakeLists.txt  (a library + two executables that use it)
// ============================================================
//
//  project(Calculator LANGUAGES CXX)
//
//  # A static library built from the calculator sources:
//  add_library(calc_lib STATIC src/calculator.cpp)
//
//  # Anyone linking calc_lib can find its headers in include/:
//  target_include_directories(calc_lib PUBLIC include)
//
//  # The app: links the library with target_link_libraries:
//  add_executable(calculator src/main.cpp)
//  target_link_libraries(calculator PRIVATE calc_lib)
//
//  # The tests: link the SAME library, so tests use real code:
//  add_executable(calc_tests tests/test_calc.cpp)
//  target_link_libraries(calc_tests PRIVATE calc_lib)
//
//  add_executable      -> a runnable program
//  add_library(STATIC) -> a .a / .lib bundled INTO programs

// Below, namespace calc simulates the linked calc_lib:
namespace calc {
    double add(double a, double b) { return a + b; }
    double divide(double a, double b) { return a / b; }
}

int main() {
    cout << "add(10, 5)    = " << calc::add(10, 5) << endl;    // 15
    cout << "divide(10, 5) = " << calc::divide(10, 5) << endl; // 2
    return 0;
}

// ✅ Expected output:
//    add(10, 5)    = 15
//    divide(10, 5) = 2

Now you try. The library and program already exist — add the one line that links them so the program can call the library's code.

#include <iostream>
using namespace std;

// 🎯 YOUR TURN — wire up the library so the app can use it.
//
//  project(Notes LANGUAGES CXX)
//
//  # A library target already exists:
//  add_library(notes_lib STATIC src/notes.cpp)
//  target_include_directories(notes_lib PUBLIC include)
//
//  # The program:
//  add_executable(notes src/main.cpp)
//
//  # 1) Link notes_lib into the "notes" executable:
//  target_link_libraries(notes PRIVATE ___)  // 👉 replace ___ with  notes_lib

int main() {
    cout << "notes app linked to notes_lib" << endl;
    return 0;
}

// ✅ Expected (after building and running ./build/notes):
//    notes app linked to notes_lib

3. Build Types: Debug vs Release

The same source code can be compiled two ways, and you choose which at configure time with CMAKE_BUILD_TYPE. Debug keeps debug symbols and turns optimisation off so a debugger maps cleanly to your lines — use it while developing. Release turns optimisation up to -O3 and strips the symbols, producing a much faster program — use it for what you ship. RelWithDebInfo is the middle ground: optimised but still debuggable.

#include <iostream>
using namespace std;

// The SAME source can be built two ways. You pick the build TYPE
// at configure time; CMake passes the right compiler flags for you.

// ============================================================
//  Debug — for developing:
//    $ cmake -B build -DCMAKE_BUILD_TYPE=Debug
//    $ cmake --build build
//    -> keeps debug symbols (-g), turns optimisation OFF (-O0)
//    -> step through code in a debugger; build is fast, runs slower
//
//  Release — for shipping:
//    $ cmake -B build -DCMAKE_BUILD_TYPE=Release
//    $ cmake --build build
//    -> turns optimisation ON (-O3), strips debug info
//    -> much faster program; harder to debug
//
//  RelWithDebInfo — optimised AND keeps symbols (a useful middle).
// ============================================================

int main() {
    // CMAKE_BUILD_TYPE does not change WHAT runs, only HOW it is compiled.
    const char* mode = "Release";
    cout << "Compiled in " << mode << " mode (optimised)." << endl;
    // Expected output:  Compiled in Release mode (optimised).
    return 0;
}

// ✅ Expected output:
//    Compiled in Release mode (optimised).

4. Using Other Libraries with find_package

You rarely write everything yourself. find_package(Name REQUIRED) locates a library already installed on the machine and hands you a target — like Threads::Threads or OpenSSL::SSL — that you drop straight into target_link_libraries. The REQUIRED keyword tells CMake to stop with a clear "package not found" message during configure, instead of letting you hit a confusing compile error much later.

#include <iostream>
using namespace std;

// find_package() locates a library already installed on the machine
// and gives you a target you can link, e.g. Threads::Threads.

// ============================================================
//  CMakeLists.txt  (using an installed library)
// ============================================================
//
//  find_package(Threads REQUIRED)     # find the system threads lib
//                                     # REQUIRED = fail now if missing
//  add_executable(worker src/main.cpp)
//  target_link_libraries(worker PRIVATE Threads::Threads)
//
//  Other common examples:
//    find_package(OpenSSL REQUIRED)
//    target_link_libraries(app PRIVATE OpenSSL::SSL)
//
//    find_package(Boost 1.75 REQUIRED COMPONENTS filesystem)
//    target_link_libraries(app PRIVATE Boost::filesystem)
//
//  REQUIRED means "stop configuring with a clear error if not found",
//  which beats a confusing compile error later on.

int main() {
    cout << "worker links Threads::Threads via find_package" << endl;
    return 0;
}
// ✅ Expected output:
//    worker links Threads::Threads via find_package

🔎 Deep Dive: configure vs build, and the build/ folder

Configure (cmake -B build) reads CMakeLists.txt and generates real build files (Makefiles or Ninja files) inside build/. Build (cmake --build build) runs those generated files to compile your code. You only re-configure when CMakeLists.txt changes; editing a .cpp just needs a re-build.

# One-time per machine / after CMakeLists.txt edits:
cmake -B build -DCMAKE_BUILD_TYPE=Release   # configure

# Every time you change a .cpp:
cmake --build build                         # build
./build/myapp                               # run

# Start fresh (safe — build/ is generated, not source):
rm -rf build && cmake -B build

Add build/ to your .gitignore — it is generated output, never source you commit.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

CommandWhat it does
cmake_minimum_required(VERSION 3.20)Require CMake 3.20+
project(MyApp LANGUAGES CXX)Name the project; set language
add_executable(app main.cpp)Build a program target
add_library(lib STATIC a.cpp)Build a reusable library
target_link_libraries(app PRIVATE lib)Link a library into a target
target_include_directories(lib PUBLIC include)Expose header folders
find_package(Threads REQUIRED)Find an installed library
cmake -B buildConfigure (generate build files)
cmake --build buildCompile the project
cmake -B build -DCMAKE_BUILD_TYPE=ReleaseOptimised release build

Mini-Challenge: A CMakeLists.txt for a Game

No blanks this time — just a brief and an outline. Write the full CMakeLists.txt in the comments (library + executable + linking), list the three build commands, then run the program to confirm the expected output. This is exactly the shape of a real project's build file.

#include <iostream>
using namespace std;

// 🎯 MINI-CHALLENGE: write a CMakeLists.txt for a small game
//
// In the COMMENTS below, write a CMakeLists.txt that:
//   1. Requires CMake 3.20 and names the project "Snake" (LANGUAGES CXX).
//   2. Sets CMAKE_CXX_STANDARD to 20 and marks it REQUIRED.
//   3. Builds a static library "snake_lib" from src/game.cpp,
//      with its headers exposed via target_include_directories(... PUBLIC include).
//   4. Builds an executable "snake" from src/main.cpp.
//   5. Links snake_lib into snake with target_link_libraries(... PRIVATE ...).
//
// Then list the THREE terminal commands that configure, build, and run it.
//
// ✅ Expected terminal flow:
//    $ cmake -B build
//    $ cmake --build build
//    $ ./build/snake     ->   Snake game ready!

int main() {
    cout << "Snake game ready!" << endl;
    return 0;
}

// your CMakeLists.txt (in comments) here

🎉 Lesson Complete

Practice quiz

What is the difference between CMake and a compiler like g++?

  • They are the same tool with different names
  • CMake compiles faster than g++
  • CMake is a build-system generator that writes build files; g++ actually compiles the code
  • g++ generates CMakeLists.txt for you

Answer: CMake is a build-system generator that writes build files; g++ actually compiles the code. CMake reads CMakeLists.txt and generates build files (Makefiles/Ninja) that then call the compiler; CMake is the manager, g++ the worker.

Which two commands build a CMake project, and in what order?

  • cmake -B build (configure), then cmake --build build (build)
  • cmake --build build, then cmake -B build
  • make, then cmake
  • g++ -B build, then cmake run

Answer: cmake -B build (configure), then cmake --build build (build). cmake -B build configures (generates build files), then cmake --build build runs the compiler.

When do you need to re-run the configure step (cmake -B build)?

  • Every time you edit any .cpp file
  • Never; it runs automatically
  • After every successful run of the program
  • Only when CMakeLists.txt itself changes

Answer: Only when CMakeLists.txt itself changes. Editing a .cpp just needs a re-build; you only re-configure when CMakeLists.txt changes.

Which command creates a runnable program target?

  • add_library
  • add_executable
  • find_package
  • target_link_libraries

Answer: add_executable. add_executable builds a runnable program (it has a main); add_library builds reusable code.

What does add_library(... STATIC ...) produce?

  • Reusable code (a .a/.lib) that other targets link against and is bundled into them
  • A runnable program
  • A CMakeLists.txt file
  • A shared .so loaded at runtime

Answer: Reusable code (a .a/.lib) that other targets link against and is bundled into them. A STATIC library is reusable code bundled straight into the programs that link it.

What does PRIVATE vs PUBLIC control in target_link_libraries?

  • Whether the library is open source
  • The compiler optimisation level
  • Who inherits the dependency: PRIVATE = only this target; PUBLIC = this target and anything that links it
  • Whether the target is an executable or a library

Answer: Who inherits the dependency: PRIVATE = only this target; PUBLIC = this target and anything that links it. PRIVATE keeps the dependency to this target; PUBLIC propagates it to anything linking this target. Start with PRIVATE.

How do you link a library called calc_lib into an executable called calculator?

  • link_libraries(calculator calc_lib)
  • target_link_libraries(calculator PRIVATE calc_lib)
  • add_executable(calculator calc_lib)
  • find_package(calc_lib REQUIRED)

Answer: target_link_libraries(calculator PRIVATE calc_lib). target_link_libraries(target PRIVATE lib) joins a library into a target.

Which build type should you use for the version you ship?

  • Debug
  • There is only one build type
  • Test
  • Release

Answer: Release. Release turns on optimisation (-O3) and strips debug info for a faster program; Debug is for developing.

What does find_package(Threads REQUIRED) do?

  • Downloads and installs the threads library
  • Locates a library already installed on the machine and fails configure with a clear error if missing
  • Compiles the threads library from source
  • Creates a new library target named Threads

Answer: Locates a library already installed on the machine and fails configure with a clear error if missing. find_package locates an installed library and gives you a target to link; REQUIRED stops configure with a clear message if not found.

Why are the per-target commands target_include_directories/target_link_libraries preferred over the global include_directories?

  • They compile faster
  • Global commands are deprecated and removed
  • Global commands leak settings into every target; target_* scopes each dependency to the target that needs it
  • There is no difference

Answer: Global commands leak settings into every target; target_* scopes each dependency to the target that needs it. The target_* commands scope dependencies precisely, avoiding the baffling build issues that global includes cause in big projects.

Continue this course

Frequently asked questions

What is the difference between CMake and a compiler like g++?

g++ compiles one or more source files into a program. CMake does not compile anything itself — it is a build-system generator that reads your CMakeLists.txt and writes the actual build files (Makefiles or Ninja files) that then call the compiler for you. CMake is the manager; g++ is the worker.

Why do I run two cmake commands (configure then build)?

The first command, cmake -B build, is the configure step: it reads CMakeLists.txt and generates build files inside a folder called build. The second, cmake --build build, is the build step: it runs the compiler using those generated files. You only re-run the configure step when CMakeLists.txt itself changes.

What does PRIVATE vs PUBLIC mean in target_link_libraries?

It controls who inherits the dependency. PRIVATE means only this target uses the library. PUBLIC means this target and anything that links to it both get it (used for libraries whose headers expose the dependency). When unsure, start with PRIVATE — it keeps dependencies from leaking.

Should I use add_executable or add_library?

Use add_executable when the output is a runnable program (it has a main). Use add_library when the output is reusable code other targets link against — a static .a/.lib or shared .so/.dll. Real projects often have one library plus several executables (the app and its tests) that link to it.

What build type should I use, Debug or Release?

Use Debug while developing — it keeps debug symbols and turns optimisation off so a debugger maps cleanly to your code. Use Release for the version you ship — it turns on optimisation (-O3) and strips debug info, making the program much faster. Set it with cmake -B build -DCMAKE_BUILD_TYPE=Release.