17、CPack与安装规则联动:理解CPack基于cmake --install的工作原理、确保安装规则正确是打包的前提
说实话,我见过太多人把CPack当成一个「黑盒」来用。写几个install()命令,然后敲cpack,能跑就行。但一旦打包出来的东西缺文件、路径不对、或者根本装不上,就开始抓瞎了。
嗯,今天我们就来把这个「黑盒」拆开看看。CPack到底是怎么工作的?它跟cmake --install是什么关系?为什么说安装规则是打包的基石?
CPack的核心逻辑:它其实是个「安装模拟器」
先讲个我自己的经历。几年前我接手一个嵌入式项目,CMakeLists.txt写得那叫一个乱。我加了install(TARGETS ...),也写了install(FILES ...),但一跑cpack,生成的安装包总是少几个动态库。我当时就纳闷:明明编译都过了,为什么打包会丢文件?
后来我才明白——CPack本质上是在模拟一次cmake --install的过程。它不会去管你的源代码目录里有什么,它只关心你通过install()命令「告诉」它要安装什么。
说白了,CPack的工作流程是这样的:
- 先编译你的项目(如果你没关掉BUILD阶段)
- 然后在一个临时目录里执行一次「虚拟安装」
- 把虚拟安装目录里的所有文件打包成你指定的格式
所以,如果你的install()规则写漏了,CPack打包出来的东西就一定缺文件。这不是CPack的bug,而是你的安装规则没写全。
cmake --install安装的内容。两者完全等价。
从cmake --install到CPack:一条隐形的管道
我们来看看这条管道是怎么连起来的。假设你有一个简单的项目:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyApp VERSION 1.0.0)
add_executable(myapp main.cpp)
install(TARGETS myapp RUNTIME DESTINATION bin)
install(FILES config.ini DESTINATION etc)
当你执行cmake --install . --prefix /tmp/staging时,CMake会做三件事:
- 把
myapp可执行文件复制到/tmp/staging/bin/ - 把
config.ini复制到/tmp/staging/etc/ - 生成一个
cmake_install.cmake脚本,记录所有安装操作
而CPack呢?它其实就是在内部调用了这个cmake_install.cmake脚本。只不过安装的目标目录不是用户指定的路径,而是CPack自己创建的一个临时目录。
我习惯把这个临时目录叫做「打包暂存区」。CPack把所有文件装进这个暂存区,然后一股脑打包成deb、rpm、或者zip。就这么简单。
安装规则写不对,打包一定翻车
这里我列几个最常见的「翻车现场」,都是我在项目中踩过的坑:
| 问题 | 表现 | 原因 |
|---|---|---|
| 缺少共享库 | 安装后程序启动报错 | 没有用install(TARGETS ... LIBRARY)安装.so文件 |
| 头文件没装 | 其他项目引用时报找不到头文件 | 忘了写install(DIRECTORY include/ DESTINATION include) |
| 配置文件路径不对 | 程序运行时找不到配置文件 | DESTINATION写成了绝对路径,或者用了错误的相对路径 |
| 资源文件缺失 | 界面图标显示不出来 | 资源文件没有通过install()纳入安装规则 |
我曾经在一个Qt项目里,漏掉了install(TARGETS ... RUNTIME DESTINATION bin)这一行。结果编译出来的exe文件根本没被打包进去。用户拿到安装包,双击没反应——因为根本就没装主程序。你说尴尬不尴尬?
install()规则后,先跑一遍cmake --install . --prefix /tmp/test_install,看看安装目录里是不是你想要的东西。确认无误后再跑cpack。这一步能省你大量调试时间。
CPack的安装流程:一张图说清楚
下面这张图展示了CPack从读取安装规则到生成安装包的完整流程。我建议你仔细看看中间那个「虚拟安装」环节——这是整个流程的核心。
你看,整个流程其实不复杂。但有一个细节很多人会忽略:CPack在虚拟安装时,使用的安装前缀是CPack自己决定的,不是你指定的CMAKE_INSTALL_PREFIX。所以如果你的install()规则里写了绝对路径,或者依赖了某个特定的目录结构,打包时就可能出问题。
如何验证你的安装规则是否正确?
我个人的工作流程是这样的,分享给你参考:
- 先编译,再安装到临时目录
cmake -B build -DCMAKE_INSTALL_PREFIX=/tmp/staging cmake --build build cmake --install build --prefix /tmp/staging - 检查临时目录的内容
tree /tmp/staging看看是不是所有需要的文件都在。缺什么就补什么。
- 确认无误后,再打包
cd build cpack
这一步看起来多花了几分钟,但能帮你避免「打包一时爽,交付火葬场」的悲剧。
add_custom_target(check-install
COMMAND ${CMAKE_COMMAND} --install . --prefix /tmp/staging
COMMAND tree /tmp/staging
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
然后跑cmake --build build --target check-install就行了。
一个完整的例子:从安装规则到CPack打包
最后,我们来看一个稍微完整点的例子。假设你有一个库项目,需要安装头文件、库文件、以及一个配置文件:
cmake_minimum_required(VERSION 3.15)
project(MyLib VERSION 2.1.0 LANGUAGES CXX)
# 添加库
add_library(mylib SHARED src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
# 安装规则
install(TARGETS mylib
EXPORT MyLibTargets
LIBRARY DESTINATION lib
PUBLIC_HEADER DESTINATION include
)
install(FILES config/mylib.conf DESTINATION etc)
install(DIRECTORY include/ DESTINATION include
FILES_MATCHING PATTERN "*.h"
)
# 导出配置
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
DESTINATION lib/cmake/MyLib
)
# CPack配置
set(CPACK_GENERATOR "DEB;TGZ")
set(CPACK_PACKAGE_NAME "mylib")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "me@example.com")
include(CPack)
这个例子涵盖了最常见的安装场景:目标文件、头文件、配置文件、以及CMake导出配置。你想想看,如果漏掉了install(DIRECTORY include/ ...)这一行,用户拿到你的库,连头文件都找不到,那这个库跟没装有什么区别?
嗯,说到头文件,我记得有一次帮同事排查问题。他打包了一个SDK,用户反馈说找不到某个头文件。我让他跑了一遍cmake --install,结果发现那个头文件根本就没在安装目录里。原因是他用了target_include_directories,但忘了加PUBLIC_HEADER属性。这种问题,光看CMakeLists.txt很难发现,但一跑安装就原形毕露了。
所以,我的建议是:把cmake --install当成你的「照妖镜」。每次修改安装规则后,都照一照。等确认没问题了,再让CPack上场。这样打包出来的东西,才真正是你想交付给用户的。