Handle Package Path Changes Safely with dpkg-maintscript-helper

dpkg-maintscript-helper gives maintainer scripts safe patterns for removing or renaming a conffile, or swapping a symlink for a directory. It coordinates the work across upgrades, failed upgrades and purges instead of making a one-way change in a single script. These examples describe dpkg-maintscript-helper from dpkg 1.22.6ubuntu6.6, installed on this machine.

Allow about twenty minutes to adapt one example to a package. You need Debian packaging knowledge, a package version where the change belongs, and access to the package's preinst, postinst and postrm maintainer scripts. The examples change installed package state when a package is upgraded, so test them in a disposable package build or virtual machine first. They are not commands to paste into an interactive root shell.

1. Understand the call boundary

The helper is designed for maintainer scripts. Put the same operation in the relevant preinst, postinst and postrm, then forward the script's arguments after a double hyphen:

dpkg-maintscript-helper COMMAND PARAMETERS -- "$@"

The environment supplied by dpkg, especially DPKG_MAINTSCRIPT_NAME, tells the helper which phase is running. The forwarded arguments let it distinguish an ordinary configuration from an aborted upgrade or installation. Omitting -- "$@" breaks that coordination.

Checkpoint: confirm the installed package version and helper path before editing package source:

$ dpkg-query -W -f='${Package} ${Version}\n'
dpkg 1.22.6ubuntu6.6
$ command -v dpkg-maintscript-helper
/usr/bin/dpkg-maintscript-helper

Do not run the examples below as ordinary shell commands. They are snippets for maintainer scripts, and they expect variables and arguments that dpkg provides during package operations.

2. Choose a version boundary

The optional prior-version says which package versions should trigger the operation. If it is omitted, the helper tries the operation on every upgrade. Supplying a boundary is usually safer because it prevents repeated attempts.

Use the version immediately before the change, followed by a tilde. For a conffile removed in package version 2.0-1, use 2.0-1~. The tilde also handles locally rebuilt versions such as 1.0-1local1 correctly: they still compare below the new boundary. If the file disappeared several versions ago, base the boundary on the version being prepared now, not on the first version that lacked the file.

The optional package argument names the owning package. For a Multi-Arch: same package it must include the architecture qualifier. Otherwise, leaving it empty normally lets the helper build the right name from DPKG_MAINTSCRIPT_PACKAGE and DPKG_MAINTSCRIPT_ARCH.

Checkpoint: write down the exact package version that introduces the path change and decide whether the operation is essential. A non-essential helper can be guarded with supports; an essential use needs an appropriate pre-dependency.

3. Remove an obsolete conffile

dpkg deliberately does not delete a conffile merely because a newer package no longer ships it. The user may have edited it, or the omission may be accidental. rm_conffile provides a staged removal that preserves local changes and can recover after an aborted upgrade.

For a package removing /etc/example/old.conf in version 2.0-1, add this same block to preinst, postinst and postrm:

dpkg-maintscript-helper rm_conffile \
    /etc/example/old.conf 2.0-1~ "" -- "$@"

The empty package argument is intentional. It tells the helper to derive the package identity from the maintainer-script environment. Replace it with an explicit package name only when the package identity rules require that.

During preinst, an unmodified conffile is staged as /etc/example/old.conf.dpkg-remove. A locally modified file is staged as /etc/example/old.conf.dpkg-backup. On successful configuration, the former is removed and the latter is retained as /etc/example/old.conf.dpkg-bak for reference. If the upgrade aborts, postrm can restore the original conffile. Purging the package also removes the retained backup.

Since dpkg 1.20.6, many simple removals can instead use the remove-on-upgrade flag in DEBIAN/conffiles. Check whether that declarative mechanism fits before adding a helper sequence. Do not use both casually: choose one owner for the removal behaviour.

Checkpoint: build the package, install the old version in a disposable environment, edit the conffile, and upgrade. Verify that the edited value is not silently lost and that the expected .dpkg-bak file exists when the transition completes.

4. Rename a conffile without losing edits

Use mv_conffile when the package moves a conffile and must carry user changes to the new path:

dpkg-maintscript-helper mv_conffile \
    /etc/example/old.conf /etc/example/new.conf 2.0-1~ "" -- "$@"

Again, place the identical call in preinst, postinst and postrm. In preinst, an unchanged old file is staged with the .dpkg-remove suffix so the new package can take over the destination. A modified old file remains available for dpkg's conffile handling. On configuration, the helper removes the staged marker and moves the old file to the new path when it is still present. On an aborted upgrade or install, postrm can put the old name back.

Do not replace this with a plain mv in preinst. That can make dpkg treat the user's edits as edits to the new conffile and prompt at the wrong point, or leave no safe recovery path if unpacking fails.

5. Switch between a symlink and a directory

dpkg does not automatically turn a symlink into a real directory or a directory into a symlink during an upgrade. These operations do not support downgrades, so plan the package version transition carefully.

For a symlink at /var/lib/example/data that used to point to /srv/example/data and becomes a directory, use:

dpkg-maintscript-helper symlink_to_dir \
    /var/lib/example/data /srv/example/data 3.0-1~ "" -- "$@"

The pathname must be absolute. The old target may be absolute or relative to the directory containing the pathname. In preinst, the helper only stages a symlink that still points to the expected old target. A locally changed symlink is left alone. The matching postinst removes the temporary .dpkg-backup symlink, while postrm can restore it after an abort.

For the reverse transition, where /var/lib/example/data is a directory and becomes a symlink to /srv/example/data, use:

dpkg-maintscript-helper dir_to_symlink \
    /var/lib/example/data /srv/example/data 3.0-1~ "" -- "$@"

This is more constrained than a directory rename. The helper checks that the directory does not contain conffiles, paths owned by another package or locally created paths before staging it. It creates a marked staging directory, moves newly created files to the new target during configuration, then replaces the pathname with the symlink. If the directory is not safe to switch, it is left in place rather than deleting data.

The new package must ship the resulting symlink or directory entries. Otherwise dpkg may be unable to remove them during purge. Treat this as a packaging requirement, not a cleanup detail.

6. Check support and dependencies

Run supports from a maintainer script when the transition is useful but not essential:

if dpkg-maintscript-helper supports rm_conffile; then
    dpkg-maintscript-helper rm_conffile \
        /etc/example/old.conf 2.0-1~ "" -- "$@"
fi

supports returns 0 when the command and required maintainer-script environment are available, and 1 otherwise. The call itself is not a substitute for forwarding the script arguments. On this machine, calling it without the dpkg environment prints warnings and returns 1; that is expected outside a real maintainer script.

If the operation is required for a correct installation, declare a pre-dependency instead. The manpage gives dpkg version 1.15.7.2 for rm_conffile and mv_conffile, and 1.17.14 for symlink_to_dir and dir_to_symlink. For the latter pair, the dependency would be:

Pre-Depends: dpkg (>= 1.17.14)

Ask the packaging helper you use whether it already generates this integration, for example through dh_installdeb. Avoid hand-maintaining duplicate snippets when the helper can produce the correct scripts.

Done means