CMake Basics
CMake is a cross-platform build system generator tool used to manage the compilation process of C/C++ projects.
By writing CMakeLists.txt configuration files, you can define the project's build rules, dependencies, and compilation options. CMake automatically generates the corresponding platform build files (such as Makefile on Unix or Visual Studio solution on Windows).
CMakeLists.txt File
CMakeLists.txt is the core configuration file of CMake. Every CMake project requires at least one CMakeLists.txt file.
CMake reads the commands in the file to understand the project structure, source file list, and compilation requirements.
Basic Syntax
CMakeLists.txt consists of a series of CMake commands, each in the format:Command name (argument list)。
Below are the most commonly used commands and their examples when building CMake projects.
Specify the minimum CMake version:
Example
# This command must be placed at the very top of CMakeLists.txt
# Example: Require CMake version 3.10 or higher
cmake_minimum_required(VERSION 3.10)
Define the project name and language:
Example
# The language parameter is optional. Common values: CXX (C++), C (C language)
# After calling project(), variables such as PROJECT_NAME are automatically set
project(MyProject CXX)
Add an executable:
Example
# Compile the specified source files to generate an executable
add_executable(MyApp main.cpp utils.cpp)
Add a library:
Example
# STATIC: static library (.a / .lib), embedded directly into the executable at compile time
# SHARED: dynamic library (.so / .dll), loaded at runtime
# If the type is not specified, it is determined by the BUILD_SHARED_LIBS variable
add_library(MyLib STATIC library.cpp)
Link libraries to a target:
Example
# Link the specified libraries to the target (executable or other libraries)
# You can link library targets from your own project, or link external libraries
target_link_libraries(MyApp PRIVATE MyLib)
Set variables:
Example
# Define a normal variable, later referenced via ${variable_name}
set(CMAKE_CXX_STANDARD 17)
# Set multiple values at once (list)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})
Specify the header file path for the target:
Example
# [BEFORE | AFTER]
# [SYSTEM]
# [PUBLIC | PRIVATE | INTERFACE]
# <path>...)
# PUBLIC: The current target and targets that depend on it can use this path
# PRIVATE: Only the current target can use it
# INTERFACE: Only other targets that depend on the current target can use it
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)
Set installation rules:
Example
# [RUNTIME DESTINATION <executable installation path>]
# [LIBRARY DESTINATION <shared library installation path>]
# [ARCHIVE DESTINATION <static library installation path>]
# [INCLUDES DESTINATION <header file installation path>])
# Define the installation locations for each type of file when running make install
install(TARGETS MyApp RUNTIME DESTINATION bin)
Conditional statements:
Example
# Conditional expressions supported by CMake include: comparison, logical operations, variable checks, etc.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# Enable debug information in Debug mode
message(STATUS "Currently in Debug build mode")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
else()
# Enable optimization in Release mode
message(STATUS "Currently in Release build mode")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
endif()
Custom commands:
Example
# TARGET <target>
# PRE_BUILD | PRE_LINK | POST_BUILD
# COMMAND <command> [arguments...]
# [COMMENT <description>]
# [VERBATIM])
# PRE_BUILD: Execute before compilation
# PRE_LINK: Execute before linking
# POST_BUILD: Execute after the build completes
add_custom_command(
TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Build complete!"
COMMENT "Print build completion information."
)
Complete Example
Below is a complete CMakeLists.txt example that uses several of the above directives.
Example
# 1. Specify the minimum CMake version (must be placed at the very top)
cmake_minimum_required(VERSION 3.10)
# 2. Define the project name, version, and language
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 3. Set the C++ standard to C++17 and enforce its use
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 4. Add a static library target
add_library(MyLib STATIC
src/lib/core.cpp # Core functionality implementation
src/lib/helper.cpp # Helper function implementation
)
# 5. Add an executable target
add_executable(MyApp
src/main.cpp # Program entry
src/utils.cpp # Utility functions
)
# 6. Specify the public header file path for the library target
# PUBLIC means targets that use MyLib can also access this path
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)
# 7. Specify the private header file path for the executable
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src)
# 8. Link MyLib to MyApp
target_link_libraries(MyApp PRIVATE MyLib)
# 9. Set the installation rules
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(TARGETS MyLib ARCHIVE DESTINATION lib)
The typical commands to build this project are as follows:
# 在项目根目录下创建 build 目录(推荐外部构建) $ mkdir build && cd build # 生成构建文件 $ cmake .. # 编译项目 $ cmake --build . # 安装到系统目录(可选) $ cmake --install .
Variables and Cache
CMake uses variables to store and pass configuration information. Variables can be defined and used in CMakeLists.txt.
Variables are divided into two categories:Normal variablesandCache variables, and their scopes and lifetimes differ.
Normal Variables
Normal variables are defined in CMakeLists.txt, and their scope is the current directory and its subdirectories.
Normal variables are recalculated on each CMake run and are not persisted.
Example
set(MY_APP_NAME "example-app")
# Define a list variable (separated by semicolons or spaces)
set(SOURCE_FILES main.cpp utils.cpp helper.cpp)
# Using variables: reference via ${variable_name}
message(STATUS "Project name: ${MY_APP_NAME}")
# Using list variables: loop through each source file
foreach(FILE ${SOURCE_FILES})
message(STATUS "Source file: ${FILE}")
endforeach()
Cache Variables
Cache variables are stored in the CMakeCache.txt file and retain their values across multiple CMake runs.
Cache variables are often used to let users customize build settings during the CMake configuration stage, such as toggling features or specifying installation paths.
Cache variable values are persistent. If you modify the default value of a cache variable in CMakeLists.txt, you need to delete CMakeCache.txt or re-run cmake for the new default to take effect.
Example
# Available types: STRING, BOOL, PATH, FILEPATH
# If the variable already exists in CMakeCache.txt, set will not overwrite the existing value
# Define a STRING cache variable (user-modifiable installation path)
set(MY_INSTALL_PATH "/usr/local" CACHE PATH "Installation path of the program")
# Define a BOOL cache variable (feature toggle)
set(ENABLE_LOGGING ON CACHE BOOL "Whether to enable the logging feature")
# Use cache variables just like any other variable
if(ENABLE_LOGGING)
message(STATUS "Logging feature enabled")
add_definitions(-DENABLE_LOGGING)
endif()
Users can modify cache variables via command-line-Dparameters:
# 在 cmake 命令中通过 -D 修改缓存变量 $ cmake .. -DMY_INSTALL_PATH=/opt/example -DENABLE_LOGGING=OFF
Header File Search Paths
In CMake, there are two ways to specify the paths where the compiler looks for header files:include_directories()andtarget_include_directories()。
Understanding the difference between the two is very important for writing clean, maintainable CMake projects.
Comparison of include_directories and target_include_directories
Both can add header file search paths for the compiler, but they differ in scope and level of control.
| Feature | include_directories() |
target_include_directories() |
|---|---|---|
| Scope | Global scope: affects all targets in the current directory and its subdirectories | Applies only to the specified target, does not affect other targets |
| Recommendation | Not recommended, except when maintaining legacy CMake projects | Recommended to use first, follows modern CMake best practices |
| Target association | Not directly associated with a specific target, may accidentally affect other targets | Explicitly bound to the specified target, dependencies are clear at a glance |
| Maintainability | Poor, easily causes global path pollution, problems are hard to debug | Good, paths are bound to targets, clear logic |
| Visibility control | Cannot precisely control propagation scope | Precisely controlled via PUBLIC, PRIVATE, INTERFACE |
Meaning of PUBLIC / PRIVATE / INTERFACE
target_include_directories()The key advantage is that the propagation of header file paths can be precisely controlled via visibility keywords.
| Keyword | Available to the current target | Available to other targets that depend on this target | Typical scenario |
|---|---|---|---|
PRIVATE |
Yes | no | Internal header files needed only by its own implementation |
PUBLIC |
Yes | Yes | Public API header files of the library, needed by both itself and consumers |
INTERFACE |
no | Yes | Header-only library |
Example
# MyLib itself needs include/, and MyApp which uses MyLib also needs include/
# PUBLIC: both itself and consumers can access this path
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)
# PRIVATE: only MyApp itself can access (internal utility function header files)
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src/internal)
# INTERFACE: MyLib is a header-only library, it doesn't need to be compiled itself, only passes the path to consumers
add_library(HeaderOnlyLib INTERFACE)
target_include_directories(HeaderOnlyLib INTERFACE ${PROJECT_SOURCE_DIR}/header_only)
Finding Libraries and Packages
Many projects depend on external libraries (such as Boost, OpenCV). CMake providesfind_package()commands to automatically detect and configure these external dependencies.
find_package Command
find_package()It searches for libraries installed on the system and sets the corresponding variables (such as header file paths, library file paths, and library target names).
Example
# REQUIRED means aborting CMake configuration if not found
find_package(Boost REQUIRED)
# Specify the minimum version: require Boost version no lower than 1.70
find_package(Boost 1.70 REQUIRED)
# Specify the search path: look for OpenCV in a custom path
find_package(OpenCV REQUIRED PATHS /path/to/opencv)
Using the Found Library
find_package()On success, the following are typically provided for use:
| Provided content | Example | Description |
|---|---|---|
| Imported targets | Boost::Boost |
Modern CMake recommended approach, used directly via target_link_libraries |
| Header file path variables | ${Boost_INCLUDE_DIRS} |
Legacy usage, used with include_directories |
| Library file path variables | ${Boost_LIBRARY_DIRS} |
Legacy usage, used with link_directories |
Strongly recommended to useImported targetsmethod (e.g., Boost::Boost) rather than manually piecing together header and library paths. Imported targets automatically handle all compilation and linking options, reducing configuration errors.
Example of Using a Third-Party Library
Below is a complete example showing how to introduce and use the Boost library in a CMake project.
Example
cmake_minimum_required(VERSION 3.10)
project(MyBoostProject CXX)
# Set the C++ standard
set(CMAKE_CXX_STANDARD 17)
# Find the Boost library; exit with an error if not found
find_package(Boost REQUIRED)
# Add an executable
add_executable(MyApp main.cpp)
# Link Boost using the imported target
# Boost::Boost automatically sets header file paths and library file paths
target_link_libraries(MyApp PRIVATE Boost::Boost)
Notes
Prefer the target_xxx family of commands.include_directories() and link_directories() are global commands that easily cause path pollution. In large projects, always use target_include_directories() and target_link_libraries(); they make dependencies clearer and easier to maintain.
Always specify the C++ standard.If CMAKE_CXX_STANDARD is not set, the compiler will use the default standard (usually C++98 or C++14, depending on the compiler version). It is recommended to also set CMAKE_CXX_STANDARD_REQUIRED to ON, so that the build fails immediately if the compiler does not support the specified standard.
Understand the difference between PUBLIC / PRIVATE / INTERFACE.Misusing these keywords is the most common pitfall for CMake beginners. Simple way to remember: needed by both self and consumers → PUBLIC; needed only by self → PRIVATE; needed only by consumers (e.g., header-only libraries) → INTERFACE.
Other extensionsDefault values of cache variables are not updated automatically.After changing the default value of set(... CACHE ...) in CMakeLists.txt, the existing CMakeCache.txt will not be overwritten. You need to delete the CMakeCache.txt in the build directory, or the entire build directory, and re-run cmake.