20、CPack与打包:CPack配置、生成器(NSIS、DEB、RPM、DMG)、组件安装、安装脚本

说到软件交付,编译通过只是第一步。你想想看,代码写得再好,用户拿到的是一堆零散的二进制文件和头文件,这体验能好吗?CPack 就是 CMake 体系里专门解决这个问题的工具。它能把你的构建产物打包成各种平台下的安装包——Windows 上的 NSIS 安装程序、Linux 上的 DEB/RPM 包、macOS 上的 DMG 镜像,统统不在话下。

我个人习惯把 CPack 看作是 CMake 的「最后一公里」工具。前面折腾了那么多目标属性、生成器表达式,最终不就是为了让用户能顺利用上你的库或应用吗?

20.1 CPack 的基本配置

CPack 的配置其实很简单。你只需要在 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 "A fantastic application")

# 联系方式
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")

# 必须包含这一行
include(CPack)

我曾经见过有人把 include(CPack) 写在了变量设置之前,结果打包出来的版本号全是空的。嗯,这里要注意:CPack 是在 include 的那一刻读取这些变量的,所以一定要先设置好。

小技巧: 如果你有多个子项目,建议在顶层 CMakeLists.txt 里统一配置 CPack 变量。这样维护起来方便,不会出现版本号不一致的尴尬。

20.2 选择打包生成器

CPack 支持多种打包格式,通过 CPACK_GENERATOR 变量来指定。你可以同时指定多个生成器,CPack 会依次生成对应的安装包。

生成器 平台 典型用途
NSIS Windows 制作 .exe 安装程序
DEB Linux (Debian/Ubuntu) 制作 .deb 包
RPM Linux (Red Hat/Fedora) 制作 .rpm 包
DMG macOS 制作 .dmg 磁盘映像
ZIP/TGZ 跨平台 通用压缩包
# 根据平台自动选择生成器
if(WIN32)
    set(CPACK_GENERATOR "NSIS")
elseif(APPLE)
    set(CPACK_GENERATOR "DMG")
elseif(UNIX)
    # 检测包管理器
    find_program(DPKG_CMD dpkg)
    find_program(RPM_CMD rpm)
    if(DPKG_CMD)
        set(CPACK_GENERATOR "DEB")
    elseif(RPM_CMD)
        set(CPACK_GENERATOR "RPM")
    else()
        set(CPACK_GENERATOR "TGZ")
    endif()
endif()

说白了,你不需要在代码里写死生成器。让 CMake 根据目标平台自动选择,这才是现代构建系统的做法。

20.3 组件安装

大型项目通常包含多个组件:核心库、开发头文件、文档、示例代码等。用户可能只需要安装运行时组件,而不需要开发文件。这时候就需要组件安装

# 安装核心库
install(TARGETS mylib
    LIBRARY DESTINATION lib
    COMPONENT runtime
)

# 安装开发头文件
install(DIRECTORY include/
    DESTINATION include
    COMPONENT development
)

# 安装文档
install(FILES README.md LICENSE
    DESTINATION share/doc/MyApp
    COMPONENT documentation
)

# 启用组件打包
set(CPACK_COMPONENTS_ALL runtime development documentation)
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "Runtime libraries and executables")
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "Headers and development files")

我在项目中遇到过这样的情况:用户只需要运行程序,却被迫安装了整套开发环境。用了组件安装后,用户可以选择只安装 runtime 组件,安装包体积直接缩小了 80%。

核心要点: 组件安装的关键在于 COMPONENT 关键字。每个 install() 命令都可以指定所属组件,然后通过 CPACK_COMPONENTS_ALL 列出所有组件。

20.4 安装脚本

有些时候,光靠 CMake 的安装规则还不够。比如你需要在安装后创建快捷方式、注册文件关联、或者运行一些后处理脚本。这时候就需要安装脚本

# 安装前执行的脚本
set(CPACK_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pre_install.cmake")

# 安装后执行的脚本
set(CPACK_POST_INSTALL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/post_install.cmake")

# NSIS 特有的脚本
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "
    CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyApp.lnk' '$INSTDIR\\\\bin\\\\myapp.exe'
")

举个例子,我曾经需要打包一个数据库驱动的应用。安装后必须执行一个脚本来初始化数据库。用 CPACK_POST_INSTALL_SCRIPT 就完美解决了这个问题。

# post_install.cmake
message("Initializing database...")
execute_process(COMMAND ${CMAKE_INSTALL_PREFIX}/bin/init_db.sh)
message("Database initialized successfully!")
注意: 安装脚本是在目标机器上执行的。不要假设目标机器上有和开发环境一样的工具链。尽量使用 CMake 自带的命令,或者确保依赖的工具已经安装。

20.5 生成器特定配置

不同的打包格式有不同的特性。CPack 允许你为每个生成器设置专属变量。

# NSIS 配置
set(CPACK_NSIS_DISPLAY_NAME "MyApp ${CPACK_PACKAGE_VERSION}")
set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Uninstall.lnk' '$INSTDIR\\\\uninstall.exe'")

# DEB 配置
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@mycompany.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl-dev (>= 1.1), libcurl4")

# RPM 配置
set(CPACK_RPM_PACKAGE_RELEASE "1")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")

# DMG 配置
set(CPACK_DMG_VOLUME_NAME "MyApp ${CPACK_PACKAGE_VERSION}")
set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/cmake/DS_Store")

你想想看,如果不做这些配置,生成的 DEB 包连依赖关系都没有,用户安装时根本不知道还需要 libssl。这包发出去,不是等着被吐槽吗?

20.6 知识体系总览

下面这张图总结了 CPack 打包的核心流程和关键配置点:

CPack 打包流程与配置体系 CMakeLists.txt 配置 核心配置变量 CPACK_PACKAGE_NAME / VERSION / VENDOR / CONTACT 组件安装 COMPONENT 关键字 安装脚本 PRE/POST 安装脚本 生成器配置 CPACK_GENERATOR 支持的打包生成器 NSIS (Windows) | DEB (Debian) | RPM (Red Hat) | DMG (macOS) | ZIP/TGZ (通用) 生成安装包 (.exe/.deb/.rpm/.dmg)

20.7 实战经验总结

最后分享几个我踩过的坑:

  • 版本号一致性: 我曾经因为忘记更新 CPACK_PACKAGE_VERSION,导致发布的 DEB 包版本号还是上一个版本的。从那以后,我养成了在发布前用 cpack --version 检查配置的习惯。
  • 依赖关系: 对于 DEB 和 RPM 包,一定要正确填写 CPACK_DEBIAN_PACKAGE_DEPENDSCPACK_RPM_PACKAGE_REQUIRES。否则用户安装时会遇到「缺少依赖」的错误。
  • 路径问题: NSIS 安装路径默认是 $PROGRAMFILES,但 64 位应用应该用 $PROGRAMFILES64。这个细节我一开始没注意,导致用户安装到了错误的目录。
  • 测试打包: 我建议在 CI 流程中加入打包测试。每次提交代码都自动生成安装包,确保打包过程不会因为代码变更而损坏。

CPack 其实不复杂,但细节很多。你只要把配置变量理清楚,组件划分好,脚本写对,就能生成专业级别的安装包。说白了,这就是一个「一次配置,到处打包」的过程。

一句话总结: CPack 的核心是「配置变量 + 组件划分 + 生成器选择」。把这三点做好,你的软件交付就成功了一大半。

公众号:蓝海资料掘金营,微信deep3321