Single Source Executable
The simplest possible CMake project turns a single .cpp file into an executable. This is the starting point for every C++ project and demonstrates CMake's core command: add_executable().
The Minimum Project
A complete CMake project requires exactly two files: a CMakeLists.txt and at least one source file.
# Project structure
my_project/
├── CMakeLists.txt
└── main.cpp
The source file:
// main.cpp
#include <iostream>
int main() {
std::cout << "Hello from CMake!\n";
return 0;
}
The CMakeLists.txt:
# CMakeLists.txt - Minimum viable project
cmake_minimum_required(VERSION 3.21)
project(HelloWorld LANGUAGES CXX)
add_executable(hello main.cpp)
Let's break down each line:
cmake_minimum_required(VERSION 3.21)— Sets the minimum CMake version. This enables modern behavior and policies. See cmake_minimum_required.project(HelloWorld LANGUAGES CXX)— Declares the project name and languages.CXXmeans C++. This triggers compiler detection. See project().add_executable(hello main.cpp)— Creates a build target calledhellofrommain.cpp. The output binary is namedhello(orhello.exeon Windows).
Build and run:
# Configure (generate build files)
cmake -S . -B build
# Build (compile and link)
cmake --build build
# Run the result
./build/hello # Linux/macOS
.\build\Debug\hello.exe # Windows (MSVC)
add_executable() creates a target. Everything in modern CMake revolves around targets — they carry properties like compile flags, include paths, and link dependencies. We'll explore targets deeply in Part 5.
Setting the C++ Standard
Always specify the C++ standard explicitly rather than relying on compiler defaults:
# CMakeLists.txt - With C++ standard
cmake_minimum_required(VERSION 3.21)
project(HelloWorld LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(hello main.cpp)
CMAKE_CXX_STANDARD 17— Request C++17CMAKE_CXX_STANDARD_REQUIRED ON— Fail if compiler doesn't support C++17 (instead of silently downgrading)CMAKE_CXX_EXTENSIONS OFF— Use-std=c++17instead of-std=gnu++17(portable, no GNU extensions)
Multiple Source Files
Real projects have dozens or hundreds of source files. CMake provides several ways to specify them.
Listing Sources Directly
The most straightforward and recommended approach is to list every source file explicitly:
# CMakeLists.txt - Multiple sources listed directly
cmake_minimum_required(VERSION 3.21)
project(Calculator LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(calculator
src/main.cpp
src/parser.cpp
src/evaluator.cpp
src/tokenizer.cpp
)
This approach is explicit, version-control friendly (diffs show exactly what changed), and guarantees CMake reconfigures when a source is added or removed.
Grouping Sources with Variables
For larger projects, you can group related sources into variables using set():
# CMakeLists.txt - Grouped source variables
cmake_minimum_required(VERSION 3.21)
project(GameEngine LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Group by component
set(CORE_SOURCES
src/core/engine.cpp
src/core/window.cpp
src/core/input.cpp
src/core/timer.cpp
)
set(RENDERER_SOURCES
src/renderer/renderer.cpp
src/renderer/shader.cpp
src/renderer/texture.cpp
src/renderer/mesh.cpp
)
set(AUDIO_SOURCES
src/audio/audio_system.cpp
src/audio/sound.cpp
)
add_executable(game
src/main.cpp
${CORE_SOURCES}
${RENDERER_SOURCES}
${AUDIO_SOURCES}
)
You can also append to lists with list(APPEND ...):
# CMakeLists.txt - Using list(APPEND)
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(SOURCES src/main.cpp)
list(APPEND SOURCES
src/config.cpp
src/logger.cpp
)
# Conditionally add platform-specific sources
if(WIN32)
list(APPEND SOURCES src/platform_win32.cpp)
elseif(UNIX)
list(APPEND SOURCES src/platform_unix.cpp)
endif()
add_executable(app ${SOURCES})
file(GLOB) and Why to Avoid It
CMake provides file(GLOB) to automatically collect source files matching a pattern:
# CMakeLists.txt - GLOB example (NOT RECOMMENDED)
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
# Collects ALL .cpp files in src/ into SOURCES variable
file(GLOB SOURCES "src/*.cpp")
add_executable(app ${SOURCES})
.cpp file, the build system has no way to know the file list changed — CMake won't automatically re-configure. You must manually re-run cmake every time. This breaks incremental workflows and causes mysterious build failures in CI.
If you insist on GLOB, use CONFIGURE_DEPENDS (CMake 3.12+) which asks the generator to check for file changes:
# CMakeLists.txt - GLOB with CONFIGURE_DEPENDS (still not ideal)
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp")
add_executable(app ${SOURCES})
However, CONFIGURE_DEPENDS adds overhead to every build (the generator must scan the filesystem), is not supported by all generators, and still has edge cases. The CMake documentation explicitly states:
"We do not recommend using GLOB to collect a list of source files from your source tree." — CMake file() documentation
Bottom line: List your sources explicitly. Modern IDEs and tools make this easy.
Header Files
Headers (.h, .hpp) are not compiled directly — they're included by source files during preprocessing. However, CMake needs to know where to find them.
Include Directories
Consider this project structure:
# Project with separate include directory
my_project/
├── CMakeLists.txt
├── include/
│ └── myproject/
│ ├── math_utils.h
│ └── string_utils.h
└── src/
├── main.cpp
├── math_utils.cpp
└── string_utils.cpp
The target_include_directories() command tells CMake where to search for headers:
# CMakeLists.txt - Include directories
cmake_minimum_required(VERSION 3.21)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(myapp
src/main.cpp
src/math_utils.cpp
src/string_utils.cpp
)
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
Now source files can include headers with:
// src/main.cpp
#include <myproject/math_utils.h>
#include <myproject/string_utils.h>
#include <iostream>
int main() {
std::cout << "Sum: " << add(3, 4) << "\n";
std::cout << "Upper: " << to_upper("hello") << "\n";
return 0;
}
include/projectname/header.h structure. The extra directory level prevents header name collisions between projects and makes includes self-documenting: #include <myproject/math_utils.h> clearly identifies where the header comes from.
PUBLIC, PRIVATE, and INTERFACE
The visibility keyword in target_include_directories() controls who sees the include path:
| Keyword | Used by Target | Propagated to Dependents | Use Case |
|---|---|---|---|
PRIVATE | ✅ Yes | ❌ No | Internal implementation headers |
PUBLIC | ✅ Yes | ✅ Yes | Headers included in your public API |
INTERFACE | ❌ No | ✅ Yes | Header-only libraries |
# CMakeLists.txt - Visibility example
cmake_minimum_required(VERSION 3.21)
project(VisibilityDemo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(demo
src/main.cpp
src/engine.cpp
)
# Headers in include/ are part of the public interface
target_include_directories(demo PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Headers in src/ are implementation details
target_include_directories(demo PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
For executables, the distinction between PUBLIC and PRIVATE is less critical (executables are endpoints). But establishing this habit now prepares you for libraries in Part 4, where visibility propagation is essential.
Should Headers Be Listed in add_executable()?
Headers don't need to be listed since they're not compiled. However, listing them helps IDEs discover and display them in the project tree:
# CMakeLists.txt - Listing headers for IDE support
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app
src/main.cpp
src/parser.cpp
include/app/parser.h # For IDE awareness only
include/app/types.h # Not compiled
)
Generators
A generator determines what native build system CMake outputs. Different generators produce different build files, each with trade-offs in speed, features, and platform support.
The -G Flag
Specify a generator during configuration with -G:
# List all available generators on your system
cmake --help
# Use specific generators
cmake -S . -B build-make -G "Unix Makefiles"
cmake -S . -B build-ninja -G "Ninja"
cmake -S . -B build-vs -G "Visual Studio 17 2022"
cmake -S . -B build-xcode -G "Xcode"
cmake -S . -B build-ninja-mc -G "Ninja Multi-Config"
build-ninja.
Comparing Generators
flowchart TD
A[Choose Generator] --> B{Platform?}
B -->|Windows| C{Need IDE?}
B -->|Linux/macOS| D{Need Speed?}
C -->|Yes| E["Visual Studio 17 2022"]
C -->|No| F{Multi-config?}
F -->|Yes| G["Ninja Multi-Config"]
F -->|No| H["Ninja"]
D -->|Maximum| H
D -->|Default OK| I["Unix Makefiles"]
D -->|Multi-config| G
| Generator | Speed | Multi-Config | Platform | Notes |
|---|---|---|---|---|
Ninja | ⚡ Fastest | ❌ Single | All | Recommended for CLI builds |
Ninja Multi-Config | ⚡ Fastest | ✅ Yes | All | Best of both worlds (CMake 3.17+) |
Unix Makefiles | 🔄 Moderate | ❌ Single | Linux/macOS | Universal default on Unix |
Visual Studio 17 2022 | 🔄 Moderate | ✅ Yes | Windows | Full IDE integration |
Xcode | 🔄 Moderate | ✅ Yes | macOS | Full Xcode IDE integration |
# Compare build times with Ninja vs Make (same project)
# First, configure with both generators
cmake -S . -B build-ninja -G Ninja
cmake -S . -B build-make -G "Unix Makefiles"
# Time the builds
time cmake --build build-ninja
time cmake --build build-make
# Ninja is typically 10-40% faster on incremental builds
# due to better dependency tracking and parallelism
Ninja Multi-Config
Ninja Multi-Config (CMake 3.17+) combines Ninja's speed with multi-configuration support. You configure once and build any configuration without re-running CMake:
# Configure once
cmake -S . -B build -G "Ninja Multi-Config"
# Build Debug
cmake --build build --config Debug
# Build Release (no reconfiguration needed!)
cmake --build build --config Release
# Build RelWithDebInfo
cmake --build build --config RelWithDebInfo
This is especially valuable for CI pipelines that test both Debug and Release from a single configure step.
Build Types
Build types control optimization level, debug information, and assertions. They fundamentally affect the compiler flags passed during compilation.
CMAKE_BUILD_TYPE
For single-configuration generators (Ninja, Unix Makefiles), set the build type at configure time via CMAKE_BUILD_TYPE:
# Configure with specific build type
cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake -S . -B build-relwithdebinfo -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake -S . -B build-minsizerel -DCMAKE_BUILD_TYPE=MinSizeRel
# Build (type is already baked into the build directory)
cmake --build build-release
CMAKE_BUILD_TYPE with single-config generators, you get an empty build type with no optimization and no debug info — the worst of both worlds. Always set it explicitly!
What Each Build Type Enables
| Build Type | GCC/Clang Flags | MSVC Flags | Purpose |
|---|---|---|---|
Debug | -g -O0 | /MDd /Zi /Ob0 /Od | Full debug info, no optimization |
Release | -O3 -DNDEBUG | /MD /O2 /Ob2 /DNDEBUG | Full optimization, no debug info |
RelWithDebInfo | -O2 -g -DNDEBUG | /MD /Zi /O2 /Ob1 /DNDEBUG | Optimized with debug info (for profiling) |
MinSizeRel | -Os -DNDEBUG | /MD /O1 /Ob1 /DNDEBUG | Optimize for size (embedded, WASM) |
Key differences:
-DNDEBUGdisablesassert()macro — present in Release, RelWithDebInfo, MinSizeRel but NOT in Debug-gincludes DWARF debug symbols for GDB/LLDB — present in Debug and RelWithDebInfo-O0means zero optimization (code maps directly to source) vs-O3(aggressive inlining, vectorization, etc.)
You can set a default build type in CMakeLists.txt if none is specified:
# CMakeLists.txt - Default build type
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
# Set a default build type if not specified
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
message(STATUS "Setting build type to 'RelWithDebInfo' (default)")
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Build type" FORCE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app src/main.cpp)
Compiler and Linker Flags
Beyond build types, you often need custom compiler warnings, sanitizers, or platform-specific flags.
Global Flags (Avoid for New Code)
The legacy approach sets flags globally via cache variables:
# CMakeLists.txt - Global flags (legacy approach)
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
# These affect ALL targets in the project
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -march=native")
add_executable(app src/main.cpp)
CMAKE_CXX_FLAGS) apply to everything — including third-party libraries fetched via FetchContent or add_subdirectory(). This causes build failures when your strict warnings break someone else's code. Prefer per-target options instead.
Per-Target Options (Modern Approach)
Modern CMake uses target_compile_options() and target_link_options() for precise control:
# CMakeLists.txt - Per-target compile options
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(app
src/main.cpp
src/engine.cpp
)
# Warnings only for OUR code
target_compile_options(app PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Wshadow>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /permissive->
)
# Debug-only sanitizers
target_compile_options(app PRIVATE
$<$<AND:$<CONFIG:Debug>,$<CXX_COMPILER_ID:GNU,Clang>>:
-fsanitize=address,undefined -fno-omit-frame-pointer>
)
target_link_options(app PRIVATE
$<$<AND:$<CONFIG:Debug>,$<CXX_COMPILER_ID:GNU,Clang>>:
-fsanitize=address,undefined>
)
The $<...> syntax is a generator expression — evaluated at generation time, not configure time. We'll cover these in depth in Part 7. For now, know that:
$<CXX_COMPILER_ID:GNU,Clang>— True when using GCC or Clang$<CONFIG:Debug>— True when building Debug configuration$<AND:...,...>— Logical AND of conditions
Common Warning Flags Reference
| GCC/Clang | MSVC | Purpose |
|---|---|---|
-Wall | /W3 | Common warnings |
-Wextra | /W4 | Extra warnings |
-Wpedantic | /permissive- | Strict standard conformance |
-Werror | /WX | Treat warnings as errors |
-Wshadow | — | Warn on variable shadowing |
-Wconversion | — | Warn on implicit type conversions |
Build Configuration
Understanding the difference between single-config and multi-config generators is crucial for writing portable CMakeLists.txt files.
Single-Config vs Multi-Config Generators
| Aspect | Single-Config | Multi-Config |
|---|---|---|
| Examples | Unix Makefiles, Ninja | Visual Studio, Xcode, Ninja Multi-Config |
| Config set at | Configure time (-DCMAKE_BUILD_TYPE=Release) | Build time (--config Release) |
| Build dirs | One per configuration | One for all configurations |
| Output location | build/myapp | build/Release/myapp |
| Variables | CMAKE_BUILD_TYPE | CMAKE_CONFIGURATION_TYPES |
# Single-config workflow (separate build dirs)
cmake -S . -B build-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build-debug
cmake --build build-release
# Multi-config workflow (one build dir, switch at build time)
cmake -S . -B build -G "Ninja Multi-Config"
cmake --build build --config Debug
cmake --build build --config Release
CMAKE_CONFIGURATION_TYPES
For multi-config generators, CMAKE_CONFIGURATION_TYPES lists the available configurations:
# CMakeLists.txt - Custom configuration types
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
# Only for multi-config generators
if(CMAKE_CONFIGURATION_TYPES)
set(CMAKE_CONFIGURATION_TYPES "Debug;Release;RelWithDebInfo" CACHE STRING "" FORCE)
endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(app src/main.cpp)
You can also define entirely custom configurations (e.g., Profile, Sanitize) by adding them to CMAKE_CONFIGURATION_TYPES and defining their associated flags.
Object Files and Intermediate Output
Understanding what happens between source and binary helps debug compilation errors and optimize build times.
flowchart LR
A[".cpp + .h
Source Files"] -->|Preprocessor
cpp| B[".i
Translation Unit"]
B -->|Compiler
cc1plus| C[".s
Assembly"]
C -->|Assembler
as| D[".o / .obj
Object File"]
D -->|Linker
ld / link.exe| E["Executable
or Library"]
F[".o (other)"] --> E
G["Libraries
.a / .so / .lib"] --> E
Each .cpp file becomes one object file (.o on Unix, .obj on Windows). The linker then combines all object files plus libraries into the final binary.
# See intermediate object files (after building)
find build -name "*.o" -o -name "*.obj"
# On a Ninja build, objects are typically in:
# build/CMakeFiles/app.dir/src/main.cpp.o
# build/CMakeFiles/app.dir/src/engine.cpp.o
Object Libraries
CMake provides object libraries — collections of object files that aren't archived into a .a or linked into a .so. They're useful for compiling sources once and using them in multiple targets:
# CMakeLists.txt - Object library
cmake_minimum_required(VERSION 3.21)
project(App LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Compile these sources once into object files
add_library(common_objs OBJECT
src/logger.cpp
src/config.cpp
src/utils.cpp
)
target_include_directories(common_objs PUBLIC include)
# Use the object files in multiple targets
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE common_objs)
add_executable(tests tests/test_main.cpp)
target_link_libraries(tests PRIVATE common_objs)
We'll cover libraries in detail in Part 4. For now, understand that object libraries let you avoid compiling the same source twice.
Building in Practice
Once configured, cmake --build wraps the native build tool. These techniques help you work efficiently.
Verbose Builds
See the exact compiler commands CMake generates:
# Method 1: --verbose flag (CMake 3.14+)
cmake --build build --verbose
# Method 2: Environment variable
CMAKE_VERBOSE_MAKEFILE=ON cmake --build build
# Method 3: Set in CMakeLists.txt (permanent)
# set(CMAKE_VERBOSE_MAKEFILE ON)
# Method 4: For Makefiles specifically
cmake --build build -- VERBOSE=1
# Method 5: For Ninja specifically
cmake --build build -- -v
Verbose output shows the full compiler invocation, which is invaluable for debugging flag issues:
# Example verbose output (Ninja)
[1/3] /usr/bin/g++ -DNDEBUG -O3 -std=c++17 -Wall -Wextra \
-I/home/user/project/include \
-MD -MT CMakeFiles/app.dir/src/main.cpp.o \
-MF CMakeFiles/app.dir/src/main.cpp.o.d \
-o CMakeFiles/app.dir/src/main.cpp.o \
-c /home/user/project/src/main.cpp
Parallel Jobs
Speed up builds by compiling multiple files simultaneously:
# Use all available cores
cmake --build build --parallel
# Use specific number of cores
cmake --build build --parallel 8
cmake --build build -j 8
# Or pass through to native tool
cmake --build build -- -j$(nproc) # Make/Ninja on Linux
cmake --build build -- -j$(sysctl -n hw.ncpu) # macOS
cmake --build build -- /maxcpucount:8 # MSBuild
nproc + 2 by default. You rarely need to specify -j with Ninja — it's already optimal out of the box.
Selective Rebuilds and Clean Targets
# Build only a specific target (when multiple targets exist)
cmake --build build --target app
cmake --build build --target tests
# Clean all build artifacts
cmake --build build --target clean
# Clean and rebuild
cmake --build build --clean-first
# Remove entire build directory and start fresh
rm -rf build
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
CMake's incremental builds are dependency-aware: modifying one .cpp file recompiles only that object file and re-links. Modifying a header recompiles all source files that include it (directly or transitively).
Common Mistakes
These errors trip up nearly every CMake beginner. Learn them once and save hours of debugging.
1. In-Source Builds
# BAD: Building in the source tree
cd my_project
cmake . # Pollutes source directory with CMakeCache.txt, CMakeFiles/, etc.
make
# GOOD: Out-of-source build
cd my_project
cmake -S . -B build
cmake --build build
In-source builds scatter generated files throughout your source tree, making git status unusable and .gitignore painful. Always use a separate build directory.
# CMakeLists.txt - Prevent in-source builds
cmake_minimum_required(VERSION 3.21)
# Guard against in-source builds
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
message(FATAL_ERROR
"In-source builds are not allowed.\n"
"Create a build directory:\n"
" cmake -S . -B build\n"
"Remove CMakeCache.txt and CMakeFiles/ if they were created."
)
endif()
project(App LANGUAGES CXX)
2. Using GLOB Without Understanding the Consequences
# BAD: Silently breaks when files are added/removed
file(GLOB SOURCES "src/*.cpp")
add_executable(app ${SOURCES})
# GOOD: Explicit list — CMake detects changes to CMakeLists.txt
add_executable(app
src/main.cpp
src/parser.cpp
src/evaluator.cpp
)
3. Forgetting to Add New Source Files
After creating a new .cpp file, you must add it to add_executable() or add_library(). Otherwise you get linker errors like "undefined reference to...".
# Symptom: Linker error after adding a new class
# /usr/bin/ld: CMakeFiles/app.dir/src/main.cpp.o: undefined reference to `Database::connect()'
# collect2: error: ld returned 1 exit status
# Fix: Add the new source to CMakeLists.txt
# add_executable(app src/main.cpp src/database.cpp) # Added database.cpp
4. Not Setting CMAKE_BUILD_TYPE
# BAD: No build type = no optimization AND no debug info
cmake -S . -B build -G Ninja
cmake --build build # Compiles with... nothing useful
# GOOD: Always specify the build type
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
5. Mixing Up Source and Build Paths
# BAD: Confusing -S and -B
cmake -B . -S build # BACKWARDS! Creates build files in source dir
# GOOD: -S = Source, -B = Build
cmake -S . -B build # Source is current dir, build in ./build/
6. Stale Build Directory After Generator Change
# BAD: Switching generator in existing build directory
cmake -S . -B build -G Ninja # First time: Ninja
cmake -S . -B build -G "Unix Makefiles" # ERROR! Can't switch generators
# GOOD: Use a fresh directory
rm -rf build
cmake -S . -B build -G "Unix Makefiles"
rm -rf build && cmake -S . -B build takes 10 seconds.
Exercises
Multi-File Calculator
Create a project with the following structure and build it with CMake:
main.cpp— CLI interface that reads two numbers and an operationcalculator.cpp/calculator.h— add, subtract, multiply, divide functionsvalidator.cpp/validator.h— input validation (division by zero check)
Requirements: Use target_include_directories() with a separate include/ directory. Set C++17. Add -Wall -Wextra warnings.
Generator Comparison
Take any project with 10+ source files and build it with three different generators. Compare:
- Initial (cold) build time with
time cmake --build build - Incremental build time after modifying one source file
- Build directory size
- Output structure (where binaries end up)
Try: Ninja, Unix Makefiles, and Ninja Multi-Config.
Build Type Impact
Create a project that performs heavy computation (e.g., matrix multiplication with nested loops). Build with all four build types and compare:
- Binary size (
ls -laorsizecommand) - Execution time
- Whether
assert()fires in each build type
Verify by including #include <cassert> and adding assert(false); — it should crash in Debug but be silently removed in Release.
Conclusion & Next Steps
You now understand the full pipeline from source files to executables in CMake:
add_executable()creates a target from source files- Explicit source lists are preferred over
GLOB target_include_directories()controls header search paths with PUBLIC/PRIVATE/INTERFACE visibility- Generators determine the native build system — Ninja is fastest for CLI workflows
- Build types control optimization and debug info — always set
CMAKE_BUILD_TYPEexplicitly - Per-target flags via
target_compile_options()are preferred over globalCMAKE_CXX_FLAGS - Multi-config generators let you switch Debug/Release without reconfiguring
Next in the Series
In Part 4: Building and Linking Libraries, we'll create static, shared, and object libraries. You'll learn add_library(), target_link_libraries(), symbol visibility, version SONAME management, and how library dependencies propagate through the target graph.