24、CMake安装与打包:install命令、CPack配置、生成deb/rpm包
写代码只是工程的一半。另一半,是怎么把成果交到用户手上。
我见过太多团队,代码写得漂亮,但交付时还在手动拷贝文件、写安装脚本。说实话,这太原始了。CMake 的 install 命令和 CPack 工具,就是帮你解决这个问题的。
install 命令:告诉 CMake 文件该去哪
install 命令说白了就是一张「搬家清单」。你告诉 CMake:这个库放 lib,那个头文件放 include,可执行文件放 bin。剩下的,CMake 帮你搞定。
# 安装可执行文件
install(TARGETS myapp DESTINATION bin)
# 安装头文件
install(FILES mylib.h DESTINATION include)
# 安装库文件
install(TARGETS mylib
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/static
)
嗯,这里要注意:DESTINATION 是相对路径。它相对于安装前缀(CMAKE_INSTALL_PREFIX)。默认是 /usr/local,你可以改。
我的习惯:开发阶段我总把 CMAKE_INSTALL_PREFIX 设成项目下的 build/install 目录。这样不会污染系统,调试也方便。
安装目录结构:别让用户找不到东西
Linux 下有个约定俗成的目录布局。我个人建议严格遵守,别搞特殊。
| 文件类型 | 目标路径 | 说明 |
|---|---|---|
| 可执行文件 | bin/ | 用户直接运行 |
| 动态库 | lib/ 或 lib/x86_64-linux-gnu/ | 运行时加载 |
| 静态库 | lib/static/ 或 lib/ | 链接时使用 |
| 头文件 | include/ | 供其他项目引用 |
| 配置文件 | etc/ 或 share/<project>/ | 运行时读取 |
| 文档 | share/doc/<project>/ | 帮助手册 |
你想想看,用户拿到你的包,解压一看,所有文件散落在根目录,他什么感受?
我曾经踩过的坑:有个项目把动态库装到了 bin/ 目录。结果用户运行时报错「找不到共享库」。折腾半天才发现是路径不对。从那以后,我每个项目都先画好目录树再写 install 命令。
CPack:一键生成安装包
install 命令定义好了文件布局,CPack 就是打包机。你给它一个配置,它吐出 deb、rpm、tar.gz……随你挑。
# 启用 CPack
include(CPack)
# 设置包基本信息
set(CPACK_PACKAGE_NAME "myapp")
set(CPACK_PACKAGE_VERSION "1.2.3")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "My Awesome Application")
set(CPACK_PACKAGE_VENDOR "My Company")
# 指定打包格式
set(CPACK_GENERATOR "DEB;RPM;TGZ")
就这么几行,CMake 就能生成三种格式的安装包。运行一下:
cmake --build . --target package
# 或者
cpack -G DEB
你会看到 build 目录下多了 myapp-1.2.3-Linux.deb、myapp-1.2.3-Linux.rpm 等文件。
核心逻辑:install 定义「装什么」,CPack 定义「怎么装」。两者配合,从源码到安装包一步到位。
生成 deb 包:给 Debian/Ubuntu 用户
deb 包有自己的一套元数据要求。CPack 提供了专门的变量来设置。
# deb 包专属配置
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "developer@example.com")
set(CPACK_DEBIAN_PACKAGE_SECTION "devel")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl1.1 (>= 1.1.1), libc6 (>= 2.31)")
# 安装后执行的脚本
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/debian/postinst"
"${CMAKE_SOURCE_DIR}/debian/prerm"
)
依赖声明特别重要。我见过一个 deb 包装完,用户运行时报段错误,就是因为缺了底层库。CPACK_DEBIAN_PACKAGE_DEPENDS 就是用来声明这些依赖的。
避坑指南:依赖版本号别写太死。比如 libssl1.1 (>= 1.1.1) 比 libssl1.1 (= 1.1.1) 更友好。用户系统可能打了安全补丁,版本号会变。
生成 rpm 包:给 Red Hat/Fedora 用户
rpm 的配置思路和 deb 类似,但变量名不同。
# rpm 包专属配置
set(CPACK_RPM_PACKAGE_RELEASE "1")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Development/Tools")
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.1.1, glibc >= 2.31")
# 指定包架构
set(CPACK_RPM_PACKAGE_ARCHITECTURE "x86_64")
rpm 的依赖检查比 deb 更严格。我曾经在 CentOS 7 上打包,忘了声明 perl 依赖,结果用户安装时直接报错退出。嗯,这个教训挺深刻的。
跨平台打包:一个 CMakeLists.txt 打天下
你可以在同一个 CMakeLists.txt 里同时配置 deb 和 rpm。CPack 会根据你指定的 GENERATOR 自动选择对应的配置。
# 通用配置
set(CPACK_GENERATOR "DEB;RPM")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_PROCESSOR}")
# 平台特定配置
if(CPACK_GENERATOR MATCHES "DEB")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31)")
elseif(CPACK_GENERATOR MATCHES "RPM")
set(CPACK_RPM_PACKAGE_REQUIRES "glibc >= 2.31")
endif()
这样,你在 Ubuntu 上跑 cpack -G DEB,在 CentOS 上跑 cpack -G RPM,各自生成正确的包。一份代码,多处交付。
SVG 流程图:CMake 安装打包全流程
高级技巧:自定义安装脚本
有些场景,光拷贝文件不够。比如安装后要创建用户、注册服务、更新缓存。这时候就需要安装脚本。
# 安装前执行
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/debian/preinst"
)
# 安装后执行
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA
"${CMAKE_SOURCE_DIR}/debian/postinst"
)
postinst 脚本示例:
#!/bin/bash
# 创建运行用户
if ! id -u myapp &>/dev/null; then
useradd -r -s /bin/false myapp
fi
# 注册 systemd 服务
systemctl daemon-reload
systemctl enable myapp.service
rpm 也有类似的机制,用 %post、%pre 等宏。CPack 通过 CPACK_RPM_POST_INSTALL_SCRIPT_FILE 来指定。
我曾经犯过的错:有次写 postinst 脚本,忘了加 set -e。结果脚本中途失败,但包管理器认为安装成功。用户机器上留下了一个半残的服务。从那以后,我所有安装脚本第一行都是 set -euo pipefail。
测试安装包:别让用户当小白鼠
生成包之后,一定要在干净的环境里测试。我个人的流程是:
- 用 Docker 启动一个对应发行版的容器
- 把生成的 deb/rpm 拷贝进去
- 执行安装、卸载、再安装
- 检查文件是否完整,服务是否正常
这一步能发现很多问题。比如依赖漏了、路径错了、脚本权限不对。别问我怎么知道的……
小技巧:在 CI 里加上打包测试步骤。每次提交代码,自动生成安装包并在容器里验证。这样问题在合并前就暴露了。
CMake 的 install 和 CPack,说白了就是帮你把「交付」这件事标准化。从手动拷贝到一键打包,从混乱的目录到规范的布局。你想想看,用户拿到一个 deb 包,双击安装,所有东西自动到位——这才是专业软件该有的样子。
公众号:蓝海资料掘金营,微信deep3321