4、第一个CPack项目:为Hello World项目添加CPack支持,生成第一个安装包

好了,前面我们聊了那么多CPack的基础概念。说实话,光说不练假把式。这一章,我们就真刀真枪地干一场。

我会带着你,从一个最简单的Hello World项目开始,一步步给它加上CPack支持。目标很明确:跑完cmake、make、cpack这三板斧,最后拿到一个实实在在的安装包。

嗯,这个过程我当年第一次做的时候,踩了不少坑。比如忘了设置版本号,结果打出来的包叫「helloworld-0.0.0-Linux.tar.gz」,看着就很不专业。还有一次,INSTALL命令写错了路径,安装完发现可执行文件跑到了系统根目录下……还好是在虚拟机里。

咱们今天就把这些坑都绕过去。

4.1 准备一个「裸」的Hello World项目

先看看我们手头有什么。一个最简单的C++项目,就两个文件:

HelloWorld/
├── CMakeLists.txt
└── main.cpp

main.cpp 的内容:

#include <iostream>

int main() {
    std::cout << "Hello, CPack World!" << std::endl;
    return 0;
}

CMakeLists.txt 的内容:

cmake_minimum_required(VERSION 3.15)
project(HelloWorld VERSION 1.0.0 LANGUAGES CXX)

add_executable(helloworld main.cpp)

这个项目现在能编译,能运行。但你要把它发给别人?抱歉,人家还得自己装CMake、自己编译。我们要做的,就是把它变成一个「双击安装,开箱即用」的安装包。

我的习惯:项目版本号一定要在 project() 里写清楚。VERSION 1.0.0 这行,别看它不起眼,后面CPack打包时,版本号就是从这儿读的。

4.2 添加安装规则:INSTALL命令

CPack本身不会魔法。它不知道你要把文件装到哪里。你得告诉它:「嘿,这个可执行文件,给我放到安装目录的bin文件夹下」。

这个指令,就是 install() 命令。

修改 CMakeLists.txt,在 add_executable 后面加上:

install(TARGETS helloworld
        RUNTIME DESTINATION bin)

解释一下这行代码的意思:

  • TARGETS helloworld:指定我们要安装的目标,就是刚才编译出来的 helloworld 可执行文件。
  • RUNTIME DESTINATION bin:说明这是一个运行时目标(可执行文件),安装到目标路径下的 bin 目录。

你可能会问:「为什么是 RUNTIME 而不是其他?」

在CMake的世界里,目标分三类:

目标类型 适用场景 典型安装目录
RUNTIME 可执行文件、DLL动态库(Windows) bin/
LIBRARY 共享库(.so, .dylib) lib/ 或 lib64/
ARCHIVE 静态库(.a, .lib)、DLL导入库 lib/ 或 lib64/

helloworld 是个可执行文件,所以用 RUNTIME。这个分类我一开始也觉得有点绕,后来做项目多了,发现它其实很合理——不同平台、不同类型的文件,安装位置天然就不一样。

4.3 引入CPack:几行配置搞定

安装规则写好了,接下来就是CPack的配置。CMake提供了一个专门的模块叫 CPack,我们只需要 include 它,再设置几个变量就行。

在 CMakeLists.txt 末尾加上:

# CPack 配置
include(CPack)

set(CPACK_PACKAGE_NAME "HelloWorld")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple Hello World program")
set(CPACK_PACKAGE_VENDOR "Your Company Name")

# 指定打包格式,这里我们先用TGZ(tar.gz)
set(CPACK_GENERATOR "TGZ")

完整的 CMakeLists.txt 现在长这样:

cmake_minimum_required(VERSION 3.15)
project(HelloWorld VERSION 1.0.0 LANGUAGES CXX)

add_executable(helloworld main.cpp)

install(TARGETS helloworld
        RUNTIME DESTINATION bin)

# CPack 配置
include(CPack)

set(CPACK_PACKAGE_NAME "HelloWorld")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A simple Hello World program")
set(CPACK_PACKAGE_VENDOR "Your Company Name")

set(CPACK_GENERATOR "TGZ")
注意:include(CPack) 这一行必须放在所有 set(CPACK_xxx) 变量的前面。为什么?因为 include 的时候,CPack模块会读取当前已定义的变量。如果你把变量写在 include 后面,那CPack就看不到了。我曾经因为这个顺序问题,折腾了半小时……

4.4 构建并打包:见证奇迹的时刻

配置写完了,接下来就是见证奇迹的时刻。打开终端,进入项目目录,执行:

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

# 第二步:编译
make

# 第三步:打包
cpack

如果一切顺利,你会在 build 目录下看到一个文件:

HelloWorld-1.0.0-Linux.tar.gz

解压看看里面有什么:

tar -xzf HelloWorld-1.0.0-Linux.tar.gz
cd HelloWorld-1.0.0-Linux
./bin/helloworld

输出:

Hello, CPack World!

成了!

这个安装包的结构是:

HelloWorld-1.0.0-Linux/
└── bin/
    └── helloworld

你看,CPack自动帮我们创建了 bin 目录,把 helloworld 放了进去。而且包名、版本号、平台信息都自动拼接好了。

4.5 试试其他打包格式

TGZ 只是开胃菜。CPack 支持很多格式,改一行配置就能切换:

# 改成生成 .deb 包(Debian/Ubuntu)
set(CPACK_GENERATOR "DEB")

# 或者生成 .rpm 包(Fedora/CentOS)
set(CPACK_GENERATOR "RPM")

# 或者生成 .sh 自解压脚本
set(CPACK_GENERATOR "STGZ")

# 甚至同时生成多种格式
set(CPACK_GENERATOR "TGZ;DEB;RPM")

我个人最常用的是 TGZ;DEB 组合。开发环境用 tar.gz 快速验证,发布时再打 deb 包给用户。

注意:生成 DEB 或 RPM 包时,系统需要安装相应的打包工具(如 dpkg-deb、rpmbuild)。如果提示找不到命令,先装一下:sudo apt install dpkg-dev 或 sudo yum install rpm-build。

4.6 避坑指南:我踩过的几个坑

最后,分享几个我当年踩过的坑,你遇到了可以少走弯路:

  • INSTALL 命令没写:CPack 打包时发现没有任何文件要安装,会生成一个空包。我第一次就犯了这错,解压后里面啥也没有。
  • 版本号不一致:project() 里写了 VERSION 1.0.0,但 CPACK_PACKAGE_VERSION 却写死了 "2.0.0"。结果包名和内部版本号对不上,用户反馈问题我都不知道是哪个版本。
  • 忘记重新 cmake:修改了 CMakeLists.txt 后,一定要重新执行 cmake ..。只执行 make 是不会生效的。这个坑我至少踩过五次。
  • Windows 上的路径分隔符:如果你在 Windows 上测试,DESTINATION 要用正斜杠 bin,而不是反斜杠 bin\。CMake 会帮你处理好平台差异。

4.7 小结

这一章我们做了什么?

  1. 准备了一个 Hello World 项目
  2. 用 install() 命令指定了安装规则
  3. 用 include(CPack) 和几个变量配置了打包参数
  4. 成功生成了第一个 tar.gz 安装包
  5. 尝试了 DEB、RPM 等其他格式

说实话,第一次看到自己打出来的安装包,还是有点小成就感的。虽然只是个 Hello World,但「从源码到安装包」这条链路,你已经跑通了。

下一章,我们会深入聊聊 CPack 的变量系统。你会发现,原来那些包名、版本号、描述信息,都可以用变量灵活控制。到时候,你的安装包就能做到「千人千面」了。