CMake Practical Exercise

This article demonstrates how to use CMake to manage a moderately complex C++ project, covering the complete process from project creation to compilation and execution.

The content covers basic configuration, library linking, unit testing, custom commands, and advanced features such as cross-platform cross-compilation.


CMake Build Process

The following diagram shows the complete CMake build process from source code to the final executable:

CMake 构建流程


Project Overview

We will build a C++ project that contains a main program, a static library, and unit tests.

The library provides mathematical operations (addition, subtraction, multiplication). The main program calls these functions, and the test code verifies their correctness.

Project Directory Structure

MyProject/
├── CMakeLists.txt          # 根构建配置(项目定义、全局设置)
├── src/
│   ├── main.cpp            # 主程序入口
│   ├── CMakeLists.txt      # src 子目录构建配置(库 + 可执行文件)
│   ├── lib/
│   │   ├── module1.cpp     # 数学模块一:加法、减法
│   │   └── module2.cpp     # 数学模块二:乘法
│   └── include/
│       └── mylib.h         # 公共头文件(函数声明)
└── tests/
    ├── test_main.cpp       # 单元测试用例
    └── CMakeLists.txt      # 测试子目录构建配置

Out-of-Source BuildIt is CMake's best practice: all build artifacts (Makefiles, object files, executables) are placed in a separate build directory, keeping the source directory clean.


Creating the CMakeLists.txt File

CMakeLists.txt is the core configuration file of CMake. It uses a hierarchical structure, with one in the root directory and one in each subdirectory.

Root Directory CMakeLists.txt

The root configuration defines project-level information and manages subdirectories via add_subdirectory.

Example

cmake_minimum_required(VERSION 3.10)    # Specify the minimum CMake version requirement
project(MyProject VERSION 1.0)          # Define the project name and version number

# Set the C++ standard to C++11 and make it mandatory
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Add a global header file search path (PROJECT_SOURCE_DIR is the project root directory)
include_directories(${PROJECT_SOURCE_DIR}/src/include)

# Add subdirectories; CMake will process their CMakeLists.txt files in order
add_subdirectory(src)      # Process the library and executable
add_subdirectory(tests)    # Process the tests

src Directory CMakeLists.txt

This file defines the library target (MyLib) and the executable target (MyExecutable), and establishes the dependency relationship.

Example

# Create the static library target MyLib, consisting of two source files
add_library(MyLib STATIC
    lib/module1.cpp
    lib/module2.cpp
)

# Specify the public header file path for MyLib (PUBLIC means that consumers will also inherit this path)
target_include_directories(MyLib PUBLIC ${CMAKE_SOURCE_DIR}/src/include)

# Create the executable target MyExecutable
add_executable(MyExecutable main.cpp)

# Link MyLib to the executable (PRIVATE means that only MyExecutable needs it)
target_link_libraries(MyExecutable PRIVATE MyLib)

The difference between PRIVATE and PUBLIC:PRIVATEIndicates that the dependency is required only by the current target,PUBLICIndicates that the dependency is propagated to other targets that link to the current target.

tests Directory CMakeLists.txt

The test configuration uses the Google Test framework; first, find and include the GTest package.

Example

# Add the following content to src/CMakeLists.txt

# Print a message after MyExecutable is built
add_custom_command(
    TARGET MyExecutable
POST_BUILD # Execute after the build
COMMAND ${CMAKE_COMMAND} -E echo "MyExecutable build complete!"
COMMENT "Print build completion message" # Comment shown in the build log
)

Custom Targets (add_custom_target)

A custom target is an independent target that does not produce output files and is used to perform specific tasks.

Example

# Add the following to src/CMakeLists.txt

# Create a run target to execute the compiled program
add_custom_target(run
    COMMAND ${CMAKE_BINARY_DIR}/src/MyExecutable
    DEPENDS MyExecutable                # Depends on MyExecutable to ensure it is built first
    COMMENT "Run MyExecutable"
)

Running a custom target:

# 在 build 目录下执行
make run
# 或
cmake --build . --target run

Differences between custom commands and custom targets:add_custom_commandAttached to a target, triggered at a specific stage of the target's build;add_custom_targetIs an independent target, executed only when explicitly specified.


Cross-platform and Cross-compilation

One of CMake's core advantages is cross-platform support, allowing easy switching of target platforms and architectures.

Specifying the Target Platform

The target platform can be specified using the CMAKE_SYSTEM_NAME variable.

# 为 Linux 平台交叉编译
cmake -DCMAKE_SYSTEM_NAME=Linux ..

# 为 Windows 平台交叉编译
cmake -DCMAKE_SYSTEM_NAME=Windows ..

Using Toolchain Files

For complex cross-compilation scenarios, it is recommended to use a toolchain file to centrally manage all platform-related configurations.

First, create the toolchain file toolchain.cmake:

Example

# File path: MyProject/toolchain.cmake
# ARM Linux cross-compilation toolchain configuration

# Specify the target system
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# Specify the cross-compiler path (adjust according to the actual environment)
set(CMAKE_C_COMPILER   arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# Specify the target system's root filesystem path (optional, used for finding libraries and header files)
set(CMAKE_FIND_ROOT_PATH /path/to/arm/sysroot)

# Only search for programs in the target sysroot (not in the host system)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# Only search for library files in the target sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
# Only search for header files in the target sysroot
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

Building with the toolchain file:

# 在 build 目录下执行
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake ..
cmake --build .

The toolchain file centrally manages platform-related configurations such as compilers, linkers, and library paths, so switching target platforms only requires specifying a different toolchain file without modifying CMakeLists.txt.


Build Types and Common Options

CMake supports multiple build types, controlled by the CMAKE_BUILD_TYPE variable.

Build type Compiler options Applicable scenarios
Debug -g (debug symbols) Development and debugging phase, when breakpoints and variable inspection are needed
Release -O2 -DNDEBUG (optimization + removing assertions) Production release, pursuing runtime speed
RelWithDebInfo -O2 -g (optimization + debug symbols) Release version that requires performance analysis or optimized debugging
MinSizeRel -Os (size optimization) Embedded systems or scenarios sensitive to size

Specifying the build type:

# Debug 构建
cmake -DCMAKE_BUILD_TYPE=Debug ..

# Release 构建
cmake -DCMAKE_BUILD_TYPE=Release ..
Other extensions