4、DEB格式详解:生成Debian包、依赖管理、适用于Ubuntu/Debian系统、配置与签名

DEB格式,说白了就是Debian系Linux的安装包标准。Ubuntu、Debian、Linux Mint这些系统都用它。我最早接触DEB包是在十年前,当时为了给公司内部部署一个监控工具,手动打了一个包,结果依赖关系没处理好,部署了三天才搞定。嗯,从那以后我就再也不敢轻视DEB的依赖管理了。

4.1 DEB包的核心结构

一个DEB包,本质上是一个ar归档文件。里面包含三个部分:

  • debian-binary:版本标记文件,内容就一行"2.0"
  • control.tar.gz:元数据压缩包,包含控制信息、脚本等
  • data.tar.gz:实际安装的文件压缩包

你想想看,这结构其实挺简洁的。但真正让DEB强大的,是control.tar.gz里的那些文件。

核心文件清单:

  • control:包名、版本、依赖、描述等元信息
  • preinst:安装前执行的脚本
  • postinst:安装后执行的脚本
  • prerm:卸载前执行的脚本
  • postrm:卸载后执行的脚本
  • conffiles:标记哪些文件是配置文件
  • md5sums:文件校验和列表

4.2 用CPack生成DEB包

我个人习惯用CPack来生成DEB包,省心。你只需要在CMakeLists.txt里配置好,剩下的交给工具链。

先看一个最小配置:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyTool VERSION 1.2.3 LANGUAGES CXX)

add_executable(mytool main.cpp)

install(TARGETS mytool DESTINATION bin)

# CPack配置
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_NAME "mytool")
set(CPACK_DEBIAN_PACKAGE_VERSION "1.2.3")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name <your@email.com>")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "A useful tool for doing things")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")

然后执行:

mkdir build && cd build
cmake ..
cpack -G DEB

搞定。你会得到一个 mytool_1.2.3_amd64.deb 文件。

小技巧: 我建议在CMakeLists.txt里加上 set(CPACK_PACKAGE_FILE_NAME "${CPACK_DEBIAN_PACKAGE_NAME}_${CPACK_DEBIAN_PACKAGE_VERSION}_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}"),这样生成的文件名更规范,方便后续自动化处理。

4.3 依赖管理——DEB的灵魂

依赖管理是DEB包最核心的功能。没有它,你的包装上去可能跑不起来。我曾经在一个项目中,因为漏写了libssl的依赖,导致客户现场部署时程序崩溃,被运维同事追着骂了一周。

CPack里配置依赖很简单:

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl-dev (>= 1.1.0), libc6 (>= 2.28), zlib1g (>= 1:1.2.0)")

常用的依赖类型:

符号 含义 示例
>= 大于等于某版本 libssl-dev (>= 1.1.0)
<< 小于某版本 libfoo (<< 2.0)
= 精确版本 libbar (= 1.0.3)
| 或关系 liba | libb

还有几个实用的CPack变量:

  • CPACK_DEBIAN_PACKAGE_RECOMMENDS:推荐安装的包,不强制
  • CPACK_DEBIAN_PACKAGE_SUGGESTS:建议安装的包,更弱的关系
  • CPACK_DEBIAN_PACKAGE_CONFLICTS:冲突的包,不能同时安装
  • CPACK_DEBIAN_PACKAGE_REPLACES:替换的包,安装时会覆盖
  • CPACK_DEBIAN_PACKAGE_BREAKS:破坏的包,安装前需要卸载

注意: 依赖版本号一定要写对。我曾经遇到过因为版本号写成了 libssl-dev (> 1.1.0)(少了个等号),结果系统里装的是1.1.0版本,包管理器认为不满足条件,死活不让装。这种低级错误,排查起来特别费时间。

4.4 配置与脚本

DEB包支持在安装和卸载的不同阶段执行脚本。这些脚本放在 control.tar.gz 里,CPack可以帮你自动生成。

举个例子,安装后需要重启服务:

# postinst脚本
#!/bin/bash
set -e

case "$1" in
    configure)
        # 重新加载systemd服务
        systemctl daemon-reload
        systemctl enable mytool.service
        systemctl start mytool.service
        ;;
    abort-upgrade|abort-remove|abort-deconfigure)
        exit 0
        ;;
    *)
        echo "postinst called with unknown argument \`$1'" >&2
        exit 1
        ;;
esac

在CMakeLists.txt里指定脚本:

set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_SOURCE_DIR}/debian/postinst;${CMAKE_SOURCE_DIR}/debian/prerm")

配置文件也需要特殊处理。DEB包管理器会智能地处理配置文件:如果用户修改过,升级时不会覆盖。用 conffiles 文件标记:

# conffiles
/etc/mytool/mytool.conf
/etc/mytool/mytool.env

CPack里配置:

set(CPACK_DEBIAN_PACKAGE_CONFFILES "/etc/mytool/mytool.conf;/etc/mytool/mytool.env")

4.5 包签名——安全性的最后一道防线

签名不是必须的,但如果你要发布到公共仓库,我强烈建议加上。没有签名的包,用户安装时会收到警告,体验很差。

签名流程分两步:

  1. 生成GPG密钥(如果还没有)
  2. 用 dpkg-sig 或 debsigs 签名

生成密钥:

gpg --full-generate-key
# 选择RSA,密钥长度4096,有效期按需设置

签名DEB包:

dpkg-sig --sign builder mytool_1.2.3_amd64.deb

CPack本身不直接支持签名,但可以在CMakeLists.txt里加一个自定义目标:

add_custom_target(sign-deb
    COMMAND dpkg-sig --sign builder ${CPACK_PACKAGE_FILE_NAME}.deb
    DEPENDS package
    COMMENT "Signing DEB package..."
)

然后执行:

cmake --build . --target sign-deb

提示: 在CI/CD流水线里,我习惯把GPG私钥存在密钥管理服务里,比如HashiCorp Vault或AWS KMS。构建时动态导入,用完立即销毁。这样既安全又自动化。

4.6 实战:一个完整的DEB包示例

最后,给你看一个我实际项目中用过的配置。这是一个Web服务工具,包含二进制、配置文件、systemd服务单元和日志轮转配置。

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(WebService VERSION 2.0.1 LANGUAGES CXX)

add_executable(webservice main.cpp)
install(TARGETS webservice DESTINATION /usr/bin)
install(FILES webservice.conf DESTINATION /etc/webservice)
install(FILES webservice.service DESTINATION /lib/systemd/system)
install(FILES webservice.logrotate DESTINATION /etc/logrotate.d)

# CPack DEB配置
set(CPACK_GENERATOR "DEB")
set(CPACK_DEBIAN_PACKAGE_NAME "webservice")
set(CPACK_DEBIAN_PACKAGE_VERSION "2.0.1")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Dev Team <dev@example.com>")
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "High-performance web service tool")
set(CPACK_DEBIAN_PACKAGE_SECTION "web")
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl1.1 (>= 1.1.1), libc6 (>= 2.31), systemd (>= 245)")
set(CPACK_DEBIAN_PACKAGE_RECOMMENDS "nginx, certbot")
set(CPACK_DEBIAN_PACKAGE_CONFFILES "/etc/webservice/webservice.conf")
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA 
    "${CMAKE_SOURCE_DIR}/debian/postinst"
    "${CMAKE_SOURCE_DIR}/debian/prerm"
    "${CMAKE_SOURCE_DIR}/debian/postrm"
)
set(CPACK_PACKAGE_FILE_NAME 
    "${CPACK_DEBIAN_PACKAGE_NAME}_${CPACK_DEBIAN_PACKAGE_VERSION}_${CPACK_DEBIAN_PACKAGE_ARCHITECTURE}"
)

生成后,用 dpkg-deb --info 查看包信息:

dpkg-deb --info webservice_2.0.1_amd64.deb

输出应该包含你配置的所有元数据。如果发现依赖写错了,别慌,用 dpkg-deb --control 解压control文件,手动修改后重新打包也行。不过我个人建议还是改CMakeLists.txt重新生成,保持一致性。

嗯,DEB格式就讲到这里。说白了,它就是一个结构清晰、依赖管理强大的包格式。只要把依赖和脚本处理好,你的包就能在Ubuntu/Debian系统上平稳运行。下一章我们聊聊RPM格式,那又是另一套玩法了。