从入门到精通:CMakeLists.txt 完全指南
CMake 是一个跨平台的自动化构建系统,它使用名为 CMakeLists.txt 的配置文件来控制软件的编译过程。无论你是刚接触 CMake 的新手,还是希望提升 CMake 技能的中级开发者,这篇指南都将带你从基础到高级全面掌握 CMakeLists.txt 的编写技巧。
一、CMake 基础入门
1.1 什么是 CMake?
CMake 是一个跨平台的开源构建系统,它通过读取 CMakeLists.txt 文件中的指令来生成标准的构建文件(如 Unix 的 Makefile 或 Windows 的 Visual Studio 项目文件)。CMake 的主要优势在于:
- 跨平台性:可以生成适用于不同操作系统和编译器的构建文件
- 简化构建过程:自动处理依赖关系和编译顺序
- 模块化设计:支持大型项目的模块化管理
- 可扩展性:可以通过自定义命令和函数扩展功能
1.2 最简单的 CMakeLists.txt
让我们从一个最简单的 "Hello World" 项目开始:
cmake
# 指定 CMake 的最低版本要求
cmake_minimum_required(VERSION 3.10)
# 设置项目名称
project(HelloWorld)
# 添加可执行文件
add_executable(HelloWorld main.cpp)
这个简单的 CMakeLists.txt 文件包含三个基本指令:
cmake_minimum_required
:指定构建此项目所需的最低 CMake 版本project
:定义项目名称和相关信息add_executable
:指定要生成的可执行文件和源文件
1.3 构建过程
使用这个 CMakeLists.txt 文件的典型构建流程是:
bash
mkdir build # 创建构建目录(推荐外部构建)
cd build # 进入构建目录
cmake .. # 生成构建系统
make # 编译项目
这种"外部构建"的方式(在单独的 build 目录中构建)是推荐的做法,因为它不会污染源代码目录。
二、CMakeLists.txt 核心语法详解
2.1 基本指令深入
2.1.1 project() 指令
project()
指令不仅可以指定项目名称,还可以设置版本、描述和使用的编程语言:
cmake
project(MyProject
VERSION 1.0.0
DESCRIPTION "A sample CMake project"
LANGUAGES CXX)
VERSION
:设置项目版本号DESCRIPTION
:项目描述信息LANGUAGES
:指定项目使用的编程语言(C 表示 C 语言,CXX 表示 C++)
2.1.2 添加可执行文件
add_executable()
指令用于生成可执行文件:
cmake
add_executable(TargetName source1.cpp source2.cpp header1.h)
CMake 会自动识别 .cpp
文件为源文件,.h
文件为头文件。虽然头文件可以列出,但通常不需要,除非它们包含需要被 moc 或其他预处理器处理的代码。
2.1.3 添加库文件
使用 add_library()
可以创建库文件:
cmake
add_library(LibraryName STATIC source1.cpp source2.cpp)
库的类型可以是:
STATIC
:静态库(.a 或 .lib)SHARED
:动态库(.so 或 .dll)MODULE
:模块库(不被链接,但可能被运行时加载)
2.2 变量与属性
2.2.1 变量设置与使用
CMake 使用 set()
命令定义变量:
cmake
set(MY_VARIABLE "Hello World")
set(SOURCE_FILES main.cpp utils.cpp)
使用 ${}
语法引用变量:
cmake
message(STATUS "The value is: ${MY_VARIABLE}")
add_executable(MyApp ${SOURCE_FILES})
2.2.2 缓存变量
缓存变量会保存在 CMakeCache.txt 中,可以在后续构建中使用:
cmake
set(MY_CACHE_VAR "DefaultValue" CACHE STRING "A description of this variable")
缓存变量可以在命令行通过 -D
选项设置:
bash
cmake -DMY_CACHE_VAR="CustomValue" ..
2.2.3 环境变量
读取和使用环境变量:
cmake
set(ENV{PATH} "$ENV{PATH}:/opt/local/bin")
message(STATUS "Current PATH: $ENV{PATH}")
2.3 控制流
2.3.1 条件语句
cmake
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(STATUS "Running on Linux")
elseif(WIN32)
message(STATUS "Running on Windows")
else()
message(STATUS "Running on unknown system")
endif()
常用条件表达式:
DEFINED var
:检查变量是否定义EXISTS path
:检查路径是否存在file1 IS_NEWER_THAN file2
:检查文件时间戳version1 VERSION_LESS version2
:版本比较
2.3.2 循环语句
cmake
# foreach 循环
foreach(i RANGE 1 10)
message(STATUS "Counter: ${i}")
endforeach()
# while 循环
set(i 0)
while(i LESS 10)
message(STATUS "Counter: ${i}")
math(EXPR i "${i} + 1")
endwhile()
2.4 文件操作
2.4.1 包含目录
cmake
include_directories(${PROJECT_SOURCE_DIR}/include)
现代 CMake 更推荐使用 target_include_directories()
:
cmake
target_include_directories(MyTarget PUBLIC include)
2.4.2 链接库
cmake
target_link_libraries(MyTarget PUBLIC MyLibrary)
PUBLIC
、PRIVATE
和 INTERFACE
关键字控制依赖的传递性:
PRIVATE
:仅当前目标使用INTERFACE
:仅依赖此目标的其他目标使用PUBLIC
:当前目标和其他依赖目标都使用
2.4.3 文件操作命令
cmake
# 查找所有 .cpp 文件
file(GLOB SOURCES "src/*.cpp")
# 复制文件
file(COPY data DESTINATION ${CMAKE_BINARY_DIR})
# 读写文件
file(READ "${PROJECT_SOURCE_DIR}/VERSION" PROJECT_VERSION)
file(WRITE "${CMAKE_BINARY_DIR}/generated.h" "#define VERSION \"${PROJECT_VERSION}\"")
三、项目结构组织
3.1 基本项目结构
一个典型的 CMake 项目结构如下:
MyProject/
├── CMakeLists.txt # 顶层 CMake 配置
├── build/ # 构建目录(外部构建)
├── include/ # 公共头文件
│ └── MyLib/
│ └── header.h
├── src/ # 源文件
│ ├── CMakeLists.txt # 子目录 CMake 配置
│ ├── main.cpp
│ └── utils.cpp
└── tests/ # 测试代码
└── CMakeLists.txt
3.2 子目录管理
使用 add_subdirectory()
将项目分解为多个子目录:
cmake
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)
add_subdirectory(src) # 包含 src 子目录
add_subdirectory(tests) # 包含 tests 子目录
每个子目录有自己的 CMakeLists.txt 文件。
3.3 源文件收集
有多种方式指定源文件:
- 显式列出所有源文件:
cmake
add_executable(MyApp src/main.cpp src/utils.cpp)
- 使用
aux_source_directory
自动收集:
cmake
aux_source_directory(. SRC_FILES)
add_executable(MyApp ${SRC_FILES})
- 使用
file(GLOB)
更灵活地匹配文件:
cmake
file(GLOB SRC_FILES "src/*.cpp" "src/*.c")
add_executable(MyApp ${SRC_FILES})
注意:GLOB
不会自动检测新增文件,需要重新运行 CMake。
四、依赖管理与查找
4.1 查找系统库
使用 find_package
查找系统安装的库:
cmake
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
target_link_libraries(MyTarget PUBLIC Boost::filesystem Boost::system)
REQUIRED
表示必须找到该包,否则报错。
4.2 自定义查找模块
如果 CMake 没有提供某个库的查找模块,可以自己编写 FindXXX.cmake
:
cmake
# FindMyLib.cmake
find_path(MYLIB_INCLUDE_DIR mylib.h HINTS /usr/local/include)
find_library(MYLIB_LIBRARY NAMES mylib HINTS /usr/local/lib)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MyLib DEFAULT_MSG MYLIB_LIBRARY MYLIB_INCLUDE_DIR)
if(MyLib_FOUND)
set(MyLib_LIBRARIES ${MYLIB_LIBRARY})
set(MyLib_INCLUDE_DIRS ${MYLIB_INCLUDE_DIR})
endif()
然后在 CMakeLists.txt 中使用:
cmake
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
find_package(MyLib REQUIRED)
target_include_directories(MyTarget PUBLIC ${MyLib_INCLUDE_DIRS})
target_link_libraries(MyTarget PUBLIC ${MyLib_LIBRARIES})
4.3 第三方依赖管理
现代 CMake 项目常用这些方法管理第三方依赖:
- Git 子模块:
bash
git submodule add https://github.com/xxx/yyy.git extern/yyy
然后在 CMakeLists.txt 中:
cmake
add_subdirectory(extern/yyy)
target_link_libraries(MyTarget PUBLIC yyy)
- FetchContent(CMake 3.11+):
cmake
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.11.0
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(MyTarget PRIVATE gtest_main)
- ExternalProject(更复杂但更灵活):
cmake
include(ExternalProject)
ExternalProject_Add(
MyExternalLib
URL "http://example.com/mylib.tar.gz"
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
)
五、高级特性与技巧
5.1 生成器表达式
生成器表达式允许在生成构建系统时进行条件判断,常用于平台特定设置:
cmake
target_compile_definitions(MyTarget
PUBLIC $<$<CONFIG:Debug>:DEBUG_MODE=1>
$<$<CXX_COMPILER_ID:GNU>:EXTRA_FEATURE=1>
)
常用生成器表达式:
$<CONFIG:cfg>
:如果配置是 cfg 则为 1$<PLATFORM_ID:platform>
:平台匹配检查$<COMPILE_LANGUAGE:lang>
:编译语言检查
5.2 自定义命令与目标
cmake
# 自定义命令
add_custom_command(
OUTPUT generated.cpp
COMMAND generator.py ${CMAKE_CURRENT_SOURCE_DIR}/input.txt > generated.cpp
DEPENDS generator.py input.txt
)
# 自定义目标
add_custom_target(
GenerateDocs ALL
COMMAND doxygen Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating documentation"
)
5.3 交叉编译
设置交叉编译工具链:
cmake
# toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /path/to/sysroot)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
然后使用:
bash
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..
5.4 测试与打包
5.4.1 添加测试
cmake
enable_testing()
add_test(NAME MyTest1
COMMAND MyTestExe --test1
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
add_test(NAME MyTest2
COMMAND MyTestExe --test2)
可以使用 CTest 运行测试:
bash
ctest -V # 运行所有测试并显示详细输出
5.4.2 安装规则
cmake
install(TARGETS MyTarget
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static)
install(DIRECTORY include/ DESTINATION include)
install(FILES README.md DESTINATION doc)
5.4.3 打包
cmake
include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_VENDOR "My Company")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_SOURCE_GENERATOR "TGZ")
include(CPack)
生成包:
bash
make package # 生成二进制包
make package_source # 生成源码包
六、现代 CMake 最佳实践
6.1 目标为中心的设计
现代 CMake 强调以目标(target)为中心的构建方式,每个库或可执行文件都是一个目标,明确指定其属性:
cmake
add_library(MyLibrary STATIC src/lib.cpp)
target_include_directories(MyLibrary PUBLIC include)
target_compile_features(MyLibrary PUBLIC cxx_std_17)
target_link_libraries(MyLibrary PUBLIC SomeOtherLib)
6.2 作用域控制
正确使用 PRIVATE
、PUBLIC
和 INTERFACE
控制依赖传递:
cmake
# MyLibrary 的 CMakeLists.txt
target_include_directories(MyLibrary
PUBLIC include # 使用者和被使用者都需要
PRIVATE src # 仅实现需要
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interface # 仅使用者需要
)
6.3 避免全局设置
避免使用全局设置如 include_directories()
和 link_directories()
,而是使用目标特定的命令。
6.4 模块化设计
将大型项目分解为多个逻辑组件,每个组件有自己的 CMakeLists.txt:
components/
├── core/
│ ├── CMakeLists.txt
│ ├── include/
│ └── src/
└── gui/
├── CMakeLists.txt
├── include/
└── src/
6.5 工具链兼容性
编写可移植的 CMake 脚本:
cmake
if(MSVC)
target_compile_options(MyTarget PRIVATE /W4 /WX)
else()
target_compile_options(MyTarget PRIVATE -Wall -Wextra -pedantic)
endif()
七、常见问题与解决方案
7.1 头文件找不到
问题:编译时报告头文件找不到。
解决方案:
- 使用
target_include_directories()
明确指定包含路径 - 确保路径正确,使用绝对路径或相对于
CMAKE_CURRENT_SOURCE_DIR
的路径 - 检查拼写错误
7.2 库链接失败
问题:链接时报告未定义的引用。
解决方案:
- 确保
target_link_libraries()
指定了所有需要的库 - 检查库文件路径是否正确
- 确保库的顺序正确(被依赖的库放在后面)
7.3 跨平台问题
问题:在 Windows 上工作正常,但在 Linux 上失败。
解决方案:
- 使用
if(WIN32)
、if(UNIX)
等条件语句处理平台差异 - 避免使用平台特定的路径分隔符(总是使用
/
) - 使用
CMAKE_CXX_COMPILER_ID
检查编译器
7.4 构建速度慢
问题:大型项目构建时间过长。
解决方案:
- 使用
ccache
缓存编译结果 - 启用并行构建:
make -j8
或cmake --build . --parallel 8
- 减少不必要的依赖和包含
7.5 调试 CMake
技巧:
- 使用
message()
打印变量值 - 添加
--trace
或--trace-expand
选项查看详细执行过程 - 使用
cmake --graphviz=graph.dot
生成依赖图
八、实战项目示例
8.1 基础项目结构
让我们看一个完整的基础项目结构:
MyApp/
├── CMakeLists.txt
├── build/
├── include/
│ └── MyApp/
│ ├── utils.h
│ └── config.h
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── utils.cpp
└── tests/
├── CMakeLists.txt
└── test_utils.cpp
顶层 CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)
src/CMakeLists.txt:
cmake
# 添加库
add_library(MyLib STATIC utils.cpp)
target_include_directories(MyLib PUBLIC ../include)
# 添加可执行文件
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE MyLib)
tests/CMakeLists.txt:
cmake
# 启用测试
enable_testing()
# 添加测试可执行文件
add_executable(TestUtils test_utils.cpp)
target_link_libraries(TestUtils PRIVATE MyLib)
# 添加测试用例
add_test(NAME TestUtils COMMAND TestUtils)
8.2 使用外部依赖的项目
更复杂的项目可能依赖第三方库:
cmake
cmake_minimum_required(VERSION 3.12)
project(AdvancedApp)
# 查找依赖
find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem)
find_package(OpenCV REQUIRED)
find_package(Threads REQUIRED)
# 添加项目目标
add_executable(AdvancedApp main.cpp)
target_link_libraries(AdvancedApp PRIVATE
Boost::system
Boost::filesystem
OpenCV::OpenCV
Threads::Threads
)
# 安装规则
install(TARGETS AdvancedApp DESTINATION bin)
install(FILES config.xml DESTINATION etc/AdvancedApp)
九、总结与进阶学习资源
9.1 CMake 学习路径
-
初学者:
- 掌握基本命令:
project()
,add_executable()
,target_link_libraries()
- 理解变量和作用域
- 学习简单的项目组织
- 掌握基本命令:
-
中级开发者:
- 掌握现代 CMake 目标为中心的方法
- 学习依赖管理和查找
- 理解生成器表达式
-
高级开发者:
- 掌握交叉编译和工具链文件
- 学习编写复杂的自定义命令和目标
- 理解 CMake 内部机制和模块开发
9.2 推荐资源
-
官方文档:
-
书籍:
- 《Professional CMake: A Practical Guide》
- 《CMake Cookbook》
-
在线教程:
-
开源项目参考:
- 学习知名开源项目(如 KDE、VTK)的 CMake 配置
- 参考 GitHub 上的现代 CMake 模板项目
9.3 结语
CMake 是一个功能强大但学习曲线较陡的工具。掌握 CMake 不仅能提高你的构建系统技能,还能让你更好地理解软件项目的组织和管理。从简单的单文件项目开始,逐步尝试更复杂的场景,最终你将能够驾驭任何规模的 CMake 项目。
记住,好的 CMake 脚本应该是:
- 模块化:易于维护和扩展
- 可移植:能在不同平台和编译器上工作
- 高效:最小化不必要的重新构建
- 明确:清晰地表达项目的结构和依赖关系