29、CPack实战案例一:构建一个C++命令行工具并打包为DEB和RPM包

说实话,讲CPack讲了这么多理论,是时候来点真家伙了。这一章,我带你亲手把一个C++命令行工具打包成DEB和RPM包。这两个格式,说白了就是Linux世界里的“安装包标准”——Debian系用DEB,Red Hat系用RPM。你写了个好工具,总不能让人家从源码编译吧?那用户体验也太差了。

我个人习惯,每次写完一个命令行工具,第一件事就是配好CPack。为什么?因为后续的CI/CD流水线、分发、版本管理,全都依赖这一步。你想想看,手动打包一次两次还行,项目迭代起来,那不得疯掉?

29.1 项目结构准备

我们先搭一个最小但完整的C++命令行工具项目。假设这个工具叫 greet-cli,功能很简单——从命令行接收一个名字,然后打印“Hello, [名字]!”。

项目目录结构如下:

greet-cli/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   └── greet.cpp
├── include/
│   └── greet.h
└── cmake/
    └── Packaging.cmake

嗯,这里要注意:我把CPack的配置单独抽到了 cmake/Packaging.cmake 里。这样做的好处是,主CMakeLists.txt不会被打包逻辑搞得太臃肿。我在项目中遇到过好几次,主文件塞了太多东西,后来维护起来简直想骂人。

29.2 主CMakeLists.txt配置

先看主CMakeLists.txt怎么写:

cmake_minimum_required(VERSION 3.15)
project(greet-cli VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 源文件
add_executable(greet-cli
    src/main.cpp
    src/greet.cpp
)

# 头文件路径
target_include_directories(greet-cli PRIVATE include)

# 安装规则
install(TARGETS greet-cli DESTINATION bin)

# 引入打包配置
include(cmake/Packaging.cmake)

这段代码没什么特别的,就是标准的CMake项目配置。关键在最后一行——我们把打包配置单独引进来。这样做,主文件干净,打包逻辑也集中,好维护。

29.3 Packaging.cmake——CPack核心配置

现在来看打包配置。这部分是今天的重头戏。

# Packaging.cmake
set(CPACK_PACKAGE_NAME "greet-cli")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple command-line greeting tool")
set(CPACK_PACKAGE_VENDOR "Your Name")
set(CPACK_PACKAGE_CONTACT "your-email@example.com")
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/yourname/greet-cli")

# 许可证
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE")

# 生成DEB包
set(CPACK_DEBIAN_PACKAGE_NAME "greet-cli")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name <your-email@example.com>")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.31)")

# 生成RPM包
set(CPACK_RPM_PACKAGE_NAME "greet-cli")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
set(CPACK_RPM_PACKAGE_GROUP "Applications/Text")
set(CPACK_RPM_PACKAGE_REQUIRES "glibc >= 2.31")

# 指定生成器
set(CPACK_GENERATOR "DEB;RPM")

# 必须包含这个
include(CPack)

这里有几个点我想强调一下:

  • CPACK_PACKAGE_VERSION:直接从 PROJECT_VERSION 取,保持版本号统一。我曾经手写版本号,结果有一次忘了同步,打出来的包版本对不上,排查了半天。
  • CPACK_DEBIAN_PACKAGE_DEPENDS:DEB包的依赖声明。如果你的工具依赖某个库,一定要写清楚。否则用户安装时可能缺依赖,跑不起来。
  • CPACK_RPM_PACKAGE_REQUIRES:RPM的依赖声明,写法跟DEB略有不同,但意思一样。
  • CPACK_GENERATOR:指定要生成哪些包格式。这里我同时要DEB和RPM,所以写 "DEB;RPM"
小提示:如果你在Windows上开发,想交叉打包Linux包,可以借助Docker。我在CI里就是这么干的——用Ubuntu容器编译并打包DEB,用CentOS容器编译并打包RPM。这样本地环境再乱也不影响打包结果。

29.4 构建与打包命令

配置写好了,怎么用?很简单,三步走:

# 第一步:配置
mkdir build && cd build
cmake ..

# 第二步:编译
make

# 第三步:打包
cpack

执行完 cpack 后,你会在 build 目录下看到两个文件:

  • greet-cli-1.0.0-Linux.deb
  • greet-cli-1.0.0-Linux.rpm

这就是我们要的安装包。你可以用 dpkg -irpm -ivh 安装试试看。

注意:如果你的系统是Ubuntu/Debian,只能安装DEB包;如果是CentOS/RHEL/Fedora,只能安装RPM包。别搞混了。我曾经在Ubuntu上试着装RPM包,结果系统差点崩了。

29.5 核心流程SVG图

下面这张图,展示了从源码到DEB/RPM包的完整流程。我建议你仔细看看,理解每一步做了什么。

CPack打包流程:从源码到DEB/RPM 源码阶段 CMakeLists.txt src/ include/ CMake配置 cmake .. 生成构建系统 编译阶段 make 生成二进制文件 CPack打包 cpack 命令 读取 Packaging.cmake 配置 DEB 包 greet-cli-1.0.0.deb RPM 包 greet-cli-1.0.0.rpm

29.6 验证安装包

包打好了,怎么验证它是不是对的?我一般会做三件事:

  1. 查看包信息:用 dpkg-deb --info greet-cli-1.0.0-Linux.debrpm -qip greet-cli-1.0.0-Linux.rpm,检查包名、版本、依赖是否正确。
  2. 安装测试:在干净的Docker容器里安装,确保没有依赖冲突。
  3. 功能测试:安装后运行 greet-cli --name World,看输出是不是 Hello, World!
重要:千万不要在生产环境直接测试安装包。我吃过这个亏——有一次包的依赖写错了,结果把系统的glibc给覆盖了,整个系统直接挂掉。从那以后,我都在Docker里测试,安全又方便。

29.7 常见问题与避坑

最后,分享几个我踩过的坑:

  • 版本号不一致:CMakeLists.txt里的 project() 版本号,和CPack里的版本号,一定要保持一致。我建议直接用 ${PROJECT_VERSION} 引用,别手写。
  • 依赖漏写:如果你的工具链接了某个库(比如libcurl),一定要在 CPACK_DEBIAN_PACKAGE_DEPENDSCPACK_RPM_PACKAGE_REQUIRES 里声明。否则用户安装后运行会报错“找不到共享库”。
  • 安装路径不对:默认情况下,可执行文件会安装到 /usr/local/bin。如果你想让它在 /usr/bin 下,需要修改 install() 的目标路径。DEB和RPM对路径有各自的规范,建议查一下。
  • 许可证文件缺失:如果 CPACK_RESOURCE_FILE_LICENSE 指向的文件不存在,CPack会报错。记得在项目根目录放一个LICENSE文件。

好了,这一章的内容就到这里。你跟着做一遍,应该就能掌握CPack打包DEB和RPM的基本流程。下一章我们会继续深入,看看怎么处理更复杂的依赖关系和跨平台打包。


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