summaryrefslogtreecommitdiffstats
path: root/hooks
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:14:39 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 14:14:39 +0000
commitee17e45964b786b48b455959dfe68715971893fb (patch)
tree118f40aa65dc838499053413b05adfd00f839c62 /hooks
parentInitial commit. (diff)
downloadmmdebstrap-ee17e45964b786b48b455959dfe68715971893fb.tar.xz
mmdebstrap-ee17e45964b786b48b455959dfe68715971893fb.zip
Adding upstream version 1.4.3.upstream/1.4.3
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'hooks')
-rwxr-xr-xhooks/busybox/extract00.sh14
-rwxr-xr-xhooks/busybox/setup00.sh17
-rwxr-xr-xhooks/copy-host-apt-sources-and-preferences/customize00.pl44
-rwxr-xr-xhooks/copy-host-apt-sources-and-preferences/setup00.sh46
-rw-r--r--hooks/eatmydata/README.txt5
-rwxr-xr-xhooks/eatmydata/customize.sh30
-rwxr-xr-xhooks/eatmydata/extract.sh75
-rwxr-xr-xhooks/file-mirror-automount/customize00.sh41
-rwxr-xr-xhooks/file-mirror-automount/setup00.sh73
-rwxr-xr-xhooks/jessie-or-older/extract00.sh16
-rwxr-xr-xhooks/jessie-or-older/extract01.sh47
-rwxr-xr-xhooks/maybe-jessie-or-older/extract00.sh37
-rwxr-xr-xhooks/maybe-jessie-or-older/extract01.sh57
-rwxr-xr-xhooks/maybe-merged-usr/essential00.sh36
-rwxr-xr-xhooks/maybe-merged-usr/extract00.sh27
-rwxr-xr-xhooks/maybe-merged-usr/setup00.sh27
-rwxr-xr-xhooks/merged-usr/essential00.sh28
-rwxr-xr-xhooks/merged-usr/extract00.sh85
-rwxr-xr-xhooks/merged-usr/setup00.sh79
l---------hooks/no-merged-usr/essential00.sh1
-rwxr-xr-xhooks/no-merged-usr/setup00.sh54
21 files changed, 839 insertions, 0 deletions
diff --git a/hooks/busybox/extract00.sh b/hooks/busybox/extract00.sh
new file mode 100755
index 0000000..7d9b6ec
--- /dev/null
+++ b/hooks/busybox/extract00.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+# Run busybox using an absolute path so that this script also works in case
+# /proc is not mounted. Busybox uses /proc/self/exe to figure out the path
+# to its executable.
+chroot "$rootdir" /bin/busybox --install -s
diff --git a/hooks/busybox/setup00.sh b/hooks/busybox/setup00.sh
new file mode 100755
index 0000000..fc65e12
--- /dev/null
+++ b/hooks/busybox/setup00.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+mkdir -p "$rootdir/bin"
+echo root:x:0:0:root:/root:/bin/sh > "$rootdir/etc/passwd"
+cat << END > "$rootdir/etc/group"
+root:x:0:
+mail:x:8:
+utmp:x:43:
+END
diff --git a/hooks/copy-host-apt-sources-and-preferences/customize00.pl b/hooks/copy-host-apt-sources-and-preferences/customize00.pl
new file mode 100755
index 0000000..53f6059
--- /dev/null
+++ b/hooks/copy-host-apt-sources-and-preferences/customize00.pl
@@ -0,0 +1,44 @@
+#!/usr/bin/perl
+#
+# This script makes sure that all packages that are installed both locally as
+# well as inside the chroot have the same version.
+#
+# It is implemented in Perl because there are no associative arrays in POSIX
+# shell.
+
+use strict;
+use warnings;
+
+sub get_pkgs {
+ my $root = shift;
+ my %pkgs = ();
+ open(my $fh, '-|', 'dpkg-query', "--root=$root", '--showformat',
+ '${binary:Package}=${Version}\n', '--show')
+ // die "cannot exec dpkg-query";
+ while (my $line = <$fh>) {
+ my ($pkg, $ver) = split(/=/, $line, 2);
+ $pkgs{$pkg} = $ver;
+ }
+ close $fh;
+ if ($? != 0) { die "failed to run dpkg-query" }
+ return %pkgs;
+}
+
+my %pkgs_local = get_pkgs('/');
+my %pkgs_chroot = get_pkgs($ARGV[0]);
+
+my @diff = ();
+foreach my $pkg (keys %pkgs_chroot) {
+ next unless exists $pkgs_local{$pkg};
+ if ($pkgs_local{$pkg} ne $pkgs_chroot{$pkg}) {
+ push @diff, $pkg;
+ }
+}
+
+if (scalar @diff > 0) {
+ print STDERR "E: packages from the host and the chroot differ:\n";
+ foreach my $pkg (@diff) {
+ print STDERR "E: $pkg $pkgs_local{$pkg} $pkgs_chroot{$pkg}\n";
+ }
+ exit 1;
+}
diff --git a/hooks/copy-host-apt-sources-and-preferences/setup00.sh b/hooks/copy-host-apt-sources-and-preferences/setup00.sh
new file mode 100755
index 0000000..07caa78
--- /dev/null
+++ b/hooks/copy-host-apt-sources-and-preferences/setup00.sh
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+if [ -n "${MMDEBSTRAP_SUITE:-}" ]; then
+ if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 1 ]; then
+ echo "W: using a non-empty suite name $MMDEBSTRAP_SUITE does not make sense with this hook and might select the wrong Essential:yes package set" >&2
+ fi
+fi
+
+rootdir="$1"
+
+SOURCELIST="/etc/apt/sources.list"
+eval "$(apt-config shell SOURCELIST Dir::Etc::SourceList/f)"
+SOURCEPARTS="/etc/apt/sources.d/"
+eval "$(apt-config shell SOURCEPARTS Dir::Etc::SourceParts/d)"
+PREFERENCES="/etc/apt/preferences"
+eval "$(apt-config shell PREFERENCES Dir::Etc::Preferences/f)"
+PREFERENCESPARTS="/etc/apt/preferences.d/"
+eval "$(apt-config shell PREFERENCESPARTS Dir::Etc::PreferencesParts/d)"
+
+for f in "$SOURCELIST" \
+ "$SOURCEPARTS"/*.list \
+ "$SOURCEPARTS"/*.sources \
+ "$PREFERENCES" \
+ "$PREFERENCESPARTS"/*; do
+ [ -e "$f" ] || continue
+ mkdir --parents "$(dirname "$rootdir/$f")"
+ if [ -e "$rootdir/$f" ]; then
+ if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 2 ]; then
+ echo "I: $f already exists in chroot, appending..." >&2
+ fi
+ # Add extra newline between old content and new content.
+ # This is required in case of deb822 files.
+ echo >> "$rootdir/$f"
+ fi
+ cat "$f" >> "$rootdir/$f"
+ if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ echo "D: contents of $f inside the chroot:" >&2
+ cat "$rootdir/$f" >&2
+ fi
+done
diff --git a/hooks/eatmydata/README.txt b/hooks/eatmydata/README.txt
new file mode 100644
index 0000000..84659e9
--- /dev/null
+++ b/hooks/eatmydata/README.txt
@@ -0,0 +1,5 @@
+Adding this directory with --hook-directory will result in mmdebstrap using
+dpkg inside an eatmydata wrapper script. This will result in spead-ups on
+systems where sync() takes some time. Using --dpkgopt=force-unsafe-io will have
+a lesser effect compared to eatmydata. See:
+https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=613428
diff --git a/hooks/eatmydata/customize.sh b/hooks/eatmydata/customize.sh
new file mode 100755
index 0000000..c675848
--- /dev/null
+++ b/hooks/eatmydata/customize.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+if [ -e "$rootdir/var/lib/dpkg/arch" ]; then
+ chrootarch=$(head -1 "$rootdir/var/lib/dpkg/arch")
+else
+ chrootarch=$(dpkg --print-architecture)
+fi
+libdir="/usr/lib/$(dpkg-architecture -a "$chrootarch" -q DEB_HOST_MULTIARCH)"
+
+# if eatmydata was actually installed properly, then we are not removing
+# anything here
+if ! chroot "$rootdir" dpkg-query --show eatmydata; then
+ rm "$rootdir/usr/bin/eatmydata"
+fi
+if ! chroot "$rootdir" dpkg-query --show libeatmydata1; then
+ rm "$rootdir$libdir"/libeatmydata.so*
+fi
+
+rm "$rootdir/usr/bin/dpkg"
+chroot "$rootdir" dpkg-divert --local --rename --remove /usr/bin/dpkg
+
+sync
diff --git a/hooks/eatmydata/extract.sh b/hooks/eatmydata/extract.sh
new file mode 100755
index 0000000..7ffe8e5
--- /dev/null
+++ b/hooks/eatmydata/extract.sh
@@ -0,0 +1,75 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+if [ -e "$rootdir/var/lib/dpkg/arch" ]; then
+ chrootarch=$(head -1 "$rootdir/var/lib/dpkg/arch")
+else
+ chrootarch=$(dpkg --print-architecture)
+fi
+
+trusted=
+eval "$(apt-config shell trusted Dir::Etc::trusted/f)"
+trustedparts=
+eval "$(apt-config shell trustedparts Dir::Etc::trustedparts/d)"
+tmpfile=$(mktemp --tmpdir="$rootdir/tmp")
+cat << END > "$tmpfile"
+Apt::Architecture "$chrootarch";
+Apt::Architectures "$chrootarch";
+Dir "$rootdir";
+Dir::Etc::Trusted "$trusted";
+Dir::Etc::TrustedParts "$trustedparts";
+END
+# we run "apt-get download --print-uris" in a temporary directory, to make sure
+# that the packages do not already exist in the current directory, or otherwise
+# nothing will be printed for them
+tmpdir=$(mktemp --directory --tmpdir="$rootdir/tmp")
+env --chdir="$tmpdir" APT_CONFIG="$tmpfile" apt-get download --print-uris eatmydata libeatmydata1 \
+ | sed -ne "s/^'\([^']\+\)'\s\+\(\S\+\)\s\+\([0-9]\+\)\s\+\(SHA256:[a-f0-9]\+\)$/\1 \2 \3 \4/p" \
+ | while read -r uri fname size hash; do
+ echo "processing $fname" >&2
+ if [ -e "$tmpdir/$fname" ]; then
+ echo "$tmpdir/$fname already exists" >&2
+ exit 1
+ fi
+ [ -z "$hash" ] && hash="Checksum-FileSize:$size"
+ env --chdir="$tmpdir" APT_CONFIG="$tmpfile" /usr/lib/apt/apt-helper download-file "$uri" "$fname" "$hash"
+ case "$fname" in
+ eatmydata_*_all.deb)
+ mkdir -p "$rootdir/usr/bin"
+ dpkg-deb --fsys-tarfile "$tmpdir/$fname" \
+ | tar --directory="$rootdir/usr/bin" --strip-components=3 --extract --verbose ./usr/bin/eatmydata
+ ;;
+ libeatmydata1_*_$chrootarch.deb)
+ libdir="/usr/lib/$(dpkg-architecture -a "$chrootarch" -q DEB_HOST_MULTIARCH)"
+ mkdir -p "$rootdir$libdir"
+ dpkg-deb --fsys-tarfile "$tmpdir/$fname" \
+ | tar --directory="$rootdir$libdir" --strip-components=4 --extract --verbose --wildcards ".$libdir/libeatmydata.so*"
+ ;;
+ *)
+ echo "unexpected filename: $fname" >&2
+ exit 1
+ ;;
+ esac
+ rm "$tmpdir/$fname"
+done
+rm "$tmpfile"
+rmdir "$tmpdir"
+
+mv "$rootdir/usr/bin/dpkg" "$rootdir/usr/bin/dpkg.distrib"
+cat << END > "$rootdir/usr/bin/dpkg"
+#!/bin/sh
+exec /usr/bin/eatmydata /usr/bin/dpkg.distrib "\$@"
+END
+chmod +x "$rootdir/usr/bin/dpkg"
+cat << END >> "$rootdir/var/lib/dpkg/diversions"
+/usr/bin/dpkg
+/usr/bin/dpkg.distrib
+:
+END
diff --git a/hooks/file-mirror-automount/customize00.sh b/hooks/file-mirror-automount/customize00.sh
new file mode 100755
index 0000000..b6b9b46
--- /dev/null
+++ b/hooks/file-mirror-automount/customize00.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+#
+# shellcheck disable=SC2086
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+if [ ! -e "$rootdir/run/mmdebstrap/file-mirror-automount" ]; then
+ exit 0
+fi
+
+xargsopts="--null --no-run-if-empty -I {} --max-args=1"
+
+case $MMDEBSTRAP_MODE in
+ root|unshare)
+ echo "unmounting the following mountpoints:" >&2 ;;
+ *)
+ echo "removing the following directories:" >&2 ;;
+esac
+
+< "$rootdir/run/mmdebstrap/file-mirror-automount" \
+ xargs $xargsopts echo " $rootdir/{}"
+
+case $MMDEBSTRAP_MODE in
+ root|unshare)
+ < "$rootdir/run/mmdebstrap/file-mirror-automount" \
+ xargs $xargsopts umount "$rootdir/{}"
+ ;;
+ *)
+ < "$rootdir/run/mmdebstrap/file-mirror-automount" \
+ xargs $xargsopts rm -r "$rootdir/{}"
+ ;;
+esac
+
+rm "$rootdir/run/mmdebstrap/file-mirror-automount"
+rmdir --ignore-fail-on-non-empty "$rootdir/run/mmdebstrap"
diff --git a/hooks/file-mirror-automount/setup00.sh b/hooks/file-mirror-automount/setup00.sh
new file mode 100755
index 0000000..61f60f2
--- /dev/null
+++ b/hooks/file-mirror-automount/setup00.sh
@@ -0,0 +1,73 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+rootdir="$1"
+
+# process all configured apt repositories
+env APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-get indextargets --no-release-info --format '$(REPO_URI)' \
+ | sed -ne 's/^file:\/\+//p' \
+ | sort -u \
+ | while read -r path; do
+ mkdir -p "$rootdir/run/mmdebstrap"
+ if [ ! -d "/$path" ]; then
+ echo "/$path is not an existing directory" >&2
+ continue
+ fi
+ case $MMDEBSTRAP_MODE in
+ root|unshare)
+ echo "bind-mounting /$path into the chroot" >&2
+ mkdir -p "$rootdir/$path"
+ mount -o ro,bind "/$path" "$rootdir/$path"
+ ;;
+ *)
+ echo "copying /$path into the chroot" >&2
+ mkdir -p "$rootdir/$path"
+ "$MMDEBSTRAP_ARGV0" --hook-helper "$rootdir" "$MMDEBSTRAP_MODE" "$MMDEBSTRAP_HOOK" env "$MMDEBSTRAP_VERBOSITY" sync-in "/$path" "/$path" <&"$MMDEBSTRAP_HOOKSOCK" >&"$MMDEBSTRAP_HOOKSOCK"
+ ;;
+ esac
+ printf '/%s\0' "$path" >> "$rootdir/run/mmdebstrap/file-mirror-automount"
+ done
+
+# process all files given via --include
+set -f # turn off pathname expansion
+IFS=',' # split by comma
+for pkg in $MMDEBSTRAP_INCLUDE; do
+ set +f; unset IFS
+ case $pkg in
+ ./*|../*|/*) : ;; # we are interested in this case
+ *) continue ;; # not a file
+ esac
+ # undo escaping
+ pkg="$(printf '%s' "$pkg" | sed 's/%2C/,/g; s/%25/%/g')"
+ # check for existance
+ if [ ! -f "$pkg" ]; then
+ echo "$pkg does not exist" >&2
+ continue
+ fi
+ # make path absolute
+ pkg="$(realpath "$pkg")"
+ case "$pkg" in
+ /*) : ;;
+ *) echo "path for $pkg is not absolute" >&2; continue;;
+ esac
+ mkdir -p "$rootdir/run/mmdebstrap"
+ mkdir -p "$rootdir/$(dirname "$pkg")"
+ case $MMDEBSTRAP_MODE in
+ root|unshare)
+ echo "bind-mounting $pkg into the chroot" >&2
+ touch "$rootdir/$pkg"
+ mount -o bind "$pkg" "$rootdir/$pkg"
+ ;;
+ *)
+ echo "copying $pkg into the chroot" >&2
+ "$MMDEBSTRAP_ARGV0" --hook-helper "$rootdir" "$MMDEBSTRAP_MODE" "$MMDEBSTRAP_HOOK" env "$MMDEBSTRAP_VERBOSITY" upload "$pkg" "$pkg" <&"$MMDEBSTRAP_HOOKSOCK" >&"$MMDEBSTRAP_HOOKSOCK"
+ ;;
+ esac
+ printf '/%s\0' "$pkg" >> "$rootdir/run/mmdebstrap/file-mirror-automount"
+done
+set +f; unset IFS
diff --git a/hooks/jessie-or-older/extract00.sh b/hooks/jessie-or-older/extract00.sh
new file mode 100755
index 0000000..f327052
--- /dev/null
+++ b/hooks/jessie-or-older/extract00.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+# not needed since dpkg 1.17.11
+for f in available diversions cmethopt; do
+ if [ ! -e "$TARGET/var/lib/dpkg/$f" ]; then
+ touch "$TARGET/var/lib/dpkg/$f"
+ fi
+done
diff --git a/hooks/jessie-or-older/extract01.sh b/hooks/jessie-or-older/extract01.sh
new file mode 100755
index 0000000..43f1540
--- /dev/null
+++ b/hooks/jessie-or-older/extract01.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+#
+# needed until init 1.33 which pre-depends on systemd-sysv
+# starting with init 1.34, init is not Essential:yes anymore
+#
+# jessie has init 1.22
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+if [ -z "${MMDEBSTRAP_ESSENTIAL+x}" ]; then
+ MMDEBSTRAP_ESSENTIAL=
+ for f in "$TARGET/var/cache/apt/archives/"*.deb; do
+ [ -f "$f" ] || continue
+ f="${f#"$TARGET"}"
+ MMDEBSTRAP_ESSENTIAL="$MMDEBSTRAP_ESSENTIAL $f"
+ done
+fi
+
+fname_base_passwd=
+fname_base_files=
+fname_dpkg=
+for pkg in $MMDEBSTRAP_ESSENTIAL; do
+ pkgname=$(dpkg-deb --show --showformat='${Package}' "$TARGET/$pkg")
+ # shellcheck disable=SC2034
+ case $pkgname in
+ base-passwd) fname_base_passwd=$pkg;;
+ base-files) fname_base_files=$pkg;;
+ dpkg) fname_dpkg=$pkg;;
+ esac
+done
+
+for var in base_passwd base_files dpkg; do
+ eval 'val=$fname_'"$var"
+ [ -z "$val" ] && continue
+ chroot "$TARGET" dpkg --install --force-depends "$val"
+done
+
+# shellcheck disable=SC2086
+chroot "$TARGET" dpkg --unpack --force-depends $MMDEBSTRAP_ESSENTIAL
+
+chroot "$TARGET" dpkg --configure --pending
diff --git a/hooks/maybe-jessie-or-older/extract00.sh b/hooks/maybe-jessie-or-older/extract00.sh
new file mode 100755
index 0000000..9a82fbf
--- /dev/null
+++ b/hooks/maybe-jessie-or-older/extract00.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+
+set -eu
+
+# we need to check the version of dpkg
+# since at this point packages are just extracted but not installed, we cannot use dpkg-query
+# since we want to support chrootless, we cannot run dpkg --version inside the chroot
+# to avoid this hook depending on dpkg-dev being installed, we do not parse the extracted changelog with dpkg-parsechangelog
+# we also want to avoid parsing the changelog because /usr/share/doc might've been added to dpkg --path-exclude
+# instead, we just ask apt about the latest version of dpkg it knows of
+# this should only fail in situations where there are multiple versions of dpkg in different suites
+ver=$(env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-cache show --no-all-versions dpkg 2>/dev/null | sed -ne 's/^Version: \(.*\)$/\1/p' || printf '')
+if [ -z "$ver" ]; then
+ echo "no package called dpkg can be installed -- not running jessie-or-older extract00 hook" >&2
+ exit 0
+fi
+
+if dpkg --compare-versions "$ver" ge 1.17.11; then
+ echo "dpkg version $ver is >= 1.17.11 -- not running jessie-or-older extract00 hook" >&2
+ exit 0
+else
+ echo "dpkg version $ver is << 1.17.11 -- running jessie-or-older extract00 hook" >&2
+fi
+
+# resolve the script path using several methods in order:
+# 1. using dirname -- "$0"
+# 2. using ./hooks
+# 3. using /usr/share/mmdebstrap/hooks/
+for p in "$(dirname -- "$0")/.." ./hooks /usr/share/mmdebstrap/hooks; do
+ if [ -x "$p/jessie-or-older/extract00.sh" ] && [ -x "$p/jessie-or-older/extract01.sh" ]; then
+ "$p/jessie-or-older/extract00.sh" "$1"
+ exit 0
+ fi
+done
+
+echo "cannot find jessie-or-older hook anywhere" >&2
+exit 1
diff --git a/hooks/maybe-jessie-or-older/extract01.sh b/hooks/maybe-jessie-or-older/extract01.sh
new file mode 100755
index 0000000..9a92d6d
--- /dev/null
+++ b/hooks/maybe-jessie-or-older/extract01.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -eu
+
+# The jessie-or-older extract01 hook has to be run up to the point where the
+# Essential:yes field was removed from the init package (with
+# init-system-helpers 1.34). Since the essential packages have only been
+# extracted but not installed, we cannot use dpkg-query to find out its
+# version. Since /usr/share/doc might be missing due to dpkg --path-exclude, we
+# also cannot check whether /usr/share/doc/init/copyright exists. There also
+# was a time (before init-system-helpers 1.20) where there was no init package
+# at all where we also want to apply this hook. So we just ask apt about the
+# candidate version for init-system-helpers. This should only fail in
+# situations where there are multiple versions of init-system-helpers in
+# different suites.
+ver=$(env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-cache show --no-all-versions init-system-helpers 2>/dev/null | sed -ne 's/^Version: \(.*\)$/\1/p' || printf '')
+if [ -z "$ver" ]; then
+ # there is no package called init-system-helpers, so either:
+ # - this is so old that init-system-helpers didn't exist yet
+ # - we are in a future where init-system-helpers doesn't exist anymore
+ # - something strange is going on
+ # we should only call the hook in the first case
+ ver=$(env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-cache show --no-all-versions base-files 2>/dev/null | sed -ne 's/^Version: \(.*\)$/\1/p' || printf '')
+ if [ -z "$ver" ]; then
+ echo "neither init-system-helpers nor base-files can be installed -- not running jessie-or-older extract01 hook" >&2
+ exit 0
+ fi
+
+ # Jessie is Debian 8
+ if dpkg --compare-versions "$ver" ge 8; then
+ echo "there is no init-system-helpers but base-files version $ver is >= 8 -- not running jessie-or-older extract01 hook" >&2
+ exit 0
+ else
+ echo "there is no init-system-helpers but base-files version $ver is << 8 -- running jessie-or-older extract01 hook" >&2
+ fi
+else
+ if dpkg --compare-versions "$ver" ge 1.34; then
+ echo "init-system-helpers version $ver is >= 1.34 -- not running jessie-or-older extract01 hook" >&2
+ exit 0
+ else
+ echo "init-system-helpers version $ver is << 1.34 -- running jessie-or-older extract01 hook" >&2
+ fi
+fi
+
+# resolve the script path using several methods in order:
+# 1. using dirname -- "$0"
+# 2. using ./hooks
+# 3. using /usr/share/mmdebstrap/hooks/
+for p in "$(dirname -- "$0")/.." ./hooks /usr/share/mmdebstrap/hooks; do
+ if [ -x "$p/jessie-or-older/extract00.sh" ] && [ -x "$p/jessie-or-older/extract01.sh" ]; then
+ "$p/jessie-or-older/extract01.sh" "$1"
+ exit 0
+ fi
+done
+
+echo "cannot find jessie-or-older hook anywhere" >&2
+exit 1
diff --git a/hooks/maybe-merged-usr/essential00.sh b/hooks/maybe-merged-usr/essential00.sh
new file mode 100755
index 0000000..a23f2f7
--- /dev/null
+++ b/hooks/maybe-merged-usr/essential00.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+
+set -eu
+
+ver=$(dpkg-query --root="$1" -f '${db:Status-Status} ${Source} ${Version}' --show usr-is-merged 2>/dev/null || printf '')
+case "$ver" in
+ '')
+ echo "no package called usr-is-merged is installed -- not running merged-usr essential hook" >&2
+ exit 0
+ ;;
+ 'installed mmdebstrap-dummy-usr-is-merged 1')
+ echo "dummy usr-is-merged package installed -- running merged-usr essential hook" >&2
+ ;;
+ 'installed usrmerge '*)
+ echo "usr-is-merged package from src:usrmerge installed -- not running merged-usr essential hook" >&2
+ exit 0
+ ;;
+ *)
+ echo "unexpected situation for package usr-is-merged: $ver" >&2
+ exit 1
+ ;;
+esac
+
+# resolve the script path using several methods in order:
+# 1. using dirname -- "$0"
+# 2. using ./hooks
+# 3. using /usr/share/mmdebstrap/hooks/
+for p in "$(dirname -- "$0")/.." ./hooks /usr/share/mmdebstrap/hooks; do
+ if [ -x "$p/merged-usr/setup00.sh" ] && [ -x "$p/merged-usr/extract00.sh" ] && [ -x "$p/merged-usr/essential00.sh" ]; then
+ "$p/merged-usr/essential00.sh" "$1"
+ exit 0
+ fi
+done
+
+echo "cannot find merged-usr hook anywhere" >&2
+exit 1
diff --git a/hooks/maybe-merged-usr/extract00.sh b/hooks/maybe-merged-usr/extract00.sh
new file mode 100755
index 0000000..dc88450
--- /dev/null
+++ b/hooks/maybe-merged-usr/extract00.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -eu
+
+env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-get update --error-on=any
+
+# if the usr-is-merged package cannot be installed with apt, do nothing
+if ! env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-cache show --no-all-versions usr-is-merged > /dev/null 2>&1; then
+ echo "no package called usr-is-merged found -- not running merged-usr extract hook" >&2
+ exit 0
+else
+ echo "package usr-is-merged found -- running merged-usr extract hook" >&2
+fi
+
+# resolve the script path using several methods in order:
+# 1. using dirname -- "$0"
+# 2. using ./hooks
+# 3. using /usr/share/mmdebstrap/hooks/
+for p in "$(dirname -- "$0")/.." ./hooks /usr/share/mmdebstrap/hooks; do
+ if [ -x "$p/merged-usr/setup00.sh" ] && [ -x "$p/merged-usr/extract00.sh" ] && [ -x "$p/merged-usr/essential00.sh" ]; then
+ "$p/merged-usr/extract00.sh" "$1"
+ exit 0
+ fi
+done
+
+echo "cannot find merged-usr hook anywhere" >&2
+exit 1
diff --git a/hooks/maybe-merged-usr/setup00.sh b/hooks/maybe-merged-usr/setup00.sh
new file mode 100755
index 0000000..a6bd712
--- /dev/null
+++ b/hooks/maybe-merged-usr/setup00.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -eu
+
+env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-get update --error-on=any
+
+# if the usr-is-merged package cannot be installed with apt, do nothing
+if ! env --chdir="$1" APT_CONFIG="$MMDEBSTRAP_APT_CONFIG" apt-cache show --no-all-versions usr-is-merged > /dev/null 2>&1; then
+ echo "no package called usr-is-merged found -- not running merged-usr setup hook" >&2
+ exit 0
+else
+ echo "package usr-is-merged found -- running merged-usr setup hook" >&2
+fi
+
+# resolve the script path using several methods in order:
+# 1. using dirname -- "$0"
+# 2. using ./hooks
+# 3. using /usr/share/mmdebstrap/hooks/
+for p in "$(dirname -- "$0")/.." ./hooks /usr/share/mmdebstrap/hooks; do
+ if [ -x "$p/merged-usr/setup00.sh" ] && [ -x "$p/merged-usr/extract00.sh" ] && [ -x "$p/merged-usr/essential00.sh" ]; then
+ "$p/merged-usr/setup00.sh" "$1"
+ exit 0
+ fi
+done
+
+echo "cannot find merged-usr hook anywhere" >&2
+exit 1
diff --git a/hooks/merged-usr/essential00.sh b/hooks/merged-usr/essential00.sh
new file mode 100755
index 0000000..d9a8130
--- /dev/null
+++ b/hooks/merged-usr/essential00.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+if [ "${MMDEBSTRAP_MODE:-}" = "chrootless" ]; then
+ APT_CONFIG=$MMDEBSTRAP_APT_CONFIG apt-get --yes install \
+ -oDPkg::Chroot-Directory= \
+ -oDPkg::Options::=--force-not-root \
+ -oDPkg::Options::=--force-script-chrootless \
+ -oDPkg::Options::=--root="$TARGET" \
+ -oDPkg::Options::=--log="$TARGET/var/log/dpkg.log" \
+ usr-is-merged
+ export DPKG_ROOT="$TARGET"
+ dpkg-query --showformat '${db:Status-Status}\n' --show usr-is-merged | grep -q '^installed$'
+ dpkg-query --showformat '${Source}\n' --show usr-is-merged | grep -q '^usrmerge$'
+ dpkg --compare-versions "1" "lt" "$(dpkg-query --showformat '${Version}\n' --show usr-is-merged)"
+else
+ APT_CONFIG=$MMDEBSTRAP_APT_CONFIG apt-get --yes install usr-is-merged
+ chroot "$TARGET" dpkg-query --showformat '${db:Status-Status}\n' --show usr-is-merged | grep -q '^installed$'
+ chroot "$TARGET" dpkg-query --showformat '${Source}\n' --show usr-is-merged | grep -q '^usrmerge$'
+ dpkg --compare-versions "1" "lt" "$(chroot "$TARGET" dpkg-query --showformat '${Version}\n' --show usr-is-merged)"
+fi
diff --git a/hooks/merged-usr/extract00.sh b/hooks/merged-usr/extract00.sh
new file mode 100755
index 0000000..7334191
--- /dev/null
+++ b/hooks/merged-usr/extract00.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+# can_usrmerge_symlink() and can_usrmerge_symlink() are
+# Copyright 2023 Helmut Grohne <helmut@subdivi.de>
+# and part of the debootstrap source in /usr/share/debootstrap/functions
+# https://salsa.debian.org/installer-team/debootstrap/-/merge_requests/96
+# https://bugs.debian.org/104989
+can_usrmerge_symlink() {
+ # Absolute symlinks can be relocated without problems.
+ test "${2#/}" = "$2" || return 0
+ while :; do
+ if test "${2#/}" != "$2"; then
+ # Handle double-slashes.
+ set -- "$1" "${2#/}"
+ elif test "${2#./}" != "$2"; then
+ # Handle ./ inside a link target.
+ set -- "$1" "${2#./}"
+ elif test "$2" = ..; then
+ # A parent directory symlink is ok if it does not
+ # cross the top level directory.
+ test "${1%/*/*}" != "$1" -a -n "${1%/*/*}"
+ return $?
+ elif test "${2#../}" != "$2"; then
+ # Symbolic link crossing / cannot be moved safely.
+ # This is prohibited by Debian Policy 10.5.
+ test "${1%/*/*}" = "$1" -o -z "${1%/*/*}" && return 1
+ set -- "${1%/*}" "${2#../}"
+ else
+ # Consider the symlink ok if its target does not
+ # contain a parent directory. When we fail here,
+ # the link target is non-minimal and doesn't happen
+ # in the archive.
+ test "${2#*/../}" = "$2"
+ return $?
+ fi
+ done
+}
+
+merge_usr_entry() {
+ # shellcheck disable=SC3043
+ local entry canon
+ canon="$TARGET/usr/${1#"$TARGET/"}"
+ test -h "$canon" &&
+ error 1 USRMERGEFAIL "cannot move %s as its destination exists as a symlink" "${1#"$TARGET"}"
+ if ! test -e "$canon"; then
+ mv "$1" "$canon"
+ return 0
+ fi
+ test -d "$1" ||
+ error 1 USRMERGEFAIL "cannot move non-directory %s as its destination exists" "${1#"$TARGET"}"
+ test -d "$canon" ||
+ error 1 USRMERGEFAIL "cannot move directory %s as its destination is not a directory" "${1#"$TARGET"}"
+ for entry in "$1/"* "$1/."*; do
+ # Some shells return . and .. on dot globs.
+ test "${entry%/.}" != "${entry%/..}" && continue
+ if test -h "$entry" && ! can_usrmerge_symlink "${entry#"$TARGET"}" "$(readlink "$entry")"; then
+ error 1 USRMERGEFAIL "cannot move relative symlink crossing top-level directory" "${entry#"$TARGET"}"
+ fi
+ # Ignore glob match failures
+ if test "${entry%'/*'}" != "${entry%'/.*'}" && ! test -e "$entry"; then
+ continue
+ fi
+ merge_usr_entry "$entry"
+ done
+ rmdir "$1"
+}
+
+# This is list includes all possible multilib directories. It must be
+# updated when new multilib directories are being added. Hopefully,
+# all new architectures use multiarch instead, so we never get to
+# update this.
+for dir in bin lib lib32 lib64 libo32 libx32 sbin; do
+ test -h "$TARGET/$dir" && continue
+ test -e "$TARGET/$dir" || continue
+ merge_usr_entry "$TARGET/$dir"
+ ln -s "usr/$dir" "$TARGET/$dir"
+done
diff --git a/hooks/merged-usr/setup00.sh b/hooks/merged-usr/setup00.sh
new file mode 100755
index 0000000..a6b08d2
--- /dev/null
+++ b/hooks/merged-usr/setup00.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+#
+# mmdebstrap does have a --merged-usr option but only as a no-op for
+# debootstrap compatibility
+#
+# Using this hook script, you can emulate what debootstrap does to set up
+# merged /usr via directory symlinks, even using the exact same shell function
+# that debootstrap uses by running mmdebstrap with:
+#
+# --setup-hook=/usr/share/mmdebstrap/hooks/merged-usr/setup00.sh
+#
+# Alternatively, you can setup merged-/usr by installing the usrmerge package:
+#
+# --include=usrmerge
+#
+# mmdebstrap will not include this functionality via a --merged-usr option
+# because there are many reasons against implementing merged-/usr that way:
+#
+# https://wiki.debian.org/Teams/Dpkg/MergedUsr
+# https://wiki.debian.org/Teams/Dpkg/FAQ#Q:_Does_dpkg_support_merged-.2Fusr-via-aliased-dirs.3F
+# https://lists.debian.org/20190219044924.GB21901@gaara.hadrons.org
+# https://lists.debian.org/YAkLOMIocggdprSQ@thunder.hadrons.org
+# https://lists.debian.org/20181223030614.GA8788@gaara.hadrons.org
+#
+# In addition, the merged-/usr-via-aliased-dirs approach violates an important
+# principle of component based software engineering one of the core design
+# ideas/goals of mmdebstrap: All the information to create a chroot of a Debian
+# based distribution should be included in its packages and their metadata.
+# Using directory symlinks as used by debootstrap contradicts this principle.
+# The information whether a distribution uses this approach to merged-/usr or
+# not is not anymore contained in its packages but in a tool from the outside.
+#
+# Example real world problem: I'm using debbisect to bisect Debian unstable
+# between 2015 and today. For which snapshot.d.o timestamp should a merged-/usr
+# chroot be created and for which ones not?
+#
+# The problem is not the idea of merged-/usr but the problem is the way how it
+# got implemented in debootstrap via directory symlinks. That way of rolling
+# out merged-/usr is bad from the dpkg point-of-view and completely opposite of
+# the vision with which in mind I wrote mmdebstrap.
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+# now install an empty "usr-is-merged" package to avoid installing the
+# usrmerge package on this system even after init-system-helpers starts
+# depending on "usrmerge | usr-is-merged".
+#
+# This package will not end up in the final chroot because the essential
+# hook replaces it with the actual usr-is-merged package from src:usrmerge.
+
+tmpdir=$(mktemp --directory --tmpdir="$TARGET/tmp")
+mkdir -p "$tmpdir/usr-is-merged/DEBIAN"
+
+cat << END > "$tmpdir/usr-is-merged/DEBIAN/control"
+Package: usr-is-merged
+Priority: optional
+Section: oldlibs
+Maintainer: Johannes Schauer Marin Rodrigues <josch@debian.org>
+Architecture: all
+Multi-Arch: foreign
+Source: mmdebstrap-dummy-usr-is-merged
+Version: 1
+Description: dummy package created by mmdebstrap merged-usr setup hook
+ This package was generated and installed by the mmdebstrap merged-usr
+ setup hook at /usr/share/mmdebstrap/hooks/merged-usr.
+ .
+ If this package is installed in the final chroot, then this is a bug
+ in mmdebstrap. Please report: https://gitlab.mister-muffin.de/josch/mmdebstrap
+END
+dpkg-deb --build "$tmpdir/usr-is-merged" "$tmpdir/usr-is-merged.deb"
+dpkg --root="$TARGET" --log="$TARGET/var/log/dpkg.log" --install "$tmpdir/usr-is-merged.deb"
+rm "$tmpdir/usr-is-merged.deb" "$tmpdir/usr-is-merged/DEBIAN/control"
+rmdir "$tmpdir/usr-is-merged/DEBIAN" "$tmpdir/usr-is-merged" "$tmpdir"
diff --git a/hooks/no-merged-usr/essential00.sh b/hooks/no-merged-usr/essential00.sh
new file mode 120000
index 0000000..5360c8f
--- /dev/null
+++ b/hooks/no-merged-usr/essential00.sh
@@ -0,0 +1 @@
+../merged-usr/essential00.sh \ No newline at end of file
diff --git a/hooks/no-merged-usr/setup00.sh b/hooks/no-merged-usr/setup00.sh
new file mode 100755
index 0000000..df50499
--- /dev/null
+++ b/hooks/no-merged-usr/setup00.sh
@@ -0,0 +1,54 @@
+#!/bin/sh
+#
+# mmdebstrap does have a --no-merged-usr option but only as a no-op for
+# debootstrap compatibility
+#
+# Using this hook script, you can emulate what debootstrap does to set up
+# a system without merged-/usr even after the essential init-system-helpers
+# package added a dependency on "usrmerge | usr-is-merged". By installing
+# a dummy usr-is-merged package, it avoids pulling in the dependencies of
+# the usrmerge package.
+
+set -eu
+
+if [ "${MMDEBSTRAP_VERBOSITY:-1}" -ge 3 ]; then
+ set -x
+fi
+
+TARGET="$1"
+
+echo "Warning: starting with Debian 12 (Bookworm), systems without merged-/usr are not supported anymore" >&2
+echo "Warning: starting with Debian 13 (Trixie), merged-/usr symlinks are shipped by packages in the essential-set making this hook ineffective" >&2
+
+echo "this system will not be supported in the future" > "$TARGET/etc/unsupported-skip-usrmerge-conversion"
+
+# now install an empty "usr-is-merged" package to avoid installing the
+# usrmerge package on this system even after init-system-helpers starts
+# depending on "usrmerge | usr-is-merged".
+#
+# This package will not end up in the final chroot because the essential
+# hook replaces it with the actual usr-is-merged package from src:usrmerge.
+
+tmpdir=$(mktemp --directory --tmpdir="$TARGET/tmp")
+mkdir -p "$tmpdir/usr-is-merged/DEBIAN"
+
+cat << END > "$tmpdir/usr-is-merged/DEBIAN/control"
+Package: usr-is-merged
+Priority: optional
+Section: oldlibs
+Maintainer: Johannes Schauer Marin Rodrigues <josch@debian.org>
+Architecture: all
+Multi-Arch: foreign
+Source: mmdebstrap-dummy-usr-is-merged
+Version: 1
+Description: dummy package created by mmdebstrap no-merged-usr setup hook
+ This package was generated and installed by the mmdebstrap no-merged-usr
+ setup hook at /usr/share/mmdebstrap/hooks/no-merged-usr.
+ .
+ If this package is installed in the final chroot, then this is a bug
+ in mmdebstrap. Please report: https://gitlab.mister-muffin.de/josch/mmdebstrap
+END
+dpkg-deb --build "$tmpdir/usr-is-merged" "$tmpdir/usr-is-merged.deb"
+dpkg --root="$TARGET" --log="$TARGET/var/log/dpkg.log" --install "$tmpdir/usr-is-merged.deb"
+rm "$tmpdir/usr-is-merged.deb" "$tmpdir/usr-is-merged/DEBIAN/control"
+rmdir "$tmpdir/usr-is-merged/DEBIAN" "$tmpdir/usr-is-merged" "$tmpdir"