20、CPack打包:如何使用CPack将你的项目打包成安装程序(如NSIS、DEB、RPM)
说实话,CMake 的项目管理能力很强,但很多朋友做完项目就卡在「怎么给别人用」这一步。你总不能让人家先装 CMake、再编译一遍吧?
这时候就需要 CPack 出场了。它是 CMake 自带的打包工具,能把你的项目一键打包成各种安装包格式——Windows 上的 NSIS 安装程序、Linux 上的 DEB/RPM 包、macOS 上的 DMG 等等。
我个人习惯在项目收尾阶段,花半小时配置好 CPack,后面每次发版就一条命令搞定。省心。
核心要点:CPack 不是独立的打包工具,它依赖 CMake 的 install 规则。你先把安装规则写清楚,CPack 负责把这些规则「装进」不同格式的安装包里。
20.1 CPack 的基本工作流程
先看一张图,理解 CPack 在整个构建流程中的位置:
流程其实很简单:你先用 CMake 配置好项目,编译通过后,用 cmake --install 把文件安装到一个临时目录。最后 CPack 把这个临时目录打包成你想要的格式。
小技巧:我建议你在开发阶段就用 cmake --install --prefix ./install 测试安装效果。确认文件都装对了位置,再跑 CPack。不然打包出来发现少了文件,还得重新来一遍。
20.2 基础配置:CMakeLists.txt 中的 CPack 设置
CPack 的配置写在 CMakeLists.txt 里,或者单独写一个 CPackConfig.cmake 文件。我个人习惯写在主 CMakeLists.txt 的末尾,这样所有配置一目了然。
# 在 CMakeLists.txt 末尾添加
include(CPack)
# 基础信息
set(CPACK_PACKAGE_NAME "MyApp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "一个实用的桌面工具")
# 联系方式
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")
# 安装目录
set(CPACK_PACKAGE_INSTALL_DIRECTORY "MyApp")
# 许可证文件(如果有)
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt")
嗯,这里要注意:include(CPack) 必须放在所有 install 规则之后。因为 CPack 模块会读取当前已经定义的 install 规则。
20.3 打包成 Windows 安装程序(NSIS)
Windows 上最常用的安装包格式是 NSIS(Nullsoft Scriptable Install System)。CPack 对 NSIS 的支持很成熟,我做过好几个项目都用它。
# NSIS 专属配置
set(CPACK_GENERATOR "NSIS")
# 安装程序图标
set(CPACK_NSIS_INSTALL_ICON "${CMAKE_SOURCE_DIR}/icons/install.ico")
# 卸载程序图标
set(CPACK_NSIS_UNINSTALL_NAME "uninstall.exe")
# 创建桌面快捷方式
set(CPACK_NSIS_CREATE_ICONS_EXTRA
"CreateShortCut '$DESKTOP\\MyApp.lnk' '$INSTDIR\\bin\\myapp.exe'")
# 创建开始菜单快捷方式
set(CPACK_NSIS_MENU_LINKS
"bin/myapp.exe" "MyApp"
"doc/readme.txt" "使用说明")
# 安装时显示许可证
set(CPACK_NSIS_MODIFY_PATH ON)
踩坑提醒:我曾经在配置 NSIS 时,忘记设置 CPACK_NSIS_INSTALL_ICON,结果生成的安装程序图标是默认的 NSIS 图标。用户反馈说「这软件看起来像病毒」。从那以后,我每次都会检查图标和版本号。
20.4 打包成 Linux 包(DEB / RPM)
Linux 上打包稍微复杂一点,因为 DEB 和 RPM 对包的结构有严格要求。不过 CPack 帮我们处理了大部分细节。
DEB 包配置
# DEB 专属配置
set(CPACK_GENERATOR "DEB")
# 依赖关系
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.28), libqt5widgets5 (>= 5.12)")
# 包维护者
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@mycompany.com")
# 优先级和分类
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
# 架构
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
RPM 包配置
# RPM 专属配置
set(CPACK_GENERATOR "RPM")
# 依赖关系
set(CPACK_RPM_PACKAGE_REQUIRES "libc >= 2.28, qt5-qtbase >= 5.12")
# 包分组
set(CPACK_RPM_PACKAGE_GROUP "Applications/Utilities")
# 许可证
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
# 发行说明
set(CPACK_RPM_PACKAGE_RELEASE "1")
经验之谈:DEB 和 RPM 的依赖名称不一样。比如 Qt5 在 Ubuntu 上叫 libqt5widgets5,在 CentOS 上叫 qt5-qtbase。我一般会在 CI 脚本里分别测试两种格式的安装和卸载。
20.5 多格式打包与条件判断
实际项目中,你可能需要同时生成多种格式。CPack 支持一次配置,多次打包。
# 同时生成 NSIS 和 DEB
set(CPACK_GENERATOR "NSIS;DEB")
# 或者根据平台自动选择
if(WIN32)
set(CPACK_GENERATOR "NSIS")
elseif(UNIX AND NOT APPLE)
set(CPACK_GENERATOR "DEB")
endif()
打包命令也很简单:
# 生成所有配置好的格式
cpack
# 指定生成某一种格式
cpack -G NSIS
# 指定配置文件
cpack -C CPackConfig.cmake
20.6 组件打包:按需安装
如果你的项目包含多个组件(比如主程序、文档、开发库),可以启用组件打包。用户安装时可以选择只装一部分。
# 启用组件安装
set(CPACK_COMPONENTS_ALL_IN_ONE_PACKAGE OFF)
set(CPACK_COMPONENTS_GROUPING "ONE_PER_GROUP")
# 定义组件
install(TARGETS myapp
DESTINATION bin
COMPONENT Runtime)
install(FILES readme.txt
DESTINATION doc
COMPONENT Documentation)
install(DIRECTORY include/
DESTINATION include
COMPONENT Development)
# 设置组件描述
set(CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "运行时")
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "程序运行所需的文件")
set(CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "文档")
set(CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION "使用说明和帮助文档")
set(CPACK_COMPONENT_DEVELOPMENT_DISPLAY_NAME "开发库")
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "二次开发所需的头文件和库")
实用场景:我做过一个 SDK 项目,用户只需要运行时组件,开发者才需要 Development 组件。组件打包让安装包体积缩小了 60%,用户反馈安装速度明显快了。
20.7 自定义安装脚本
有些安装步骤 CPack 无法自动处理,比如注册服务、写注册表、创建用户。这时候需要自定义脚本。
# NSIS 安装前/后脚本
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "
ExecWait '$INSTDIR\\bin\\register_service.exe'
")
set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "
ExecWait '$INSTDIR\\bin\\unregister_service.exe'
")
# DEB 安装后脚本
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/debian/postinst"
"${CMAKE_SOURCE_DIR}/debian/prerm"
)
注意:自定义脚本要小心处理权限问题。我曾经在 postinst 脚本里写了一个 chmod 777,结果被安全团队打回来说有安全隐患。建议用最小权限原则。
20.8 版本管理与发布
打包时版本号要跟项目版本保持一致。我习惯用 Git 标签来管理版本。
# 从 Git 获取版本号
execute_process(
COMMAND git describe --tags --always
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
set(CPACK_PACKAGE_VERSION ${GIT_VERSION})
# 打包文件名包含版本
set(CPACK_PACKAGE_FILE_NAME
"${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}")
这样打包出来的文件名类似 MyApp-1.2.3-Linux.deb,一看就知道是什么版本、什么平台。
20.9 常见问题与排查
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 打包后缺少文件 | install 规则没覆盖所有文件 | 用 cmake --install 测试安装目录 |
| DEB 包安装报依赖错误 | 依赖名称写错或版本不对 | 用 dpkg -I 查看包信息 |
| NSIS 安装程序图标不显示 | 图标路径不对或格式不支持 | 使用 .ico 格式,路径用绝对路径测试 |
| RPM 包签名失败 | 没有配置 GPG 密钥 | 配置 %_gpg_name 宏 |
调试技巧:CPack 会在构建目录生成 _CPack_Packages 文件夹,里面是打包的中间文件。如果打包失败,进去看看文件结构对不对。我经常用这招排查问题。
好了,CPack 的核心用法就这些。说白了,它就是把你写好的 install 规则「翻译」成各种安装包格式。你只要把 install 规则写清楚,CPack 就能帮你搞定剩下的工作。
我个人觉得,CPack 最爽的地方在于——你只需要维护一套 install 规则,就能同时生成 Windows、Linux、macOS 的安装包。省下的时间,喝杯咖啡不香吗?
公众号:蓝海资料掘金营,微信deep3321