23. 6.3 CPack打包系统

导语

在前两节中,我们学习了如何通过 install() 规则将构建产物部署到系统目录,以及如何导出目标与配置包,让其他 CMake 项目能够方便地引用我们的库。然而,手动复制文件、整理目录结构、编写安装说明,这些工作在面对最终用户时仍然显得原始而繁琐。一个成熟的软件项目,除了能编译、能安装,还必须能够以标准安装包的形式分发给用户——无论是 Linux 下的 .deb.rpm,Windows 下的 .exe 安装向导,还是 macOS 下的 .dmg 磁盘镜像。

CMake 早就为我们准备好了这一环:CPack。它是与 CMake 紧密集成的打包工具,能够自动收集 install() 指令中声明的文件,结合你提供的元数据,一键生成各种平台原生的安装包。本节将系统讲解 CPack 的配置方法、多平台生成器选择、依赖声明、安装脚本以及多组件打包等核心技能。读完本节,你将能够为自己的项目赋予”一键打包、全平台分发”的能力。

CPack 基础配置

CPack 的使用门槛极低,但功能极其强大。它的核心思想是:复用已有的 install() 规则。你不需要重新告诉 CPack 要打包哪些文件,只需要在已有的 CMakeLists.txt 末尾引入 CPack 模块,并设置一些描述软件的变量即可。

最简化的 CPack 配置如下所示:

# 在 CMakeLists.txt 的最末尾
set(CPACK_PACKAGE_NAME "MyAwesomeApp")
set(CPACK_PACKAGE_VERSION_MAJOR 1)
set(CPACK_PACKAGE_VERSION_MINOR 2)
set(CPACK_PACKAGE_VERSION_PATCH 3)
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A brief description of my app")
set(CPACK_PACKAGE_FILE_NAME "MyAwesomeApp-1.2.3-Linux")

include(CPack)

请注意一个极易踩坑的细节:所有 CPACK_ 开头的变量必须在 include(CPack) 之前设置。因为 include(CPack) 会根据当前已定义的变量,在底层生成对应的 CPack 配置文件。如果你把 set 放在 include 之后,这些值将不会被采纳。

引入 CPack 后,你的构建目录中会自动生成两个关键文件:CPackConfig.cmakeCPackSourceConfig.cmake。前者用于打包二进制产物,后者用于打包源代码分发包(Source Package)。

生成器选择:为你的平台穿上正装

CPack 的强大之处在于其插件式的生成器(Generator)架构。同一个构建树,通过切换生成器,可以产出不同格式的安装包。常见的生成器包括:

  • DEB:Debian/Ubuntu 系的 .deb 软件包。
  • RPM:Fedora/RHEL/openSUSE 系的 .rpm 软件包。
  • NSIS:Windows 上的 Nullsoft Scriptable Install System,生成 .exe 安装向导。
  • NSIS64:64 位版本的 NSIS 生成器。
  • DragNDrop:macOS 上的 .dmg 磁盘镜像,支持拖拽安装。
  • productbuild:macOS 上更现代的 pkg 安装包(适合提交 Mac App Store)。
  • TGZ / TBZ2 / TXZ:压缩归档格式,跨平台通用。
  • ZIP:Windows 上最常见的压缩包格式。
  • CygwinBinary / CygwinSource:针对 Cygwin 环境的包。

你可以通过变量预设默认生成器,也可以在命令行动态覆盖:

# 在 CMakeLists.txt 中设置默认生成器(Linux 示例)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
    set(CPACK_GENERATOR "DEB;RPM;TGZ")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
    set(CPACK_GENERATOR "NSIS;ZIP")
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
    set(CPACK_GENERATOR "DragNDrop;productbuild")
endif()

在命令行中,使用 -G 参数指定生成器:

# 仅生成 DEB 包
cpack -G DEB

# 生成 NSIS 安装程序(需先安装 NSIS)
cpack -G NSIS

# 同时生成多种格式(用分号或多次 -G)
cpack -G "DEB;RPM" -C Release

需要注意的是,某些生成器依赖于系统工具。例如,生成 DEB 需要系统安装 dpkg-deb 工具;生成 RPM 需要 rpmbuild;生成 NSIS 则需要在 Windows 上安装 NSIS 程序,并将其添加到 PATH 中。如果工具缺失,CPack 会在打包阶段给出明确的错误提示。

包元数据配置:让软件包拥有完整身份

一个专业的安装包绝不只是文件的压缩集合,它必须包含软件名称、版本、描述、维护者、许可协议等元数据。CPack 提供了一套丰富的变量来定义这些信息。

以下是一份较为完整的元数据配置模板:

set(CPACK_PACKAGE_NAME "MyProject")
set(CPACK_PACKAGE_VERSION_MAJOR "2")
set(CPACK_PACKAGE_VERSION_MINOR "0")
set(CPACK_PACKAGE_VERSION_PATCH "1")
set(CPACK_PACKAGE_VERSION "2.0.1")

# 软件描述
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A modern C++ utility library")
set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")

# 供应商与联系信息
set(CPACK_PACKAGE_VENDOR "OpenSource Corp")
set(CPACK_PACKAGE_CONTACT "support@example.com")
set(CPACK_PACKAGE_MAINTAINER "John Doe ")

# 安装包文件名(可选,默认由 CPack 自动生成)
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}")

# 图标与品牌(主要用于 NSIS 和 DragNDrop)
set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/icon.ico")
set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/icon.ico")
set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/icon_uninstall.ico")

# 安装目录相关
set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/myproject")  # 对 DEB/RPM 生效
set(CPACK_NSIS_INSTALL_ROOT "C:\Program Files\MyProject")  # 对 NSIS 生效

其中,CPACK_PACKAGE_DESCRIPTION_FILE 指向的文件内容会被嵌入到安装包的描述字段中;CPACK_RESOURCE_FILE_LICENSE 的内容则会在图形化安装向导(如 NSIS 或 DragNDrop)中展示给用户,要求用户确认许可协议。

依赖声明:正确处理包级别依赖

在第 5 章中,我们讨论了构建阶段的依赖管理(find_packagetarget_link_libraries)。而在打包阶段,你需要告诉操作系统的包管理器:当用户安装你的软件包时,系统还必须预先安装哪些其他软件包。

不同包格式有不同的依赖声明变量。以下是 Debian 和 RPM 的示例:

# Debian (.deb) 特定依赖
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl1.1 (>= 1.1.1), libcurl4, zlib1g")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "https://example.com/myproject")

# RPM (.rpm) 特定依赖
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1.1, libcurl, zlib")
set(CPACK_RPM_PACKAGE_GROUP "Development/Libraries")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_URL "https://example.com/myproject")

# NSIS (Windows) 目前不直接支持声明系统级依赖,
# 但你可以通过 NSIS 脚本片段在安装时检测依赖是否存在
set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "
    ExecWait '\"$INSTDIR\\check_deps.exe\"'
")

对于 DEB 包,CPACK_DEBIAN_PACKAGE_DEPENDS 遵循 Debian 控制文件的依赖语法,支持版本约束(如 (>= 1.1.1))、逗号分隔的列表以及逻辑或(package1 | package2)。对于 RPM 包,CPACK_RPM_PACKAGE_REQUIRES 遵循 RPM 的规范。

重要提示:CPack 不会自动解析你构建时链接的库并转换成包依赖。你必须根据目标发行版的软件包命名规范,手动维护这些依赖字符串。对于复杂的跨平台项目,通常建议在 CMake 中配合 find_package 的结果,通过条件判断动态设置这些变量。

预安装与后安装脚本:安装前后的自定义操作

许多软件在安装前需要创建系统用户、停止旧版服务,或在安装后注册服务、更新动态链接库缓存。CPack 允许你为不同的包格式指定预安装(pre-install)后安装(post-install)脚本。

在 Debian 体系中,这些脚本对应经典的 preinstpostinstprermpostrm。在 RPM 中则对应 %pre%post%preun%postun。CPack 通过变量将它们一一映射:

# Debian 系列脚本
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/preinst"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/postinst"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/prerm"
    "${CMAKE_CURRENT_SOURCE_DIR}/packaging/debian/postrm"
)

# 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")

以 Debian 的 postinst 为例,一个典型的脚本可能长这样:

#!/bin/sh
set -e

# 更新动态链接器缓存
ldconfig

# 创建必要的日志目录
mkdir -p /var/log/myproject
chmod 755 /var/log/myproject

# 如果是首次安装,启动服务
if [ "$1" = "configure" ] && [ -z "$2" ]; then
    systemctl daemon-reload
    systemctl enable myproject.service
    systemctl start myproject.service
fi

exit 0

对于 NSIS,除了简单的 CPACK_NSIS_EXTRA_INSTALL_COMMANDS 外,你还可以通过 CPACK_NSIS_CREATE_ICONS_EXTRACPACK_NSIS_DELETE_ICONS_EXTRA 来创建或删除开始菜单/桌面的快捷方式。如果需要更复杂的逻辑,建议直接编写自定义的 NSIS 模板文件,并通过 CPACK_NSIS_INSTALLER_MUI_ICON_CODE 等变量注入。

多组件打包:精细化分发策略

在第 6.1 节中,我们介绍了 install()COMPONENT 参数。当与 CPack 结合时,这一特性的威力被完全释放:你可以将软件拆分为多个逻辑组件(如运行时、开发头文件、文档、示例),让用户按需安装。

首先,确保你的 install() 规则已经标注了组件名:

# 运行时库与可执行文件
install(TARGETS mylib myapp
    RUNTIME DESTINATION bin COMPONENT Runtime
    LIBRARY DESTINATION lib COMPONENT Runtime
    ARCHIVE DESTINATION lib COMPONENT Development
)

# 头文件
install(DIRECTORY include/ DESTINATION include COMPONENT Development)

