导语
在前两节中,我们学习了如何通过 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.cmake 和 CPackSourceConfig.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_package 与 target_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 体系中,这些脚本对应经典的 preinst、postinst、prerm、postrm。在 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_EXTRA 和 CPACK_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 生成器,多组件打包会生成一个带有复选框的安装向导,用户可以自由选择要安装的组件。对于 DEB 和 RPM,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() 精心组织的安装规则,向下对接操作系统原生的包管理生态。通过本节的学习,你掌握了:
- 如何通过
include(CPack)和CPACK_变量开启打包能力; - 如何根据目标平台选择合适的生成器(
DEB、RPM、NSIS、DragNDrop等); - 如何填充包元数据(版本、描述、维护者、许可协议),让安装包显得专业且完整;
- 如何通过特定格式的变量声明包级别的依赖关系;
- 如何利用预安装/后安装脚本,在安装生命周期中执行系统级操作;
- 如何基于
COMPONENT实现多组件、可选安装的精细化打包; - 如何在命令行中调用
cpack生成最终的安装包。
至此,第六章”安装、打包与发布”的内容已接近尾声。在下一节中,我们将把目光投向更广阔的生态系统——如何编写 vcpkg 的 portfile 和 vcpkg.json,以及如何将你的库发布到 Conan 中心仓库,让你的项目真正融入现代 C++ 的包管理世界。


没有回复内容