C++ CMake build systems CMakeLists.txt
|

C++ CMake & Build Systems: CMakeLists.txt Complete Guide 2026

Why You Need a Build System

Compiling a single-file C++ program with g++ main.cpp -o main works fine for learning. But real projects have dozens or hundreds of source files, external library dependencies, platform-specific settings, debug and release configurations, and test suites. Managing all of this with manual compiler commands is impossible.

A build system automates compilation, linking, and dependency management. CMake is the industry standard for C++ — it generates platform-specific build files (Makefiles on Linux, Visual Studio projects on Windows, Xcode projects on macOS) from a single configuration file. Every major C++ project uses CMake: LLVM, Qt, OpenCV, Boost, KDE, and thousands more.

This lesson teaches you CMake from your first CMakeLists.txt to managing real project structures with external dependencies. If you’ve written code across multiple files using namespaces and file I/O, CMake is what ties them all together into a buildable project.

CMake Basics

CMake is not a build system itself — it’s a build system generator. You write a CMakeLists.txt file describing your project, then CMake generates the actual build files for your platform. The workflow is: configure (CMake reads your config), generate (creates build files), build (compiles and links).

CMake uses its own scripting language. Commands are case-insensitive by convention but typically written in lowercase. Variables are set with set() and accessed with ${VAR}. Comments start with #.

Your First CMakeLists.txt

# Minimum CMake version required
cmake_minimum_required(VERSION 3.16)

# Project name and language
project(MyApp VERSION 1.0 LANGUAGES CXX)

# Set C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Create executable from source file
add_executable(myapp main.cpp)

That’s it — five lines for a complete build configuration. cmake_minimum_required ensures compatibility. project() names your project. set(CMAKE_CXX_STANDARD 20) enables C++20. add_executable() defines what to build and from which source files.

Building a CMake Project

# Create a build directory (out-of-source build)
mkdir build
cd build

# Configure — CMake reads CMakeLists.txt and generates build files
cmake ..

# Build — compile and link
cmake --build .

# Run
./myapp

# Or in one line (CMake 3.13+):
cmake -S . -B build
cmake --build build
./build/myapp

# Specify generator explicitly
cmake -G "Unix Makefiles" -S . -B build
cmake -G "Ninja" -S . -B build  # Ninja is faster than Make

Always use out-of-source builds (build in a separate directory). This keeps your source tree clean and lets you have multiple build configurations (debug, release) simultaneously.

Multiple Source Files

cmake_minimum_required(VERSION 3.16)
project(Calculator LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

# List all source files
add_executable(calculator
    src/main.cpp
    src/math/add.cpp
    src/math/multiply.cpp
    src/utils/logger.cpp
)

# Tell CMake where to find header files
target_include_directories(calculator PRIVATE
    ${CMAKE_SOURCE_DIR}/include
)

# Alternative: glob (not recommended for source files)
# file(GLOB_RECURSE SOURCES "src/*.cpp")
# add_executable(calculator ${SOURCES})
# Problem: CMake won't detect new files until you re-run cmake

Libraries — Static and Shared

cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

# Create a static library (.a on Linux, .lib on Windows)
add_library(mathlib STATIC
    src/math/add.cpp
    src/math/multiply.cpp
)
target_include_directories(mathlib PUBLIC include)

# Create a shared library (.so on Linux, .dll on Windows)
add_library(utils SHARED
    src/utils/logger.cpp
    src/utils/timer.cpp
)
target_include_directories(utils PUBLIC include)

# Create executable and link libraries
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mathlib utils)

target_link_libraries and Modern CMake

# Modern CMake is target-based. Everything is attached to targets.

add_library(mylib src/mylib.cpp)

# PUBLIC: applies to mylib AND anything that links against it
target_include_directories(mylib PUBLIC include)
target_compile_definitions(mylib PUBLIC MYLIB_ENABLED=1)

# PRIVATE: applies only to mylib itself
target_compile_options(mylib PRIVATE -Wall -Wextra)

# INTERFACE: applies only to consumers, not mylib itself
target_include_directories(mylib INTERFACE external/include)

# When you link, properties propagate automatically:
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE mylib)
# app automatically gets mylib's PUBLIC include dirs and definitions!

# OLD STYLE (avoid):
# include_directories(...)          # Global, affects everything
# add_definitions(...)              # Global
# link_libraries(...)               # Global
# These are legacy CMake — don't use in new projects

Finding External Packages

# find_package locates installed libraries
find_package(Threads REQUIRED)        # pthreads
find_package(OpenSSL REQUIRED)        # OpenSSL
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
find_package(CURL REQUIRED)

add_executable(myapp src/main.cpp)

target_link_libraries(myapp PRIVATE
    Threads::Threads        # Modern imported target
    OpenSSL::SSL
    OpenSSL::Crypto
    Boost::filesystem
    Boost::system
    CURL::libcurl
)

# If a package isn't found, CMake errors out (REQUIRED)
# Without REQUIRED, check with:
# if(OpenSSL_FOUND) ... endif()

