Skip to content

Build Systems (CMake) — CMakeLists.txt, Targets, Dependencies, FetchContent, CPack, Cross-Platform Builds

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Build Systems (CMake). We cover key concepts, practical examples, and best practices to help you master this topic.

CMake generates native build files (Make, Ninja, Visual Studio, Xcode) from cross-platform CMakeLists.txt scripts, defining targets, libraries, dependencies, and install rules for C++ projects.

What You'll Learn

You will write CMakeLists.txt files for executables and libraries, manage dependencies with find_package and FetchContent, configure build types (Debug, Release, RelWithDebInfo), organize multi-directory projects with add_subdirectory, use modern CMake targets with target_include_directories and target_link_libraries, package projects with CPack, and integrate testing with CTest.

Why It Matters

C++ has no standard build system built into the language. CMake is the de facto standard — used by LLVM, Qt, Boost, and thousands of projects. Learning CMake is essential for collaborating on any non-trivial C++ project. Modern CMake (3.x) with target-based design is cleaner and more maintainable than the old variable-based approach.

Learning Path

graph LR
    A["64: File I/O & Serialization"] --> B["65: Build Systems (CMake)"]
    B --> C["66: Unit Testing"]
    C --> D["67: Debugging (GDB)"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Basic CMakeLists.txt

The minimal CMake project for an executable.

# CMakeLists.txt — minimum required version
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

# C++ standard
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)  # Use -std=c++20, not -std=gnu++20

# Define executable target
add_executable(myapp main.cpp utils.cpp)

# Link libraries (if any)
# target_link_libraries(myapp PRIVATE some_lib)

# Include directories (modern style — per-target)
target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
// main.cpp
#include <iostream>
#include "utils.h"

int main() {
    std::cout << "Hello from CMake project!\n";
    return 0;
}

Building with CMake

# Configure (generates build files)
cmake -B build -DCMAKE_BUILD_TYPE=Release

# Build
cmake --build build

# Build with specific target
cmake --build build --target myapp

# Build in parallel
cmake --build build -j$(nproc)

# Debug build
cmake -B build_debug -DCMAKE_BUILD_TYPE=Debug
cmake --build build_debug

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

# Clean
cmake --build build --target clean

Libraries: Static and Shared

Creating and linking libraries.

cmake_minimum_required(VERSION 3.20)
project(Geometry VERSION 1.0.0 LANGUAGES CXX)

# Static library
add_library(geometry STATIC
    src/shapes.cpp
    src/circle.cpp
    src/rectangle.cpp
)

# Shared library
add_library(geometry_shared SHARED
    src/shapes.cpp
    src/circle.cpp
    src/rectangle.cpp
)

# Include directories for the library (PUBLIC: consumers also get them)
target_include_directories(geometry
    PUBLIC
        ${CMAKE_CURRENT_SOURCE_DIR}/include
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/src
)

# Executable using the library
add_executable(geometry_app main.cpp)
target_link_libraries(geometry_app PRIVATE geometry)

# Alias for easy reference
add_library(geometry::geometry ALIAS geometry)
// main.cpp
#include "shapes.h"
#include <iostream>

int main() {
    Circle c(5.0);
    std::cout << "Area: " << c.area() << "\n";
    return 0;
}

FindPackage — Using External Libraries

cmake_minimum_required(VERSION 3.20)
project(DataProcessor)

# C++ standard
set(CMAKE_CXX_STANDARD 17)

# Find installed packages
find_package(OpenSSL REQUIRED)
find_package(ZLIB REQUIRED)
find_package(Boost REQUIRED COMPONENTS filesystem json)

# Optional package (may not exist)
find_package(nlohmann_json QUIET)
if(nlohmann_json_FOUND)
    message(STATUS "Using nlohmann_json")
    add_compile_definitions(HAS_JSON)
else()
    message(STATUS "nlohmann_json not found, using fallback")
endif()

add_executable(processor main.cpp)

target_link_libraries(processor PRIVATE
    OpenSSL::SSL
    OpenSSL::Crypto
    ZLIB::ZLIB
    Boost::filesystem
    Boost::json
)

if(nlohmann_json_FOUND)
    target_link_libraries(processor PRIVATE nlohmann_json::nlohmann_json)
endif()

FetchContent — Dependencies from Source

Modern CMake can download and build dependencies during configure.

cmake_minimum_required(VERSION 3.20)
project(MyProject)

include(FetchContent)

# Download and build Google Test
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)

# Download nlohmann/json (single-header)
FetchContent_Declare(
    json
    GIT_REPOSITORY https://github.com/nlohmann/json.git
    GIT_TAG v3.11.3
)

# Make dependencies available
FetchContent_MakeAvailable(googletest json)

# Now targets from these packages are available
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE nlohmann_json::nlohmann_json)

# Tests use GTest
add_executable(mytests test_main.cpp)
target_link_libraries(mytests PRIVATE gtest gtest_main)
include(GoogleTest)
gtest_discover_tests(mytests)

Multi-Directory Projects

Organizing larger projects with subdirectories.

# Top-level CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(WalletApp)

add_subdirectory(src)
add_subdirectory(tests)
add_subdirectory(tools)

# Global options
option(ENABLE_TESTS "Build tests" ON)
option(ENABLE_TOOLS "Build tools" OFF)

if(ENABLE_TESTS)
    enable_testing()
    add_subdirectory(tests)
endif()
# src/CMakeLists.txt
add_library(wallet_lib
    account.cpp
    transaction.cpp
    database.cpp
)

target_include_directories(wallet_lib
    PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
)

