CMake 入门与实践指南
1. 什么是 CMake?
CMake 是一个开源、跨平台的自动化构建系统生成工具。它本身并不直接编译代码或构建项目,而是使用一种名为 CMakeLists.txt
的配置文件,根据目标平台和编译器,生成对应构建系统(如 Unix Makefiles、Ninja、Visual Studio、Xcode 等)所需的构建文件(例如 Makefile
或 .sln
文件)。然后,开发者可以使用平台原生的构建工具(如 make
、ninja
、MSBuild 等)来实际编译和链接项目。
为什么选择 CMake?
- 跨平台性:只需编写一次
CMakeLists.txt
,即可在 Windows、Linux、macOS 等多种操作系统上生成相应的构建文件。 - 编译器无关性:支持多种编译器,如 GCC、Clang、MSVC 等。
- 强大的依赖管理:可以方便地查找和链接外部库(通过
find_package
)。 - 支持复杂项目:能够很好地管理包含多个子目录、库和可执行文件的复杂项目结构。
- 集成测试与打包:内置 CTest 和 CPack 工具,分别用于测试和软件打包。
- 广泛的社区支持和生态:许多流行的 C++ 库和项目都使用 CMake。
- “Out-of-Source” 构建:推荐将构建产生的文件(如目标文件、可执行文件)与源代码分离,保持源码目录的整洁。
2. CMake 基本语法与格式要求 (CMakeLists.txt
)
CMakeLists.txt
文件是 CMake 的核心。它包含了一系列的命令,用于描述项目的构建过程。
基本格式规则:
- 命令不区分大小写:
project()
和PROJECT()
是等效的,但推荐使用小写或统一风格(如官方文档倾向于小写)。 - 变量区分大小写:
${MY_VARIABLE}
和${my_variable}
是不同的变量。 - 注释:使用
#
号开始注释,直到行尾。 - 命令参数:命令名后跟括号
()
,参数之间用空格分隔。如果参数本身包含空格,需要用双引号""
括起来。command(ARG1 ARG2 "Argument with spaces" ARG4)
- 变量引用:使用
${VARIABLE_NAME}
的形式引用变量的值。 - 列表(字符串分号分隔):CMake 中的列表实际上是以分号
;
分隔的字符串。set(MY_LIST item1 item2 item3) # MY_LIST 的值是 "item1;item2;item3" add_executable(my_app ${MY_LIST}) # 等价于 add_executable(my_app item1 item2 item3)
常用命令:
-
cmake_minimum_required(VERSION <major>.<minor>[.<patch>])
- 作用:指定项目所需的最低 CMake 版本。必须放在
CMakeLists.txt
文件的最开始。 - 示例:
cmake_minimum_required(VERSION 3.10)
- 作用:指定项目所需的最低 CMake 版本。必须放在
-
project(<project_name> [VERSION <version>] [LANGUAGES <lang>...])
- 作用:定义项目名称,并可选地指定项目版本和使用的编程语言(默认为 C 和 CXX 即 C++)。
- 示例:
project(MyAwesomeApp VERSION 1.0 LANGUAGES CXX)
-
set(<variable> <value>... [PARENT_SCOPE])
- 作用:设置或修改一个变量的值。可以是普通变量、缓存变量(
CACHE
)或环境变量(ENV
)。PARENT_SCOPE
用于在函数或子目录中设置父作用域的变量。 - 示例:
set(SOURCE_FILES main.cpp utils.cpp) set(MY_OPTION ON CACHE BOOL "Enable my optional feature")
- 作用:设置或修改一个变量的值。可以是普通变量、缓存变量(
-
add_executable(<name> [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
- 作用:定义一个可执行文件的目标(Target)。指定目标名称和源文件列表。
- 示例:
add_executable(my_app main.cpp utils.cpp)
-
add_library(<name> [STATIC | SHARED | MODULE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
- 作用:定义一个库目标。可以是静态库 (
STATIC
)、动态库/共享库 (SHARED
) 或模块库 (MODULE
)。 - 示例:
add_library(my_lib STATIC helpers.cpp io.cpp)
- 作用:定义一个库目标。可以是静态库 (
-
target_include_directories(<target> [SYSTEM] [BEFORE] <INTERFACE|PUBLIC|PRIVATE> [items1...])
- 作用:为目标指定包含目录(头文件搜索路径)。
PRIVATE
:仅影响目标自身的构建。INTERFACE
:仅影响链接到此目标的其他目标。PUBLIC
:同时影响目标自身和链接到它的其他目标(等同于PRIVATE
+INTERFACE
)。- 示例:
target_include_directories(my_app PRIVATE include)
-
target_link_libraries(<target> ... <item>... ...)
- 作用:为目标指定需要链接的库。可以是其他 CMake 目标、库文件的完整路径或库名称。
- 示例:
# 链接另一个 CMake 目标 'my_lib' target_link_libraries(my_app PRIVATE my_lib) # 链接系统库 (例如 POSIX 线程库) target_link_libraries(my_app PRIVATE pthread) # 链接 Boost 库 (假设通过 find_package 找到) target_link_libraries(my_app PRIVATE Boost::filesystem Boost::system)
-
target_compile_definitions(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...])
- 作用:为目标添加编译定义(例如
-DOPTION
)。 - 示例:
target_compile_definitions(my_app PRIVATE DEBUG USE_FEATURE_X)
- 作用:为目标添加编译定义(例如
-
target_compile_options(<target> <INTERFACE|PUBLIC|PRIVATE> [items1...])
- 作用:为目标添加编译选项(例如
-Wall
,/W4
)。 - 示例:
target_compile_options(my_app PRIVATE -Wall -Wextra -pedantic)
- 作用:为目标添加编译选项(例如
-
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE] [REQUIRED] [[COMPONENTS] [components...]] [OPTIONAL_COMPONENTS components...] ...)
- 作用:查找并加载外部库或包的设置。这是 CMake 管理依赖的关键。它会查找
Find<PackageName>.cmake
(Module 模式) 或<PackageName>Config.cmake
/<lower-case-package-name>-config.cmake
(Config 模式) 文件。 - 示例:
find_package(Boost 1.67 REQUIRED COMPONENTS filesystem system)
- 作用:查找并加载外部库或包的设置。这是 CMake 管理依赖的关键。它会查找
-
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
- 作用:将子目录添加到构建中。CMake 会进入该子目录并处理其下的
CMakeLists.txt
文件。 - 示例:
add_subdirectory(src)
- 作用:将子目录添加到构建中。CMake 会进入该子目录并处理其下的
-
include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])
- 作用:加载并运行指定 CMake 文件或模块中的代码。常用于包含自定义函数或宏。
- 示例:
include(MyCustomFunctions)
-
控制流:CMake 支持
if()/elseif()/else()/endif()
、foreach()/endforeach()
、while()/endwhile()
等控制流语句。if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(my_app PRIVATE -Wall) elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") target_compile_options(my_app PRIVATE /W4) endif() set(MY_SOURCES main.cpp foo.cpp bar.cpp) foreach(SRC ${MY_SOURCES}) message(STATUS "Processing source file: ${SRC}") endforeach()
3. CMake 使用流程
典型的 CMake 使用流程遵循“配置 (Configure) -> 生成 (Generate) -> 构建 (Build)”的模式,并强烈推荐**“Out-of-Source”(外部构建)**。
-
编写
CMakeLists.txt
文件:- 在项目的根目录创建
CMakeLists.txt
文件。 - 根据项目结构,可能需要在子目录中也创建
CMakeLists.txt
文件,并使用add_subdirectory()
从根CMakeLists.txt
中调用。
- 在项目的根目录创建
-
创建构建目录:
- 在项目根目录之外(或之内,但通常推荐之外,例如同级)创建一个用于存放构建输出的目录。这可以保持源代码目录的干净。
# 在项目根目录下 mkdir build cd build
-
配置 (Configure) 与 生成 (Generate):
- 在构建目录 (
build
) 中运行cmake
命令,并指向包含顶层CMakeLists.txt
的源代码目录(通常是..
)。 - CMake 会检查编译器、系统环境,处理
CMakeLists.txt
文件,并生成特定构建系统(如 Makefiles 或 Visual Studio solution)所需的文件。 - 可以指定生成器 (
-G
),如果不指定,CMake 会尝试选择一个合适的默认生成器。
# 在 build 目录下 # 使用默认生成器 (例如 Unix Makefiles on Linux/macOS, Visual Studio on Windows) cmake .. # 或者显式指定生成器 (例如 Ninja) cmake -G Ninja .. # 或者指定 Visual Studio 2019 (64-bit) # cmake -G "Visual Studio 16 2019" -A x64 ..
- 在这一步,CMake 可能会打印出检测到的编译器信息、找到的库等。如果
find_package
失败或配置有误,会在这里报错。 - 可以通过
-D
选项设置 CMake 缓存变量,例如:cmake -D CMAKE_BUILD_TYPE=Release -D MY_OPTION=ON ..
- 在构建目录 (
-
构建 (Build):
- 在构建目录 (
build
) 中,使用 CMake 生成的构建系统对应的命令来编译和链接项目。 - CMake 提供了一个通用接口
--build
,可以自动调用底层的构建工具。
# 使用 CMake 的通用构建命令 (推荐) cmake --build . # 或者,如果生成的是 Makefiles,可以直接使用 make # make # 或者,如果生成的是 Ninja 文件 # ninja # 或者,如果生成的是 Visual Studio solution,可以用 MSBuild 或打开 .sln 文件在 IDE 中构建 # msbuild YourProject.sln /p:Configuration=Release
- 构建成功后,可执行文件、库文件等会出现在构建目录中。
- 若要指定构建配置(Debug/Release),可以在配置时用
CMAKE_BUILD_TYPE
或在构建时(对于多配置生成器如 VS)用--config
:# 配置时指定 (适用于单配置生成器如 Makefiles, Ninja) cmake -D CMAKE_BUILD_TYPE=Release .. cmake --build . # 构建时指定 (适用于多配置生成器如 Visual Studio) cmake .. # 配置时不用指定 CMAKE_BUILD_TYPE cmake --build . --config Release cmake --build . --config Debug
- 在构建目录 (
-
测试 (Test) (可选):
- 如果
CMakeLists.txt
中定义了测试(使用enable_testing()
和add_test()
),可以在构建目录下运行 CTest:
# 在 build 目录下 ctest # 或者指定配置 # ctest -C Release
- 如果
-
安装 (Install) (可选):
- 如果
CMakeLists.txt
中定义了安装规则(使用install()
命令),可以将构建好的文件(可执行文件、库、头文件等)安装到指定位置(通常由CMAKE_INSTALL_PREFIX
变量控制)。
# 在 build 目录下 # 配置时可以指定安装路径 # cmake -D CMAKE_INSTALL_PREFIX=/path/to/install .. # cmake --build . cmake --install . # 或者 cmake --install . --prefix /path/to/install # 覆盖配置时的路径 # 或者(旧方式,但仍可用) # make install
- 如果
-
打包 (Package) (可选):
- 如果
CMakeLists.txt
中配置了 CPack(使用include(CPack)
和设置CPACK_*
变量),可以生成源码包或二进制安装包。
# 在 build 目录下 cpack # 或者指定生成器 # cpack -G ZIP # 生成 ZIP 包 # cpack -G DEB # 生成 Debian 包
- 如果
4. CMake 使用流程图 (Mermaid 语法)
graph TD
A[编写/修改 CMakeLists.txt] --> B{创建 Build 目录?};
B -- Yes --> C[进入 Build 目录];
B -- No --> C;
C --> D[运行 'cmake <source_dir> [options]' (配置与生成)];
D --> E{配置/生成成功?};
E -- Yes --> F[运行 'cmake --build .' (构建)];
E -- No --> A;
F --> G{构建成功?};
G -- Yes --> H{需要测试?};
G -- No --> A;
H -- Yes --> I[运行 'ctest'];
H -- No --> J{需要安装?};
I --> J;
J -- Yes --> K[运行 'cmake --install .'];
J -- No --> L{需要打包?};
K --> L;
L -- Yes --> M[运行 'cpack'];
L -- No --> Z[完成];
M --> Z;
subgraph "开发循环"
A
D
F
end
subgraph "可选步骤"
I
K
M
end
style Z fill:#f9f,stroke:#333,stroke-width:2px
流程图说明:
- 从编写或修改
CMakeLists.txt
开始。 - 通常需要创建一个单独的
build
目录并进入(Out-of-Source Build)。 - 在
build
目录中运行cmake ..
来配置项目并生成构建文件。如果失败,返回修改CMakeLists.txt
。 - 配置成功后,运行
cmake --build .
来编译和链接项目。如果构建失败,返回修改代码或CMakeLists.txt
。 - 构建成功后,可以选择性地运行
ctest
进行测试。 - 可以选择性地运行
cmake --install .
将文件安装到指定位置。 - 可以选择性地运行
cpack
创建软件包。 - 完成构建流程。
5. CMake 注意事项与最佳实践
- 始终使用 Out-of-Source 构建:这是最重要的实践之一。将构建文件与源代码分开,使源代码目录保持干净,易于版本控制,并且可以轻松删除所有构建产物(只需删除构建目录)。
- 明确指定最低版本:
cmake_minimum_required()
应该总是放在CMakeLists.txt
的第一行,确保项目能在预期的 CMake 版本下工作,并启用相应版本的策略(Policies)。 - 使用
project()
命令:定义项目名称,并考虑指定VERSION
和LANGUAGES
。 - 优先使用
target_*
命令 (Modern CMake):尽量使用target_include_directories()
,target_link_libraries()
,target_compile_definitions()
,target_compile_options()
等命令,而不是旧的include_directories()
,link_libraries()
,add_definitions()
,add_compile_options()
。target_*
命令提供了更细粒度的控制(PRIVATE
,PUBLIC
,INTERFACE
),使得属性(如包含目录、链接库)可以随目标传递,提高了模块化和可维护性。 - 理解
PUBLIC
,PRIVATE
,INTERFACE
:PRIVATE
: 仅影响当前目标。INTERFACE
: 仅影响链接到当前目标的其他目标。PUBLIC
: 同时影响当前目标和链接到它的目标。- 正确使用这些关键字对于管理复杂依赖关系至关重要。例如,如果一个库的目标
my_lib
需要包含目录include
来编译自身,并且链接到my_lib
的目标也需要这个include
目录,那么应该使用target_include_directories(my_lib PUBLIC include)
。如果只有my_lib
自己需要,则用PRIVATE
。如果my_lib
自身不需要,但链接者需要(例如纯头文件库),则用INTERFACE
。
- 正确使用
find_package()
:- 了解 Config 模式和 Module 模式的区别。优先使用 Config 模式(通常由库开发者提供),因为它更可靠。
- 使用
REQUIRED
关键字,如果找不到包则让 CMake 失败,避免后续的构建错误。 - 指定需要的
COMPONENTS
,而不是查找整个包。 - 检查
find_package
是否成功找到(例如,检查<PackageName>_FOUND
变量)。
- 变量作用域:注意
set()
命令设置的变量默认只在当前CMakeLists.txt
文件或当前函数/宏的作用域内有效。子目录默认继承父目录的变量,但修改时需要PARENT_SCOPE
才能影响父作用域。 - 避免硬编码路径和平台特定设置:使用 CMake 提供的变量(如
CMAKE_BINARY_DIR
,CMAKE_SOURCE_DIR
,CMAKE_CURRENT_SOURCE_DIR
)和平台检查(如IF(WIN32)
,IF(APPLE)
,IF(UNIX)
)来保持跨平台性。使用生成器表达式(Generator Expressions)可以在生成阶段根据配置、平台等条件动态设置属性。 - 编写模块化 CMake 代码:对于复杂的项目,将功能性的 CMake 代码封装在函数(
function()
)或宏(macro()
)中,并使用include()
将它们包含进来,可以提高可读性和复用性。 - 添加注释:解释复杂的 CMake 逻辑或不明显的设置。
- 使用
CMAKE_BUILD_TYPE
:对于单配置生成器(如 Makefiles, Ninja),使用-D CMAKE_BUILD_TYPE=Debug|Release|RelWithDebInfo|MinSizeRel
来控制构建类型。 - 集成测试 (CTest):为你的项目编写测试,并使用
enable_testing()
和add_test()
将它们集成到 CMake/CTest 中。 - 考虑安装和打包 (CPack):如果需要分发你的软件,使用
install()
命令定义安装规则,并使用 CPack 配置来生成安装包。
6. 简单示例 CMakeLists.txt
假设项目结构如下:
my_project/
├── CMakeLists.txt
├── main.cpp
└── src/
├── CMakeLists.txt
├── hello.cpp
└── include/
└── hello.h
my_project/CMakeLists.txt
(根目录):
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0 LANGUAGES CXX)
message(STATUS "Configuring ${PROJECT_NAME} version ${PROJECT_VERSION}")
# 添加 src 子目录,它包含 hello 库
add_subdirectory(src)
# 添加可执行文件目标
add_executable(my_app main.cpp)
# 将可执行文件链接到 hello 库
# 注意:hello 库目标是在 src/CMakeLists.txt 中定义的
target_link_libraries(my_app PRIVATE hello_lib)
# (可选) 如果 main.cpp 需要 hello.h,并且 hello_lib 将其 include 目录设为 INTERFACE 或 PUBLIC
# target_include_directories(my_app PRIVATE src/include)
# 上面这行通常不需要,因为 target_link_libraries 会传递 INTERFACE/PUBLIC 的包含目录
# (可选) 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
message(STATUS "Configuration finished.")
my_project/src/CMakeLists.txt
(子目录):
# 定义 hello 库 (静态库)
add_library(hello_lib STATIC hello.cpp)
# 为 hello_lib 添加包含目录
target_include_directories(hello_lib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # 构建时,自己和链接者都需要
$<INSTALL_INTERFACE:include> # 安装后,链接者需要的路径 (假设头文件安装到 <prefix>/include)
PRIVATE
# 如果有仅内部使用的头文件目录,放在这里
)
# (可选) 如果 hello_lib 需要链接其他库
# target_link_libraries(hello_lib PRIVATE some_other_lib)
# (可选) 定义安装规则
# install(TARGETS hello_lib EXPORT HelloLibTargets
# ARCHIVE DESTINATION lib
# LIBRARY DESTINATION lib
# RUNTIME DESTINATION bin)
# install(FILES include/hello.h DESTINATION include)
# install(EXPORT HelloLibTargets
# FILE HelloLibTargets.cmake
# NAMESPACE HelloLib::
# DESTINATION lib/cmake/HelloLib)
message(STATUS "Added library target: hello_lib")
这个示例展示了如何组织一个包含子目录和库的项目,并使用了现代 CMake 的 target_*
命令。
7. 总结
CMake 是现代 C++ 项目(以及其他语言项目)构建管理的强大工具。它通过抽象底层构建系统,实现了跨平台、跨编译器的构建配置。掌握 CMake 的基本语法、遵循最佳实践(尤其是 Out-of-Source 构建和 Modern CMake 用法),能够显著提高开发效率和项目的可维护性。虽然初学时可能觉得有些复杂,但投入时间学习 CMake 是非常值得的。
**说明:**
1. **格式**: 全文使用 Markdown 格式,包括标题、列表、代码块、粗体等。
2. **内容**:
* **介绍**: 解释了 CMake 是什么,以及为什么使用它。
* **语法与格式**: 详细介绍了 `CMakeLists.txt` 的基本规则和常用命令,并给出了示例。
* **使用流程**: 分步描述了从编写 `CMakeLists.txt` 到配置、生成、构建、测试、安装、打包的完整流程,强调了 Out-of-Source 构建。
* **流程图**: 使用 Mermaid 语法绘制了 CMake 的典型工作流程图。这个图可以在支持 Mermaid 的 Markdown 查看器(如 GitHub、GitLab、Typora、VS Code 插件等)中正确渲染。
* **注意事项**: 列出了使用 CMake 时需要注意的关键点和推荐的最佳实践,特别是 Modern CMake 的用法。
* **示例**: 提供了一个包含子目录和库的基本项目结构的 `CMakeLists.txt` 示例。
* **总结**: 概括了 CMake 的重要性。
3. **字数**: 文章内容远超 1000 字符的要求。
4. **语言**: 使用中文编写。
希望这篇详细的文章能帮助你理解和使用 CMake!