# pkg-config fallback for libraries without CMake support
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBPNG REQUIRED libpng)
target_link_libraries(myapp PRIVATE ${LIBPNG_LIBRARIES})
target_include_directories(myapp PRIVATE ${LIBPNG_INCLUDE_DIRS})

Compiler Flags and Options

# Per-target flags (preferred)
target_compile_options(myapp PRIVATE
    -Wall -Wextra -Wpedantic  # Warnings
    -Werror                    # Treat warnings as errors
)

# Conditional flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(myapp PRIVATE -fconcepts)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    target_compile_options(myapp PRIVATE -stdlib=libc++)
elseif(MSVC)
    target_compile_options(myapp PRIVATE /W4 /WX)
endif()

# Compile definitions (preprocessor macros)
target_compile_definitions(myapp PRIVATE
    APP_VERSION="1.0.0"
    DEBUG_MODE=$<CONFIG:Debug>   # 1 in Debug, 0 otherwise
)

# Sanitizers (for debugging)
option(ENABLE_SANITIZERS "Enable ASan and UBSan" OFF)
if(ENABLE_SANITIZERS)
    target_compile_options(myapp PRIVATE -fsanitize=address,undefined)
    target_link_options(myapp PRIVATE -fsanitize=address,undefined)
endif()

Build Types

# CMake supports four standard build types:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug       # -g, no optimization
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release      # -O3, no debug info
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo  # -O2 -g
cmake -S . -B build -DCMAKE_BUILD_TYPE=MinSizeRel   # -Os
# Set default build type if none specified
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build type" FORCE)
endif()

# Different flags per build type
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_BUILD>
    $<$<CONFIG:Release>:NDEBUG>
)

Subdirectories and Project Structure

# Typical project layout:
# project/
# ├── CMakeLists.txt          (root)
# ├── src/
# │   ├── CMakeLists.txt
# │   └── main.cpp
# ├── lib/
# │   ├── CMakeLists.txt
# │   ├── math.cpp
# │   └── math.hpp
# └── tests/
#     ├── CMakeLists.txt
#     └── test_math.cpp

# Root CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

add_subdirectory(lib)
add_subdirectory(src)

option(BUILD_TESTS "Build tests" ON)
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

# lib/CMakeLists.txt:
add_library(mathlib math.cpp)
target_include_directories(mathlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# src/CMakeLists.txt:
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mathlib)

# tests/CMakeLists.txt:
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE mathlib)
add_test(NAME MathTests COMMAND test_math)

FetchContent — Download Dependencies

include(FetchContent)

# Download Google Test at configure time
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Download fmt library
FetchContent_Declare(
    fmt
    GIT_REPOSITORY https://github.com/fmtlib/fmt.git
    GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)

# Now link as if they were local
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt gtest_main)

FetchContent downloads and builds dependencies automatically during the CMake configure step. It’s the simplest way to add third-party libraries without requiring users to install them first.

Installing Your Project

# Install rules — where files go when user runs 'cmake --install build'
install(TARGETS myapp RUNTIME DESTINATION bin)
install(TARGETS mathlib LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)
install(FILES include/math.hpp DESTINATION include/myproject)

# Install with cmake:
# cmake --install build --prefix /usr/local

CMake Presets

// CMakePresets.json — standardized build configurations
{
    "version": 6,
    "configurePresets": [
        {
            "name": "debug",
            "binaryDir": "build/debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug",
                "CMAKE_CXX_STANDARD": "20"
            }
        },
        {
            "name": "release",
            "binaryDir": "build/release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release",
                "CMAKE_CXX_STANDARD": "20"
            }
        }
    ]
}
# Use presets
cmake --preset debug
cmake --build build/debug

Common Mistakes

# MISTAKE 1: In-source builds
# Don't run cmake in the source directory!
# Always: mkdir build && cd build && cmake ..

# MISTAKE 2: Using global commands
# DON'T: include_directories(/some/path)  # Affects ALL targets
# DO:    target_include_directories(myapp PRIVATE /some/path)

# MISTAKE 3: GLOB for source files
# file(GLOB SRCS "*.cpp")  # New files won't be detected!
# Explicitly list source files instead

# MISTAKE 4: Wrong PUBLIC/PRIVATE/INTERFACE
# PUBLIC = I use it AND my consumers need it
# PRIVATE = only I use it
# INTERFACE = only my consumers need it (not me)

Practice Exercises

Exercise 1: Create a CMake project with a main.cpp and a separate mathlib library (in a subdirectory). Link the library to the executable. Build and run it.

Exercise 2: Add Google Test to your project using FetchContent. Write a test file that tests your math library. Run tests with ctest.

Exercise 3: Create a project that uses find_package(Threads REQUIRED) and links against pthreads. Write a multithreaded program using the threads you learned.

Exercise 4: Set up a CMakePresets.json with debug, release, and sanitizer presets. Build your project in each configuration and note the differences.

CMake is essential tooling knowledge for any C++ developer. Every job listing and open-source project expects it. With CMake handling your builds, you can focus on writing code rather than managing compilation. Next, we’ll learn how to find and fix bugs with debugging tools.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *