Appearance
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 服务,我们需要在卸载前完成以下操作:
- 停止正在运行的服务
- 解注册 systemd 服务(仅完全卸载时)
- 保留所有业务数据(仅删除自身,不删用户数据)
完整的 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 # 用户数据全没了!正确做法:数据由 postrm 在 purge 阶段处理,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 --auditremove 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 purge | prerm 只处理 remove,数据清理在 postrm |
deconfigure 最复杂 | 只停止服务,不做破坏性操作 |
使用 deb-systemd-invoke | 不要直接 systemctl |
| 数据保护第一原则 | 永远不要在 prerm 中删除用户数据 |
下一步
在下一章中,我们将探讨 postrm(卸载后脚本)——特别是 purge 和 remove 的边界,以及如何避免因脚本错误导致系统无法重新安装软件包的致命问题。
