Skip to content
Home
CMake: A Complete Guide to the C++ Build System

CMake: A Complete Guide to the C++ Build System

C/C++ C/C++ 8 min read 1518 words Beginner ExcellentWiki Editorial Team

CMake is the de facto standard build system for C++ projects. It generates native build files (Makefiles, Ninja, Visual Studio solutions) from a single CMakeLists.txt configuration. This means you write your build configuration once and build on any platform.

This guide covers modern CMake (3.15+) — targets, dependencies, testing, installation, and cross-platform builds using best practices from the professional C++ ecosystem.

Getting Started

Hello World with CMake

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

add_executable(myapp main.cpp)
# Configure and build
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/myapp

The cmake_minimum_required sets the minimum CMake version. project() declares the project name, version, and languages. add_executable creates a build target from source files.

Modern CMake: Targets and Properties

Modern CMake revolves around targets — executables, libraries, and custom utilities. Properties are attached to targets, not stored in global variables:

add_library(mylib STATIC
    src/parser.cpp
    src/tokenizer.cpp
    src/ast.cpp
)

# Set include directories for this target
target_include_directories(mylib
    PUBLIC include
    PRIVATE src
)

# Link dependencies
target_link_libraries(mylib
    PUBLIC fmt::fmt
    PRIVATE nlohmann_json::nlohmann_json
)

Target Types

CommandType
add_executableExecutable
add_library(... STATIC)Static library (.a / .lib)
add_library(... SHARED)Shared library (.so / .dll)
add_library(... INTERFACE)Header-only library
add_library(... OBJECT)Object library (compiled but not archived)

Visibility Keywords

  • PUBLIC — Used by the target and by consumers that link to it
  • PRIVATE — Used only by the target itself
  • INTERFACE — Used only by consumers, not the target itself
target_include_directories(mylib
    PUBLIC include/mylib    # Exposed to downstream targets
    PRIVATE src             # Internal only
)

target_compile_features(mylib
    PUBLIC cxx_std_17       # Downstream targets must use C++17 too
)

Libraries and Dependencies

Finding External Libraries

# Find a system-installed package
find_package(fmt REQUIRED)
find_package(Boost REQUIRED COMPONENTS filesystem system)

# Find a specific version
find_package(OpenCV 4.5 REQUIRED)

# Link to your target
target_link_libraries(myapp
    PRIVATE fmt::fmt
    PRIVATE Boost::filesystem
    PRIVATE ${OpenCV_LIBS}
)

FetchContent — Download Dependencies at Build Time

include(FetchContent)

FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)

FetchContent_MakeAvailable(nlohmann_json)

target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)

FetchContent downloads and builds dependencies as part of your project. This is the modern replacement for ExternalProject_Add when you want the dependency to be part of your build.

CMake Package Registry

Add your own package to the registry for other projects to find:

install(TARGETS mylib EXPORT MyLibTargets)
install(EXPORT MyLibTargets
    FILE MyLibTargets.cmake
    NAMESPACE mylib::
    DESTINATION lib/cmake/MyLib
)

Compiler Options and Configurations

Setting Standards

# Require C++17
target_compile_features(mylib PUBLIC cxx_std_17)

# Or set standard explicitly
set_target_properties(mylib PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED ON
    CXX_EXTENSIONS OFF
)

Platform-Specific Flags

if(MSVC)
    target_compile_options(mylib PRIVATE /W4 /utf-8)
    target_link_options(mylib PRIVATE /LTCG)
else()
    target_compile_options(mylib PRIVATE -Wall -Wextra -Wpedantic -Werror)
    target_link_options(mylib PRIVATE -flto)
endif()

if(APPLE)
    target_link_options(mylib PRIVATE -framework CoreFoundation)
elseif(WIN32)
    target_link_options(mylib PRIVATE /SUBSYSTEM:CONSOLE)
endif()

Build Types

# Set default build type if not specified
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# Custom compile options per configuration
target_compile_definitions(mylib
    PRIVATE
        $<$<CONFIG:Debug>:DEBUG_BUILD>
        $<$<CONFIG:Release>:NDEBUG>
)

