23. 6.3 CPack打包系统

引言:从”交钥匙”到”精装礼盒”

在前两节(6.1和6.2)中,我们的”施工队长”CMake已经完成了大楼的建造(编译)、房间钥匙的交付(install),甚至把建筑图纸(Export配置包)分享给了其他开发商。但对于真正的软件产品来说,这还不够——你不能要求普通用户自己打开工地大门、找到电梯间、手动把家具搬进去。

最终用户需要的是一个可双击运行的安装包:Windows用户想要.exe安装向导,Ubuntu用户想要.deb包一键安装,macOS用户期待漂亮的.dmg磁盘镜像。把已安装的文件、元数据、依赖关系和安装逻辑封装成平台原生的分发格式,这就是CPack的使命。

如果把install规则比作”交房仪式”,那么CPack就是楼盘的精装礼盒部:它把房子、家具、说明书、保修卡按不同客户的习俗打包成不同样式的礼盒。最棒的是,它几乎不增加额外工作量——因为它直接复用你在6.1中写好的所有install规则!

CPack基础配置

启用CPack非常简单,通常只需在你的顶层CMakeLists.txt末尾(所有install规则之后)添加:

include(CPack)

这行命令就像对CMake说:”请启动打包部门。”但一个合格的安装包至少需要名字、版本和描述,否则用户收到的是一个没有标签的盲盒。我们来补充最基本的元数据:

set(CPACK_PACKAGE_NAME "MyAwesomeApp")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_VERSION_MAJOR "1")
set(CPACK_PACKAGE_VERSION_MINOR "2")
set(CPACK_PACKAGE_VERSION_PATCH "3")
set(CPACK_PACKAGE_VERSION "${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A cross-platform C++ application")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://example.com")
set(CPACK_PACKAGE_CONTACT "support@example.com")

include(CPack)

关键约定:这些变量必须include(CPack)之前设置,因为CPack模块被包含时会读取这些变量并生成打包配置。如果顺序写反,CPack只能使用默认值。

生成器选择:给不同客户不同的礼盒

CPack的强大之处在于”一次配置,到处打包”。它内置了多种生成器(Generator),能把同样的install输出转换成不同平台原生的包格式。

常用生成器一览

  • DEB:Debian/Ubuntu系的.deb软件包,适合apt安装。
  • RPM:Fedora/RHEL/openSUSE系的.rpm软件包,适合yum/dnf安装。
  • NSIS:Windows上的Nullsoft脚本安装系统,生成经典的Setup.exe向导。
  • DragNDrop:macOS上的.dmg磁盘镜像,支持拖拽安装。
  • productbuild:macOS更现代的pkg安装包格式。
  • TGZ / TXZ / ZIP:跨平台的压缩包,开箱即用,没有复杂的安装向导。
  • WIX:Windows Installer XML,生成.msi包,适合企业部署。

如何指定生成器

有两种方式。第一种是在CMakeLists.txt中设置默认生成器列表:

set(CPACK_GENERATOR "DEB;RPM;TGZ")

这样当你在Linux上运行cpack时,它会同时产出.deb.rpm.tar.gz三种格式。

第二种更灵活:在命令行通过-G选项临时覆盖,适合CI/CD流水线针对不同平台分别打包:

# Windows上生成NSIS安装程序
cpack -G NSIS

# Ubuntu上生成DEB包
cpack -G DEB

# macOS上生成DMG
cpack -G DragNDrop

小贴士:并不是所有生成器在所有平台都能工作。例如,DEB生成器需要Linux环境和dpkg工具链;NSIS生成器需要Windows上安装NSIS软件。CPack会自动检测不可用的生成器并跳过,但你也可以通过set(CPACK_GENERATOR "TGZ")确保在任意机器上至少能生成压缩包。

包元数据配置:让包管理器认识你

现代操作系统的包管理器(如apt、dnf、brew)非常”挑剔”:它们需要准确的名称、版本、描述、授权协议,甚至精美的欢迎界面。CPack允许你把这些信息全部写进CMakeLists.txt。

核心元数据变量

  • CPACK_PACKAGE_NAME:包名,通常全小写,无空格(如myapp)。
  • CPACK_PACKAGE_VERSION:完整的版本字符串(如1.2.3)。
  • CPACK_PACKAGE_VENDOR:开发商或组织名称。
  • CPACK_PACKAGE_DESCRIPTION_SUMMARY:单行摘要,显示在包管理器列表中。
  • CPACK_PACKAGE_DESCRIPTION_FILE:指向一个包含长描述的文本文件路径。
  • CPACK_RESOURCE_FILE_LICENSE:许可证文件路径,安装向导通常会展示它让用户确认。
  • CPACK_RESOURCE_FILE_README:README文件路径。
  • CPACK_PACKAGE_CONTACT:维护者邮箱,DEB和RPM包必填。