add_executable(wallet main.cpp)
target_link_libraries(wallet PRIVATE wallet_lib)
# tests/CMakeLists.txt
add_executable(unit_tests
    test_account.cpp
    test_transaction.cpp
)

target_link_libraries(unit_tests PRIVATE
    wallet_lib
    GTest::gtest_main
)

# Register tests with CTest
include(GoogleTest)
gtest_discover_tests(unit_tests)

Build Types and Compiler Flags

cmake_minimum_required(VERSION 3.20)
project(OptimizedApp)

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

# Compiler-specific flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
    # Common flags
    add_compile_options(-Wall -Wextra -Wpedantic -Werror)

    # Debug flags
    set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG -fsanitize=address,undefined")

    # Release flags
    set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG -march=native")

    # Profile flags
    set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
elseif(MSVC)
    add_compile_options(/W4 /WX)
    set(CMAKE_CXX_FLAGS_DEBUG "/Zi /Od /DDEBUG")
    set(CMAKE_CXX_FLAGS_RELEASE "/O2 /DNDEBUG")
endif()

# Target-specific flags
add_executable(myapp main.cpp)
target_compile_options(myapp PRIVATE -Wall -Wextra)
target_compile_definitions(myapp PRIVATE APP_VERSION="1.0.0")

Installing and Packaging with CPack

cmake_minimum_required(VERSION 3.20)
project(Toolkit VERSION 2.0.0)

add_library(toolkit SHARED core.cpp utils.cpp)
target_include_directories(toolkit PUBLIC include)

add_executable(tool main.cpp)
target_link_libraries(tool PRIVATE toolkit)

# Install
install(TARGETS toolkit tool
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
)

install(DIRECTORY include/ DESTINATION include)

# CPack packaging
set(CPACK_PACKAGE_NAME "Toolkit")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "C++ Toolkit Library")
set(CPACK_GENERATOR "TGZ;DEB;RPM")

include(CPack)

# Build package with: cpack

Common Mistakes

Mistake 1: Using old-style variable-based CMake

# BAD (old style):
set(SOURCES main.cpp utils.cpp)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
add_executable(myapp ${SOURCES})

# GOOD (modern target-based):
add_executable(myapp main.cpp utils.cpp)
target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)

Mistake 2: Not setting C++ standard properly

set(CMAKE_CXX_STANDARD 20)  # Must be before add_executable/add_library
set(CMAKE_CXX_STANDARD_REQUIRED ON)

Mistake 3: Using global include_directories instead of target_include_directories

Global commands affect all targets and can cause header conflicts.

Mistake 4: Hardcoding paths instead of using generator expressions

# Use generator expressions for configuration-dependent settings:
target_compile_definitions(myapp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)

Mistake 5: Not adding tests to CTest

After defining tests, call enable_testing() and add tests with add_test() or gtest_discover_tests().

Practice Questions

  1. What is the minimum CMakeLists.txt for an executable? Answer: cmake_minimum_required + project + add_executable with source files.

  2. What does target_include_directories with PUBLIC do? Answer: Adds the directory to the include path for this target AND any target that links to it.

  3. What is FetchContent used for? Answer: Download and build external dependencies from source during CMake configure.

  4. How do you build a Release version with CMake? Answer: cmake -B build -DCMAKE_BUILD_TYPE=Release

  5. What is the difference between PRIVATE, PUBLIC, and INTERFACE in target properties? Answer: PRIVATE: only for the target. PUBLIC: for the target and its dependents. INTERFACE: only for dependents (header-only libraries).

FAQ

What is CMake used for in C++

CMake generates platform-specific build files (Make, Ninja, VS projects) from cross-platform CMakeLists.txt scripts. It manages compilation, linking, dependencies, and installation.

What is modern CMake

Modern CMake (3.x+) uses target-based design: targets own their properties (include dirs, compile flags, link libraries). This replaces the old variable-based approach.

How do I add external dependencies in CMake

Use find_package for system-installed libraries, FetchContent for downloaded source, or add_subdirectory for submodules.

What is CTest

CTest is CMake's testing tool. It discovers tests registered with add_test() and provides test execution, parallelization, and reporting.

How do I create a multi-directory CMake project

Use add_subdirectory() in the top-level CMakeLists.txt to include subdirectories, each with their own CMakeLists.txt defining targets.

Mini Project

Set up a CMake project for a small library with unit tests and a CLI tool:

# CMakeLists.txt — build a markdown-to-HTML converter

# Your CMakeLists.txt should:
# 1. Define the project
# 2. Create a library for the markdown parser
# 3. Create CLI tool using the library
# 4. Add unit tests with Google Test (via FetchContent)
# 5. Support Release and Debug configurations
# 6. Install targets properly
// main.cpp — CLI tool
#include "markdown.h"
#include <iostream>
#include <fstream>

int main(int argc, char* argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: md2html <input.md> <output.html>\n";
        return 1;
    }

    std::ifstream input(argv[1]);
    std::string content((std::istreambuf_iterator<char>(input)),
                         std::istreambuf_iterator<char>());

    std::string html = markdown::toHtml(content);

    std::ofstream output(argv[2]);
    output << html;

    std::cout << "Converted " << argv[1] << " to " << argv[2] << "\n";
    return 0;
}

This project mirrors how real C++ projects are structured with CMake — a pattern used by thousands of open-source C++ libraries.

What's Next

You now know how to build C++ projects with CMake. Next, you will learn Unit Testing with Catch2 and Google Test — essential tools for ensuring code correctness and preventing regressions.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro