Cmake Projects

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.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C++ course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

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.

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.

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.

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.

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.

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.

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.

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.

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

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.

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.