# Optimizations per config
set_target_properties(mylib PROPERTIES
    INTERPROCEDURAL_OPTIMIZATION_RELEASE ON
    INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON
)

Testing with CTest and GoogleTest

Registering Tests

# CMakeLists.txt
enable_testing()
add_subdirectory(tests)
# tests/CMakeLists.txt
find_package(GTest REQUIRED)

add_executable(test_parser
    test_parser.cpp
    test_tokenizer.cpp
)

target_link_libraries(test_parser
    PRIVATE mylib
    PRIVATE GTest::gtest GTest::gtest_main
)

target_compile_definitions(test_parser PRIVATE $<$<CONFIG:Debug>:DEBUG_BUILD>)

include(GoogleTest)
gtest_discover_tests(test_parser)
# Build and run tests
cmake --build build
cd build && ctest --output-on-failure

# Run specific test
ctest -R parser --verbose

# Run with parallel workers
ctest -j8

Custom Test Targets

add_test(NAME integration_test
    COMMAND $<TARGET_FILE:test_runner> --config ${CMAKE_SOURCE_DIR}/tests/config.json
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/tests
)

set_tests_properties(integration_test PROPERTIES
    TIMEOUT 30
    ENVIRONMENT "DATABASE_URL=sqlite::memory:"
    LABELS "integration;slow"
)

Installation

Installing Targets

# Library installation
install(TARGETS mylib
    EXPORT MyLibTargets
    LIBRARY DESTINATION lib          # Shared libs
    ARCHIVE DESTINATION lib          # Static libs
    RUNTIME DESTINATION bin          # DLLs on Windows
    INCLUDES DESTINATION include
)

# Header installation
install(DIRECTORY include/mylib
    DESTINATION include
    FILES_MATCHING PATTERN "*.hpp"
)

# Executable installation
install(TARGETS myapp RUNTIME DESTINATION bin)

# Config file installation
install(FILES config.ini DESTINATION etc/myapp)

CPack Packaging

# Enable packaging
set(CPACK_GENERATOR "TGZ;DEB;RPM")
set(CPACK_PACKAGE_NAME "mylib")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@example.com")

include(CPack)

# Build packages
# cpack -G DEB -C Release

Cross-Platform Considerations

Platform Detection

if(WIN32)
    add_definitions(-DWIN32_LEAN_AND_MEAN)
    target_link_libraries(myapp PRIVATE ws2_32)
elseif(APPLE)
    set_target_properties(myapp PROPERTIES
        MACOSX_BUNDLE TRUE
        MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    )
elseif(UNIX)
    target_link_libraries(myapp PRIVATE pthread)
endif()

Generator Expressions

Generator expressions evaluate at build time and work across generators:

# Link different libraries on different platforms
target_link_libraries(myapp
    PRIVATE
        $<$<PLATFORM_ID:Linux>:rt>
        $<$<PLATFORM_ID:Windows>:winmm>
)

# Different definitions per config
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:ENABLE_LOGGING>
    $<$<CONFIG:Release>:NDEBUG>
)

# Include path only if exists
target_include_directories(myapp PRIVATE
    $<$<BOOL:${USE_GPU}>:${CUDA_INCLUDE_DIRS}>
)

Organizing Larger Projects

project/
├── CMakeLists.txt          # Root
├── cmake/
│   ├── FindSomething.cmake # Custom find modules
│   └── Options.cmake       # Option definitions
├── src/
│   ├── CMakeLists.txt
│   ├── main.cpp
│   └── app/
│       ├── CMakeLists.txt
│       └── app.cpp
├── lib/
│   ├── CMakeLists.txt
│   └── core/
│       ├── CMakeLists.txt
│       └── core.cpp
├── tests/
│   ├── CMakeLists.txt
│   ├── test_core.cpp
│   └── test_app.cpp
└── extern/
    └── CMakeLists.txt      # FetchContent calls

Root CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)

# Options
option(BUILD_TESTS "Build tests" ON)
option(BUILD_EXAMPLES "Build examples" OFF)

# Dependencies
add_subdirectory(extern)

# Libraries
add_subdirectory(lib)

# Executable
add_subdirectory(src)