# 文档
install(FILES README.md LICENSE DESTINATION share/doc/myproject COMPONENT Documentation)

# 示例程序
install(DIRECTORY examples/ DESTINATION share/myproject/examples COMPONENT Examples)

接下来,在 CPack 配置中声明这些组件及其分组信息:

set(CPACK_COMPONENTS_ALL Runtime Development Documentation Examples)

# 为组件设置更友好的显示名称
set(CPACK_COMPONENT_RUNTIME_DISPLAY_NAME "Runtime")
set(CPACK_COMPONENT_DEVELOPMENT_DISPLAY_NAME "Development Files")
set(CPACK_COMPONENT_DOCUMENTATION_DISPLAY_NAME "Documentation")
set(CPACK_COMPONENT_EXAMPLES_DISPLAY_NAME "Example Programs")

# 设置组件描述
set(CPACK_COMPONENT_RUNTIME_DESCRIPTION "Libraries and executables required to run the application")
set(CPACK_COMPONENT_DEVELOPMENT_DESCRIPTION "Header files and static libraries for development")

# 将组件归入逻辑组(可选,对 NSIS 等图形安装器特别有用)
set(CPACK_COMPONENT_RUNTIME_GROUP "Application")
set(CPACK_COMPONENT_DEVELOPMENT_GROUP "Development")
set(CPACK_COMPONENT_DOCUMENTATION_GROUP "Development")
set(CPACK_COMPONENT_EXAMPLES_GROUP "Development")

# 设置组的显示名称
set(CPACK_COMPONENT_GROUP_APPLICATION_DISPLAY_NAME "Core Application")
set(CPACK_COMPONENT_GROUP_DEVELOPMENT_DISPLAY_NAME "Development Tools")

# 设置默认安装的组件(未指定时)
set(CPACK_COMPONENT_RUNTIME_REQUIRED ON)  # 强制安装
set(CPACK_COMPONENT_DEVELOPMENT_DISABLED ON)  # 默认不勾选
set(CPACK_COMPONENT_DOCUMENTATION_DISABLED ON)

对于 NSIS 生成器,多组件打包会生成一个带有复选框的安装向导,用户可以自由选择要安装的组件。对于 DEBRPM,CPack 可以为每个组件生成独立的 .deb.rpm 包,文件名通常以 -componentname 为后缀。

如果你希望生成单个体积庞大的单体包而不是多个分包,可以显式关闭组件打包模式:

set(CPACK_MONOLITHIC_INSTALL ON)

生成命令与实战流程

配置完成后,打包的操作非常简单。首先按常规流程构建项目并执行安装规则(这一步确保 install() 所依赖的目标都已生成):

cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
cmake --install build --config Release --prefix ./staging

然后,进入构建目录,调用 cpack 命令:

cd build

# 使用默认生成器(由 CPACK_GENERATOR 预设)
cpack -C Release

# 显式指定生成器
cpack -G DEB -C Release
cpack -G NSIS64 -C Release
cpack -G DragNDrop -C Release

# 仅打包特定组件
cpack -G DEB -C Release --component Development

# 打包源代码(生成 tar.gz 源码包)
cpack -G TGZ --config CPackSourceConfig.cmake

# 指定输出目录(默认为 build 目录)
cpack -G ZIP -C Release -B ../packages

其中,-C Release 对于单配置生成器(如 Makefile、Ninja)来说非常重要,它告诉 CPack 去打包 Release 配置下的产物。对于多配置生成器(如 Visual Studio、Xcode),CPack 默认会使用你最近一次构建的配置,但显式指定 -C 可以避免意外。

如果你希望在 CI/CD 流水线中自动打包,通常推荐在脚本中显式设置 CPACK_GENERATOR 环境变量,或者通过 cmake -DCPACK_GENERATOR=DEB 传入,然后统一执行 cpack

小结

CPack 是 CMake 生态中承上启下的关键一环:它向上承接你通过 install() 精心组织的安装规则,向下对接操作系统原生的包管理生态。通过本节的学习,你掌握了:

  1. 如何通过 include(CPack)CPACK_ 变量开启打包能力;
  2. 如何根据目标平台选择合适的生成器(DEBRPMNSISDragNDrop 等);
  3. 如何填充包元数据(版本、描述、维护者、许可协议),让安装包显得专业且完整;
  4. 如何通过特定格式的变量声明包级别的依赖关系;
  5. 如何利用预安装/后安装脚本,在安装生命周期中执行系统级操作;
  6. 如何基于 COMPONENT 实现多组件、可选安装的精细化打包;
  7. 如何在命令行中调用 cpack 生成最终的安装包。

至此,第六章”安装、打包与发布”的内容已接近尾声。在下一节中,我们将把目光投向更广阔的生态系统——如何编写 vcpkg 的 portfilevcpkg.json,以及如何将你的库发布到 Conan 中心仓库,让你的项目真正融入现代 C++ 的包管理世界。

请登录后发表评论

    没有回复内容

正在唤醒异次元光景……