Skip to content

prerm 脚本的作用与正确使用姿势

prerm 脚本的作用与正确使用姿势示意图

"卸载一个软件包时,数据丢了!"——这是新手在打包时最容易犯的灾难性错误。

prerm(pre-removal)是 Debian 包在被移除前执行的脚本,它的职责是在文件被删除之前完成清理工作。但由于它涉及"删除"这一敏感操作,一个错误的 prerm 脚本可能导致:

  • 业务数据被误删
  • 依赖包被破坏
  • 升级过程无法回滚

本文将深入解读 prerm 的所有触发场景,并给出生产级的安全实践。

prerm 是什么?

prerm 是 Debian 软件包中的卸载前执行脚本,在包的文件被从系统删除之前运行。它的职责包括:

  • 停止正在运行的服务(优雅关闭)
  • 解注册 systemd 服务(disable
  • 清理临时文件或缓存
  • 处理依赖冲突时让出资源

📌 命名规范:脚本位于 debian/ 目录下,打包后被安装到 /var/lib/dpkg/info/<package>.prerm

prerm 的触发场景(比 postinst 更复杂)

场景第一个参数第二个参数说明
软件包卸载remove用户主动卸载(保留配置文件)
版本升级upgrade新版本号升级到新版本,旧版本执行 prerm
依赖冲突deconfigure被配置的包名最复杂:因依赖问题导致包被强制解配置
安装失败回滚failed-upgrade旧版本号升级失败后回滚到旧版本

⚠️ 重点deconfigure 是 prerm 独有的场景,很多新手完全忽略它,导致复杂的依赖场景下系统状态混乱。

prerm 的标准模板

基础模板(推荐)

bash
#!/bin/sh
set -e

case "$1" in
    remove)
        # 用户卸载:停止服务,但不删除数据
        ;;
    upgrade)
        # 升级到新版本:优雅停止旧版本服务
        ;;
    deconfigure)
        # 依赖冲突:包被强制解配置
        # 这是最复杂的场景,需要特别小心
        ;;
    failed-upgrade)
        # 升级失败回滚:什么都不做,让旧版本恢复
        ;;
    *)
        echo "prerm called with unknown argument: $1" >&2
        exit 1
        ;;
esac

exit 0

实战:为 Go 应用编写 prerm

延续第一章的 myapp Go 服务,我们需要在卸载前完成以下操作:

  1. 停止正在运行的服务
  2. 解注册 systemd 服务(仅完全卸载时)
  3. 保留所有业务数据(仅删除自身,不删用户数据)

完整的 prerm 脚本

bash
#!/bin/sh
set -e

PKGNAME="$DPKG_MAINTSCRIPT_PACKAGE"

case "$1" in
    remove)
        # 用户卸载:停止服务,但保留所有数据和配置文件
        # 使用 deb-systemd-invoke 优雅停止
        if [ -x /usr/bin/deb-systemd-invoke ]; then
            deb-systemd-invoke stop myapp.service >/dev/null 2>&1 || true
        fi

        # 禁用服务(但保留配置文件)
        if [ -x /usr/bin/deb-systemd-helper ]; then
            deb-systemd-helper disable myapp.service >/dev/null 2>&1 || true
        fi
        ;;

    upgrade)
        # 升级到新版本:优雅关闭旧版本
        # 注意:不要在这里 disable 服务,否则新版本会无法启用
        if [ -x /usr/bin/deb-systemd-invoke ]; then
            deb-systemd-invoke stop myapp.service >/dev/null 2>&1 || true
        fi
        ;;

    deconfigure)
        # ⚠️ 依赖冲突导致的强制解配置
        # 这是最棘手的场景:其他包依赖我们,但那个包被卸载了

        # 场景示例:myapp 依赖 mysql,但 mysql 要升级时发现版本不兼容
        # 这时 myapp 会被 deconfigure,直到冲突解决

        # 处理方式:只停止服务,但不要做任何破坏性操作
        if [ -x /usr/bin/deb-systemd-invoke ]; then
            deb-systemd-invoke stop myapp.service >/dev/null 2>&1 || true
        fi

        # ❌ 错误:不要在 deconfigure 中删除数据或禁用服务
        # 因为 deconfigure 后包仍然安装着,只是暂时不可用
        ;;

    failed-upgrade)
        # 升级失败回滚:什么都不做
        # 系统会自动恢复到旧版本,旧版本的文件还在
        # 我们只需要让旧版本的 postinst 来处理恢复逻辑
        ;;

    *)
        echo "prerm called with unknown argument: $1" >&2
        exit 1
        ;;
esac

# 调用 debhelper 自动生成的代码
if [ -x /usr/share/debconf/confmodule ]; then
    . /usr/share/debconf/confmodule
fi

exit 0

关键最佳实践

1. 区分 remove 和 purge

prerm 只在 remove 阶段运行,purge(彻底清除)由 postrm 处理。

bash
# ❌ 错误:在 prerm 中删除配置文件
remove)
    rm -rf /etc/myapp  # 配置文件被删除,用户无法保留

# ✅ 正确:prerm 只停止服务,不删除任何文件
remove)
    deb-systemd-invoke stop myapp.service
    # 数据保留,postrm 的 purge 阶段才会删除

原则prerm 负责"停止",postrm 负责"删除"。

2. 使用 deb-systemd-invoke 而非直接 systemctl

