Greetings dpkg devs,I was trying to understand how dpkg-maintscript-helper's rm_conffile and mv_conffile commands handle (or not) config files that are not conffiles -- either shipped but not in the Conffiles status field, or not shipped and generated during postinst configure (e.g., ucf). The documentation is not clear, so I wrote some tests and documentation improvements.
Attached is a series of patches (apply them by running `git am *.patch`) that do the following:
* Add extensive tests for rm_conffile and mv_conffile.* Document how rm_conffile and mv_conffile handle (or not) files not listed in the package's conffiles.
* Fix some (minor) issues uncovered by the tests.I can use 'git send-email' to send a separate email per commit if that would be preferred; I didn't want to spam the mailing list without knowing your preferences first.
Feedback would be appreciated. Thanks, Richard
From 104ca2345078fb390941f2dace021b2f92c8f4cd Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Wed, 12 Feb 2025 17:16:02 -0500 Subject: [PATCH 01/10] tests: Move .gitignore out of dpkginst Now the initial state of dpkginst is empty which makes it easier for tests to ensure that everything was cleaned up after purge. For example: # Ensure that purge removed everything. test -z "$$(ls -A '$(DPKG_INSTDIR)')" --- tests/.gitignore | 2 ++ tests/Test.mk | 13 ++++++++++++- tests/dpkginst/.gitignore | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) delete mode 100644 tests/dpkginst/.gitignore diff --git a/tests/.gitignore b/tests/.gitignore index 7498a3d16..3d3853d94 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,3 +1,5 @@ !Makefile *.deb /.pkg-tests.conf +/dpkginst/ +dpkginst.stamp diff --git a/tests/Test.mk b/tests/Test.mk index a830d5d49..a13a508b8 100644 --- a/tests/Test.mk +++ b/tests/Test.mk @@ -110,14 +110,25 @@ pkg_is_not_installed = $(call stdout_has,$(PKG_STATUS) $(1) 2>/dev/null, ok not- pkg_status_is = $(call stdout_is,$(PKG_STATUS) $(1),$(2)) pkg_field_is = $(call stdout_is,$(DPKG_QUERY) -f '$${$(2)}' -W $(1),$(3)) -%.deb: % +# Avoid using $(DPKG_INSTDIR) as a dependency because it would be immediately +# expanded, making it impossible for individual tests to override DPKG_INSTDIR. +# (Variable expansion in a recipe is deferred.) +dpkginst.stamp: | dpkginst + touch '$@' +.PHONY: dpkginst +dpkginst: + mkdir -p '$(DPKG_INSTDIR)' + +%.deb: % dpkginst.stamp $(DPKG_BUILD_DEB) $< $@ TEST_CASES := build: build-hook $(DEB) +build-hook: dpkginst.stamp test: build test-case test-clean +test-case: dpkginst.stamp clean: clean-hook $(RM) $(DEB) diff --git a/tests/dpkginst/.gitignore b/tests/dpkginst/.gitignore deleted file mode 100644 index 72e8ffc0d..000000000 --- a/tests/dpkginst/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* -- 2.48.1
From 7108bde0838f3393f347fa502a8e05454cfe1a15 Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Thu, 13 Feb 2025 02:46:44 -0500 Subject: [PATCH 02/10] tests: dpkg-m-h: New rm_conffile tests to improve coverage This commit adds exhaustive (I hope) test cases for rm_conffile. Current state of rm_conffile: Removing a conffile: dpkg-maintscript-helper works as expected. Removing a shipped-but-non-conffile: dpkg-maintscript-helper works as expected, except: * When upgrading, dpkg-m-h treats all non-conffiles as if they were modified, so it creates unnecessary .dpkg-bak files even for unmodified files. (These unnecessary .dpkg-bak files are not created when the user does remove-then-install instead of upgrade because dpkg deletes the non-conffile files before the new version's postinst runs, not after.) A TODO comment was added. * When upgrading, dpkg-m-h backs up modified files. I consider this behavior to be a feature, not a bug, but it differs from the behavior of mv_conffile (which does not preserve modifications to non-conffiles) and from the remove-then-install case (where it is impossible to make a backup because dpkg deletes the modified non-conffile before preinst has the opportunity to make a backup). Removing a non-shipped file: dpkg-maintscript-helper silently ignores any attempt to remove a file not listed in `dpkg-query -L`, as expected. As a consequence, dpkg-m-h cannot be used to remove a config file created during postinst by ucf or via debconf answers. --- tests/Makefile | 1 + tests/t-maintscript-rm/Makefile | 226 ++++++++++++++++++ .../pkg-rm-0/DEBIAN/conffiles | 1 + .../t-maintscript-rm/pkg-rm-0/DEBIAN/control | 7 + .../t-maintscript-rm/pkg-rm-0/DEBIAN/postinst | 8 + tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm | 7 + tests/t-maintscript-rm/pkg-rm-0/conf | 1 + tests/t-maintscript-rm/pkg-rm-0/nonconf | 1 + .../t-maintscript-rm/pkg-rm-1/DEBIAN/control | 7 + .../t-maintscript-rm/pkg-rm-1/DEBIAN/postinst | 9 + tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm | 5 + .../t-maintscript-rm/pkg-rm-1/DEBIAN/preinst | 8 + 12 files changed, 281 insertions(+) create mode 100644 tests/t-maintscript-rm/Makefile create mode 100644 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles create mode 100644 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control create mode 100755 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst create mode 100755 tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm create mode 100644 tests/t-maintscript-rm/pkg-rm-0/conf create mode 100644 tests/t-maintscript-rm/pkg-rm-0/nonconf create mode 100644 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm create mode 100755 tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst diff --git a/tests/Makefile b/tests/Makefile index bdd9dd318..ff66e108f 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -41,6 +41,7 @@ ifdef DPKG_AS_ROOT TESTS_PASS += t-unpack-device endif TESTS_PASS += t-maintscript-leak +TESTS_PASS += t-maintscript-rm TESTS_PASS += t-filtering TESTS_PASS += t-depends TESTS_PASS += t-dir-leftover-parents diff --git a/tests/t-maintscript-rm/Makefile b/tests/t-maintscript-rm/Makefile new file mode 100644 index 000000000..d1d342fc1 --- /dev/null +++ b/tests/t-maintscript-rm/Makefile @@ -0,0 +1,226 @@ +TESTS_DEB := pkg-rm-0 pkg-rm-1 + +include ../Test.mk + +# Explanation of the cases: +# - conf: pkg-rm-0 ships this and lists it in its conffiles +# - nonconf: pkg-rm-0 ships this but does NOT list it in its conffiles +# - unowned: pkg-rm-0 does NOT ship this file; it is generated in postinst +cases = conf nonconf unowned + +define check-clean = +out=$$(ls -A '$(DPKG_INSTDIR)') || exit 1 && test -z "$$out" || { \ + printf %s\\n "install directory "'$(DPKG_INSTDIR)'" not clean:" "$$out" >&2; \ + exit 1; \ +} +endef + +define prepare-unmodified = +$(check-clean) +$(DPKG_INSTALL) pkg-rm-0.deb +$(foreach case,$(cases),$\ +test -f '$(DPKG_INSTDIR)/$(case)' +test "$$(cat '$(DPKG_INSTDIR)/$(case)')" = "original" +) +endef + +define prepare-modified = +$(prepare-unmodified) +$(foreach case,$(cases),$\ +$(BEROOT) sh -c "echo modified >'"'$(DPKG_INSTDIR)/$(case)'"'" +test -f '$(DPKG_INSTDIR)/$(case)' +test "$$(cat '$(DPKG_INSTDIR)/$(case)')" = "modified" +) +endef + +define prepare-abort = +$(BEROOT) touch '$(DPKG_INSTDIR)'/fail-preinst +$(DPKG_INSTALL) pkg-rm-1.deb || true +$(BEROOT) rm -f '$(DPKG_INSTDIR)'/fail-preinst +endef + +define clean-up = +# Cleaning up leftover /$(1) which should have contents $(2) +test -f '$(DPKG_INSTDIR)/$(1)' +test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)' +$(BEROOT) rm -f '$(DPKG_INSTDIR)/$(1)' +endef + +define check-nexists = +test ! -f '$(DPKG_INSTDIR)/$(1)' +endef + +define check-exists = +test -f '$(DPKG_INSTDIR)/$(1)' +test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)' +endef + +define check-config-file = +# $(1)($(2)) +$(if $(3),$\ + $(call check-exists,$(2),$(3)),$\ + $(call check-nexists,$(2))) +$(call check-nexists,$(2).dpkg-backup) +$(if $(4),$\ + $(call check-exists,$(2).dpkg-bak,$(4)),$\ + $(call check-nexists,$(2).dpkg-bak)) +$(call check-nexists,$(2).dpkg-remove) + +endef + +# Asserts that $(1) and backup files do not exist. +check-removed-clean = \ + $(call check-config-file,check-removed-clean,$(1),,) + +# Asserts that case $(1) with "modified" contents was removed, with +# $(1).dpkg-bak containing $(2). +check-removed-with-backup = \ + $(call check-config-file,check-removed-with-backup,$(1),,$(2)) + +# Asserts that the attempt to remove $(1) was ignored; it still exists with +# contents $(2). No backup files exist. +check-noop = \ + $(call check-config-file,check-noop,$(1),$(2),) + +TEST_CASES += test-install +test-install: + $(check-clean) + $(DPKG_INSTALL) pkg-rm-1.deb + $(call check-removed-clean,conf) + $(call check-removed-clean,nonconf) + $(call check-removed-clean,unowned) + $(DPKG_PURGE) pkg-rm + $(check-clean) + +TEST_CASES += test-unmodified-upgrade +test-unmodified-upgrade: + $(prepare-unmodified) + $(DPKG_INSTALL) pkg-rm-1.deb + $(call check-removed-clean,conf) + # TODO: rm_conffile mistakenly treats unmodified files that are shipped + # but not in conffiles as if they were modified. + $(call check-removed-with-backup,nonconf,original) + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call check-noop,unowned,original) + $(DPKG_PURGE) pkg-rm + $(call clean-up,unowned,original) + $(check-clean) + +TEST_CASES += test-modified-upgrade +test-modified-upgrade: + $(prepare-modified) + $(DPKG_INSTALL) pkg-rm-1.deb + $(call check-removed-with-backup,conf,modified) + $(call check-removed-with-backup,nonconf,modified) + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call check-noop,unowned,modified) + $(DPKG_PURGE) pkg-rm + $(call clean-up,unowned,modified) + $(check-clean) + +TEST_CASES += test-unmodified-remove-install +test-unmodified-remove-install: + $(prepare-unmodified) + $(DPKG_REMOVE) pkg-rm + $(DPKG_INSTALL) pkg-rm-1.deb + $(call check-removed-clean,conf) + $(call check-removed-clean,nonconf) + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call check-noop,unowned,original) + $(DPKG_PURGE) pkg-rm + $(call clean-up,unowned,original) + $(check-clean) + +TEST_CASES += test-modified-remove-install +test-modified-remove-install: + $(prepare-modified) + $(DPKG_REMOVE) pkg-rm + $(DPKG_INSTALL) pkg-rm-1.deb + $(call check-removed-with-backup,conf,modified) + # When an old version of a package is removed before the new version is + # installed (as opposed to upgrading from the old to the new), it is + # impossible for dpkg-m-h to back up the modified contents of a file + # that was shipped but not listed in conffiles. This is because dpkg + # unconditionally deletes each non-conffile when removing a package, and + # does so without making a backup even if the file is modified. This + # removal happens before the new version's preinst runs; contrast this + # with package upgrades where the new version's preinst runs before + # the old version's files are deleted. + $(call check-removed-clean,nonconf) + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call check-noop,unowned,modified) + $(DPKG_PURGE) pkg-rm + $(call clean-up,unowned,modified) + $(check-clean) + +TEST_CASES += test-unmodified-abort +test-unmodified-abort: + $(prepare-unmodified) + $(prepare-abort) + $(call check-noop,conf,original) + $(call check-noop,nonconf,original) + $(call check-noop,unowned,original) + $(DPKG_PURGE) pkg-rm + $(check-clean) + +TEST_CASES += test-modified-abort +test-modified-abort: + $(prepare-modified) + $(prepare-abort) + $(call check-noop,conf,modified) + $(call check-noop,nonconf,modified) + $(call check-noop,unowned,modified) + $(DPKG_PURGE) pkg-rm + $(check-clean) + +TEST_CASES += test-unmodified-unpack-purge +test-unmodified-unpack-purge: + $(prepare-unmodified) + $(DPKG_UNPACK) pkg-rm-1.deb +# We don't really care about the state of the files between unpacking and +# purging: +# - There's no defined semantics for the 'unpacked' state. +# - A package is not expected to be in the 'unpacked' state for long on a real +# system, so it's better to test the possible final states. +# - The state of the files depends on dpkg implementation choices, which +# should be tested elsewhere. + $(DPKG_PURGE) pkg-rm + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call clean-up,unowned,original) + $(check-clean) + +TEST_CASES += test-modified-unpack-purge +test-modified-unpack-purge: + $(prepare-modified) + $(DPKG_UNPACK) pkg-rm-1.deb +# We don't really care about the state of the files between unpacking and +# purging: +# - There's no defined semantics for the 'unpacked' state. +# - A package is not expected to be in the 'unpacked' state for long on a real +# system, so it's better to test the possible final states. +# - The state of the files depends on dpkg implementation choices, which +# should be tested elsewhere. + $(DPKG_PURGE) pkg-rm + # Attempts to remove unowned files are ignored, and pkg-rm-1 + # intentionally does not clean up any unowned files installed by + # pkg-rm-0. + $(call clean-up,unowned,modified) + $(check-clean) + +.PHONY: $(TEST_CASES) + +test-case: $(TEST_CASES) + +test-clean: + $(DPKG_PURGE) pkg-rm + $(check-clean) diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles new file mode 100644 index 000000000..c9a5c234a --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/conffiles @@ -0,0 +1 @@ +/conf diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control new file mode 100644 index 000000000..8f2f0d3c2 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/control @@ -0,0 +1,7 @@ +Package: pkg-rm +Version: 0 +Section: test +Priority: extra +Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org> +Architecture: all +Description: test package - rm_conffile diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst new file mode 100755 index 000000000..0669af0ba --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postinst @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +case $1 in + configure) + # Simulate creation of the config file via ucf or debconf. + echo original >$DPKG_ROOT/unowned + ;; +esac diff --git a/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm new file mode 100755 index 000000000..47c82c688 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/DEBIAN/postrm @@ -0,0 +1,7 @@ +#!/bin/sh +set -e +case $1 in + purge) + rm -f "$DPKG_ROOT"/unowned + ;; +esac diff --git a/tests/t-maintscript-rm/pkg-rm-0/conf b/tests/t-maintscript-rm/pkg-rm-0/conf new file mode 100644 index 000000000..4b48deed3 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/conf @@ -0,0 +1 @@ +original diff --git a/tests/t-maintscript-rm/pkg-rm-0/nonconf b/tests/t-maintscript-rm/pkg-rm-0/nonconf new file mode 100644 index 000000000..4b48deed3 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-0/nonconf @@ -0,0 +1 @@ +original diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control new file mode 100644 index 000000000..dae93a783 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/control @@ -0,0 +1,7 @@ +Package: pkg-rm +Version: 1 +Section: test +Priority: extra +Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org> +Architecture: all +Description: test package - rm_conffile diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst new file mode 100755 index 000000000..84501c341 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postinst @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +# dpkg-maintscript-helper's rm_conffile command doesn't support unowned files, +# so the postinst script would normally delete the unowned file itself and not +# attempt to use rm_conffile. rm_conffile is intentionally misused here to test +# its behavior when given an unowned file. +for tc in conf nonconf unowned; do + dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@" +done diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm new file mode 100755 index 000000000..344ea141e --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/postrm @@ -0,0 +1,5 @@ +#!/bin/sh +set -e +for tc in conf nonconf unowned; do + dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@" +done diff --git a/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst new file mode 100755 index 000000000..c3ea10bd8 --- /dev/null +++ b/tests/t-maintscript-rm/pkg-rm-1/DEBIAN/preinst @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +for tc in conf nonconf unowned; do + dpkg-maintscript-helper rm_conffile "/$tc" 0 -- "$@" +done +if [ -e "$DPKG_ROOT/fail-preinst" ]; then + exit 1 +fi -- 2.48.1
From 430ab82615903c336aab972eaed5cf9b0cbc4c3d Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Thu, 30 Jan 2025 20:53:16 -0500 Subject: [PATCH 03/10] tests: dpkg-m-h: New mv_conffile tests to improve coverage This commit adds exhaustive (I hope) test cases for mv_conffile. Current state of mv_conffile: Moving a conffile: dpkg-maintscript-helper works as expected regardless of whether the destination is another conffile path, a shipped-but-non-conffile path, or a non-shipped path (e.g., normally created during postinst by ucf or via debconf answers). However, there are a couple of minor purge bugs, marked with TODO comments: * If (a) the src conffile is unmodified and (b) the new package is unpacked but never configured (or configuration failed before mv_conffile was invoked) and (c) the package is purged, then dpkg-maintscript-helper will not delete the .dpkg-remove file it created during preinst. * If (a) the src conffile is modified and (b) the dst path is a non-shipped path and (c) the dst path exists before mv_conffile is invoked in postinst and (d) the package is purged, then the .dpkg-new file created by dpkg-m-h during postinst is not deleted. (This is not a problem for the conffile and shipped-but-non-conffile cases thanks to reliance on undocumented(?) dpkg behavior.) Moving a shipped-but-non-conffile: dpkg-maintscript-helper does not preserve the contents of a modified src file if it was shipped in the previously installed version but was not listed as a conffile. This could be problematic if the previously installed version erroneously omitted the file from conffiles. If the user performs remove-then-install instead of upgrade, it is impossible for dpkg-m-h to preserve modifications because dpkg deletes the modified non-conffile before the new version's preinst runs. However, for upgrades it is technically possible to preserve modifications. Doing so would align with the current behavior of rm_conffile (which creates a backup of a modified non-conffile) but would not be consistent with the remove-then-install behavior. Moving a non-shipped file: dpkg-maintscript-helper silently ignores any attempt to move a file not listed in `dpkg-query -L`, as expected. As a consequence, dpkg-m-h cannot be used to rename a config file created during postinst by ucf or via debconf answers. --- tests/Makefile | 1 + tests/t-maintscript-mv/.gitignore | 4 + tests/t-maintscript-mv/Makefile | 391 +++++++++++++++++++++++++++ tests/t-maintscript-mv/control.in | 7 + tests/t-maintscript-mv/postinst-0.in | 10 + tests/t-maintscript-mv/postinst-1.in | 17 ++ tests/t-maintscript-mv/postrm-0.in | 9 + tests/t-maintscript-mv/postrm-1.in | 9 + tests/t-maintscript-mv/preinst-1.in | 8 + 9 files changed, 456 insertions(+) create mode 100644 tests/t-maintscript-mv/.gitignore create mode 100644 tests/t-maintscript-mv/Makefile create mode 100644 tests/t-maintscript-mv/control.in create mode 100755 tests/t-maintscript-mv/postinst-0.in create mode 100755 tests/t-maintscript-mv/postinst-1.in create mode 100755 tests/t-maintscript-mv/postrm-0.in create mode 100755 tests/t-maintscript-mv/postrm-1.in create mode 100755 tests/t-maintscript-mv/preinst-1.in diff --git a/tests/Makefile b/tests/Makefile index ff66e108f..8414dec21 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -41,6 +41,7 @@ ifdef DPKG_AS_ROOT TESTS_PASS += t-unpack-device endif TESTS_PASS += t-maintscript-leak +TESTS_PASS += t-maintscript-mv TESTS_PASS += t-maintscript-rm TESTS_PASS += t-filtering TESTS_PASS += t-depends diff --git a/tests/t-maintscript-mv/.gitignore b/tests/t-maintscript-mv/.gitignore new file mode 100644 index 000000000..baca1433c --- /dev/null +++ b/tests/t-maintscript-mv/.gitignore @@ -0,0 +1,4 @@ +/control-0.in +/control-1.in +/pkg-mv-0/ +/pkg-mv-1/ diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile new file mode 100644 index 000000000..79a11412b --- /dev/null +++ b/tests/t-maintscript-mv/Makefile @@ -0,0 +1,391 @@ +TESTS_DEB := pkg-mv-0 pkg-mv-1 + +include ../Test.mk + +define newline = + + +endef + +# Source file (installed by pkg-mv-0.deb) to destination file (installed by +# pkg-mv-1.deb) combinations: +# - conf-to-*: src file is shipped and listed in pkg-mv-0's conffiles +# - nonconf-to-*: src file is shipped in pkg-mv-0 but is not in conffiles +# - unowned-to-*: src file is created in pkg-mv-0's postinst, not shipped +# - *-to-conf: dst file is shipped and listed in pkg-mv-1's conffiles +# - *-to-nonconf: dst file is shipped in pkg-mv-1 but is not in conffiles +# - *-to-unowned: dst file is created in pkg-mv-1's postinst, not shipped +cases = \ + $(foreach src,conf nonconf unowned,$\ + $(foreach dst,conf nonconf unowned,$\ + $(src)-to-$(dst))) + +$(foreach case,$(cases),$\ + $(if $(filter conf-to-%,$(case)),$(eval conffiles-0 += $(case)-src))$\ + $(if $(filter nonconf-to-%,$(case)),$(eval nonconffiles-0 += $(case)-src))$\ + $(if $(filter unowned-to-%,$(case)),$(eval unowned-0 += $(case)-src))$\ + $(if $(filter %-to-conf,$(case)),$(eval conffiles-1 += $(case)-dst))$\ + $(if $(filter %-to-nonconf,$(case)),$(eval nonconffiles-1 += $(case)-dst))$\ + $(if $(filter %-to-unowned,$(case)),$(eval unowned-1 += $(case)-dst))) + +controlfiles-0 = conffiles control postinst postrm +controlfiles-1 = conffiles control preinst postinst postrm + +# $(1) is the package version number (either 0 or 1) +define pkg-mv-template = +controlfiles-src-$(1) = $$(controlfiles-$(1):%=pkg-mv-$(1)/DEBIAN/%) +shipped-$(1) = $$(conffiles-$(1)) $$(nonconffiles-$(1)) +shipped-src-$(1) = $$(shipped-$(1):%=pkg-mv-$(1)/%) + +$$(shipped-src-$(1)): | pkg-mv-$(1) + +pkg-mv-$(1).deb: \ + pkg-mv-$(1)/DEBIAN/conffiles \ + $$(shipped-src-$(1)) \ + $$(controlfiles-src-$(1)) + +pkg-mv-$(1)/DEBIAN/%: %-$(1).in Makefile | pkg-mv-$(1)/DEBIAN + rm -f '$$@'.tmp + sed \ + -e 's|%cases%|$$(cases)|g' \ + -e 's|%unowned%|$$(unowned-$(v))|g' \ + -e 's|%version%|$(v)|g' \ + '$$<' >'$$@'.tmp + case '$$<' in control-*|conffiles-*);; *) chmod +x '$$@'.tmp;; esac + mv '$$@'.tmp '$$@' + +%-$(1).in: %.in + cp '$$<' '$$@' + +pkg-mv-$(1)/DEBIAN/conffiles: Makefile | pkg-mv-$(1)/DEBIAN + rm -f '$$@'.tmp + $$(foreach cf,$$(conffiles-$(1)),$\ + printf %s\\n '/$$(cf)' >>'$$@'.tmp$$(newline)) + mv '$$@'.tmp '$$@' + +pkg-mv-$(1)/DEBIAN: | pkg-mv-$(1) + +pkg-mv-$(1)/DEBIAN pkg-mv-$(1): + mkdir -p '$$@' +endef + +$(foreach v,0 1,$(eval $(call pkg-mv-template,$(v)))) + +$(shipped-src-0): + echo old >'$@' + +$(shipped-src-1): + echo new >'$@' + +define check-clean = +out=$$(ls -A '$(DPKG_INSTDIR)') || exit 1 && test -z "$$out" || { \ + printf %s\\n "install directory "'$(DPKG_INSTDIR)'" not clean:" "$$out" >&2; \ + exit 1; \ +} +endef + +define prepare-unmodified = +$(check-clean) +$(DPKG_INSTALL) pkg-mv-0.deb +$(foreach case,$(cases),$\ +test -f '$(DPKG_INSTDIR)/$(case)'-src +test "$$(cat '$(DPKG_INSTDIR)/$(case)'-src)" = "old" +) +endef + +define prepare-modified = +$(prepare-unmodified) +$(foreach case,$(cases),$\ +$(BEROOT) sh -c "echo modified >'$(DPKG_INSTDIR)/$(case)'-src" +test -f '$(DPKG_INSTDIR)/$(case)'-src +test "$$(cat '$(DPKG_INSTDIR)/$(case)'-src)" = "modified" +) +endef + +define prepare-abort = +$(BEROOT) touch '$(DPKG_INSTDIR)'/fail-preinst +$(DPKG_INSTALL) pkg-mv-1.deb || true +$(BEROOT) rm -f '$(DPKG_INSTDIR)'/fail-preinst +endef + +define clean-up = +# Cleaning up leftover /$(1) which should have contents $(2) +test -f '$(DPKG_INSTDIR)/$(1)' +test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)' +$(BEROOT) rm -f '$(DPKG_INSTDIR)/$(1)' +endef + +define check-nexists = +test ! -f '$(DPKG_INSTDIR)/$(1)' +endef + +define check-exists = +test -f '$(DPKG_INSTDIR)/$(1)' +test "$$(cat '$(DPKG_INSTDIR)/$(1)')" = '$(2)' +endef + +define check-config-files = +# $(1)($(2)) +$(if $(3),$\ + $(call check-exists,$(2)-src,$(3)),$\ + $(call check-nexists,$(2)-src)) +$(call check-nexists,$(2)-src.dpkg-remove) +$(if $(4),$\ + $(call check-exists,$(2)-dst,$(4)),$\ + $(call check-nexists,$(2)-dst)) +$(if $(5),$\ + $(call check-exists,$(2)-dst.dpkg-new,$(5)),$\ + $(call check-nexists,$(2)-dst.dpkg-new)) + +endef + +# Asserts that case $(1) was cleanly installed/upgraded with "new" contents. +check-clean-installed = \ + $(call check-config-files,check-clean-installed,$(1),,new,) + +# Asserts that the attempt to upgrade case $(1) was ignored, dst has "new" +# contents, and src still exists with contents $(2). +check-upgrade-ignored = \ + $(call check-config-files,check-upgrade-ignored,$(1),$(2),new,) + +# Asserts that case $(1) with "modified" contents was cleanly upgraded. +check-upgraded-modified = \ + $(call check-config-files,check-upgraded-modified,$(1),,modified,new) + +# Asserts that no changes were made for case $(1); src still contains $(2). +check-noop = $(call check-config-files,check-noop,$(1),$(2),,) + +TEST_CASES += test-install +test-install: + $(check-clean) + $(DPKG_INSTALL) pkg-mv-1.deb + $(foreach case,$(cases),$(call check-clean-installed,$(case))) + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). + $(call clean-up,conf-to-unowned-dst,new) + $(call clean-up,nonconf-to-unowned-dst,new) + $(call clean-up,unowned-to-unowned-dst,new) + $(check-clean) + +TEST_CASES += test-unmodified-upgrade +test-unmodified-upgrade: + $(prepare-unmodified) + $(DPKG_INSTALL) pkg-mv-1.deb + $(call check-clean-installed,conf-to-conf) + $(call check-clean-installed,conf-to-nonconf) + $(call check-clean-installed,conf-to-unowned) + $(call check-clean-installed,nonconf-to-conf) + $(call check-clean-installed,nonconf-to-nonconf) + $(call check-clean-installed,nonconf-to-unowned) + # Attempts to move unowned files are ignored, and pkg-mv-1 intentionally + # does not clean up any unowned files installed by pkg-mv-0. + $(call check-upgrade-ignored,unowned-to-conf,old) + $(call check-upgrade-ignored,unowned-to-nonconf,old) + $(call check-upgrade-ignored,unowned-to-unowned,old) + + ######## Test cleanup. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). + $(call clean-up,unowned-to-conf-src,old) + $(call clean-up,unowned-to-nonconf-src,old) + $(call clean-up,unowned-to-unowned-src,old) + $(call clean-up,conf-to-unowned-dst,new) + $(call clean-up,nonconf-to-unowned-dst,new) + $(call clean-up,unowned-to-unowned-dst,new) + $(check-clean) + +TEST_CASES += test-modified-upgrade +test-modified-upgrade: + $(prepare-modified) + $(DPKG_INSTALL) pkg-mv-1.deb + $(call check-upgraded-modified,conf-to-conf) + $(call check-upgraded-modified,conf-to-nonconf) + $(call check-upgraded-modified,conf-to-unowned) + # TODO: dpkg-m-h could check each nonconf-to-*-src against the package's + # md5sums control file and back it up if modified. That would prevent + # loss of configuration data if the nonconf file was erroneously omitted + # from conffiles in the previous version of the package. + $(call check-clean-installed,nonconf-to-conf) + $(call check-clean-installed,nonconf-to-nonconf) + $(call check-clean-installed,nonconf-to-unowned) + # Attempts to move unowned files are ignored, and pkg-mv-1 intentionally + # does not clean up any unowned files installed by pkg-mv-0. + $(call check-upgrade-ignored,unowned-to-conf,modified) + $(call check-upgrade-ignored,unowned-to-nonconf,modified) + $(call check-upgrade-ignored,unowned-to-unowned,modified) + + ######## Test cleanup. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). + $(call clean-up,unowned-to-conf-src,modified) + $(call clean-up,unowned-to-nonconf-src,modified) + $(call clean-up,unowned-to-unowned-src,modified) + $(call clean-up,conf-to-unowned-dst,modified) + # TODO: dpkg deletes the modified src before postinst has the chance to + # move it to dst (hence "new" instead of "modified"). + $(call clean-up,nonconf-to-unowned-dst,new) + # Attempts to move unowned files are ignored, so the modified src does + # not propagate to the dst (hence "new" instead of "modified"). + $(call clean-up,unowned-to-unowned-dst,new) + # TODO: Either ignore attempts to move to an unowned dst or delete the + # .dpkg-new in postrm purge. + $(call clean-up,conf-to-unowned-dst.dpkg-new,new) + $(check-clean) + +TEST_CASES += test-unmodified-remove-install +test-unmodified-remove-install: + $(prepare-unmodified) + $(DPKG_REMOVE) pkg-mv + $(DPKG_INSTALL) pkg-mv-1.deb + $(call check-clean-installed,conf-to-conf) + $(call check-clean-installed,conf-to-nonconf) + $(call check-clean-installed,conf-to-unowned) + $(call check-clean-installed,nonconf-to-conf) + $(call check-clean-installed,nonconf-to-nonconf) + $(call check-clean-installed,nonconf-to-unowned) + # Attempts to move unowned files are ignored, and pkg-mv-1 intentionally + # does not clean up any unowned files installed by pkg-mv-0. + $(call check-upgrade-ignored,unowned-to-conf,old) + $(call check-upgrade-ignored,unowned-to-nonconf,old) + $(call check-upgrade-ignored,unowned-to-unowned,old) + + ######## Test cleanup. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). + $(call clean-up,unowned-to-conf-src,old) + $(call clean-up,unowned-to-nonconf-src,old) + $(call clean-up,unowned-to-unowned-src,old) + $(call clean-up,conf-to-unowned-dst,new) + $(call clean-up,nonconf-to-unowned-dst,new) + $(call clean-up,unowned-to-unowned-dst,new) + $(check-clean) + +TEST_CASES += test-modified-remove-install +test-modified-remove-install: + $(prepare-modified) + $(DPKG_REMOVE) pkg-mv + $(DPKG_INSTALL) pkg-mv-1.deb + $(call check-upgraded-modified,conf-to-conf) + $(call check-upgraded-modified,conf-to-nonconf) + $(call check-upgraded-modified,conf-to-unowned) + # When an old version of a package is removed before the new version is + # installed (as opposed to upgrading from the old to the new), it is + # impossible for dpkg-m-h to preserve the modified contents of a file + # that was shipped but not listed in conffiles. This is because dpkg + # unconditionally deletes each non-conffile when removing a package, and + # does so without making a backup even if the file is modified. This + # removal happens before the new version's preinst runs; contrast this + # with package upgrades where the new version's preinst runs before the + # old version's files are deleted. + $(call check-clean-installed,nonconf-to-conf) + $(call check-clean-installed,nonconf-to-nonconf) + $(call check-clean-installed,nonconf-to-unowned) + # Attempts to move unowned files are ignored, and pkg-mv-1 intentionally + # does not clean up any unowned files installed by pkg-mv-0. + $(call check-upgrade-ignored,unowned-to-conf,modified) + $(call check-upgrade-ignored,unowned-to-nonconf,modified) + $(call check-upgrade-ignored,unowned-to-unowned,modified) + + ######## Test cleanup. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). + $(call clean-up,unowned-to-conf-src,modified) + $(call clean-up,unowned-to-nonconf-src,modified) + $(call clean-up,unowned-to-unowned-src,modified) + $(call clean-up,conf-to-unowned-dst,modified) + # dpkg-m-h can't preserve the modified contents of nonconf files when + # the package is removed then installed (hence "new" instead of + # "modified"). + $(call clean-up,nonconf-to-unowned-dst,new) + # Attempts to move unowned files are ignored, so the modified src does + # not propagate to the dst (hence "new" instead of "modified"). + $(call clean-up,unowned-to-unowned-dst,new) + # TODO: Either ignore attempts to move to an unowned dst or delete the + # .dpkg-new in postrm purge. + $(call clean-up,conf-to-unowned-dst.dpkg-new,new) + $(check-clean) + +TEST_CASES += test-unmodified-abort +test-unmodified-abort: + $(prepare-unmodified) + $(prepare-abort) + $(foreach case,$(cases),$(call check-noop,$(case),old)) + $(DPKG_PURGE) pkg-mv + $(check-clean) + +TEST_CASES += test-modified-abort +test-modified-abort: + $(prepare-modified) + $(prepare-abort) + $(foreach case,$(cases),$(call check-noop,$(case),modified)) + $(DPKG_PURGE) pkg-mv + $(check-clean) + +TEST_CASES += test-unmodified-unpack-purge +test-unmodified-unpack-purge: + $(prepare-unmodified) + $(DPKG_UNPACK) pkg-mv-1.deb +# We don't really care about the state of the files between unpacking and +# purging: +# - The semantics of the 'unpacked' state is out of scope for this test. +# - A package is not expected to be in the 'unpacked' state for long on a real +# system, so it's better to test the possible final states. +# - The state of the files depends on dpkg implementation choices, which +# should be tested elsewhere. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). Note: pkg-mv-1 + # didn't get a chance to create the *-to-unowned-dst files so they don't + # need to be cleaned up. + $(call clean-up,unowned-to-conf-src,old) + $(call clean-up,unowned-to-nonconf-src,old) + $(call clean-up,unowned-to-unowned-src,old) + # TODO: Delete the .dpkg-remove files in postrm purge. + $(call clean-up,conf-to-conf-src.dpkg-remove,old) + $(call clean-up,conf-to-nonconf-src.dpkg-remove,old) + $(call clean-up,conf-to-unowned-src.dpkg-remove,old) + $(check-clean) + +TEST_CASES += test-modified-unpack-purge +test-modified-unpack-purge: + $(prepare-modified) + $(DPKG_UNPACK) pkg-mv-1.deb +# We don't really care about the state of the files between unpacking and +# purging: +# - The semantics of the 'unpacked' state is out of scope for this test. +# - A package is not expected to be in the 'unpacked' state for long on a real +# system, so it's better to test the possible final states. +# - The state of the files depends on dpkg implementation choices, which +# should be tested elsewhere. + $(DPKG_PURGE) pkg-mv + # mv_conffile doesn't support unowned src or dst files, and the test + # package intentionally does not clean them up (so that mv_conffile's + # behavior with unowned files can be tested here). Note: pkg-mv-1 + # didn't get a chance to create the *-to-unowned-dst files so they don't + # need to be cleaned up. + $(call clean-up,unowned-to-conf-src,modified) + $(call clean-up,unowned-to-nonconf-src,modified) + $(call clean-up,unowned-to-unowned-src,modified) + $(check-clean) + +.PHONY: $(TEST_CASES) + +test-case: $(TEST_CASES) + +test-clean: + $(DPKG_PURGE) pkg-mv + $(check-clean) + +clean-hook: + rm -rf $(TESTS_DEB) diff --git a/tests/t-maintscript-mv/control.in b/tests/t-maintscript-mv/control.in new file mode 100644 index 000000000..0b9a37ff3 --- /dev/null +++ b/tests/t-maintscript-mv/control.in @@ -0,0 +1,7 @@ +Package: pkg-mv +Version: %version% +Section: test +Priority: extra +Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org> +Architecture: all +Description: test package - mv_conffile diff --git a/tests/t-maintscript-mv/postinst-0.in b/tests/t-maintscript-mv/postinst-0.in new file mode 100755 index 000000000..5592eae6d --- /dev/null +++ b/tests/t-maintscript-mv/postinst-0.in @@ -0,0 +1,10 @@ +#!/bin/sh +set -e +case $1 in + configure) + # Simulate creation of the config file via ucf or debconf. + for f in %unowned%; do + echo old >$DPKG_ROOT/$f + done + ;; +esac diff --git a/tests/t-maintscript-mv/postinst-1.in b/tests/t-maintscript-mv/postinst-1.in new file mode 100755 index 000000000..7280f6333 --- /dev/null +++ b/tests/t-maintscript-mv/postinst-1.in @@ -0,0 +1,17 @@ +#!/bin/sh +set -e +case $1 in + configure) + # Simulate creation of the new config file via ucf or debconf. + for f in %unowned%; do + echo new >$DPKG_ROOT/$f + done + ;; +esac +# dpkg-maintscript-helper's mv_conffile command doesn't support unowned src +# files, so the postinst script would normally rename the unowned file itself +# and not attempt to use mv_conffile. mv_conffile is intentionally misused here +# to test its behavior when given an unowned src. +for tc in %cases%; do + dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@" +done diff --git a/tests/t-maintscript-mv/postrm-0.in b/tests/t-maintscript-mv/postrm-0.in new file mode 100755 index 000000000..ea13ab288 --- /dev/null +++ b/tests/t-maintscript-mv/postrm-0.in @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +case $1 in + purge) + for f in %unowned%; do + rm -f "$DPKG_ROOT/$f" + done + ;; +esac diff --git a/tests/t-maintscript-mv/postrm-1.in b/tests/t-maintscript-mv/postrm-1.in new file mode 100755 index 000000000..8f30a8c04 --- /dev/null +++ b/tests/t-maintscript-mv/postrm-1.in @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +# dpkg-maintscript-helper's mv_conffile command doesn't support unowned dst +# files, so the postrm script would normally delete the unowned file itself +# during purge. mv_conffile is intentionally misused here to test its behavior +# when given an unowned dst. +for tc in %cases%; do + dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@" +done diff --git a/tests/t-maintscript-mv/preinst-1.in b/tests/t-maintscript-mv/preinst-1.in new file mode 100755 index 000000000..72b9b3cfa --- /dev/null +++ b/tests/t-maintscript-mv/preinst-1.in @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +for tc in %cases%; do + dpkg-maintscript-helper mv_conffile "/$tc-src" "/$tc-dst" 0 -- "$@" +done +if [ -e "$DPKG_ROOT/fail-preinst" ]; then + exit 1 +fi -- 2.48.1
From d5070c6e57feb412018270f7d8b9032c0775c5b3 Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Wed, 29 Jan 2025 19:23:46 -0500 Subject: [PATCH 04/10] man: dpkg-m-h: {rm,mv}_conffile is only meant for ${Conffiles} --- man/dpkg-maintscript-helper.pod | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod index ee938e911..2adaeba2a 100644 --- a/man/dpkg-maintscript-helper.pod +++ b/man/dpkg-maintscript-helper.pod @@ -153,6 +153,17 @@ Z<> =back I<conffile> is the filename of the conffile to remove. +I<conffile> must not be shipped in the version of the package invoking +B<rm_conffile>. +This command has no effect (no error, no warning) if either I<conffile> does not +exist on the user's system or if I<conffile> is not listed in the files +installed by the package (i.e., the output of B<dpkg-query --listfiles>). +In particular, this command does nothing if I<conffile> was created in +B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to +L<debconf(7)> questions). +The behavior is undefined if I<conffile> exists and is listed in the files +installed by the package but is not listed in the package's B<Conffiles> status +field. Current implementation: in the B<preinst>, it checks if the conffile was modified and renames it either to I<conffile>B<.dpkg-remove> (if not @@ -189,6 +200,24 @@ Z<> I<old-conffile> and I<new-conffile> are the old and new name of the conffile to rename. +I<old-conffile> must not be shipped in the version of the package invoking +B<mv_conffile>. +This command has no effect (no error, no warning) if either I<old-conffile> does +not exist on the user's system or if I<old-conffile> is not listed in the files +installed by the package (i.e., the output of B<dpkg-query --listfiles>). +In particular, this command does nothing if I<old-conffile> was created in +B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to +L<debconf(7)> questions). +The behavior is undefined if I<old-conffile> exists and is listed in the files +installed by the package but is not listed in the package's B<Conffiles> status +field. + +The behavior is undefined if I<new-conffile> is not listed in the package's +I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation). +In particular, the behavior is undefined if I<new-conffile> is created in +B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to +L<debconf(7)> questions). + Current implementation: the B<preinst> checks if the conffile has been modified, if yes it's left on place otherwise it's renamed to I<old-conffile>B<.dpkg-remove>. -- 2.48.1
From 4745f5825d922f6f6427c5ad9e67fb5264b6157d Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Mon, 24 Feb 2025 02:22:37 -0500 Subject: [PATCH 05/10] man: dpkg-m-h: mv_conffile doesn't prompt if orig conffile changed --- man/dpkg-maintscript-helper.pod | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod index 2adaeba2a..5ba5423f1 100644 --- a/man/dpkg-maintscript-helper.pod +++ b/man/dpkg-maintscript-helper.pod @@ -227,6 +227,13 @@ to I<new-conffile> if I<old-conffile> is still available. On abort-upgrade/abort-install, the B<postrm> renames I<old-conffile>B<.dpkg-remove> back to I<old-conffile> if required. +B<mv_conffile> does not currently prompt the user to update a moved conffile if +the new unmodified version differs from the old unmodified version (see L<bug +#711598|https://bugs.debian.org/711598>). +If the package installs the new unmodified config file to I<new-conffile> before +it invokes B<mv_conffile> in B<postinst> during configuration, B<mv_conffile> +will rename it to I<new-conffile>B<.dpkg-new> for user convenience or for +post-processing in B<postinst> after B<mv_conffile> returns. =head1 SYMLINK AND DIRECTORY SWITCHES -- 2.48.1
From 54f42ef220df185a93024aacc3b1305ff0b60180 Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Wed, 29 Jan 2025 19:27:51 -0500 Subject: [PATCH 06/10] dpkg-m-h: Improve robustness of conffile md5 extraction This avoids problems if the conffile name contains regex metacharacters. --- src/dpkg-maintscript-helper.sh | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh index c7de6edf9..155c017ce 100755 --- a/src/dpkg-maintscript-helper.sh +++ b/src/dpkg-maintscript-helper.sh @@ -98,9 +98,8 @@ prepare_rm_conffile() { ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 local md5sum old_md5sum + old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE") md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')" - old_md5sum="$(dpkg-query -W -f='${Conffiles}' "$PACKAGE" | \ - sed -n -e "\\'^ $CONFFILE ' { s/ obsolete$//; s/.* //; p }")" if [ "$md5sum" != "$old_md5sum" ]; then mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-backup" else @@ -212,9 +211,8 @@ prepare_mv_conffile() { ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 local md5sum old_md5sum + old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE") md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')" - old_md5sum="$(dpkg-query -W -f='${Conffiles}' "$PACKAGE" | \ - sed -n -e "\\'^ $CONFFILE ' { s/ obsolete$//; s/.* //; p }")" if [ "$md5sum" = "$old_md5sum" ]; then mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-remove" fi @@ -526,6 +524,21 @@ validate_optional_version() { fi } +conffile_md5() { + local out line + out=$(dpkg-query -W -f '${Conffiles}' "$1") + [ -n "$out" ] || return 0 + # Setting IFS to <space><tab> strips leading and trailing space from the line. + while IFS=' ' read -r line; do + line=${line% obsolete} + [ "${line% *}" = "$2" ] || continue + printf %s\\n "${line##* }" + return 0 + done <<EOF +${out} +EOF +} + ensure_package_owns_file() { local PACKAGE="$1" local FILE="$2" -- 2.48.1
From c9d4f6bfe8a690cc44edb204786fc5f7b506024e Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Wed, 29 Jan 2025 19:32:43 -0500 Subject: [PATCH 07/10] dpkg-m-h: rm_conffile, mv_conffile: Skip if not a conffile Before this commit, if the user ran `rm_conffile` or `mv_conffile` on a non-conffile belonging to the package, it would treat the file as if it was a modified conffile: * `ensure_package_owns_file` returns true on non-conffiles because it is based on `dpkg-query -L`, which includes non-conffiles. * `rm_conffile` and `mv_conffile` did not return if the file was missing from `Conffiles`. Instead, they behaved as if the file's original md5 was the empty string, which always compares unequal to any real md5. Now `rm_conffile` and `mv_conffile` silently ignore non-conffiles because they test file existence in the `Conffiles` field, not existence in the complete list of package files. --- man/dpkg-maintscript-helper.pod | 14 ++++---------- src/dpkg-maintscript-helper.sh | 17 +++++++++++------ tests/t-maintscript-rm/Makefile | 10 ++++++---- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod index 5ba5423f1..129a3963b 100644 --- a/man/dpkg-maintscript-helper.pod +++ b/man/dpkg-maintscript-helper.pod @@ -156,14 +156,11 @@ I<conffile> is the filename of the conffile to remove. I<conffile> must not be shipped in the version of the package invoking B<rm_conffile>. This command has no effect (no error, no warning) if either I<conffile> does not -exist on the user's system or if I<conffile> is not listed in the files -installed by the package (i.e., the output of B<dpkg-query --listfiles>). +exist on the user's system or if I<conffile> is not listed in the package's +B<Conffiles> status field. In particular, this command does nothing if I<conffile> was created in B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to L<debconf(7)> questions). -The behavior is undefined if I<conffile> exists and is listed in the files -installed by the package but is not listed in the package's B<Conffiles> status -field. Current implementation: in the B<preinst>, it checks if the conffile was modified and renames it either to I<conffile>B<.dpkg-remove> (if not @@ -203,14 +200,11 @@ conffile to rename. I<old-conffile> must not be shipped in the version of the package invoking B<mv_conffile>. This command has no effect (no error, no warning) if either I<old-conffile> does -not exist on the user's system or if I<old-conffile> is not listed in the files -installed by the package (i.e., the output of B<dpkg-query --listfiles>). +not exist on the user's system or if I<old-conffile> is not listed in the +package's B<Conffiles> status field. In particular, this command does nothing if I<old-conffile> was created in B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to L<debconf(7)> questions). -The behavior is undefined if I<old-conffile> exists and is listed in the files -installed by the package but is not listed in the package's B<Conffiles> status -field. The behavior is undefined if I<new-conffile> is not listed in the package's I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation). diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh index 155c017ce..8ef957a0e 100755 --- a/src/dpkg-maintscript-helper.sh +++ b/src/dpkg-maintscript-helper.sh @@ -95,10 +95,10 @@ prepare_rm_conffile() { local PACKAGE="$2" [ -e "$DPKG_ROOT$CONFFILE" ] || return 0 - ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 local md5sum old_md5sum old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE") + [ -n "$old_md5sum" ] || return 0 md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')" if [ "$md5sum" != "$old_md5sum" ]; then mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-backup" @@ -125,7 +125,7 @@ abort_rm_conffile() { local CONFFILE="$1" local PACKAGE="$2" - ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 + package_owns_conffile "$PACKAGE" "$CONFFILE" || return 0 if [ -e "$DPKG_ROOT$CONFFILE.dpkg-remove" ]; then echo "Reinstalling $DPKG_ROOT$CONFFILE that was moved away" @@ -208,10 +208,9 @@ prepare_mv_conffile() { [ -e "$DPKG_ROOT$CONFFILE" ] || return 0 - ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 - local md5sum old_md5sum old_md5sum=$(conffile_md5 "$PACKAGE" "$CONFFILE") + [ -n "$old_md5sum" ] || return 0 md5sum="$(md5sum "$DPKG_ROOT$CONFFILE" | sed -e 's/ .*//')" if [ "$md5sum" = "$old_md5sum" ]; then mv -f "$DPKG_ROOT$CONFFILE" "$DPKG_ROOT$CONFFILE.dpkg-remove" @@ -226,7 +225,7 @@ finish_mv_conffile() { rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove" [ -e "$DPKG_ROOT$OLDCONFFILE" ] || return 0 - ensure_package_owns_file "$PACKAGE" "$OLDCONFFILE" || return 0 + package_owns_conffile "$PACKAGE" "$OLDCONFFILE" || return 0 echo "Preserving user changes to $DPKG_ROOT$NEWCONFFILE (renamed from $DPKG_ROOT$OLDCONFFILE)..." if [ -e "$DPKG_ROOT$NEWCONFFILE" ]; then @@ -239,7 +238,7 @@ abort_mv_conffile() { local CONFFILE="$1" local PACKAGE="$2" - ensure_package_owns_file "$PACKAGE" "$CONFFILE" || return 0 + package_owns_conffile "$PACKAGE" "$CONFFILE" || return 0 if [ -e "$DPKG_ROOT$CONFFILE.dpkg-remove" ]; then echo "Reinstalling $DPKG_ROOT$CONFFILE that was moved away" @@ -539,6 +538,12 @@ ${out} EOF } +package_owns_conffile() { + local md5 + md5=$(conffile_md5 "$@") + [ -n "$md5" ] && true +} + ensure_package_owns_file() { local PACKAGE="$1" local FILE="$2" diff --git a/tests/t-maintscript-rm/Makefile b/tests/t-maintscript-rm/Makefile index d1d342fc1..efe2a7682 100644 --- a/tests/t-maintscript-rm/Makefile +++ b/tests/t-maintscript-rm/Makefile @@ -97,9 +97,7 @@ test-unmodified-upgrade: $(prepare-unmodified) $(DPKG_INSTALL) pkg-rm-1.deb $(call check-removed-clean,conf) - # TODO: rm_conffile mistakenly treats unmodified files that are shipped - # but not in conffiles as if they were modified. - $(call check-removed-with-backup,nonconf,original) + $(call check-removed-clean,nonconf) # Attempts to remove unowned files are ignored, and pkg-rm-1 # intentionally does not clean up any unowned files installed by # pkg-rm-0. @@ -113,7 +111,11 @@ test-modified-upgrade: $(prepare-modified) $(DPKG_INSTALL) pkg-rm-1.deb $(call check-removed-with-backup,conf,modified) - $(call check-removed-with-backup,nonconf,modified) + # TODO: dpkg-m-h could check the nonconf file against the package's + # md5sums control file and back it up if modified. That would prevent + # loss of configuration data if the nonconf file was erroneously omitted + # from conffiles in the previous version of the package. + $(call check-removed-clean,nonconf) # Attempts to remove unowned files are ignored, and pkg-rm-1 # intentionally does not clean up any unowned files installed by # pkg-rm-0. -- 2.48.1
From fe33acbf7b74bedf1cd9e4b2c31aa82d9eb056cd Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Fri, 31 Jan 2025 17:55:29 -0500 Subject: [PATCH 08/10] dpkg-m-h: mv_conffile: Delete .dpkg-remove on purge --- src/dpkg-maintscript-helper.sh | 5 +++++ tests/t-maintscript-mv/Makefile | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh index 8ef957a0e..b328a99c3 100755 --- a/src/dpkg-maintscript-helper.sh +++ b/src/dpkg-maintscript-helper.sh @@ -190,6 +190,11 @@ mv_conffile() { fi ;; postrm) + if [ "$1" = "purge" ]; then + # $OLDCONFFILE.dpkg-remove is deleted here in case the package was + # unpacked but never configured before it was purged. + rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove" + fi if [ "$1" = "abort-install" -o "$1" = "abort-upgrade" ] && [ -n "$2" ] && dpkg --compare-versions -- "$2" le-nl "$LASTVERSION"; then diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile index 79a11412b..c15c8aad4 100644 --- a/tests/t-maintscript-mv/Makefile +++ b/tests/t-maintscript-mv/Makefile @@ -351,10 +351,6 @@ test-unmodified-unpack-purge: $(call clean-up,unowned-to-conf-src,old) $(call clean-up,unowned-to-nonconf-src,old) $(call clean-up,unowned-to-unowned-src,old) - # TODO: Delete the .dpkg-remove files in postrm purge. - $(call clean-up,conf-to-conf-src.dpkg-remove,old) - $(call clean-up,conf-to-nonconf-src.dpkg-remove,old) - $(call clean-up,conf-to-unowned-src.dpkg-remove,old) $(check-clean) TEST_CASES += test-modified-unpack-purge -- 2.48.1
From 59fb217c5f1aaa7395bc3dcb6abab519a63c348d Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Sat, 1 Feb 2025 02:36:56 -0500 Subject: [PATCH 09/10] dpkg-m-h: mv_conffile: Delete .dpkg-new on purge --- src/dpkg-maintscript-helper.sh | 8 ++++++++ tests/t-maintscript-mv/Makefile | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/dpkg-maintscript-helper.sh b/src/dpkg-maintscript-helper.sh index b328a99c3..0eea4f434 100755 --- a/src/dpkg-maintscript-helper.sh +++ b/src/dpkg-maintscript-helper.sh @@ -194,6 +194,14 @@ mv_conffile() { # $OLDCONFFILE.dpkg-remove is deleted here in case the package was # unpacked but never configured before it was purged. rm -f "$DPKG_ROOT$OLDCONFFILE.dpkg-remove" + # dpkg deletes *.dpkg-new corresponding to shipped files (both conffiles + # and non-conffiles) during purge, but $NEWCONFFILE might not be a shipped + # file (e.g., it might be created during postinst via ucf(1) or using + # debconf(7) answers). + rm -f "$DPKG_ROOT$NEWCONFFILE.dpkg-new" + # $NEWCONFFILE is not deleted during purge here: dpkg itself deletes + # shipped files, and the package's postrm script is expected to delete + # non-shipped (created during postinst) config files itself. fi if [ "$1" = "abort-install" -o "$1" = "abort-upgrade" ] && [ -n "$2" ] && diff --git a/tests/t-maintscript-mv/Makefile b/tests/t-maintscript-mv/Makefile index c15c8aad4..b56e18143 100644 --- a/tests/t-maintscript-mv/Makefile +++ b/tests/t-maintscript-mv/Makefile @@ -233,9 +233,6 @@ test-modified-upgrade: # Attempts to move unowned files are ignored, so the modified src does # not propagate to the dst (hence "new" instead of "modified"). $(call clean-up,unowned-to-unowned-dst,new) - # TODO: Either ignore attempts to move to an unowned dst or delete the - # .dpkg-new in postrm purge. - $(call clean-up,conf-to-unowned-dst.dpkg-new,new) $(check-clean) TEST_CASES += test-unmodified-remove-install @@ -310,9 +307,6 @@ test-modified-remove-install: # Attempts to move unowned files are ignored, so the modified src does # not propagate to the dst (hence "new" instead of "modified"). $(call clean-up,unowned-to-unowned-dst,new) - # TODO: Either ignore attempts to move to an unowned dst or delete the - # .dpkg-new in postrm purge. - $(call clean-up,conf-to-unowned-dst.dpkg-new,new) $(check-clean) TEST_CASES += test-unmodified-abort -- 2.48.1
From 61f719ec116520c8fa93ca1e28a239422eed3a30 Mon Sep 17 00:00:00 2001 From: Richard Hansen <rhan...@rhansen.org> Date: Mon, 24 Feb 2025 02:37:49 -0500 Subject: [PATCH 10/10] man: dpkg-m-h: mv_conffile now supports non-conffile destination --- man/dpkg-maintscript-helper.pod | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/man/dpkg-maintscript-helper.pod b/man/dpkg-maintscript-helper.pod index 129a3963b..6ae29575d 100644 --- a/man/dpkg-maintscript-helper.pod +++ b/man/dpkg-maintscript-helper.pod @@ -190,12 +190,12 @@ scripts: Z<> dpkg-maintscript-helper mv_conffile \ - I<old-conffile> I<new-conffile> I<prior-version> I<package> -- "$@" + I<old-conffile> I<new-configfile> I<prior-version> I<package> -- "$@" =back -I<old-conffile> and I<new-conffile> are the old and new name of the -conffile to rename. +I<old-conffile> is the name of the conffile to rename, and I<new-configfile> is +its new name. I<old-conffile> must not be shipped in the version of the package invoking B<mv_conffile>. @@ -206,28 +206,27 @@ In particular, this command does nothing if I<old-conffile> was created in B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to L<debconf(7)> questions). -The behavior is undefined if I<new-conffile> is not listed in the package's -I<conffiles> control file (i.e., I<DEBIAN/conffiles> during package creation). -In particular, the behavior is undefined if I<new-conffile> is created in -B<postinst> (e.g., copied via L<ucf(1)> or generated using answers to -L<debconf(7)> questions). +The package is not required to ship I<new-configfile> or create it during +configuration before invoking B<mv_conffile>. +The package is responsible for ensuring that I<new-configfile> does not belong +to another package. Current implementation: the B<preinst> checks if the conffile has been modified, if yes it's left on place otherwise it's renamed to I<old-conffile>B<.dpkg-remove>. On configuration, the B<postinst> removes I<old-conffile>B<.dpkg-remove> and renames I<old-conffile> -to I<new-conffile> if I<old-conffile> is still available. +to I<new-configfile> if I<old-conffile> is still available. On abort-upgrade/abort-install, the B<postrm> renames I<old-conffile>B<.dpkg-remove> back to I<old-conffile> if required. B<mv_conffile> does not currently prompt the user to update a moved conffile if the new unmodified version differs from the old unmodified version (see L<bug #711598|https://bugs.debian.org/711598>). -If the package installs the new unmodified config file to I<new-conffile> before -it invokes B<mv_conffile> in B<postinst> during configuration, B<mv_conffile> -will rename it to I<new-conffile>B<.dpkg-new> for user convenience or for -post-processing in B<postinst> after B<mv_conffile> returns. +If the package installs the new unmodified config file to I<new-configfile> +before it invokes B<mv_conffile> in B<postinst> during configuration, +B<mv_conffile> will rename it to I<new-configfile>B<.dpkg-new> for user +convenience or for post-processing in B<postinst> after B<mv_conffile> returns. =head1 SYMLINK AND DIRECTORY SWITCHES -- 2.48.1
OpenPGP_signature.asc
Description: OpenPGP digital signature