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"。
29.4 构建与打包命令
配置写好了,怎么用?很简单,三步走:
# 第一步:配置
mkdir build && cd build
cmake ..
# 第二步:编译
make
# 第三步:打包
cpack
执行完 cpack 后,你会在 build 目录下看到两个文件:
greet-cli-1.0.0-Linux.debgreet-cli-1.0.0-Linux.rpm
这就是我们要的安装包。你可以用 dpkg -i 或 rpm -ivh 安装试试看。
29.5 核心流程SVG图
下面这张图,展示了从源码到DEB/RPM包的完整流程。我建议你仔细看看,理解每一步做了什么。
29.6 验证安装包
包打好了,怎么验证它是不是对的?我一般会做三件事:
- 查看包信息:用
dpkg-deb --info greet-cli-1.0.0-Linux.deb或rpm -qip greet-cli-1.0.0-Linux.rpm,检查包名、版本、依赖是否正确。 - 安装测试:在干净的Docker容器里安装,确保没有依赖冲突。
- 功能测试:安装后运行
greet-cli --name World,看输出是不是Hello, World!。
29.7 常见问题与避坑
最后,分享几个我踩过的坑:
- 版本号不一致:CMakeLists.txt里的
project()版本号,和CPack里的版本号,一定要保持一致。我建议直接用${PROJECT_VERSION}引用,别手写。 - 依赖漏写:如果你的工具链接了某个库(比如libcurl),一定要在
CPACK_DEBIAN_PACKAGE_DEPENDS和CPACK_RPM_PACKAGE_REQUIRES里声明。否则用户安装后运行会报错“找不到共享库”。 - 安装路径不对:默认情况下,可执行文件会安装到
/usr/local/bin。如果你想让它在/usr/bin下,需要修改install()的目标路径。DEB和RPM对路径有各自的规范,建议查一下。 - 许可证文件缺失:如果
CPACK_RESOURCE_FILE_LICENSE指向的文件不存在,CPack会报错。记得在项目根目录放一个LICENSE文件。
好了,这一章的内容就到这里。你跟着做一遍,应该就能掌握CPack打包DEB和RPM的基本流程。下一章我们会继续深入,看看怎么处理更复杂的依赖关系和跨平台打包。
公众号:蓝海资料掘金营,微信 deep3321