[arch-projects] [devtools] [PATCH] makechrootpkg: Be recursive when deleting btrfs subvolumes.

lukeshu at lukeshu.com lukeshu at lukeshu.com
Fri Feb 10 08:46:08 UTC 2017

From: Luke Shumaker <lukeshu at sbcglobal.net>


  When installing the necessaryssary dependencies in the chroot, the
  ALPM hooks run; and if 'systemd' is a dependency, then one of the
  hooks is to run systemd-tmpfiles.  There are several tmpfiles.d(5)
  commands that instruct it to create btrfs subvolumes if on btrfs
  (the `v`, `q`, and `Q` commands).

  This causes a problem when we go to delete the chroot.  The command
  `btrfs subvolume delete` won't recursively delete subvolumes; if a
  child subvolume was created, it will fail with the fairly unhelpful
  error message "directory not empty".


  Because the subvolume that gets mounted isn't necessarily the
  toplevel subvolume, and `btrfs subvolume list` gives us paths
  relative to the toplevel; we need to figure out how our path relates
  to the toplevel.  Figure out the mountpoint (which turns out to be
  slightly tricky; see below), and call `btrfs subvolume list -a` on
  it to get the list of subvolumes that are visible to us (and quite
  possibly some that aren't; the logic for determining which ones it
  shows is... absurd).  This gives us a list of subvolumes with
  numeric IDs, and paths relative to the toplevel (actually it gives
  us more than that, and we use a hopefully-correct `sed` expression
  to trim it down; the format certainly isn't human-friendly, but it's
  not machine-friendly either.)  So then we look at that list of pairs
  and find the one that matches the ID of the subvolume we're trying
  to delete (which is easy to get with `btrfs subvolume show`); once
  we've found the path of our subvolume, we can use that to filter and
  trim the complete list of paths.  From there the remainder of the
  solution is obvious.

  Now, back to "figure out the mountpoint"; the normal `stat -c %m`
  doesn't work.  It gives the mounted path of the subvolume closest to
  the path we give it, not the actual mountpoint.  Now, it turns out
  that `df` can figure out the correct mountpoint (though I haven't
  investigated how it knows when stat doesn't; but I suspect it parses
  `/proc/mounts`).  So we are reduced to parsing `df`'s output.
 makechrootpkg.in | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 57 insertions(+), 2 deletions(-)

diff --git a/makechrootpkg.in b/makechrootpkg.in
index 5c4b530..01e9e96 100644
--- a/makechrootpkg.in
+++ b/makechrootpkg.in
@@ -80,6 +80,61 @@ load_vars() {
 	return 0
+# Usage: btrfs_subvolume_id $SUBVOLUME
+btrfs_subvolume_id() (
+	set -o pipefail
+	LC_ALL=C btrfs subvolume show "$1" | sed -n 's/^\tSubvolume ID:\s*//p'
+# Usage: btrfs_subvolume_list_all $FILEPATH
+# Given $FILEPATH somewhere on a mounted btrfs filesystem, print the
+# ID and full path of every subvolume on the filesystem, one per line
+# in the format "$ID $PATH".
+btrfs_subvolume_list_all() (
+	set -o pipefail
+	local mountpoint
+	mountpoint="$(df --output=target "$1" | sed 1d)" || return $?
+	LC_ALL=C btrfs subvolume list -a "$mountpoint" | sed -r 's|^ID ([0-9]+) .* path (<FS_TREE>/)?(\S*).*|\1 \3|'
+# Usage: btrfs_subvolume_list $SUBVOLUME
+# Assuming that $SUBVOLUME is a btrfs subvolume, list all child
+# subvolumes; from most deeply nested to most shallowly nested.
+# This is intended to be a sane version of `btrfs subvolume list`.
+btrfs_subvolume_list() {
+	local subvolume=$1
+	local id all path subpath
+	id="$(btrfs_subvolume_id "$subvolume")" || return $?
+	all="$(btrfs_subvolume_list_all "$subvolume")" || return $?
+	path="$(sed -n "s/^$id //p" <<<"$all")"
+	while read -r id subpath; do
+		if [[ "$subpath" = "$path"/* ]]; then
+			printf '%s\n' "${subpath#"${path}/"}"
+		fi
+	done <<<"$all" | LC_ALL=C sort --reverse
+# Usage: btrfs_subvolume_delete $SUBVOLUME
+# Assuming that $SUBVOLUME is a btrfs subvolume, delete it and all
+# subvolumes below it.
+# This is intended to be a recursive version of
+# `btrfs subvolume delete`.
+btrfs_subvolume_delete() {
+	local dir="$1"
+	local subvolumes subvolume
+	subvolumes=($(btrfs_subvolume_list "$dir")) || return $?
+	for subvolume in "${subvolumes[@]}"; do
+		btrfs subvolume delete "$dir/$subvolume" || return $?
+	done
+	btrfs subvolume delete "$dir"
 create_chroot() {
 	# Lock the chroot we want to use. We'll keep this lock until we exit.
 	lock 9 "$copydir.lock" "Locking chroot copy [%s]" "$copy"
@@ -92,7 +147,7 @@ create_chroot() {
 		stat_busy "Creating clean working copy [%s]" "$copy"
 		if [[ "$chroottype" == btrfs ]] && ! mountpoint -q "$copydir"; then
 			if [[ -d $copydir ]]; then
-				btrfs subvolume delete "$copydir" >/dev/null ||
+				btrfs_subvolume_delete "$copydir" >/dev/null ||
 					die "Unable to delete subvolume %s" "$copydir"
 			btrfs subvolume snapshot "$chrootdir/root" "$copydir" >/dev/null ||
@@ -114,7 +169,7 @@ create_chroot() {
 clean_temporary() {
 	stat_busy "Removing temporary copy [%s]" "$copy"
 	if [[ "$chroottype" == btrfs ]] && ! mountpoint -q "$copydir"; then
-		btrfs subvolume delete "$copydir" >/dev/null ||
+		btrfs_subvolume_delete "$copydir" >/dev/null ||
 			die "Unable to delete subvolume %s" "$copydir"
 		# avoid change of filesystem in case of an umount failure

More information about the arch-projects mailing list