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 -B build 编译构建 cmake --build build 安装到临时目录 cmake --install build CPack 打包 cpack -G NSIS 安装包产物 .exe / .deb / .rpm CPack 工作流程:配置 → 编译 → 安装 → 打包 CPack 打包流程 注意:CPack 读取的是 install 规则,不是编译产物本身

流程其实很简单:你先用 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