# Tests
if(BUILD_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()

Advanced Features

Custom Commands

# Generate a file at build time
add_custom_command(
    OUTPUT ${CMAKE_SOURCE_DIR}/generated/version.h
    COMMAND ${CMAKE_COMMAND}
        -DOUTPUT_FILE=${CMAKE_SOURCE_DIR}/generated/version.h
        -DVERSION=${PROJECT_VERSION}
        -P ${CMAKE_SOURCE_DIR}/cmake/gen_version.cmake
    DEPENDS ${CMAKE_SOURCE_DIR}/cmake/gen_version.cmake
    COMMENT "Generating version header"
)

add_executable(myapp
    main.cpp
    ${CMAKE_SOURCE_DIR}/generated/version.h
)

Presets (CMake 3.19+)

# CMakePresets.json
{
    "version": 3,
    "configurePresets": [
        {
            "name": "default",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release",
                "BUILD_TESTS": "ON"
            }
        },
        {
            "name": "debug",
            "inherits": "default",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        }
    ],
    "buildPresets": [
        { "name": "default", "configurePreset": "default" },
        { "name": "debug", "configurePreset": "debug" }
    ]
---
cmake --preset debug
cmake --build --preset debug

Best Practices

  • Use target_* commands, not global onestarget_include_directories over include_directories. Target-based commands compose correctly and respect encapsulation.
  • Prefer FetchContent for dependencies — It integrates smoothly into your build. Avoid ExternalProject_Add unless you need a separate configure step.
  • Create INTERFACE targets for header-only libraries — They propagate usage requirements without building anything.
  • Use cmake --build instead of invoking the generator directly — It works regardless of whether the generator is Make, Ninja, or Visual Studio.
  • Export your package — Use install(EXPORT ...) so other projects can find_package() your library.
  • Keep CMake minimum version low — Use modern CMake syntax but do not require bleeding-edge CMake versions unnecessarily.

Summary

CMake is the standard build system for C++ projects across platforms. Modern CMake uses target-based commands with PUBLIC/PRIVATE/INTERFACE visibility for correct dependency propagation. FetchContent integrates external dependencies, CTest provides testing infrastructure, and CPack handles packaging. Generator expressions make build logic portable across compilers and platforms. With presets, CMake configuration is reproducible and shareable across the team.

CMake Targets and Properties

A CMake target — created by add_executable() or add_library() — is the fundamental build unit. Targets have properties that control compilation, linking, and behavior:

add_library(my_lib STATIC src/lib.cpp)
target_include_directories(my_lib PUBLIC include)
target_compile_features(my_lib PUBLIC cxx_std_17)
target_link_libraries(my_app PRIVATE my_lib)

The PUBLIC, PRIVATE, and INTERFACE keywords control how properties propagate through the dependency graph. PRIVATE applies only to the target itself. PUBLIC applies to the target and its dependents. INTERFACE applies only to dependents — perfect for header-only libraries.

Generator Expressions

Generator expressions evaluate at build time rather than configure time, enabling platform- and configuration-specific logic:

target_compile_definitions(my_app PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE=1>
    $<$<PLATFORM_ID:Linux>:LINUX_BUILD>
)

Common generator expressions check configuration type, platform, compiler ID, and feature availability. They are essential for cross-platform projects where different flags or source files are needed per platform.

FetchContent and External Dependencies

Modern CMake uses FetchContent to download and integrate external dependencies at configure time:

include(FetchContent)
FetchContent_Declare(
    nlohmann_json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)

target_link_libraries(my_app PRIVATE nlohmann_json::nlohmann_json)

This replaces older approaches like ExternalProject (which builds at build time) and git submodules, providing cleaner dependency management with version pinning and transitive dependency handling.

Cross-Compilation with Toolchain Files

CMake supports cross-compilation through toolchain files that specify the target compiler, sysroot, and architecture:

# arm-cross.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)

Pass the toolchain file with -DCMAKE_TOOLCHAIN_FILE=arm-cross.cmake to configure for the target platform.

FAQ

What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.

How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.

What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.

Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.

What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.

For a comprehensive overview, read our article on C File Io Guide.

For a comprehensive overview, read our article on C Memory Management.

Section: C/C++ 1518 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top