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 安装打包全流程

CMake 安装打包全流程 CMakeLists.txt + 源码 cmake -DCMAKE_INSTALL_PREFIX=... 生成构建系统 + 解析 install 命令 cmake --build . 编译生成目标文件 cpack -G DEB 或 cpack -G RPM 读取 CPack 配置,生成安装包 myapp-1.2.3.deb myapp-1.2.3.rpm myapp-1.2.3.tar.gz

高级技巧:自定义安装脚本

有些场景,光拷贝文件不够。比如安装后要创建用户、注册服务、更新缓存。这时候就需要安装脚本。

# 安装前执行
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。

测试安装包:别让用户当小白鼠

生成包之后,一定要在干净的环境里测试。我个人的流程是:

  1. 用 Docker 启动一个对应发行版的容器
  2. 把生成的 deb/rpm 拷贝进去
  3. 执行安装、卸载、再安装
  4. 检查文件是否完整,服务是否正常

这一步能发现很多问题。比如依赖漏了、路径错了、脚本权限不对。别问我怎么知道的……

小技巧:在 CI 里加上打包测试步骤。每次提交代码,自动生成安装包并在容器里验证。这样问题在合并前就暴露了。

CMake 的 install 和 CPack,说白了就是帮你把「交付」这件事标准化。从手动拷贝到一键打包,从混乱的目录到规范的布局。你想想看,用户拿到一个 deb 包,双击安装,所有东西自动到位——这才是专业软件该有的样子。


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