一个更完整的示例

set(CPACK_PACKAGE_NAME "calcengine")
set(CPACK_PACKAGE_VERSION "2.1.0")
set(CPACK_PACKAGE_VENDOR "OpenMath Org")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A high-performance calculation engine")
set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.txt")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt")
set(CPACK_PACKAGE_CONTACT "dev@openmath.org")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://openmath.org/calcengine")

include(CPack)

打包后,用户在安装calcengine_2.1.0_amd64.deb时,apt就能显示完整的描述和版权信息,而不是冷冰冰的文件名。

依赖声明:自动处理” Prerequisites”

一个C++程序很少孤立运行。你的应用可能依赖Qt、OpenSSL或特定的系统库。CPack允许你在打包时就声明这些依赖,让包管理器自动帮你安装它们。

注意:不同打包格式的依赖语法不同,CPack为它们准备了不同的变量:

DEB包的依赖

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl3 (>= 3.0.0), zlib1g, libqt6core6")

语法遵循Debian控制文件规范:包名之间用逗号分隔,版本约束写在括号内。

RPM包的依赖

set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 3.0, zlib, qt6-qtbase")

RPM的依赖通常用逗号或空格分隔。CPack会把它写进.spec文件,最终用户用dnf install your-package.rpm时,系统会自动解析并拉取缺失的库。

NSIS的依赖

Windows没有统一的包管理器,NSIS安装程序通常会把所有依赖的DLL(如VC++运行库)直接捆绑进安装包,或者提供一个”下载并安装运行库”的选项。这需要在NSIS脚本模板中额外配置,已超出基础教学范围,但你可以通过CPACK_NSIS_EXTRA_INSTALL_COMMANDS嵌入自定义NSIS指令。

通用建议

对于跨平台项目,建议这样组织依赖配置:

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    find_program(DPKG_PROGRAM dpkg)
    if(DPKG_PROGRAM)
        set(CPACK_GENERATOR "${CPACK_GENERATOR};DEB")
        set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl3, zlib1g")
    endif()
    
    find_program(RPMBUILD_PROGRAM rpmbuild)
    if(RPMBUILD_PROGRAM)
        set(CPACK_GENERATOR "${CPACK_GENERATOR};RPM")
        set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 3.0, zlib")
    endif()
endif()

这样CMake会自动检测当前Linux发行版支持哪种包格式,并设置对应的依赖。

预安装与后安装脚本:定制安装流程

有时候,标准的”复制文件到目录”无法满足需求。比如:

  • 安装前需要检查系统中是否已有旧版本,并停止相关服务。
  • 安装后需要创建系统用户、注册服务或更新动态链接器缓存(ldconfig)。
  • 卸载前需要备份配置文件。

这类”安装前/后”的自定义操作,可以通过控制脚本(Control Scripts)实现。不同打包格式支持不同的脚本注入方式。

DEB包的维护者脚本

Debian系支持四个经典脚本:preinst(安装前)、postinst(安装后)、prerm(卸载前)、postrm(卸载后)。你可以把它们放在项目目录中,然后告诉CPack:

set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA 
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/preinst"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/postinst"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/prerm"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/postrm")

这些脚本必须是可执行的shell脚本。例如一个典型的postinst可能包含:

#!/bin/sh
set -e
# 更新共享库缓存
ldconfig
# 创建日志目录
mkdir -p /var/log/calcengine
chown nobody:nogroup /var/log/calcengine

RPM包的脚本

RPM使用类似的机制,但变量名不同:

