Build Systems (CMake) — CMakeLists.txt, Targets, Dependencies, FetchContent, CPack, Cross-Platform Builds
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
What is the minimum CMakeLists.txt for an executable? Answer: cmake_minimum_required + project + add_executable with source files.
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.
What is FetchContent used for? Answer: Download and build external dependencies from source during CMake configure.
How do you build a Release version with CMake? Answer:
cmake -B build -DCMAKE_BUILD_TYPE=ReleaseWhat 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
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