Skip to content
C Codeloom
C++

C++ CMake Tutorial: Build Your First Project

Learn modern CMake for C++ — targets, properties, and dependencies — and configure a small project that compiles cleanly across platforms.

·4 min read · By Codeloom
Beginner 9 min read

What you'll learn

  • Write a minimal modern CMakeLists.txt
  • Define libraries and executables as targets
  • Use target_link_libraries and target_include_directories
  • Manage dependencies with FetchContent
  • Build and configure across platforms

Prerequisites

  • Install and first program — see /blog/cpp-install-and-first-program

CMake is the de facto build system for C++. It generates platform-native build files — Makefiles, Ninja, Visual Studio projects — from a single description. Modern CMake (3.15+) focuses on targets and properties rather than global variables, and it is far less painful than the CMake of a decade ago.

What and Why

A C++ build orchestrates a compiler, a linker, include paths, defines, optimization flags, and library dependencies — separately for debug and release, possibly across operating systems. Writing this by hand is tedious and error-prone. CMake describes the project once and produces the right build commands for each environment.

You will encounter CMake in nearly every open-source C++ project. Reading and writing it confidently is a baseline skill.

Mental Model

Think of CMake as a graph of targets. A target is an executable or a library. Each target has properties: sources, include directories, link dependencies, compile features. Properties are either private (used only when building the target), interface (exposed to consumers), or public (both).

The golden rule of modern CMake: never set global variables for include paths or flags. Instead, attach everything to a target.

Hands-on Example

Build a small library and a program that uses it.

cmake_minimum_required(VERSION 3.20)
project(HelloApp LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(greet STATIC src/greet.cpp)
target_include_directories(greet PUBLIC include)

add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE greet)

With files include/greet.hpp, src/greet.cpp, and src/main.cpp, you can configure and build:

cmake -B build -S .
cmake --build build
./build/app
  ┌──────────────┐
│   app (exe)  │
└──────┬───────┘
       │ link
       v
┌──────────────┐    PUBLIC includes ──> consumers see /include
│ greet (lib)  │    PRIVATE sources ──> only build greet itself
└──────────────┘
CMake target graph: app depends on greet, which exports its include path.

The library exposes its include directory PUBLIC, so the executable automatically inherits it. No global include_directories() needed.

Common Pitfalls

A frequent mistake is using include_directories() or add_definitions() at the top of the file. These set state for everything below — fine for tiny projects, painful at scale. Use target_include_directories() and target_compile_definitions() instead.

Another pitfall is committing the build directory. Always build out-of-source with -B build and add build/ to .gitignore.

Forgetting target_link_libraries(app PRIVATE greet) produces baffling linker errors. The verb here is “link with” — it pulls in not just libraries but transitive include paths and compile options.

Mixing PRIVATE, INTERFACE, and PUBLIC incorrectly leads to either hidden symbols or accidentally exposed implementation headers. The rule: use PUBLIC only when consumers genuinely need the property.

Practical Tips

Use Ninja as the generator when possible. It is faster than Make and works on every platform: cmake -B build -G Ninja.

Set warnings as a target property, not a global flag: target_compile_options(greet PRIVATE -Wall -Wextra -Wpedantic).

For external dependencies, prefer FetchContent for small libs and a package manager (vcpkg or Conan) for bigger ones. Avoid git submodule for build inputs unless you really need it.

Split your CMakeLists.txt once the project grows. Each subdirectory can have its own file, included via add_subdirectory().

Use presets (CMakePresets.json) to capture common configure and build options so contributors do not need to remember flags.

Wrap-up

Modern CMake is target-centric: define libraries and executables, attach properties to them, and let consumers inherit what they need. With a cmake_minimum_required of 3.20 or higher, a clear separation of PRIVATE and PUBLIC, and an out-of-source build directory, you have a foundation that scales from a single file to a multi-module project across platforms.