set(CPACK_RPM_PRE_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm_preinst.sh")
set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm_postinst.sh")
set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm_prerm.sh")
set(CPACK_RPM_POST_UNINSTALL_SCRIPT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/packaging/rpm_postrm.sh")

跨平台的抽象

如果你希望逻辑统一,可以在CMake中写一个函数,根据当前打包格式自动映射到对应的变量。但对于零基础学习者,建议先掌握格式特定的变量,再考虑抽象。

多组件打包:给用户选择权

在6.1节中,我们介绍了install(... COMPONENT ...)可以把安装内容划分为不同组件(如Runtime、Development、Documentation)。CPack可以把这些组件变成可选的安装项,让用户在安装时自由选择要装哪些。

启用组件化打包

默认情况下,CPack会把所有组件打成一个巨大的包。要启用组件化安装,需要设置:

set(CPACK_COMPONENTS_ALL Runtime Development Documentation)
set(CPACK_COMPONENTS_GROUPING "ONE_PER_GROUP")  # 或 IGNORE, ALL_COMPONENTS_IN_ONE

各选项含义:

  • ALL_COMPONENTS_IN_ONE:默认值,所有组件塞进同一个包。
  • ONE_PER_GROUP:每个组件(或组件组)单独成一个包。
  • IGNORE:忽略组件信息,按传统方式打包。

为组件设置人类可读的名字

set(CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "核心应用程序")
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "运行程序所需的库和可执行文件")
set(CPACK_COMPONENT_DEVELOPMENT_DISPLAY_NAME "开发文件")
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "头文件和静态库,用于二次开发")
set(CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "文档")
set(CPACK_COMPONENT_DOCUMENTATION_DESCRIPTION "用户手册和API参考")

在NSIS或WIX生成器中,这些名字会直接显示在安装向导的复选框列表里。

安装类型(Installation Types)

对于支持图形化安装的生成器(如NSIS),你还可以预定义”典型”、”最小”、”完整”等安装模式:

set(CPACK_NSIS_COMPONENT_INSTALL ON)
set(CPACK_NSIS_MODIFY_PATH ON)  # 提供添加PATH的选项

set(CPACK_COMPONENTS_ALL Runtime Development)

cpack_add_install_type(Full DISPLAY_NAME "完整安装")
cpack_add_install_type(Minimal DISPLAY_NAME "最小安装")

cpack_add_component(Runtime REQUIRED INSTALL_TYPES Full Minimal)
cpack_add_component(Development INSTALL_TYPES Full)

这样,选择”最小安装”的用户只会得到Runtime组件,而选择”完整安装”的用户则会得到全部内容。

生成命令:运行打包流水线

配置完成后,生成安装包就是几条命令的事。假设你已经在build目录完成配置和构建:

基本用法

cd build
cpack

这会使用CPACK_GENERATOR中列出的所有生成器,在build目录下产出最终的包文件。

常用命令行选项

  • cpack -G DEB:临时指定生成器,覆盖CMakeLists中的默认值。
  • cpack -C Release:显式指定构建配置(对多配置生成器如Visual Studio特别重要)。
  • cpack -D CPACK_PACKAGE_VERSION=2.1.1:临时覆盖某个CPack变量,适合CI流水线动态注入版本号。
  • cpack --verbose:输出详细的打包过程,排查问题时很有用。

输出位置

CPack默认会把生成的包放在build目录下。例如:

build/
├── _CPack_Packages/          # 中间文件(可删除)
│   └── Linux/
│       └── DEB/
│           └── ...           # 临时展开的树
├── calcengine_2.1.0_amd64.deb
├── calcengine-2.1.0-Linux.rpm
└── calcengine-2.1.0-Linux.tar.gz

通常你只需要把最终那几个.deb.rpm.exe文件分发给用户即可,_CPack_Packages目录是中间缓存,可以安全删除。

小结

CPack是CMake生态中容易被忽视但极其强大的一环。它让你无需学习DEB控制文件、RPM spec语法或NSIS脚本语言,就能产出专业级的跨平台安装包。核心要点回顾:

  1. Minimal Setup:设置元数据变量 → include(CPack) → 完成。
  2. 复用Install规则:CPack自动收集你在6.1中定义的所有install目标,无需重复劳动。
  3. 平台适配:通过CPACK_GENERATOR和条件判断,为不同操作系统生成最合适的包格式。
  4. 元数据与依赖:完善的描述和依赖声明,让你的包能融入系统包管理器的生态。
  5. 生命周期脚本:利用preinst/postinst等脚本处理安装前后的系统级操作。
  6. 组件化分发:结合install的COMPONENT,实现模块化安装包。

至此,第六章”安装、打包与发布”的三节内容已经完整:我们学会了如何把编译好的产物安装到系统目录(6.1),如何把项目导出为其他CMake项目可用的配置包(6.2),以及如何用CPack生成面向终端用户的平台原生安装包(6.3)。

在下一章,我们将进入测试与质量保障的领域,学习如何让CMake自动运行测试、分析代码覆盖率,并集成静态分析工具,确保我们的”建筑”不仅外观漂亮,结构也足够坚固。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……