17、内置模块:CMake内置模块概览、FetchContent(外部依赖管理)、CTest(测试支持)、CPack(打包支持)
聊到CMake的内置模块,我其实挺感慨的。早些年做C++项目,依赖管理全靠手写Find脚本,测试和打包更是各搞各的。后来CMake把这些东西都收编了,说实话,省了我不少头发。
这一节我们就把CMake自带的几个核心模块捋一遍。你想想看,一个项目从拉取依赖、到编译测试、再到打包发布,CMake全给你管了。是不是挺省心的?
CMake内置模块概览
CMake自带了几十个模块,分布在Modules/目录下。我挑几个最常用的说说:
- FetchContent:拉取外部源码,直接集成到构建中
- CTest:测试驱动模块,帮你跑单元测试、集成测试
- CPack:打包模块,生成安装包、压缩包、甚至DEB/RPM
- FindXxx系列:查找系统已安装的库,比如FindOpenSSL、FindBoost
- ExternalProject:更底层的外部项目管理,适合复杂场景
我个人习惯,能用FetchContent就不用ExternalProject。为什么?因为FetchContent更现代,集成更平滑,不会把构建搞得乱七八糟。
FetchContent:外部依赖管理
FetchContent说白了就是帮你把别人的源码拉到你的构建树里,然后直接add_subdirectory。它比git submodule好用,也比手动下载靠谱。
来看一个实际例子。假设你的项目依赖nlohmann/json这个JSON库:
include(FetchContent)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(nlohmann_json)
# 现在可以直接用 json 库了
target_link_libraries(my_app PRIVATE nlohmann_json::nlohmann_json)
就这么几行,依赖就拉下来了。我曾经在一个嵌入式项目里用这个方式管理了七八个第三方库,编译脚本干净得不像话。
FetchContent还支持URL下载、SVN、甚至本地路径。我个人最常用的是GIT_REPOSITORY + GIT_TAG,因为版本锁定最可靠。
_deps文件夹里。如果你想让多个项目共享同一份缓存,可以设置FETCHCONTENT_BASE_DIR变量。我踩过这个坑——每个构建目录都重新下载一遍,磁盘空间直接爆炸。
CTest:测试支持
CTest是CMake自带的测试驱动工具。它不负责写测试,而是帮你组织、运行、报告测试结果。你可以用任何测试框架——Google Test、Catch2、Boost.Test,甚至自己写的测试程序。
启用CTest很简单,在顶层CMakeLists.txt里加一行:
enable_testing()
然后注册测试用例:
add_test(NAME my_unit_test COMMAND ./run_tests --gtest_output=xml)
更常见的做法是配合Google Test:
include(GoogleTest)
# 假设你有一个测试可执行文件 test_foo
add_executable(test_foo test_foo.cpp)
target_link_libraries(test_foo PRIVATE GTest::GTest GTest::Main)
# 自动发现所有 TEST() 宏
gtest_discover_tests(test_foo)
然后你就可以用ctest命令运行所有测试了:
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure
CTest还支持并行测试、超时控制、标签过滤。我在一个大型项目中用过ctest -R "integration" --timeout 60 -j4,只跑集成测试,4个任务并行,超时60秒。效果不错。
CPack:打包支持
CPack是CMake的打包模块。它能生成各种格式的安装包:
- ZIP/TGZ:通用压缩包
- DEB:Debian/Ubuntu的包格式
- RPM:Red Hat/Fedora的包格式
- NSIS:Windows安装程序
- DMG:macOS磁盘映像
启用CPack也很简单。先配置安装规则:
install(TARGETS my_app DESTINATION bin)
install(FILES config.json DESTINATION etc)
然后在CMakeLists.txt末尾加上:
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A cool application")
set(CPACK_GENERATOR "TGZ;DEB")
include(CPack)
打包时运行:
cpack -G DEB # 生成 .deb 包
cpack -G TGZ # 生成 .tar.gz 包
我曾经给一个Linux桌面应用配置过CPack,同时生成DEB和RPM包。一开始DEB包依赖写错了,安装时提示缺少libfoo。后来我加了CPACK_DEBIAN_PACKAGE_DEPENDS才搞定。嗯,这里要注意,不同打包格式的配置变量名不一样,别搞混了。
CPACK_GENERATOR和CPACK_PACKAGE_VERSION开始,慢慢加。别一上来就想把所有格式都配好,容易乱。
知识体系总览
下面这张图把CMake内置模块的核心逻辑串起来了。你可以看到,从依赖管理到测试再到打包,是一条完整的流水线。
这张图把四个核心模块的关系讲清楚了。FetchContent负责「拿来」,CTest负责「验证」,CPack负责「交付」,FindXxx系列负责「查找」。各司其职,互不干扰。
我个人建议,新项目起步时先把FetchContent和CTest配好。依赖管理和测试是基本功,这两样搞定了,项目质量就有底了。CPack可以等产品稳定了再配,不急。