Debian 包管理系统依赖一系列维护者脚本来完成安装、升级与卸载过程中的状态变更。这些脚本包括 preinst、postinst、prerm 和 postrm,它们位于软件包的 DEBIAN 目录内,由 dpkg 在特定阶段自动调用。卸载一个包时,prerm 先于文件删除执行,postrm 则在文件删除后运行。两者配合可以确保服务被安全停止、配置得到合理保留或清理。

prerm 与 postrm 的执行时机与参数约定
prerm 脚本在 dpkg 真正删除包文件之前被调用,它的主要职责是执行那些必须在文件消失前完成的操作,例如停止正在运行的服务、解除 update-alternatives 注册、备份用户数据。postrm 则在文件从系统中移除后运行,适合清理空目录、删除缓存文件,或者根据动作参数决定是否删除配置文件。
这两个脚本都会收到一个由 dpkg 传入的位置参数,用来区分当前卸载的具体动作。最常见的参数是 remove 和 purge。remove 表示普通卸载,配置文件会被保留在 /etc 下;purge 表示彻底清除,连配置文件也要一并删除。除此之外,升级场景下还会出现 upgrade、failed-upgrade、abort-install、abort-upgrade、disappear 等参数。脚本内不能只针对 remove 编写逻辑,否则在 purge 或升级流程中可能遗漏关键操作。
与 preinst 和 postinst 相比,prerm 与 postrm 的一个显著特点是它们可能在系统处于半更新状态时执行。例如升级失败回滚时,prerm 会收到 failed-upgrade 参数,此时原版本的文件可能还没有被完全替换。因此脚本需要保持幂等性,并且在遇到无法处理的参数时不要随意返回非零值,否则会中断 dpkg 的事务,让软件包进入不稳定状态。
prerm 脚本的常见任务与编写示例
prerm 脚本最常见的用途是停止与当前包关联的服务。在 Debian 体系中,服务通常由 systemd 单元或 init.d 脚本管理。卸载前如果不停止服务,正在运行的进程会继续占用已删除的文件,Linux 内核会保留这些文件的 inode,直到进程退出。对于需要重启服务的升级场景,旧版本服务必须在文件替换前停止,否则新文件无法正确写入。
另一个重要任务是通过 update-alternatives 撤销本包提供的备选项。如果软件包在 postinst 中使用 update-alternatives 注册了某个命令的替代版本,那么在 prerm 阶段应当调用 update-alternatives --remove 清除该注册,避免卸载后系统中残留指向不存在文件的符号链接。
下面是一个典型的 prerm 脚本示例,它根据传入参数停止服务,并在 remove 和 upgrade 动作下备份当前配置:
#!/bin/sh
set -e
case "$1" in
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
# 停止服务,使用 || true 防止服务未启动时返回错误
if [ -x /etc/init.d/myapp ]; then
/etc/init.d/myapp stop || true
fi
# 卸载 update-alternatives 注册
if command -v update-alternatives >/dev/null 2>&1; then
update-alternatives --remove myapp /usr/bin/myapp || true
fi
# 备份配置,仅在普通卸载和升级时执行
if [ "$1" = remove ] || [ "$1" = upgrade ]; then
if [ -d /etc/myapp ]; then
cp -a /etc/myapp /var/backups/myapp.$(date +%Y%m%d%H%M%S) || true
fi
fi
;;
*)
echo "prerm called with unknown argument: $1" >&2
exit 1
;;
esac
exit 0
上述代码注意两点:一是使用 || true 来容忍服务未启动或 update-alternatives 不存在的情况,避免脚本因非关键错误返回非零;二是对未知参数显式返回错误,这可以帮助发现参数处理遗漏。不过 Debian 政策建议如果参数不影响删除流程,也可以直接 exit 0,以保证向后兼容。
还需要注意 prerm 脚本必须具备可执行权限。打包时在 DEBIAN 目录下创建脚本后,应通过 chmod 0755 设置权限。如果脚本没有执行位,dpkg 会跳过执行并发出警告,但服务可能因此未被停止,导致升级或卸载出现故障。
postrm 脚本的清理策略与 purge 参数处理
postrm 脚本的核心任务是在包文件删除后完成清理工作。根据用户执行的是 remove 还是 purge,清理范围有很大差异。remove 卸载时,dpkg 会保留 /etc 下的配置文件,postrm 通常只删除运行时生成的日志、PID 文件和空目录;而 purge 卸载时,所有配置文件都必须清除,postrm 应负责删除 /etc/myapp 目录、用户数据目录以及可能的缓存文件。
为了正确处理,postrm 必须解析 $1 参数。当参数为 remove 时,不要删除 /etc/myapp;当参数为 purge 时,才执行彻底清理。很多开发者省略 purge 分支,导致用户执行 dpkg --purge 后配置文件残留,这既不符合 Debian 政策,也会在重新安装时引入旧的配置状态。
以下是一个 postrm 脚本示例,它区分了 remove 和 purge,并在 purge 时清理系统用户和配置文件:
#!/bin/sh
set -e
case "$1" in
remove)
# 普通卸载:删除运行时文件,保留 /etc/myapp
rm -rf /var/run/myapp /var/log/myapp
# 删除空目录,如果 /var/log/myapp 不存在则忽略
rmdir --ignore-fail-on-non-empty /var/log/myapp 2>/dev/null || true
;;
purge)
# 彻底清除:删除配置、数据及运行时文件
rm -rf /etc/myapp /var/lib/myapp /var/run/myapp /var/log/myapp
# 删除系统用户
if getent passwd myapp >/dev/null 2>&1; then
userdel myapp || true
fi
if getent group myapp >/dev/null 2>&1; then
groupdel myapp || true
fi
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
# 升级或失败回滚时不执行清理
exit 0
;;
*)
echo "postrm called with unknown argument: $1" >&2
exit 1
;;
esac
exit 0
在 purge 分支中调用 userdel 和 groupdel 需要谨慎,因为其他软件包可能依赖同一个系统用户。如果用户被多个包共用,删除它会导致其他包权限异常。通常只有明确由该包创建且不会共享的用户才应在 purge 时删除,否则应跳过用户删除逻辑。
另一个容易出错的点是删除目录时使用 rm -rf。在 postrm 中直接删除 /etc/myapp 可能误删用户手动修改的配置文件,但 purge 的语义就是强制清除,因此政策允许。但务必确保路径变量没有拼写错误,并且不要使用通配符去匹配可能跨越目录边界的路径,否则可能破坏系统文件。
调试与常见错误排查
开发 prerm 和 postrm 时,可以直接使用 dpkg 命令手动触发卸载流程并观察脚本行为。执行 dpkg --remove myapp 会触发 prerm 的 remove 参数和 postrm 的 remove 参数;执行 dpkg --purge myapp 则先触发 prerm 的 remove 参数,随后触发 postrm 的 purge 参数。可以在脚本开头加入 set -x 来打印每一步命令,方便定位问题。
dpkg 提供了一些环境变量供脚本使用,其中 DPKG_MAINTSCRIPT_PACKAGE 表示当前正在处理的软件包名,DPKG_MAINTSCRIPT_ARCH 表示架构。当同一个维护者脚本被多个包共用时,可以利用这些变量区分行为。例如通过包名判断是否执行特定清理逻辑,而不是写多个几乎相同的脚本。
常见的错误包括:脚本没有可执行权限,dpkg 会报告 unable to execute 并继续卸载但不执行脚本;脚本在处理未知参数时返回非零值,导致 dpkg 事务中断,包状态停留在 half-configured 或 triggers-pending;在 postrm 的 remove 分支中删除了配置目录,违背了 remove 与 purge 的区分;在 prerm 中停止了服务却没有在 postinst 中恢复,导致升级后服务未启动。
还有一种情况是 upgrade 动作的处理。升级时旧包 prerm 会收到 upgrade 参数,新包 preinst 会收到 upgrade 参数。如果 prerm 没有正确停止旧版本服务,新文件解包后可能出现二进制文件被占用无法替换的问题。因此在编写 prerm 时,一定要覆盖 upgrade 和 failed-upgrade 分支,确保升级路径下的行为符合预期。
总之,prerm 与 postrm 虽然看起来是两个简单的 shell 脚本,但它们直接影响 Debian 包的生命周期完整性和用户体验。理解参数含义、保持脚本幂等、区分 remove 和 purge,是写出可靠维护者脚本的关键。