bash
# ❌ 错误:直接调用 systemctl
systemctl stop myapp.service

# ✅ 正确:使用 deb-systemd-invoke
deb-systemd-invoke stop myapp.service

原因

  • deb-systemd-invoke 会正确处理 --no-restart-on-upgrade 等策略
  • deconfigure 场景下行为更安全
  • 避免在容器或 chroot 环境中报错

3. 升级时不要 disable 服务

bash
# ❌ 错误:升级时禁用服务
upgrade)
    systemctl stop myapp.service
    systemctl disable myapp.service  # 新版本将无法自动启用
    ;;

# ✅ 正确:只停止,不禁用
upgrade)
    deb-systemd-invoke stop myapp.service
    # 新版本会在 postinst 中重新 enable
    ;;

4. deconfigure 场景要特别小心

deconfigure 是最容易被忽略的场景。它发生在:

  • 包 A 依赖包 B
  • 包 B 即将被升级,但新版本与包 A 不兼容
  • dpkg 决定先 "deconfigure" 包 A,让出依赖关系
  • 包 B 升级完成后,包 A 再重新配置
bash
deconfigure)
    # 只停止服务,不做任何破坏性操作
    deb-systemd-invoke stop myapp.service >/dev/null 2>&1 || true

    # ❌ 不要做:
    # - 删除任何文件
    # - 禁用 systemd 服务
    # - 删除用户
    # - 清理数据库
    ;;

常见陷阱与排坑

陷阱1:在 prerm 中删除用户数据

bash
# ❌ 灾难性错误
remove)
    userdel myapp
    rm -rf /var/lib/myapp  # 用户数据全没了!

正确做法:数据由 postrmpurge 阶段处理,prerm 只负责停止服务。

陷阱2:使用 set -e 但命令失败

bash
# ❌ 如果服务未运行,stop 命令会失败,导致安装中断
set -e
systemctl stop myapp.service

# ✅ 正确处理失败情况
set -e
systemctl stop myapp.service || true

陷阱3:在 deconfigure 中删除配置文件

bash
# ❌ 错误
deconfigure)
    rm -rf /etc/myapp  # deconfigure 时删除配置,重新配置时发现配置丢失

正确做法deconfigure 是临时状态,包随后会被重新配置,不要做任何不可逆操作。

陷阱4:升级时长时间阻塞

bash
# ❌ 错误:wait 等待服务完全停止
upgrade)
    systemctl stop myapp.service --timeout=300  # 可能阻塞 5 分钟

正确做法:设置合理的超时,或在 postinst 中处理迁移逻辑。

prerm 与其他脚本的协作

测试你的 prerm

模拟卸载(保留配置)

bash
# 安装包
sudo dpkg -i myapp_1.0.0.deb

# 卸载(触发 prerm remove)
sudo dpkg -r myapp

# 检查服务是否停止
systemctl status myapp  # 应显示 not found 或 inactive

模拟升级

bash
# 安装旧版本
sudo dpkg -i myapp_0.9.0.deb

# 升级(触发 prerm upgrade)
sudo dpkg -i myapp_1.0.0.deb

模拟 deconfigure(复杂依赖场景)

bash
# 创建两个互相依赖的包 myapp 和 mylib
# 当 mylib 需要升级但 myapp 不兼容时触发

# 查看 deconfigure 日志
sudo tail -f /var/log/dpkg.log | grep deconfigure

查看 prerm 执行日志

bash
# dpkg 详细日志
sudo grep prerm /var/log/dpkg.log

# 或使用
sudo dpkg --audit

remove vs purge vs deconfigure 速查表

场景触发命令prerm 做的事postrm 做的事
卸载保留配置dpkg -r停止服务(不运行)
彻底清除dpkg -P停止服务删除配置、日志、用户
升级dpkg -i new停止旧版本新版本 postinst 启动
依赖冲突自动触发停止服务(临时)重新配置时恢复

安全清单:prerm 脚本检查表

在发布你的 Debian 包之前,对照以下清单检查 prerm

  • [ ] remove 场景只停止服务,不删除任何文件
  • [ ] upgrade 场景只停止服务,不禁用服务
  • [ ] deconfigure 场景只停止服务,不做破坏性操作
  • [ ] failed-upgrade 场景为空或只做无害恢复
  • [ ] 使用 deb-systemd-invoke 而非直接 systemctl
  • [ ] 所有命令都做了错误处理(|| true 或条件判断)
  • [ ] 脚本以 exit 0 结尾
  • [ ] 不依赖其他未安装的包
  • [ ] 在 remove 场景中保留所有用户数据

小结

要点说明
prerm 在文件删除前运行负责"停止"而非"删除"
四种触发场景remove、upgrade、deconfigure、failed-upgrade
remove vs purgeprerm 只处理 remove,数据清理在 postrm
deconfigure 最复杂只停止服务,不做破坏性操作
使用 deb-systemd-invoke不要直接 systemctl
数据保护第一原则永远不要在 prerm 中删除用户数据

下一步

在下一章中,我们将探讨 postrm(卸载后脚本)——特别是 purgeremove 的边界,以及如何避免因脚本错误导致系统无法重新安装软件包的致命问题。


下一篇预告第三章:postrm 脚本的清理逻辑与 purge 陷阱

最后更新2026/07/25 14:21
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal