17、CPack与安装规则联动:理解CPack基于cmake --install的工作原理、确保安装规则正确是打包的前提

说实话,我见过太多人把CPack当成一个「黑盒」来用。写几个install()命令,然后敲cpack,能跑就行。但一旦打包出来的东西缺文件、路径不对、或者根本装不上,就开始抓瞎了。

嗯,今天我们就来把这个「黑盒」拆开看看。CPack到底是怎么工作的?它跟cmake --install是什么关系?为什么说安装规则是打包的基石?

CPack的核心逻辑:它其实是个「安装模拟器」

先讲个我自己的经历。几年前我接手一个嵌入式项目,CMakeLists.txt写得那叫一个乱。我加了install(TARGETS ...),也写了install(FILES ...),但一跑cpack,生成的安装包总是少几个动态库。我当时就纳闷:明明编译都过了,为什么打包会丢文件?

后来我才明白——CPack本质上是在模拟一次cmake --install的过程。它不会去管你的源代码目录里有什么,它只关心你通过install()命令「告诉」它要安装什么。

说白了,CPack的工作流程是这样的:

  1. 先编译你的项目(如果你没关掉BUILD阶段)
  2. 然后在一个临时目录里执行一次「虚拟安装」
  3. 把虚拟安装目录里的所有文件打包成你指定的格式

所以,如果你的install()规则写漏了,CPack打包出来的东西就一定缺文件。这不是CPack的bug,而是你的安装规则没写全。

核心结论:CPack打包的内容 = 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基于cmake --install的工作原理 CMake配置阶段 解析install()命令 生成安装脚本 cmake_install.cmake CPack打包 调用安装脚本 虚拟安装过程(CPack内部执行) 创建临时目录 /tmp/_CPack_Packages/ 执行安装脚本 cmake_install.cmake 文件复制 到暂存区 生成安装包 .deb / .rpm / .tar.gz / .zip ... 关键点:CPack打包的内容 = cmake --install 安装的内容

你看,整个流程其实不复杂。但有一个细节很多人会忽略:CPack在虚拟安装时,使用的安装前缀是CPack自己决定的,不是你指定的CMAKE_INSTALL_PREFIX。所以如果你的install()规则里写了绝对路径,或者依赖了某个特定的目录结构,打包时就可能出问题。

如何验证你的安装规则是否正确?

我个人的工作流程是这样的,分享给你参考:

  1. 先编译,再安装到临时目录
    cmake -B build -DCMAKE_INSTALL_PREFIX=/tmp/staging
    cmake --build build
    cmake --install build --prefix /tmp/staging
  2. 检查临时目录的内容
    tree /tmp/staging

    看看是不是所有需要的文件都在。缺什么就补什么。

  3. 确认无误后,再打包
    cd build
    cpack

这一步看起来多花了几分钟,但能帮你避免「打包一时爽,交付火葬场」的悲剧。

小技巧:你可以在CMakeLists.txt里加一个自定义target,一键完成「安装到临时目录 + 检查」的操作:
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上场。这样打包出来的东西,才真正是你想交付给用户的。