summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/apt-patterns8
-rw-r--r--tests/apt-patterns-custom9
-rw-r--r--tests/aptopt9
-rw-r--r--tests/arm64-without-qemu-support18
-rw-r--r--tests/as-debootstrap-unshare-wrapper133
-rw-r--r--tests/ascii-armored-keys21
-rw-r--r--tests/aspcud-apt-solver11
-rw-r--r--tests/auto-mode-as-normal-user22
-rw-r--r--tests/auto-mode-without-unshare-capabilities14
-rw-r--r--tests/automatic-mirror-from-suite14
-rw-r--r--tests/check-against-debootstrap-dist228
-rw-r--r--tests/check-for-bit-by-bit-identical-format-output28
-rw-r--r--tests/chroot-directory-not-accessible-by-apt-user8
-rw-r--r--tests/chrootless16
-rw-r--r--tests/chrootless-fakeroot43
-rw-r--r--tests/chrootless-foreign68
-rw-r--r--tests/compare-output-with-pre-seeded-var-cache-apt-archives44
-rw-r--r--tests/copy-mirror10
-rw-r--r--tests/create-directory9
-rw-r--r--tests/create-directory-dry-run29
-rw-r--r--tests/create-foreign-tarball77
-rw-r--r--tests/create-gzip-compressed-tarball20
-rw-r--r--tests/create-tarball-dry-run27
-rw-r--r--tests/create-tarball-with-tmp-mounted-nodev12
-rw-r--r--tests/custom-tmpdir33
-rw-r--r--tests/customize-hook16
-rw-r--r--tests/cwd-directory-not-accessible-by-unshared-user30
-rw-r--r--tests/deb822-1-245
-rw-r--r--tests/deb822-2-244
-rw-r--r--tests/debootstrap10
-rw-r--r--tests/debootstrap-no-op-options6
-rw-r--r--tests/debug17
-rw-r--r--tests/debug-output-on-fake-tty6
-rw-r--r--tests/dev-ptmx149
-rw-r--r--tests/directory-ending-in-tar12
-rw-r--r--tests/dist-using-codename13
-rw-r--r--tests/dpkgopt10
-rw-r--r--tests/eatmydata-via-hook-dir43
-rw-r--r--tests/empty-sources.list8
-rw-r--r--tests/error-if-stdout-is-tty12
-rw-r--r--tests/essential-hook21
-rw-r--r--tests/existing-directory-with-lost-found9
-rw-r--r--tests/existing-empty-directory7
-rw-r--r--tests/fail-installing-to-existing-file13
-rw-r--r--tests/fail-installing-to-non-empty-lost-found13
-rw-r--r--tests/fail-installing-to-non-empty-target-directory13
-rw-r--r--tests/fail-installing-to-root9
-rw-r--r--tests/fail-with-missing-lz49
-rw-r--r--tests/fail-with-path-with-quotes12
-rw-r--r--tests/fail-without-etc-subuid16
-rw-r--r--tests/fail-without-username-in-etc-subuid17
-rw-r--r--tests/failing-customize-hook10
-rw-r--r--tests/file-mirror13
-rw-r--r--tests/file-mirror-automount-hook20
-rw-r--r--tests/help6
-rw-r--r--tests/hook-directory49
-rw-r--r--tests/i386-which-can-be-executed-without-qemu41
-rw-r--r--tests/include12
-rw-r--r--tests/include-deb-file40
-rw-r--r--tests/include-foreign-libmagic-mgc47
-rw-r--r--tests/include-foreign-libmagic-mgc-with-multiple-arch-options48
-rw-r--r--tests/include-with-multiple-apt-sources10
-rw-r--r--tests/install-busybox-based-sub-essential-system41
-rw-r--r--tests/install-doc-debian56
-rw-r--r--tests/install-doc-debian-and-output-tarball23
-rw-r--r--tests/install-doc-debian-and-test-hooks59
-rw-r--r--tests/install-libmagic-mgc-on-foreign69
-rw-r--r--tests/invalid-mirror10
-rw-r--r--tests/jessie-or-older42
-rw-r--r--tests/keyring18
-rw-r--r--tests/keyring-overwrites15
-rw-r--r--tests/logfile22
-rw-r--r--tests/man7
-rw-r--r--tests/merged-fakechroot-inside-unmerged-chroot49
-rw-r--r--tests/mirror-is-deb6
-rw-r--r--tests/mirror-is-real-file9
-rw-r--r--tests/mirror-is-stdin6
-rw-r--r--tests/missing-dev-sys-proc-inside-the-chroot20
-rw-r--r--tests/missing-device-nodes-outside-the-chroot12
-rw-r--r--tests/mmdebstrap20
-rw-r--r--tests/mount-is-missing13
-rw-r--r--tests/multiple-include21
-rw-r--r--tests/no-sbin-in-path28
-rw-r--r--tests/not-having-to-install-apt-in-include-because-a-hook-did-it-before9
-rw-r--r--tests/pass-distribution-but-implicitly-write-to-stdout14
-rw-r--r--tests/pivot_root54
-rw-r--r--tests/preserve-mode-of-etc-resolv-conf-and-etc-hostname102
-rw-r--r--tests/progress-bars-on-fake-tty6
-rw-r--r--tests/quiet6
-rw-r--r--tests/read-from-stdin-write-to-stdout6
-rw-r--r--tests/remove-start-stop-daemon-and-policy-rc-d-in-hook8
-rw-r--r--tests/root-mode-inside-chroot28
-rw-r--r--tests/root-mode-inside-unshare-chroot40
-rw-r--r--tests/root-without-cap-sys-admin17
-rw-r--r--tests/sigint-during-customize-hook22
-rw-r--r--tests/signed-by-with-host-keys7
-rw-r--r--tests/signed-by-without-host-keys17
-rw-r--r--tests/skip-mount12
-rw-r--r--tests/skip-output-dev35
-rw-r--r--tests/skip-output-mknod30
-rw-r--r--tests/skip-start-stop-daemon-policy-rc10
-rw-r--r--tests/skip-tar-in-mknod28
-rw-r--r--tests/special-hooks-using-helpers28
-rw-r--r--tests/special-hooks-using-helpers-and-env-vars31
-rw-r--r--tests/special-hooks-with-mode-mode148
-rw-r--r--tests/stable-default-mirror20
-rw-r--r--tests/supply-components-manually7
-rw-r--r--tests/tarfilter-idshift58
-rw-r--r--tests/unpack-doc-debian57
-rw-r--r--tests/unshare-as-root-user9
-rw-r--r--tests/unshare-as-root-user-inside-chroot28
-rw-r--r--tests/unshare-include-deb49
-rw-r--r--tests/variant-custom-timeout11
-rw-r--r--tests/verbose17
-rw-r--r--tests/version6
-rw-r--r--tests/without-etc-resolv-conf-and-etc-hostname14
-rw-r--r--tests/xz-compressed-tarball7
117 files changed, 3254 insertions, 0 deletions
diff --git a/tests/apt-patterns b/tests/apt-patterns
new file mode 100644
index 0000000..c87e932
--- /dev/null
+++ b/tests/apt-patterns
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=essential \
+ --include '?or(?exact-name(dummy-does-not-exist),?exact-name(apt))' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | grep -v ./var/lib/apt/extended_states | diff -u tar1.txt -
diff --git a/tests/apt-patterns-custom b/tests/apt-patterns-custom
new file mode 100644
index 0000000..2348a76
--- /dev/null
+++ b/tests/apt-patterns-custom
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=custom \
+ --include '?narrow(?archive(^{{ DIST }}$),?essential)' \
+ --include apt \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/aptopt b/tests/aptopt
new file mode 100644
index 0000000..c757c30
--- /dev/null
+++ b/tests/aptopt
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/config" EXIT INT TERM
+echo 'Acquire::Languages "none";' > /tmp/config
+{{ CMD }} --mode=root --variant=apt --aptopt='Acquire::Check-Valid-Until "false"' --aptopt=/tmp/config {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf 'Acquire::Check-Valid-Until "false";\nAcquire::Languages "none";\n' | cmp /tmp/debian-chroot/etc/apt/apt.conf.d/99mmdebstrap -
+rm /tmp/debian-chroot/etc/apt/apt.conf.d/99mmdebstrap
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/arm64-without-qemu-support b/tests/arm64-without-qemu-support
new file mode 100644
index 0000000..98b4724
--- /dev/null
+++ b/tests/arm64-without-qemu-support
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+apt-get remove --yes qemu-user-static binfmt-support qemu-user
+# the following is not necessary anymore since systemd-binfmt
+# successfully disables support upon removal of qemu-user with
+# the upload of src:systemd 251.2-4: https://bugs.debian.org/1012163
+#echo 0 > /proc/sys/fs/binfmt_misc/qemu-aarch64
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt --architectures=arm64 {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/as-debootstrap-unshare-wrapper b/tests/as-debootstrap-unshare-wrapper
new file mode 100644
index 0000000..b3f7a44
--- /dev/null
+++ b/tests/as-debootstrap-unshare-wrapper
@@ -0,0 +1,133 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# debootstrap uses apt-config to figure out whether the system running it has
+# any proxies configured and then runs the binary to set the http_proxy
+# environment variable. This will fail if debootstrap is run in a linux user
+# namespace because auto-apt-proxy will see /tmp/.auto-apt-proxy-0 as being
+# owned by the user "nobody" and group "nogroup" and fail with:
+# insecure cache dir /tmp/.auto-apt-proxy-0. Must be owned by UID 0 and have permissions 700
+# We cannot overwrite a configuration item using the APT_CONFIG environment
+# variable, so instead we use it to set the Dir configuration option
+# to /dev/null to force all apt settings to their defaults.
+# There is currently no better way to disable this behavior. See also:
+# https://bugs.debian.org/1031105
+# https://salsa.debian.org/installer-team/debootstrap/-/merge_requests/90
+AUTOPROXY=
+eval "$(apt-config shell AUTOPROXY Acquire::http::Proxy-Auto-Detect)"
+if [ -n "$AUTOPROXY" ] && [ -x "$AUTOPROXY" ] && [ -e /tmp/.auto-apt-proxy-0 ]; then
+ TMP_APT_CONFIG=$(mktemp)
+ echo "Dir \"/dev/null\";" > "$TMP_APT_CONFIG"
+ chmod 644 "$TMP_APT_CONFIG"
+fi
+
+$prefix {{ CMD }} --variant=custom --mode={{ MODE }} \
+ --setup-hook='env '"${AUTOPROXY:+APT_CONFIG='$TMP_APT_CONFIG'}"' debootstrap --variant={{ VARIANT }} unstable "$1" {{ MIRROR }}' \
+ - /tmp/debian-mm.tar {{ MIRROR }}
+if [ -n "$AUTOPROXY" ] && [ -x "$AUTOPROXY" ] && [ -e /tmp/.auto-apt-proxy-0 ]; then
+ rm "$TMP_APT_CONFIG"
+fi
+
+mkdir /tmp/debian-mm
+tar --xattrs --xattrs-include='*' -C /tmp/debian-mm -xf /tmp/debian-mm.tar
+
+mkdir /tmp/debian-debootstrap
+tar --xattrs --xattrs-include='*' -C /tmp/debian-debootstrap -xf "cache/debian-unstable-{{ VARIANT }}.tar"
+
+# diff cannot compare device nodes, so we use tar to do that for us and then
+# delete the directory
+tar -C /tmp/debian-debootstrap -cf dev1.tar ./dev
+tar -C /tmp/debian-mm -cf dev2.tar ./dev
+cmp dev1.tar dev2.tar >&2
+rm dev1.tar dev2.tar
+rm -r /tmp/debian-debootstrap/dev /tmp/debian-mm/dev
+
+# remove downloaded deb packages
+rm /tmp/debian-debootstrap/var/cache/apt/archives/*.deb
+# remove aux-cache
+rm /tmp/debian-debootstrap/var/cache/ldconfig/aux-cache
+# remove logs
+rm /tmp/debian-debootstrap/var/log/dpkg.log \
+ /tmp/debian-debootstrap/var/log/bootstrap.log \
+ /tmp/debian-debootstrap/var/log/alternatives.log \
+ /tmp/debian-mm/var/log/bootstrap.log
+
+# clear out /run except for /run/lock
+find /tmp/debian-debootstrap/run/ -mindepth 1 -maxdepth 1 ! -name lock -print0 | xargs --no-run-if-empty -0 rm -r
+
+# debootstrap doesn't clean apt
+rm /tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_main_binary-{{ HOSTARCH }}_Packages \
+ /tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_InRelease \
+ /tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_Release \
+ /tmp/debian-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_unstable_Release.gpg
+
+if [ -e /tmp/debian-debootstrap/etc/machine-id ]; then
+ rm /tmp/debian-debootstrap/etc/machine-id /tmp/debian-mm/etc/machine-id
+fi
+rm /tmp/debian-mm/var/cache/apt/archives/lock
+rm /tmp/debian-mm/var/lib/apt/lists/lock
+rm /tmp/debian-mm/var/lib/dpkg/arch
+
+# also needed for users that are created by systemd-sysusers before systemd 252
+# https://github.com/systemd/systemd/pull/24534
+for f in shadow shadow-; do
+ if [ ! -e /tmp/debian-debootstrap/etc/$f ]; then
+ continue
+ fi
+ if ! cmp /tmp/debian-debootstrap/etc/$f /tmp/debian-mm/etc/$f >&2; then
+ echo patching /etc/$f >&2
+ awk -v FS=: -v OFS=: -v SDE={{ SOURCE_DATE_EPOCH }} '{ print $1,$2,int(SDE/60/60/24),$4,$5,$6,$7,$8,$9 }' < /tmp/debian-mm/etc/$f > /tmp/debian-mm/etc/$f.bak
+ cat /tmp/debian-mm/etc/$f.bak > /tmp/debian-mm/etc/$f
+ rm /tmp/debian-mm/etc/$f.bak
+ else
+ echo no difference for /etc/$f >&2
+ fi
+done
+
+# isc-dhcp-client postinst doesn't create this file in debootstrap run with
+# unshared wrapper. The responsible postinst snippet was automatically added
+# by dh_apparmor since isc-dhcp-client 4.4.3-P1-1.1
+if [ -e /tmp/debian-debootstrap/etc/apparmor.d/local/sbin.dhclient ] && [ ! -s /tmp/debian-debootstrap/etc/apparmor.d/local/sbin.dhclient ]; then
+ echo /sbin/setcap > /tmp/debian-debootstrap/etc/apparmor.d/local/sbin.dhclient
+fi
+
+# check if the file content differs
+diff --unified --no-dereference --recursive /tmp/debian-debootstrap /tmp/debian-mm >&2
+
+# check permissions, ownership, symlink targets, modification times using tar
+# mtimes of directories created by mmdebstrap will differ, thus we equalize them first
+for d in etc/apt/preferences.d/ etc/apt/sources.list.d/ etc/dpkg/dpkg.cfg.d/ var/log/apt/; do
+ touch --date="@{{ SOURCE_DATE_EPOCH }}" /tmp/debian-debootstrap/$d /tmp/debian-mm/$d
+done
+# debootstrap never ran apt -- fixing permissions
+for d in ./var/lib/apt/lists/partial ./var/cache/apt/archives/partial; do
+ chroot /tmp/debian-debootstrap chmod 0700 $d
+ chroot /tmp/debian-debootstrap chown _apt:root $d
+done
+tar -C /tmp/debian-debootstrap --numeric-owner --xattrs --xattrs-include='*' --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/root1.tar .
+tar -C /tmp/debian-mm --numeric-owner --xattrs --xattrs-include='*' --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/root2.tar .
+tar --full-time --verbose -tf /tmp/root1.tar > /tmp/root1.tar.list
+tar --full-time --verbose -tf /tmp/root2.tar > /tmp/root2.tar.list
+# despite SOURCE_DATE_EPOCH and --clamp-mtime, the timestamps in the tarball
+# will slightly differ from each other in the sub-second precision (last
+# decimals) so the tarballs will not be identical, so we use diff to compare
+# content and tar to compare attributes
+diff -u /tmp/root1.tar.list /tmp/root2.tar.list >&2
+rm /tmp/root1.tar /tmp/root2.tar /tmp/root1.tar.list /tmp/root2.tar.list
+
+rm /tmp/debian-mm.tar
+rm -r /tmp/debian-debootstrap /tmp/debian-mm
diff --git a/tests/ascii-armored-keys b/tests/ascii-armored-keys
new file mode 100644
index 0000000..518991c
--- /dev/null
+++ b/tests/ascii-armored-keys
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+for f in /etc/apt/trusted.gpg.d/*.gpg /etc/apt/trusted.gpg.d/*.asc; do
+ [ -e "$f" ] || continue
+ rm "$f"
+done
+rmdir /etc/apt/trusted.gpg.d
+mkdir /etc/apt/trusted.gpg.d
+for f in /usr/share/keyrings/*.gpg; do
+ name=$(basename "$f" .gpg)
+ gpg --no-default-keyring --keyring="/usr/share/keyrings/$name.gpg" --armor --output="/etc/apt/trusted.gpg.d/$name.asc" --export
+ rm "/usr/share/keyrings/$name.gpg"
+done
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot.tar
diff --git a/tests/aspcud-apt-solver b/tests/aspcud-apt-solver
new file mode 100644
index 0000000..bc0fbc3
--- /dev/null
+++ b/tests/aspcud-apt-solver
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=custom \
+ --include "$(tr '\n' ',' < pkglist.txt)" \
+ --aptopt='APT::Solver "aspcud"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort \
+ | grep -v '^./etc/apt/apt.conf.d/99mmdebstrap$' \
+ | diff -u tar1.txt -
diff --git a/tests/auto-mode-as-normal-user b/tests/auto-mode-as-normal-user
new file mode 100644
index 0000000..e8ab828
--- /dev/null
+++ b/tests/auto-mode-as-normal-user
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -f /tmp/debian-chroot.tar.gz" EXIT INT TERM
+
+[ {{ MODE }} = "auto" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar.gz {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar.gz | sort | diff -u tar1.txt -
diff --git a/tests/auto-mode-without-unshare-capabilities b/tests/auto-mode-without-unshare-capabilities
new file mode 100644
index 0000000..17244b8
--- /dev/null
+++ b/tests/auto-mode-without-unshare-capabilities
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+useradd --home-dir /home/user --create-home user
+if [ -e /proc/sys/kernel/unprivileged_userns_clone ] && [ "$(sysctl -n kernel.unprivileged_userns_clone)" = "1" ]; then
+ sysctl -w kernel.unprivileged_userns_clone=0
+fi
+runuser -u user -- {{ CMD }} --mode=auto --variant=apt {{ DIST }} /tmp/debian-chroot.tar.gz {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar.gz | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar.gz
diff --git a/tests/automatic-mirror-from-suite b/tests/automatic-mirror-from-suite
new file mode 100644
index 0000000..7cff5a6
--- /dev/null
+++ b/tests/automatic-mirror-from-suite
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+cat << HOSTS >> /etc/hosts
+127.0.0.1 deb.debian.org
+127.0.0.1 security.debian.org
+HOSTS
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/check-against-debootstrap-dist b/tests/check-against-debootstrap-dist
new file mode 100644
index 0000000..b5706c6
--- /dev/null
+++ b/tests/check-against-debootstrap-dist
@@ -0,0 +1,228 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH"
+
+# we create the apt user ourselves or otherwise its uid/gid will differ
+# compared to the one chosen in debootstrap because of different installation
+# order in comparison to the systemd users
+# https://bugs.debian.org/969631
+# we cannot use useradd because passwd is not Essential:yes
+{{ CMD }} --variant={{ VARIANT }} --mode={{ MODE }} \
+ --essential-hook='[ {{ DIST }} = oldstable ] && [ {{ VARIANT }} = - ] && echo _apt:*:100:65534::/nonexistent:/usr/sbin/nologin >> "$1"/etc/passwd || :' \
+ "$(if [ {{ DIST }} = oldstable ]; then echo --merged-usr; else echo --hook-dir=./hooks/merged-usr; fi)" \
+ "$(case {{ DIST }} in oldstable) echo --include=e2fsprogs,mount,tzdata,gcc-9-base;; stable) echo --include=e2fsprogs,mount,tzdata;; *) echo --include=base-files ;; esac )" \
+ {{ DIST }} /tmp/debian-{{ DIST }}-mm.tar {{ MIRROR }}
+
+mkdir /tmp/debian-{{ DIST }}-mm
+tar --xattrs --xattrs-include='*' -C /tmp/debian-{{ DIST }}-mm -xf /tmp/debian-{{ DIST }}-mm.tar
+rm /tmp/debian-{{ DIST }}-mm.tar
+
+mkdir /tmp/debian-{{ DIST }}-debootstrap
+tar --xattrs --xattrs-include='*' -C /tmp/debian-{{ DIST }}-debootstrap -xf "cache/debian-{{ DIST }}-{{ VARIANT }}.tar"
+
+# diff cannot compare device nodes, so we use tar to do that for us and then
+# delete the directory
+tar -C /tmp/debian-{{ DIST }}-debootstrap -cf /tmp/dev1.tar ./dev
+tar -C /tmp/debian-{{ DIST }}-mm -cf /tmp/dev2.tar ./dev
+ret=0
+cmp /tmp/dev1.tar /tmp/dev2.tar >&2 || ret=$?
+if [ "$ret" -ne 0 ]; then
+ if type diffoscope >/dev/null; then
+ diffoscope /tmp/dev1.tar /tmp/dev2.tar
+ exit 1
+ else
+ echo "no diffoscope installed" >&2
+ fi
+ if type base64 >/dev/null; then
+ base64 /tmp/dev1.tar
+ base64 /tmp/dev2.tar
+ exit 1
+ else
+ echo "no base64 installed" >&2
+ fi
+ if type xxd >/dev/null; then
+ xxd /tmp/dev1.tar
+ xxd /tmp/dev2.tar
+ exit 1
+ else
+ echo "no xxd installed" >&2
+ fi
+ exit 1
+fi
+rm /tmp/dev1.tar /tmp/dev2.tar
+rm -r /tmp/debian-{{ DIST }}-debootstrap/dev /tmp/debian-{{ DIST }}-mm/dev
+
+# remove downloaded deb packages
+rm /tmp/debian-{{ DIST }}-debootstrap/var/cache/apt/archives/*.deb
+# remove aux-cache
+rm /tmp/debian-{{ DIST }}-debootstrap/var/cache/ldconfig/aux-cache
+# remove logs
+rm /tmp/debian-{{ DIST }}-debootstrap/var/log/dpkg.log \
+ /tmp/debian-{{ DIST }}-debootstrap/var/log/bootstrap.log \
+ /tmp/debian-{{ DIST }}-debootstrap/var/log/alternatives.log
+# remove *-old files
+rm /tmp/debian-{{ DIST }}-debootstrap/var/cache/debconf/config.dat-old \
+ /tmp/debian-{{ DIST }}-mm/var/cache/debconf/config.dat-old
+rm /tmp/debian-{{ DIST }}-debootstrap/var/cache/debconf/templates.dat-old \
+ /tmp/debian-{{ DIST }}-mm/var/cache/debconf/templates.dat-old
+rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/dpkg/status-old \
+ /tmp/debian-{{ DIST }}-mm/var/lib/dpkg/status-old
+# remove dpkg files
+rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/dpkg/available
+rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/dpkg/cmethopt
+# remove /var/lib/dpkg/arch
+rm /tmp/debian-{{ DIST }}-mm/var/lib/dpkg/arch
+# since we installed packages directly from the .deb files, Priorities differ
+# thus we first check for equality and then remove the files
+chroot /tmp/debian-{{ DIST }}-debootstrap dpkg --list > /tmp/dpkg1
+chroot /tmp/debian-{{ DIST }}-mm dpkg --list > /tmp/dpkg2
+diff -u /tmp/dpkg1 /tmp/dpkg2 >&2
+rm /tmp/dpkg1 /tmp/dpkg2
+grep -v '^Priority: ' /tmp/debian-{{ DIST }}-debootstrap/var/lib/dpkg/status > /tmp/status1
+grep -v '^Priority: ' /tmp/debian-{{ DIST }}-mm/var/lib/dpkg/status > /tmp/status2
+diff -u /tmp/status1 /tmp/status2 >&2
+rm /tmp/status1 /tmp/status2
+rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/dpkg/status /tmp/debian-{{ DIST }}-mm/var/lib/dpkg/status
+# debootstrap exposes the hosts's kernel version
+if [ -e /tmp/debian-{{ DIST }}-debootstrap/etc/apt/apt.conf.d/01autoremove-kernels ]; then
+ rm /tmp/debian-{{ DIST }}-debootstrap/etc/apt/apt.conf.d/01autoremove-kernels
+fi
+if [ -e /tmp/debian-{{ DIST }}-mm/etc/apt/apt.conf.d/01autoremove-kernels ]; then
+ rm /tmp/debian-{{ DIST }}-mm/etc/apt/apt.conf.d/01autoremove-kernels
+fi
+# clear out /run except for /run/lock
+find /tmp/debian-{{ DIST }}-debootstrap/run/ -mindepth 1 -maxdepth 1 ! -name lock -print0 | xargs --no-run-if-empty -0 rm -r
+# debootstrap doesn't clean apt
+rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_main_binary-{{ HOSTARCH }}_Packages \
+ /tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_InRelease \
+ /tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_Release \
+ /tmp/debian-{{ DIST }}-debootstrap/var/lib/apt/lists/127.0.0.1_debian_dists_{{ DIST }}_Release.gpg
+
+if [ "{{ VARIANT }}" = "-" ]; then
+ rm /tmp/debian-{{ DIST }}-debootstrap/etc/machine-id
+ rm /tmp/debian-{{ DIST }}-mm/etc/machine-id
+ rm /tmp/debian-{{ DIST }}-debootstrap/var/lib/systemd/catalog/database
+ rm /tmp/debian-{{ DIST }}-mm/var/lib/systemd/catalog/database
+
+ cap=$(chroot /tmp/debian-{{ DIST }}-debootstrap /sbin/getcap /bin/ping)
+ expected="/bin/ping cap_net_raw=ep"
+ if [ "$cap" != "$expected" ]; then
+ echo "expected bin/ping to have capabilities $expected" >&2
+ echo "but debootstrap produced: $cap" >&2
+ exit 1
+ fi
+ cap=$(chroot /tmp/debian-{{ DIST }}-mm /sbin/getcap /bin/ping)
+ if [ "$cap" != "$expected" ]; then
+ echo "expected bin/ping to have capabilities $expected" >&2
+ echo "but mmdebstrap produced: $cap" >&2
+ exit 1
+ fi
+fi
+rm /tmp/debian-{{ DIST }}-mm/var/cache/apt/archives/lock
+rm /tmp/debian-{{ DIST }}-mm/var/lib/apt/extended_states
+rm /tmp/debian-{{ DIST }}-mm/var/lib/apt/lists/lock
+
+# the list of shells might be sorted wrongly
+# /var/lib/dpkg/triggers/File might be sorted wrongly
+for f in "/var/lib/dpkg/triggers/File" "/etc/shells"; do
+ f1="/tmp/debian-{{ DIST }}-debootstrap/$f"
+ f2="/tmp/debian-{{ DIST }}-mm/$f"
+ # both chroots must have the file
+ if [ ! -e "$f1" ] || [ ! -e "$f2" ]; then
+ continue
+ fi
+ # the file must be different
+ if cmp "$f1" "$f2" >&2; then
+ continue
+ fi
+ # then sort both
+ sort -o "$f1" "$f1"
+ sort -o "$f2" "$f2"
+done
+
+# Because of unreproducible uids (#969631) we created the _apt user ourselves
+# and because passwd is not Essential:yes we didn't use useradd. But newer
+# versions of adduser and shadow will create a different /etc/shadow
+if [ "{{ VARIANT }}" = "-" ] && [ "{{ DIST}}" = oldstable ]; then
+ for f in shadow shadow-; do
+ if grep -q '^_apt:!:' /tmp/debian-{{ DIST }}-debootstrap/etc/$f; then
+ sed -i 's/^_apt:\*:\([^:]\+\):0:99999:7:::$/_apt:!:\1::::::/' /tmp/debian-{{ DIST }}-mm/etc/$f
+ fi
+ done
+fi
+
+for log in faillog lastlog; do
+ if ! cmp /tmp/debian-{{ DIST }}-debootstrap/var/log/$log /tmp/debian-{{ DIST }}-mm/var/log/$log >&2;then
+ # if the files differ, make sure they are all zeroes
+ cmp -n "$(stat -c %s "/tmp/debian-{{ DIST }}-debootstrap/var/log/$log")" "/tmp/debian-{{ DIST }}-debootstrap/var/log/$log" /dev/zero >&2
+ cmp -n "$(stat -c %s "/tmp/debian-{{ DIST }}-mm/var/log/$log")" "/tmp/debian-{{ DIST }}-mm/var/log/$log" /dev/zero >&2
+ # then delete them
+ rm /tmp/debian-{{ DIST }}-debootstrap/var/log/$log /tmp/debian-{{ DIST }}-mm/var/log/$log
+ fi
+done
+
+# the order in which systemd and cron get installed differ and thus the order
+# of lines in /etc/group and /etc/gshadow differs
+if [ "{{ VARIANT }}" = "-" ]; then
+ for f in group group- gshadow gshadow-; do
+ for d in mm debootstrap; do
+ sort /tmp/debian-{{ DIST }}-$d/etc/$f > /tmp/debian-{{ DIST }}-$d/etc/$f.bak
+ mv /tmp/debian-{{ DIST }}-$d/etc/$f.bak /tmp/debian-{{ DIST }}-$d/etc/$f
+ done
+ done
+fi
+
+# since debootstrap 1.0.133 there is no tzdata in the buildd variant and thus
+# debootstrap creates its own /etc/localtime
+if [ "{{ VARIANT }}" = "buildd" ] && [ "{{ DIST }}" != "stable" ] && [ "{{ DIST }}" != "oldstable" ]; then
+ [ "$(readlink /tmp/debian-{{ DIST }}-debootstrap/etc/localtime)" = /usr/share/zoneinfo/UTC ]
+ rm /tmp/debian-{{ DIST }}-debootstrap/etc/localtime
+fi
+
+# starting with systemd 255 upstream dropped splitusr support and depending on
+# the installation order, symlink targets are prefixed with /usr or not
+# See #1060000 and #1054137
+case {{ DIST }} in testing|unstable)
+ for f in multi-user.target.wants/e2scrub_reap.service timers.target.wants/apt-daily-upgrade.timer timers.target.wants/apt-daily.timer timers.target.wants/e2scrub_all.timer; do
+ for d in mm debootstrap; do
+ [ -L "/tmp/debian-{{ DIST }}-$d/etc/systemd/system/$f" ] || continue
+ oldlink="$(readlink "/tmp/debian-{{ DIST }}-$d/etc/systemd/system/$f")"
+ case $oldlink in
+ /usr/*) : ;;
+ /*) oldlink="/usr$oldlink" ;;
+ *) echo unexpected >&2; exit 1 ;;
+ esac
+ ln -sf "$oldlink" "/tmp/debian-{{ DIST }}-$d/etc/systemd/system/$f"
+ done
+ done
+ ;;
+esac
+
+# check if the file content differs
+diff --unified --no-dereference --recursive /tmp/debian-{{ DIST }}-debootstrap /tmp/debian-{{ DIST }}-mm >&2
+
+# check permissions, ownership, symlink targets, modification times using tar
+# directory mtimes will differ, thus we equalize them first
+find /tmp/debian-{{ DIST }}-debootstrap /tmp/debian-{{ DIST }}-mm -type d -print0 | xargs -0 touch --date="@{{ SOURCE_DATE_EPOCH }}"
+# debootstrap never ran apt -- fixing permissions
+for d in ./var/lib/apt/lists/partial ./var/cache/apt/archives/partial; do
+ unmergedPATH="$PATH$(if [ "{{ DIST }}" = oldstable ]; then echo :/bin:/sbin; fi)"
+ PATH="$unmergedPATH" chroot /tmp/debian-{{ DIST }}-debootstrap chmod 0700 $d
+ PATH="$unmergedPATH" chroot /tmp/debian-{{ DIST }}-debootstrap chown "$(id -u _apt):root" $d
+done
+tar -C /tmp/debian-{{ DIST }}-debootstrap --numeric-owner --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/root1.tar .
+tar -C /tmp/debian-{{ DIST }}-mm --numeric-owner --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/root2.tar .
+tar --full-time --verbose -tf /tmp/root1.tar > /tmp/root1.tar.list
+tar --full-time --verbose -tf /tmp/root2.tar > /tmp/root2.tar.list
+diff -u /tmp/root1.tar.list /tmp/root2.tar.list >&2
+rm /tmp/root1.tar /tmp/root2.tar /tmp/root1.tar.list /tmp/root2.tar.list
+
+# check if file properties (permissions, ownership, symlink names, modification time) differ
+#
+# we cannot use this (yet) because it cannot cope with paths that have [ or @ in them
+#fmtree -c -p /tmp/debian-{{ DIST }}-debootstrap -k flags,gid,link,mode,size,time,uid | sudo fmtree -p /tmp/debian-{{ DIST }}-mm
+
+rm -r /tmp/debian-{{ DIST }}-debootstrap /tmp/debian-{{ DIST }}-mm
diff --git a/tests/check-for-bit-by-bit-identical-format-output b/tests/check-for-bit-by-bit-identical-format-output
new file mode 100644
index 0000000..6cbab90
--- /dev/null
+++ b/tests/check-for-bit-by-bit-identical-format-output
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+trap "rm -f /tmp/debian-chroot-{{ MODE }}.{{ FORMAT }}" EXIT INT TERM
+
+case {{ MODE }} in unshare|fakechroot) : ;; *) exit 1;; esac
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} {{ DIST }} /tmp/debian-chroot-{{ MODE }}.{{ FORMAT }} {{ MIRROR }}
+cmp ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.{{ FORMAT }} /tmp/debian-chroot-{{ MODE }}.{{ FORMAT }} \
+ || diffoscope ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.{{ FORMAT }} /tmp/debian-chroot-{{ MODE }}.{{ FORMAT }}
+
+# we cannot test chrootless mode here, because mmdebstrap relies on the
+# usrmerge package to set up merged-/usr and that doesn't work in chrootless
+# mode
diff --git a/tests/chroot-directory-not-accessible-by-apt-user b/tests/chroot-directory-not-accessible-by-apt-user
new file mode 100644
index 0000000..eb2d343
--- /dev/null
+++ b/tests/chroot-directory-not-accessible-by-apt-user
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+mkdir /tmp/debian-chroot
+chmod 700 /tmp/debian-chroot
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/chrootless b/tests/chrootless
new file mode 100644
index 0000000..77490c3
--- /dev/null
+++ b/tests/chrootless
@@ -0,0 +1,16 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+trap "rm -f /tmp/chrootless.tar /tmp/root.tar" EXIT INT TERM
+# we need --hook-dir=./hooks/merged-usr because usrmerge does not understand
+# DPKG_ROOT
+for INCLUDE in '' 'apt' 'apt,build-essential' 'systemd-sysv'; do
+ for MODE in root chrootless; do
+ {{ CMD }} --mode=$MODE --variant={{ VARIANT }} --hook-dir=./hooks/merged-usr \
+ ${INCLUDE:+--include="$INCLUDE"} --skip=check/chrootless \
+ {{ DIST }} "/tmp/$MODE.tar" {{ MIRROR }}
+ done
+ cmp /tmp/root.tar /tmp/chrootless.tar || diffoscope /tmp/root.tar /tmp/chrootless.tar
+ rm /tmp/chrootless.tar /tmp/root.tar
+done
diff --git a/tests/chrootless-fakeroot b/tests/chrootless-fakeroot
new file mode 100644
index 0000000..8821fa6
--- /dev/null
+++ b/tests/chrootless-fakeroot
@@ -0,0 +1,43 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+trap "rm -f /tmp/chrootless.tar /tmp/root.tar" EXIT INT TERM
+
+[ {{ MODE }} = chrootless ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+MMTARFILTER=
+[ -x /usr/bin/mmtarfilter ] && MMTARFILTER=/usr/bin/mmtarfilter
+[ -x ./tarfilter ] && MMTARFILTER=./tarfilter
+
+# we need --hook-dir=./hooks/merged-usr because usrmerge does not understand
+# DPKG_ROOT
+# permissions drwxr-sr-x and extended attributes of ./var/log/journal/ cannot
+# be preserved under fakeroot
+# this applies to 'z' lines in files in /usr/lib/tmpfiles.d/
+for INCLUDE in '' 'apt' 'apt,build-essential' 'systemd-sysv'; do
+ {{ CMD }} --variant={{ VARIANT }} --hook-dir=./hooks/merged-usr \
+ ${INCLUDE:+--include="$INCLUDE"} \
+ {{ DIST }} - {{ MIRROR }} \
+ | "$MMTARFILTER" --path-exclude="/var/log/journal" --path-exclude="/etc/credstore*" \
+ >/tmp/root.tar
+ $prefix fakeroot {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --hook-dir=./hooks/merged-usr \
+ ${INCLUDE:+--include="$INCLUDE"} \
+ {{ DIST }} - {{ MIRROR }} \
+ | "$MMTARFILTER" --path-exclude="/var/log/journal" --path-exclude="/etc/credstore*" \
+ > /tmp/chrootless.tar
+ cmp /tmp/root.tar /tmp/chrootless.tar || diffoscope /tmp/root.tar /tmp/chrootless.tar
+ rm /tmp/chrootless.tar /tmp/root.tar
+done
diff --git a/tests/chrootless-foreign b/tests/chrootless-foreign
new file mode 100644
index 0000000..03203d0
--- /dev/null
+++ b/tests/chrootless-foreign
@@ -0,0 +1,68 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+trap "rm -f /tmp/chrootless.tar /tmp/root.tar" EXIT INT TERM
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+
+deb2qemu() {
+ case "$1" in
+ amd64) echo x86_64;;
+ arm64) echo aarch64;;
+ armel|armhf) echo arm;;
+ ppc64el) echo ppc64le;;
+ *) echo "$1";;
+ esac
+}
+if [ "$(dpkg --print-architecture)" = "arm64" ]; then
+ arch=amd64
+else
+ arch=arm64
+fi
+
+[ "$(id -u)" -eq 0 ]
+[ -e "/proc/sys/fs/binfmt_misc/qemu-$(deb2qemu "$arch")" ]
+
+
+# we need --hook-dir=./hooks/merged-usr because usrmerge does not understand
+# DPKG_ROOT
+#
+# dpkg is unable to install architecture arch:all packages with a
+# dependency on an arch:any package (perl-modules-5.34 in this case)
+# inside foreign architecture chrootless chroots, because dpkg will use
+# its own architecture as the native architecture, see #825385 and #1020533
+# So we are not testing the installation of apt,build-essential here.
+for INCLUDE in '' 'apt' 'systemd-sysv'; do
+ echo 1 > "/proc/sys/fs/binfmt_misc/qemu-$(deb2qemu "$arch")"
+ arch-test "$arch"
+ {{ CMD }} --mode=root --architecture="$arch" --variant={{ VARIANT }} \
+ --hook-dir=./hooks/merged-usr ${INCLUDE:+--include="$INCLUDE"} \
+ {{ DIST }} "/tmp/root.tar" {{ MIRROR }}
+ echo 0 > "/proc/sys/fs/binfmt_misc/qemu-$(deb2qemu "$arch")"
+ arch-test "$arch" && exit 1
+ {{ CMD }} --mode=chrootless --architecture="$arch" --variant={{ VARIANT }} \
+ --hook-dir=./hooks/merged-usr ${INCLUDE:+--include="$INCLUDE"} \
+ --skip=check/chrootless {{ DIST }} "/tmp/chrootless.tar" {{ MIRROR }}
+ # when creating a foreign architecture chroot, the tarballs are not
+ # bit-by-bit identical but contain a few remaining differences:
+ #
+ # * /etc/ld.so.cache -- hard problem, must be solved in glibc upstream
+ # * /var/lib/dpkg/triggers -- #990712
+ # * /var/cache/debconf/*.dat-old -- needs investigation
+ for tar in root chrootless; do
+ <"/tmp/$tar.tar" \
+ ./tarfilter \
+ --path-exclude=/var/cache/debconf/config.dat-old \
+ --path-exclude=/var/cache/debconf/templates.dat-old \
+ --path-exclude=/etc/ld.so.cache \
+ --path-exclude=/var/lib/dpkg/triggers/File \
+ --path-exclude=/var/lib/dpkg/triggers/ldconfig \
+ > "/tmp/$tar.tar.tmp"
+ mv "/tmp/$tar.tar.tmp" "/tmp/$tar.tar"
+ done
+ cmp /tmp/root.tar /tmp/chrootless.tar || diffoscope /tmp/root.tar /tmp/chrootless.tar
+ rm /tmp/chrootless.tar /tmp/root.tar
+done
diff --git a/tests/compare-output-with-pre-seeded-var-cache-apt-archives b/tests/compare-output-with-pre-seeded-var-cache-apt-archives
new file mode 100644
index 0000000..f0e132c
--- /dev/null
+++ b/tests/compare-output-with-pre-seeded-var-cache-apt-archives
@@ -0,0 +1,44 @@
+#!/bin/sh
+#
+# test that the user can drop archives into /var/cache/apt/archives as well as
+# into /var/cache/apt/archives/partial
+
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test requires the cache directory to be mounted on /mnt and should only be run inside a container" >&2
+ exit 1
+fi
+tmpdir=$(mktemp -d)
+trap 'rm -f "$tmpdir"/*.deb /tmp/orig.tar /tmp/test1.tar /tmp/test2.tar; rmdir "$tmpdir"' EXIT INT TERM
+
+include="--include=doc-debian"
+if [ "{{ VARIANT }}" = "custom" ]; then
+ include="$include,base-files,base-passwd,coreutils,dash,diffutils,dpkg,libc-bin,sed"
+fi
+{{ CMD }} $include --mode={{ MODE }} --variant={{ VARIANT }} \
+ --setup-hook='mkdir -p "$1"/var/cache/apt/archives/partial' \
+ --setup-hook='touch "$1"/var/cache/apt/archives/lock' \
+ --setup-hook='chmod 0640 "$1"/var/cache/apt/archives/lock' \
+ {{ DIST }} - {{ MIRROR }} > /tmp/orig.tar
+# somehow, when trying to create a tarball from the 9p mount, tar throws the
+# following error: tar: ./doc-debian_6.4_all.deb: File shrank by 132942 bytes; padding with zeros
+# to reproduce, try: tar --directory /mnt/cache/debian/pool/main/d/doc-debian/ --create --file - . | tar --directory /tmp/ --extract --file -
+# this will be different:
+# md5sum /mnt/cache/debian/pool/main/d/doc-debian/*.deb /tmp/*.deb
+# another reason to copy the files into a new directory is, that we can use shell globs
+cp /mnt/cache/debian/pool/main/b/busybox/busybox_*"_{{ HOSTARCH }}.deb" /mnt/cache/debian/pool/main/a/apt/apt_*"_{{ HOSTARCH }}.deb" "$tmpdir"
+{{ CMD }} $include --mode={{ MODE }} --variant={{ VARIANT }} \
+ --setup-hook='mkdir -p "$1"/var/cache/apt/archives/partial' \
+ --setup-hook='sync-in "'"$tmpdir"'" /var/cache/apt/archives/partial' \
+ {{ DIST }} - {{ MIRROR }} > /tmp/test1.tar
+cmp /tmp/orig.tar /tmp/test1.tar
+{{ CMD }} $include --mode={{ MODE }} --variant={{ VARIANT }} \
+ --customize-hook='touch "$1"/var/cache/apt/archives/partial' \
+ --setup-hook='mkdir -p "$1"/var/cache/apt/archives/' \
+ --setup-hook='sync-in "'"$tmpdir"'" /var/cache/apt/archives/' \
+ --setup-hook='chmod 0755 "$1"/var/cache/apt/archives/' \
+ --customize-hook='find "'"$tmpdir"'" -type f -exec md5sum "{}" \; | sed "s|"'"$tmpdir"'"|$1/var/cache/apt/archives|" | md5sum --check' \
+ {{ DIST }} - {{ MIRROR }} > /tmp/test2.tar
+cmp /tmp/orig.tar /tmp/test2.tar
diff --git a/tests/copy-mirror b/tests/copy-mirror
new file mode 100644
index 0000000..1903925
--- /dev/null
+++ b/tests/copy-mirror
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test requires the cache directory to be mounted on /mnt and should only be run inside a container" >&2
+ exit 1
+fi
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar "deb copy:///mnt/cache/debian {{ DIST }} main"
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/create-directory b/tests/create-directory
new file mode 100644
index 0000000..2d5461b
--- /dev/null
+++ b/tests/create-directory
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+chroot /tmp/debian-chroot dpkg-query --showformat '${binary:Package}\n' --show > pkglist.txt
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort > tar1.txt
diff --git a/tests/create-directory-dry-run b/tests/create-directory-dry-run
new file mode 100644
index 0000000..03226e4
--- /dev/null
+++ b/tests/create-directory-dry-run
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+{{ CMD }} --mode={{ MODE }} --dry-run --variant=apt \
+ --setup-hook="exit 1" \
+ --essential-hook="exit 1" \
+ --customize-hook="exit 1" \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+rm /tmp/debian-chroot/dev/console
+rm /tmp/debian-chroot/dev/fd
+rm /tmp/debian-chroot/dev/full
+rm /tmp/debian-chroot/dev/null
+rm /tmp/debian-chroot/dev/ptmx
+rm /tmp/debian-chroot/dev/random
+rm /tmp/debian-chroot/dev/stderr
+rm /tmp/debian-chroot/dev/stdin
+rm /tmp/debian-chroot/dev/stdout
+rm /tmp/debian-chroot/dev/tty
+rm /tmp/debian-chroot/dev/urandom
+rm /tmp/debian-chroot/dev/zero
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/create-foreign-tarball b/tests/create-foreign-tarball
new file mode 100644
index 0000000..bf8f13a
--- /dev/null
+++ b/tests/create-foreign-tarball
@@ -0,0 +1,77 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+case "$(dpkg --print-architecture)" in
+ arm64)
+ native_arch=arm64
+ native_gnu=aarch64-linux-gnu
+ foreign_arch=amd64
+ foreign_gnu=x86_64-linux-gnu
+ ;;
+ amd64)
+ native_arch=amd64
+ native_gnu=x86_64-linux-gnu
+ foreign_arch=arm64
+ foreign_gnu=aarch64-linux-gnu
+ ;;
+ *)
+ echo "unsupported native architecture" >&2
+ exit 1
+ ;;
+esac
+
+[ "{{ MODE }}" = "fakechroot" ] && prefix="$prefix fakechroot fakeroot"
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --architectures="$foreign_arch" \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+# we ignore differences between architectures by ignoring some files
+# and renaming others
+{ tar -tf /tmp/debian-chroot.tar \
+ | grep -v '^\./usr/bin/i386$' \
+ | grep -v '^\./usr/bin/x86_64$' \
+ | grep -v '^\./lib64$' \
+ | grep -v '^\./usr/lib64/$' \
+ | grep -v '^\./usr/lib64/ld-linux-x86-64\.so\.2$' \
+ | grep -v '^\./usr/lib/ld-linux-aarch64\.so\.1$' \
+ | grep -v "^\\./usr/lib/$foreign_gnu/ld-linux-aarch64\\.so\\.1$" \
+ | grep -v "^\\./usr/lib/$foreign_gnu/ld-linux-x86-64\\.so\\.2$" \
+ | grep -v "^\\./usr/lib/$foreign_gnu/perl/5\\.[0-9][.0-9]\\+/.*\\.ph$" \
+ | grep -v "^\\./usr/lib/$foreign_gnu/libmvec\\.so\\.1$" \
+ | grep -v "^\\./usr/share/doc/[^/]\\+/changelog\\(\\.Debian\\)\\?\\.$foreign_arch\\.gz$" \
+ | grep -v '^\./usr/share/man/man8/i386\.8\.gz$' \
+ | grep -v '^\./usr/share/man/man8/x86_64\.8\.gz$' \
+ | sed "s/$foreign_gnu/$native_gnu/" \
+ | sed "s/$foreign_arch/$native_arch/";
+} | sort > /tmp/tar2.txt
+{ < tar1.txt \
+ grep -v '^\./usr/bin/i386$' \
+ | grep -v '^\./usr/bin/x86_64$' \
+ | grep -v '^\./lib32$' \
+ | grep -v '^\./lib64$' \
+ | grep -v '^\./libx32$' \
+ | grep -v '^\./usr/lib32/$' \
+ | grep -v '^\./usr/libx32/$' \
+ | grep -v '^\./usr/lib64/$' \
+ | grep -v '^\./usr/lib64/ld-linux-x86-64\.so\.2$' \
+ | grep -v '^\./usr/lib/ld-linux-aarch64\.so\.1$' \
+ | grep -v "^\\./usr/lib/$native_gnu/ld-linux-x86-64\\.so\\.2$" \
+ | grep -v "^\\./usr/lib/$native_gnu/ld-linux-aarch64\\.so\\.1$" \
+ | grep -v "^\\./usr/lib/$native_gnu/libmvec\\.so\\.1$" \
+ | grep -v "^\\./usr/lib/$native_gnu/perl/5\\.[0-9][.0-9]\\+/.*\\.ph$" \
+ | grep -v "^\\./usr/share/doc/[^/]\\+/changelog\\(\\.Debian\\)\\?\\.$native_arch\\.gz$" \
+ | grep -v '^\./usr/share/man/man8/i386\.8\.gz$' \
+ | grep -v '^\./usr/share/man/man8/x86_64\.8\.gz$';
+} | sort | diff -u - /tmp/tar2.txt >&2
+rm /tmp/debian-chroot.tar /tmp/tar2.txt
diff --git a/tests/create-gzip-compressed-tarball b/tests/create-gzip-compressed-tarball
new file mode 100644
index 0000000..1492df2
--- /dev/null
+++ b/tests/create-gzip-compressed-tarball
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar.gz {{ MIRROR }}
+printf '\037\213\010' | cmp --bytes=3 /tmp/debian-chroot.tar.gz -
+tar -tf /tmp/debian-chroot.tar.gz | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar.gz
diff --git a/tests/create-tarball-dry-run b/tests/create-tarball-dry-run
new file mode 100644
index 0000000..f4c5fe2
--- /dev/null
+++ b/tests/create-tarball-dry-run
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# we are testing all variants here because with 0.7.5 we had a bug:
+# mmdebstrap sid /dev/null --simulate ==> E: cannot read /var/cache/apt/archives/
+
+set -eu
+export LC_ALL=C.UTF-8
+prefix=
+include=,
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != root ] && [ "{{ MODE }}" != auto ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+ if [ "{{ VARIANT }}" = extract ] || [ "{{ VARIANT }}" = custom ]; then
+ include="$(tr '\n' ',' < pkglist.txt)"
+ fi
+fi
+$prefix {{ CMD }} --mode={{ MODE }} --include="$include" --dry-run --variant={{ VARIANT }} {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+if [ -e /tmp/debian-chroot.tar ]; then
+ echo "/tmp/debian-chroot.tar must not be created with --dry-run" >&2
+ exit 1
+fi
diff --git a/tests/create-tarball-with-tmp-mounted-nodev b/tests/create-tarball-with-tmp-mounted-nodev
new file mode 100644
index 0000000..61ff320
--- /dev/null
+++ b/tests/create-tarball-with-tmp-mounted-nodev
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+mount -t tmpfs -o nodev,nosuid,size=400M tmpfs /tmp
+# use --customize-hook to exercise the mounting/unmounting code of block devices in root mode
+{{ CMD }} --mode=root --variant=apt --customize-hook='mount | grep /dev/full' --customize-hook='test "$(echo foo | tee /dev/full 2>&1 1>/dev/null)" = "tee: /dev/full: No space left on device"' {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/custom-tmpdir b/tests/custom-tmpdir
new file mode 100644
index 0000000..bfd3651
--- /dev/null
+++ b/tests/custom-tmpdir
@@ -0,0 +1,33 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+[ "$(id -u)" -eq 0 ]
+[ {{ MODE }} = "unshare" ]
+
+if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+fi
+prefix="runuser -u ${SUDO_USER:-user} --"
+
+# https://www.etalabs.net/sh_tricks.html
+quote () { printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/" ; }
+homedir=$($prefix sh -c 'cd && pwd')
+# apt:test/integration/test-apt-key
+TMPDIR_ADD="This is fü\$\$ing cràzy, \$(apt -v)\$!"
+$prefix mkdir "$homedir/$TMPDIR_ADD"
+# make sure the unshared user can traverse into the TMPDIR
+chmod 711 "$homedir"
+# set permissions and sticky bit like the real /tmp
+chmod 1777 "$homedir/$TMPDIR_ADD"
+$prefix env TMPDIR="$homedir/$TMPDIR_ADD" {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --setup-hook='case "$1" in '"$(quote "$homedir/$TMPDIR_ADD/mmdebstrap.")"'??????????) exit 0;; *) echo "$1"; exit 1;; esac' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+# use rmdir as a quick check that nothing is remaining in TMPDIR
+$prefix rmdir "$homedir/$TMPDIR_ADD"
+rm /tmp/debian-chroot.tar
diff --git a/tests/customize-hook b/tests/customize-hook
new file mode 100644
index 0000000..6437eac
--- /dev/null
+++ b/tests/customize-hook
@@ -0,0 +1,16 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/customize.sh" EXIT INT TERM
+cat << 'SCRIPT' > /tmp/customize.sh
+#!/bin/sh
+chroot "$1" whoami > "$1/output2"
+chroot "$1" pwd >> "$1/output2"
+SCRIPT
+chmod +x /tmp/customize.sh
+{{ CMD }} --mode=root --variant=apt --customize-hook='chroot "$1" sh -c "whoami; pwd" > "$1/output1"' --customize-hook=/tmp/customize.sh {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf "root\n/\n" | cmp /tmp/debian-chroot/output1
+printf "root\n/\n" | cmp /tmp/debian-chroot/output2
+rm /tmp/debian-chroot/output1
+rm /tmp/debian-chroot/output2
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/cwd-directory-not-accessible-by-unshared-user b/tests/cwd-directory-not-accessible-by-unshared-user
new file mode 100644
index 0000000..859cf6b
--- /dev/null
+++ b/tests/cwd-directory-not-accessible-by-unshared-user
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+[ "$(id -u)" -eq 0 ]
+[ {{ MODE }} = "unshare" ]
+
+if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+fi
+prefix="runuser -u ${SUDO_USER:-user} --"
+
+mkdir /tmp/debian-chroot
+chmod 700 /tmp/debian-chroot
+chown "${SUDO_USER:-user}:${SUDO_USER:-user}" /tmp/debian-chroot
+set -- env --chdir=/tmp/debian-chroot
+if [ "{{ CMD }}" = "./mmdebstrap" ]; then
+ set -- "$@" "$(realpath --canonicalize-existing ./mmdebstrap)"
+elif [ "{{ CMD }}" = "perl -MDevel::Cover=-silent,-nogcov ./mmdebstrap" ]; then
+ set -- "$@" perl -MDevel::Cover=-silent,-nogcov "$(realpath --canonicalize-existing ./mmdebstrap)"
+else
+ set -- "$@" {{ CMD }}
+fi
+$prefix "$@" --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/deb822-1-2 b/tests/deb822-1-2
new file mode 100644
index 0000000..1459117
--- /dev/null
+++ b/tests/deb822-1-2
@@ -0,0 +1,45 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/sources.list /tmp/deb822.sources" EXIT INT TERM
+cat << SOURCES > /tmp/deb822.sources
+Types: deb
+URIs: {{ MIRROR }}1
+Suites: {{ DIST }}
+Components: main
+SOURCES
+echo "deb {{ MIRROR }}2 {{ DIST }} main" > /tmp/sources.list
+echo "deb {{ MIRROR }}3 {{ DIST }} main" \
+ | {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} \
+ /tmp/debian-chroot \
+ /tmp/deb822.sources \
+ {{ MIRROR }}4 \
+ - \
+ "deb {{ MIRROR }}5 {{ DIST }} main" \
+ {{ MIRROR }}6 \
+ /tmp/sources.list
+test ! -e /tmp/debian-chroot/etc/apt/sources.list
+cat << SOURCES | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0000deb822.sources -
+Types: deb
+URIs: {{ MIRROR }}1
+Suites: {{ DIST }}
+Components: main
+SOURCES
+cat << SOURCES | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0001main.list -
+deb {{ MIRROR }}4 {{ DIST }} main
+
+deb {{ MIRROR }}3 {{ DIST }} main
+
+deb {{ MIRROR }}5 {{ DIST }} main
+
+deb {{ MIRROR }}6 {{ DIST }} main
+SOURCES
+echo "deb {{ MIRROR }}2 {{ DIST }} main" | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0002sources.list -
+tar -C /tmp/debian-chroot --one-file-system -c . \
+ | {
+ tar -t \
+ | grep -v "^./etc/apt/sources.list.d/0000deb822.sources$" \
+ | grep -v "^./etc/apt/sources.list.d/0001main.list$" \
+ | grep -v "^./etc/apt/sources.list.d/0002sources.list";
+ printf "./etc/apt/sources.list\n";
+ } | sort | diff -u tar1.txt -
diff --git a/tests/deb822-2-2 b/tests/deb822-2-2
new file mode 100644
index 0000000..c533264
--- /dev/null
+++ b/tests/deb822-2-2
@@ -0,0 +1,44 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/sources /tmp/deb822" EXIT INT TERM
+cat << SOURCES > /tmp/deb822
+Types: deb
+URIs: {{ MIRROR }}1
+Suites: {{ DIST }}
+Components: main
+SOURCES
+echo "deb {{ MIRROR }}2 {{ DIST }} main" > /tmp/sources
+cat << SOURCES | {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} \
+ /tmp/debian-chroot \
+ /tmp/deb822 \
+ - \
+ /tmp/sources
+Types: deb
+URIs: {{ MIRROR }}3
+Suites: {{ DIST }}
+Components: main
+SOURCES
+test ! -e /tmp/debian-chroot/etc/apt/sources.list
+ls -lha /tmp/debian-chroot/etc/apt/sources.list.d/
+cat << SOURCES | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0000deb822.sources -
+Types: deb
+URIs: {{ MIRROR }}1
+Suites: {{ DIST }}
+Components: main
+SOURCES
+cat << SOURCES | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0001main.sources -
+Types: deb
+URIs: {{ MIRROR }}3
+Suites: {{ DIST }}
+Components: main
+SOURCES
+echo "deb {{ MIRROR }}2 {{ DIST }} main" | cmp /tmp/debian-chroot/etc/apt/sources.list.d/0002sources.list -
+tar -C /tmp/debian-chroot --one-file-system -c . \
+ | {
+ tar -t \
+ | grep -v "^./etc/apt/sources.list.d/0000deb822.sources$" \
+ | grep -v "^./etc/apt/sources.list.d/0001main.sources$" \
+ | grep -v "^./etc/apt/sources.list.d/0002sources.list$";
+ printf "./etc/apt/sources.list\n";
+ } | sort | diff -u tar1.txt -
diff --git a/tests/debootstrap b/tests/debootstrap
new file mode 100644
index 0000000..63c217d
--- /dev/null
+++ b/tests/debootstrap
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+tmpdir="$(mktemp -d)"
+chmod 755 "$tmpdir"
+debootstrap "$([ "{{ DIST }}" = oldstable ] && echo --no-merged-usr || echo --merged-usr)" --variant={{ VARIANT }} {{ DIST }} "$tmpdir" {{ MIRROR }}
+tar --sort=name --mtime=@$SOURCE_DATE_EPOCH --clamp-mtime --numeric-owner --one-file-system --xattrs -C "$tmpdir" -c . > "./cache/debian-{{ DIST }}-{{ VARIANT }}.tar"
+rm -r "$tmpdir"
diff --git a/tests/debootstrap-no-op-options b/tests/debootstrap-no-op-options
new file mode 100644
index 0000000..cd41681
--- /dev/null
+++ b/tests/debootstrap-no-op-options
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+{{ CMD }} --mode=root --variant=apt --resolve-deps --merged-usr --no-merged-usr --force-check-gpg {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/debug b/tests/debug
new file mode 100644
index 0000000..5612115
--- /dev/null
+++ b/tests/debug
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+
+# we use variant standard in verbose mode to see the maximum number of packages
+# that was chosen in case of USE_HOST_APT_CONFIG=yes
+# we use variant important on arches where variant standard is not bit-by-bit
+# reproducible due to #1031276
+case {{ VARIANT }} in standard|-) : ;; *) exit 1;; esac
+
+{{ CMD }} --variant={{ VARIANT }} --debug {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+
+cmp ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.tar /tmp/debian-chroot.tar \
+ || diffoscope ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.tar /tmp/debian-chroot.tar
diff --git a/tests/debug-output-on-fake-tty b/tests/debug-output-on-fake-tty
new file mode 100644
index 0000000..c8c8a87
--- /dev/null
+++ b/tests/debug-output-on-fake-tty
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+script -qfc "{{ CMD }} --mode={{ MODE }} --debug --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}" /dev/null
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/dev-ptmx b/tests/dev-ptmx
new file mode 100644
index 0000000..5eb7bd0
--- /dev/null
+++ b/tests/dev-ptmx
@@ -0,0 +1,149 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+if [ {{ MODE }} != unshare ] && [ {{ MODE }} != root ]; then
+ echo "test requires root or unshare mode" >&2
+ exit 1
+fi
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# this mimics what apt does in apt-pkg/deb/dpkgpm.cc/pkgDPkgPM::StartPtyMagic()
+cat > /tmp/test.c << 'END'
+#define _GNU_SOURCE
+
+#include <stdlib.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <signal.h>
+
+int main() {
+ int ret;
+ int fd = posix_openpt(O_RDWR | O_NOCTTY);
+ if (fd < 0) {
+ perror("posix_openpt");
+ return 1;
+ }
+ char buf[64]; // 64 is used by apt
+ ret = ptsname_r(fd, buf, sizeof(buf));
+ if (ret != 0) {
+ perror("ptsname_r");
+ return 1;
+ }
+ ret = grantpt(fd);
+ if (ret == -1) {
+ perror("grantpt");
+ return 1;
+ }
+ struct termios origtt;
+ ret = tcgetattr(STDIN_FILENO, &origtt);
+ if (ret != 0) {
+ perror("tcgetattr1");
+ return 1;
+ }
+ struct termios tt;
+ ret = tcgetattr(STDOUT_FILENO, &tt);
+ if (ret != 0) {
+ perror("tcgetattr2");
+ return 1;
+ }
+ struct winsize win;
+ ret = ioctl(STDOUT_FILENO, TIOCGWINSZ, &win);
+ if (ret < 0) {
+ perror("ioctl stdout TIOCGWINSZ");
+ return 1;
+ }
+ ret = ioctl(fd, TIOCSWINSZ, &win);
+ if (ret < 0) {
+ perror("ioctl fd TIOCGWINSZ");
+ return 1;
+ }
+ ret = tcsetattr(fd, TCSANOW, &tt);
+ if (ret != 0) {
+ perror("tcsetattr1");
+ return 1;
+ }
+ cfmakeraw(&tt);
+ tt.c_lflag &= ~ECHO;
+ tt.c_lflag |= ISIG;
+ sigset_t sigmask;
+ sigset_t sigmask_old;
+ ret = sigemptyset(&sigmask);
+ if (ret != 0) {
+ perror("sigemptyset");
+ return 1;
+ }
+ ret = sigaddset(&sigmask, SIGTTOU);
+ if (ret != 0) {
+ perror("sigaddset");
+ return 1;
+ }
+ ret = sigprocmask(SIG_BLOCK,&sigmask, &sigmask_old);
+ if (ret != 0) {
+ perror("sigprocmask1");
+ return 1;
+ }
+ ret = tcsetattr(STDIN_FILENO, TCSAFLUSH, &tt);
+ if (ret != 0) {
+ perror("tcsetattr2");
+ return 1;
+ }
+ ret = sigprocmask(SIG_BLOCK,&sigmask_old, NULL);
+ if (ret != 0) {
+ perror("sigprocmask2");
+ return 1;
+ }
+ ret = tcsetattr(STDIN_FILENO, TCSAFLUSH, &origtt);
+ if (ret != 0) {
+ perror("tcsetattr3");
+ return 1;
+ }
+ return 0;
+}
+END
+
+# use script to create a fake tty
+# run all tests as root and as a normal user (the latter requires ptmxmode=666)
+script -qfec "$prefix {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --include=gcc,libc6-dev,python3,passwd \
+ --customize-hook='chroot \"\$1\" useradd --home-dir /home/user --create-home user' \
+ --customize-hook='chroot \"\$1\" python3 -c \"import pty; print(pty.openpty())\"' \
+ --customize-hook='chroot \"\$1\" runuser -u user -- python3 -c \"import pty; print(pty.openpty())\"' \
+ --customize-hook='chroot \"\$1\" script -c \"echo foobar\"' \
+ --customize-hook='chroot \"\$1\" runuser -u user -- env --chdir=/home/user script -c \"echo foobar\"' \
+ --customize-hook='chroot \"\$1\" apt-get install --yes doc-debian 2>&1 | tee \"\$1\"/tmp/log' \
+ --customize-hook=\"copy-in /tmp/test.c /tmp\" \
+ --customize-hook='chroot \"\$1\" gcc /tmp/test.c -o /tmp/test' \
+ --customize-hook='chroot \"\$1\" /tmp/test' \
+ --customize-hook='chroot \"\$1\" runuser -u user -- /tmp/test' \
+ --customize-hook='rm \"\$1\"/tmp/test \"\$1\"/tmp/test.c' \
+ --customize-hook=\"copy-out /tmp/log /tmp\" \
+ {{ DIST }} /dev/null {{ MIRROR }}" /dev/null
+
+fail=0
+[ -r /tmp/log ] || fail=1
+grep '^E:' /tmp/log && fail=1
+grep 'Can not write log' /tmp/log && fail=1
+grep 'posix_openpt' /tmp/log && fail=1
+grep 'No such file or directory' /tmp/log && fail=1
+if [ $fail -eq 1 ]; then
+ echo "apt failed to write log:" >&2
+ cat /tmp/log >&2
+ exit 1
+fi
+
+rm /tmp/test.c /tmp/log
diff --git a/tests/directory-ending-in-tar b/tests/directory-ending-in-tar
new file mode 100644
index 0000000..b44e35a
--- /dev/null
+++ b/tests/directory-ending-in-tar
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+[ "$(whoami)" = "root" ]
+trap "rm -rf /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt --format=directory {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+ftype=$(stat -c %F /tmp/debian-chroot.tar)
+if [ "$ftype" != directory ]; then
+ echo "expected directory but got: $ftype" >&2
+ exit 1
+fi
+tar -C /tmp/debian-chroot.tar --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/dist-using-codename b/tests/dist-using-codename
new file mode 100644
index 0000000..96d8929
--- /dev/null
+++ b/tests/dist-using-codename
@@ -0,0 +1,13 @@
+#!/bin/sh
+#
+# make sure that using codenames works https://bugs.debian.org/1003191
+
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f InRelease; rm -rf /tmp/debian-chroot.tar /tmp/expected" EXIT INT TERM
+/usr/lib/apt/apt-helper download-file "{{ MIRROR }}/dists/{{ DIST }}/InRelease" InRelease
+codename=$(awk '/^Codename: / { print $2; }' InRelease)
+{{ CMD }} --mode={{ MODE }} --variant=apt "$codename" /tmp/debian-chroot.tar {{ MIRROR }}
+echo "deb {{ MIRROR }} $codename main" > /tmp/expected
+tar --to-stdout --extract --file /tmp/debian-chroot.tar ./etc/apt/sources.list \
+ | diff -u /tmp/expected -
diff --git a/tests/dpkgopt b/tests/dpkgopt
new file mode 100644
index 0000000..1a41da4
--- /dev/null
+++ b/tests/dpkgopt
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/config" EXIT INT TERM
+echo no-pager > /tmp/config
+{{ CMD }} --mode=root --variant=apt --dpkgopt="path-exclude=/usr/share/doc/*" --dpkgopt=/tmp/config --dpkgopt="path-include=/usr/share/doc/dpkg/copyright" {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf 'path-exclude=/usr/share/doc/*\nno-pager\npath-include=/usr/share/doc/dpkg/copyright\n' | cmp /tmp/debian-chroot/etc/dpkg/dpkg.cfg.d/99mmdebstrap -
+rm /tmp/debian-chroot/etc/dpkg/dpkg.cfg.d/99mmdebstrap
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort > tar2.txt
+{ grep -v '^./usr/share/doc/.' tar1.txt; echo ./usr/share/doc/dpkg/; echo ./usr/share/doc/dpkg/copyright; } | sort | diff -u - tar2.txt
diff --git a/tests/eatmydata-via-hook-dir b/tests/eatmydata-via-hook-dir
new file mode 100644
index 0000000..0df72df
--- /dev/null
+++ b/tests/eatmydata-via-hook-dir
@@ -0,0 +1,43 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+cat << SCRIPT > /tmp/checkeatmydata.sh
+#!/bin/sh
+set -exu
+cat << EOF | diff - "\$1"/usr/bin/dpkg
+#!/bin/sh
+exec /usr/bin/eatmydata /usr/bin/dpkg.distrib "\\\$@"
+EOF
+[ -e "\$1"/usr/bin/eatmydata ]
+SCRIPT
+chmod +x /tmp/checkeatmydata.sh
+# first four bytes: magic
+elfheader="\\177ELF"
+# fifth byte: bits
+case "$(dpkg-architecture -qDEB_HOST_ARCH_BITS)" in
+ 32) elfheader="$elfheader\\001";;
+ 64) elfheader="$elfheader\\002";;
+ *) echo "bits not supported"; exit 1;;
+esac
+# sixth byte: endian
+case "$(dpkg-architecture -qDEB_HOST_ARCH_ENDIAN)" in
+ little) elfheader="$elfheader\\001";;
+ big) elfheader="$elfheader\\002";;
+ *) echo "endian not supported"; exit 1;;
+esac
+# seventh and eigth byte: elf version (1) and abi (unset)
+elfheader="$elfheader\\001\\000"
+{{ CMD }} --mode=root --variant=apt \
+ --customize-hook=/tmp/checkeatmydata.sh \
+ --essential-hook=/tmp/checkeatmydata.sh \
+ --extract-hook='printf "'"$elfheader"'" | cmp --bytes=8 - "$1"/usr/bin/dpkg' \
+ --hook-dir=./hooks/eatmydata \
+ --customize-hook='printf "'"$elfheader"'" | cmp --bytes=8 - "$1"/usr/bin/dpkg' \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+ tar -C /tmp/debian-chroot --one-file-system -c . \
+ | tar -t \
+ | sort \
+ | grep -v '^\./var/lib/dpkg/diversions\(-old\)\?$' \
+ | diff -u tar1.txt -
+rm /tmp/checkeatmydata.sh
+rm -r /tmp/debian-chroot
diff --git a/tests/empty-sources.list b/tests/empty-sources.list
new file mode 100644
index 0000000..bf384f3
--- /dev/null
+++ b/tests/empty-sources.list
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+printf '' | {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --setup-hook='echo "deb {{ MIRROR }} {{ DIST }} main" > "$1"/etc/apt/sources.list' \
+ {{ DIST }} /tmp/debian-chroot.tar -
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/error-if-stdout-is-tty b/tests/error-if-stdout-is-tty
new file mode 100644
index 0000000..b4f6923
--- /dev/null
+++ b/tests/error-if-stdout-is-tty
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+set -eu
+
+export LC_ALL=C.UTF-8
+
+ret=0
+script -qfec "{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} - {{ MIRROR }}" /dev/null || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/essential-hook b/tests/essential-hook
new file mode 100644
index 0000000..dc2b01f
--- /dev/null
+++ b/tests/essential-hook
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rm -f /tmp/essential.sh" EXIT INT TERM
+cat << 'SCRIPT' > /tmp/essential.sh
+#!/bin/sh
+echo tzdata tzdata/Zones/Europe select Berlin | chroot "$1" debconf-set-selections
+SCRIPT
+chmod +x /tmp/essential.sh
+{{ CMD }} --mode=root --variant=apt --include=tzdata --essential-hook='echo tzdata tzdata/Areas select Europe | chroot "$1" debconf-set-selections' --essential-hook=/tmp/essential.sh {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+[ "$(readlink /tmp/debian-chroot/etc/localtime)" = "/usr/share/zoneinfo/Europe/Berlin" ]
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort \
+ | grep -v '^./etc/localtime' \
+ | grep -v '^./etc/timezone' \
+ | grep -v '^./usr/sbin/tzconfig' \
+ | grep -v '^./usr/share/doc/tzdata' \
+ | grep -v '^./usr/share/lintian/overrides/tzdata' \
+ | grep -v '^./usr/share/zoneinfo' \
+ | grep -v '^./var/lib/dpkg/info/tzdata.' \
+ | grep -v '^./var/lib/apt/extended_states$' \
+ | diff -u tar1.txt -
diff --git a/tests/existing-directory-with-lost-found b/tests/existing-directory-with-lost-found
new file mode 100644
index 0000000..9757d06
--- /dev/null
+++ b/tests/existing-directory-with-lost-found
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+mkdir /tmp/debian-chroot
+mkdir /tmp/debian-chroot/lost+found
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+rmdir /tmp/debian-chroot/lost+found
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/existing-empty-directory b/tests/existing-empty-directory
new file mode 100644
index 0000000..23efeea
--- /dev/null
+++ b/tests/existing-empty-directory
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+mkdir /tmp/debian-chroot
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/fail-installing-to-existing-file b/tests/fail-installing-to-existing-file
new file mode 100644
index 0000000..916c98c
--- /dev/null
+++ b/tests/fail-installing-to-existing-file
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -f /tmp/exists" EXIT INT TERM
+
+touch /tmp/exists
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/exists {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-installing-to-non-empty-lost-found b/tests/fail-installing-to-non-empty-lost-found
new file mode 100644
index 0000000..4130bf5
--- /dev/null
+++ b/tests/fail-installing-to-non-empty-lost-found
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm /tmp/debian-chroot/lost+found/exists; rmdir /tmp/debian-chroot/lost+found /tmp/debian-chroot" EXIT INT TERM
+mkdir /tmp/debian-chroot
+mkdir /tmp/debian-chroot/lost+found
+touch /tmp/debian-chroot/lost+found/exists
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-installing-to-non-empty-target-directory b/tests/fail-installing-to-non-empty-target-directory
new file mode 100644
index 0000000..2606b7f
--- /dev/null
+++ b/tests/fail-installing-to-non-empty-target-directory
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rmdir /tmp/debian-chroot/lost+found; rm /tmp/debian-chroot/exists; rmdir /tmp/debian-chroot" EXIT INT TERM
+mkdir /tmp/debian-chroot
+mkdir /tmp/debian-chroot/lost+found
+touch /tmp/debian-chroot/exists
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-installing-to-root b/tests/fail-installing-to-root
new file mode 100644
index 0000000..ded6b5d
--- /dev/null
+++ b/tests/fail-installing-to-root
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} / {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-with-missing-lz4 b/tests/fail-with-missing-lz4
new file mode 100644
index 0000000..71c6d60
--- /dev/null
+++ b/tests/fail-with-missing-lz4
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar.lz4 {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-with-path-with-quotes b/tests/fail-with-path-with-quotes
new file mode 100644
index 0000000..5483ff1
--- /dev/null
+++ b/tests/fail-with-path-with-quotes
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap 'rm -rf /tmp/quoted\"path' EXIT INT TERM
+
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/quoted\"path {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/fail-without-etc-subuid b/tests/fail-without-etc-subuid
new file mode 100644
index 0000000..7a0b146
--- /dev/null
+++ b/tests/fail-without-etc-subuid
@@ -0,0 +1,16 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+useradd --home-dir /home/user --create-home user
+rm /etc/subuid
+ret=0
+runuser -u user -- {{ CMD }} --mode=unshare --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
+[ ! -e /tmp/debian-chroot ]
diff --git a/tests/fail-without-username-in-etc-subuid b/tests/fail-without-username-in-etc-subuid
new file mode 100644
index 0000000..319eadf
--- /dev/null
+++ b/tests/fail-without-username-in-etc-subuid
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+useradd --home-dir /home/user --create-home user
+awk -F: '$1!="user"' /etc/subuid > /etc/subuid.tmp
+mv /etc/subuid.tmp /etc/subuid
+ret=0
+runuser -u user -- {{ CMD }} --mode=unshare --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }} || ret=$?
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
+[ ! -e /tmp/debian-chroot ]
diff --git a/tests/failing-customize-hook b/tests/failing-customize-hook
new file mode 100644
index 0000000..8ecc065
--- /dev/null
+++ b/tests/failing-customize-hook
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+ret=0
+{{ CMD }} --mode=root --variant=apt --customize-hook='chroot "$1" sh -c "exit 1"' {{ DIST }} /tmp/debian-chroot {{ MIRROR }} || ret=$?
+rm -r /tmp/debian-chroot
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/file-mirror b/tests/file-mirror
new file mode 100644
index 0000000..b0388bb
--- /dev/null
+++ b/tests/file-mirror
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test requires the cache directory to be mounted on /mnt and should only be run inside a container" >&2
+ exit 1
+fi
+{{ CMD }} --mode={{ MODE }} --variant=apt \
+ --setup-hook='mkdir -p "$1"/mnt/cache/debian; mount -o ro,bind /mnt/cache/debian "$1"/mnt/cache/debian' \
+ --customize-hook='umount "$1"/mnt/cache/debian; rmdir "$1"/mnt/cache/debian "$1"/mnt/cache' \
+ {{ DIST }} /tmp/debian-chroot.tar "deb file:///mnt/cache/debian {{ DIST }} main"
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/file-mirror-automount-hook b/tests/file-mirror-automount-hook
new file mode 100644
index 0000000..11ab330
--- /dev/null
+++ b/tests/file-mirror-automount-hook
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test requires the cache directory to be mounted on /mnt and should only be run inside a container" >&2
+ exit 1
+fi
+if [ "$(id -u)" -eq 0 ] && ! id -u user > /dev/null 2>&1; then
+ useradd --home-dir /home/user --create-home user
+fi
+prefix=
+[ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && prefix="runuser -u user --"
+[ "{{ MODE }}" = "fakechroot" ] && prefix="$prefix fakechroot fakeroot"
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --hook-dir=./hooks/file-mirror-automount \
+ --customize-hook='[ ! -e "$1"/mnt/cache/debian/ ] || rmdir "$1"/mnt/cache/debian/' \
+ --customize-hook='rmdir "$1"/mnt/cache' \
+ {{ DIST }} /tmp/debian-chroot.tar "deb file:///mnt/cache/debian {{ DIST }} main"
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/help b/tests/help
new file mode 100644
index 0000000..535eeba
--- /dev/null
+++ b/tests/help
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+# we redirect to /dev/null instead of using --quiet to not cause a broken pipe
+# when grep exits before mmdebstrap was able to write all its output
+{{ CMD }} --help | grep --fixed-strings 'mmdebstrap [OPTION...] [SUITE [TARGET [MIRROR...]]]' >/dev/null
diff --git a/tests/hook-directory b/tests/hook-directory
new file mode 100644
index 0000000..c9b22f9
--- /dev/null
+++ b/tests/hook-directory
@@ -0,0 +1,49 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+for h in hookA hookB; do
+ mkdir /tmp/$h
+ for s in setup extract essential customize; do
+ cat << SCRIPT > /tmp/$h/${s}00.sh
+#!/bin/sh
+echo $h/${s}00 >> "\$1/$s"
+SCRIPT
+ chmod +x /tmp/$h/${s}00.sh
+ cat << SCRIPT > /tmp/$h/${s}01.sh
+echo $h/${s}01 >> "\$1/$s"
+SCRIPT
+ chmod +x /tmp/$h/${s}01.sh
+ done
+done
+{{ CMD }} --mode=root --variant=apt \
+ --setup-hook='echo cliA/setup >> "$1"/setup' \
+ --extract-hook='echo cliA/extract >> "$1"/extract' \
+ --essential-hook='echo cliA/essential >> "$1"/essential' \
+ --customize-hook='echo cliA/customize >> "$1"/customize' \
+ --hook-dir=/tmp/hookA \
+ --setup-hook='echo cliB/setup >> "$1"/setup' \
+ --extract-hook='echo cliB/extract >> "$1"/extract' \
+ --essential-hook='echo cliB/essential >> "$1"/essential' \
+ --customize-hook='echo cliB/customize >> "$1"/customize' \
+ --hook-dir=/tmp/hookB \
+ --setup-hook='echo cliC/setup >> "$1"/setup' \
+ --extract-hook='echo cliC/extract >> "$1"/extract' \
+ --essential-hook='echo cliC/essential >> "$1"/essential' \
+ --customize-hook='echo cliC/customize >> "$1"/customize' \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf "cliA/setup\nhookA/setup00\nhookA/setup01\ncliB/setup\nhookB/setup00\nhookB/setup01\ncliC/setup\n" | diff -u - /tmp/debian-chroot/setup
+printf "cliA/extract\nhookA/extract00\nhookA/extract01\ncliB/extract\nhookB/extract00\nhookB/extract01\ncliC/extract\n" | diff -u - /tmp/debian-chroot/extract
+printf "cliA/essential\nhookA/essential00\nhookA/essential01\ncliB/essential\nhookB/essential00\nhookB/essential01\ncliC/essential\n" | diff -u - /tmp/debian-chroot/essential
+printf "cliA/customize\nhookA/customize00\nhookA/customize01\ncliB/customize\nhookB/customize00\nhookB/customize01\ncliC/customize\n" | diff -u - /tmp/debian-chroot/customize
+for s in setup extract essential customize; do
+ rm /tmp/debian-chroot/$s
+done
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+for h in hookA hookB; do
+ for s in setup extract essential customize; do
+ rm /tmp/$h/${s}00.sh
+ rm /tmp/$h/${s}01.sh
+ done
+ rmdir /tmp/$h
+done
+rm -r /tmp/debian-chroot
diff --git a/tests/i386-which-can-be-executed-without-qemu b/tests/i386-which-can-be-executed-without-qemu
new file mode 100644
index 0000000..91c53df
--- /dev/null
+++ b/tests/i386-which-can-be-executed-without-qemu
@@ -0,0 +1,41 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+# remove qemu just to be sure
+apt-get remove --yes qemu-user-static binfmt-support qemu-user
+{{ CMD }} --mode={{ MODE }} --variant=apt --architectures=i386 {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+# we ignore differences between architectures by ignoring some files
+# and renaming others
+{ tar -tf /tmp/debian-chroot.tar \
+ | grep -v '^\./usr/bin/i386$' \
+ | grep -v '^\./usr/lib/ld-linux\.so\.2$' \
+ | grep -v '^\./usr/lib/i386-linux-gnu/ld-linux\.so\.2$' \
+ | grep -v '^\./usr/lib/gcc/i686-linux-gnu/$' \
+ | grep -v '^\./usr/lib/gcc/i686-linux-gnu/[0-9]\+/$' \
+ | grep -v '^\./usr/share/man/man8/i386\.8\.gz$' \
+ | grep -v '^\./usr/share/doc/[^/]\+/changelog\(\.Debian\)\?\.i386\.gz$' \
+ | sed 's/i386-linux-gnu/x86_64-linux-gnu/' \
+ | sed 's/i386/amd64/' \
+ | sed 's/\/stubs-32.ph$/\/stubs-64.ph/';
+} | sort > tar2.txt
+{ < tar1.txt \
+ grep -v '^\./usr/bin/i386$' \
+ | grep -v '^\./usr/bin/x86_64$' \
+ | grep -v '^\./usr/lib32/$' \
+ | grep -v '^\./lib32$' \
+ | grep -v '^\./lib64$' \
+ | grep -v '^\./usr/lib64/$' \
+ | grep -v '^\./usr/lib64/ld-linux-x86-64\.so\.2$' \
+ | grep -v '^\./usr/lib/gcc/x86_64-linux-gnu/$' \
+ | grep -v '^\./usr/lib/gcc/x86_64-linux-gnu/[0-9]\+/$' \
+ | grep -v '^\./usr/lib/x86_64-linux-gnu/ld-linux-x86-64\.so\.2$' \
+ | grep -v '^\./usr/lib/x86_64-linux-gnu/libmvec\.so\.1$' \
+ | grep -v '^\./usr/share/doc/[^/]\+/changelog\(\.Debian\)\?\.amd64\.gz$' \
+ | grep -v '^\./usr/share/man/man8/i386\.8\.gz$' \
+ | grep -v '^\./usr/share/man/man8/x86_64\.8\.gz$';
+} | sort | diff -u - tar2.txt >&2
+rm /tmp/debian-chroot.tar
diff --git a/tests/include b/tests/include
new file mode 100644
index 0000000..e284b7d
--- /dev/null
+++ b/tests/include
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+{{ CMD }} --mode=root --variant=apt --include=doc-debian {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+rm /tmp/debian-chroot/usr/share/doc-base/doc-debian.debian-*
+rm -r /tmp/debian-chroot/usr/share/doc/debian
+rm -r /tmp/debian-chroot/usr/share/doc/doc-debian
+rm /tmp/debian-chroot/var/lib/apt/extended_states
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.list
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.md5sums
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/include-deb-file b/tests/include-deb-file
new file mode 100644
index 0000000..ad31de2
--- /dev/null
+++ b/tests/include-deb-file
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -rf /tmp/dummypkg.deb /tmp/dummypkg" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# instead of obtaining a .deb from our cache, we create a new package because
+# otherwise apt might decide to download the package with the same name and
+# version from the cache instead of using the local .deb
+mkdir -p /tmp/dummypkg/DEBIAN
+cat << END > "/tmp/dummypkg/DEBIAN/control"
+Package: dummypkg
+Priority: optional
+Section: oldlibs
+Maintainer: Johannes Schauer Marin Rodrigues <josch@debian.org>
+Architecture: all
+Multi-Arch: foreign
+Source: dummypkg
+Version: 1
+Description: dummypkg
+END
+dpkg-deb --build "/tmp/dummypkg" "/tmp/dummypkg.deb"
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include="/tmp/dummypkg.deb" \
+ --hook-dir=./hooks/file-mirror-automount \
+ --customize-hook='chroot "$1" dpkg-query -W -f="\${Status}\n" dummypkg | grep "^install ok installed$"' \
+ {{ DIST }} /dev/null {{ MIRROR }}
diff --git a/tests/include-foreign-libmagic-mgc b/tests/include-foreign-libmagic-mgc
new file mode 100644
index 0000000..127a84e
--- /dev/null
+++ b/tests/include-foreign-libmagic-mgc
@@ -0,0 +1,47 @@
+#!/bin/sh
+#
+# to test foreign architecture package installation we choose a package which
+# - is not part of the native installation set
+# - does not have any dependencies
+# - installs only few files
+# - doesn't change its name regularly (like gcc-*-base)
+
+case "$(dpkg --print-architecture)" in
+ arm64)
+ native_arch=arm64
+ foreign_arch=amd64
+ ;;
+ amd64)
+ native_arch=amd64
+ foreign_arch=arm64
+ ;;
+ *)
+ echo "unsupported native architecture" >&2
+ exit 1
+ ;;
+esac
+
+set -eu
+export LC_ALL=C.UTF-8
+{{ CMD }} --mode=root --variant=apt \
+ --architectures="$native_arch,$foreign_arch" \
+ --include="libmagic-mgc:$foreign_arch" \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+{ echo "$native_arch"; echo "$foreign_arch"; } | cmp /tmp/debian-chroot/var/lib/dpkg/arch -
+rm /tmp/debian-chroot/usr/lib/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/README.Debian
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/"changelog.Debian.$foreign_arch.gz"
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.Debian.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/copyright
+rm /tmp/debian-chroot/usr/share/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/misc/magic.mgc
+rm /tmp/debian-chroot/var/lib/apt/extended_states
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.list
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.md5sums
+rmdir /tmp/debian-chroot/usr/share/doc/libmagic-mgc/
+rmdir /tmp/debian-chroot/usr/share/file/magic/
+rmdir /tmp/debian-chroot/usr/share/file/
+rmdir /tmp/debian-chroot/usr/lib/file/
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/include-foreign-libmagic-mgc-with-multiple-arch-options b/tests/include-foreign-libmagic-mgc-with-multiple-arch-options
new file mode 100644
index 0000000..3108134
--- /dev/null
+++ b/tests/include-foreign-libmagic-mgc-with-multiple-arch-options
@@ -0,0 +1,48 @@
+#!/bin/sh
+#
+# to test foreign architecture package installation we choose a package which
+# - is not part of the native installation set
+# - does not have any dependencies
+# - installs only few files
+# - doesn't change its name regularly (like gcc-*-base)
+
+case "$(dpkg --print-architecture)" in
+ arm64)
+ native_arch=arm64
+ foreign_arch=amd64
+ ;;
+ amd64)
+ native_arch=amd64
+ foreign_arch=arm64
+ ;;
+ *)
+ echo "unsupported native architecture" >&2
+ exit 1
+ ;;
+esac
+
+set -eu
+export LC_ALL=C.UTF-8
+{{ CMD }} --mode=root --variant=apt \
+ --architectures="$native_arch" \
+ --architectures="$foreign_arch" \
+ --include="libmagic-mgc:$foreign_arch" \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+{ echo "$native_arch"; echo "$foreign_arch"; } | cmp /tmp/debian-chroot/var/lib/dpkg/arch -
+rm /tmp/debian-chroot/usr/lib/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/README.Debian
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/"changelog.Debian.$foreign_arch.gz"
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.Debian.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/copyright
+rm /tmp/debian-chroot/usr/share/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/misc/magic.mgc
+rm /tmp/debian-chroot/var/lib/apt/extended_states
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.list
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.md5sums
+rmdir /tmp/debian-chroot/usr/share/doc/libmagic-mgc/
+rmdir /tmp/debian-chroot/usr/share/file/magic/
+rmdir /tmp/debian-chroot/usr/share/file/
+rmdir /tmp/debian-chroot/usr/lib/file/
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/include-with-multiple-apt-sources b/tests/include-with-multiple-apt-sources
new file mode 100644
index 0000000..1d335d4
--- /dev/null
+++ b/tests/include-with-multiple-apt-sources
@@ -0,0 +1,10 @@
+#!/bin/sh
+#
+# This checks for https://bugs.debian.org/976166
+# Since $DEFAULT_DIST varies, we hardcode stable and unstable.
+
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+{{ CMD }} --mode=root --variant=minbase --include=doc-debian unstable /tmp/debian-chroot "deb {{ MIRROR }} unstable main" "deb {{ MIRROR }} stable main"
+chroot /tmp/debian-chroot dpkg-query --show doc-debian
diff --git a/tests/install-busybox-based-sub-essential-system b/tests/install-busybox-based-sub-essential-system
new file mode 100644
index 0000000..7854f0e
--- /dev/null
+++ b/tests/install-busybox-based-sub-essential-system
@@ -0,0 +1,41 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+
+pkgs=base-files,base-passwd,busybox,debianutils,dpkg,libc-bin,mawk,tar
+# busybox --install -s will install symbolic links into the rootfs, leaving
+# existing files untouched. It has to run after extraction (otherwise there is
+# no busybox binary) and before first configuration
+{{ CMD }} --mode=root --variant=custom \
+ --include=$pkgs \
+ --setup-hook='mkdir -p "$1/bin"' \
+ --setup-hook='echo root:x:0:0:root:/root:/bin/sh > "$1/etc/passwd"' \
+ --setup-hook='printf "root:x:0:\nmail:x:8:\nutmp:x:43:\n" > "$1/etc/group"' \
+ --extract-hook='chroot "$1" busybox --install -s' \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+echo "$pkgs" | tr ',' '\n' > /tmp/expected
+chroot /tmp/debian-chroot dpkg-query -f '${binary:Package}\n' -W \
+ | comm -12 - /tmp/expected \
+ | diff -u - /tmp/expected
+rm /tmp/expected
+for cmd in echo cat sed grep; do
+ test -L /tmp/debian-chroot/bin/$cmd
+ test "$(readlink /tmp/debian-chroot/bin/$cmd)" = "/usr/bin/busybox"
+done
+for cmd in sort tee; do
+ test -L /tmp/debian-chroot/usr/bin/$cmd
+ test "$(readlink /tmp/debian-chroot/usr/bin/$cmd)" = "/usr/bin/busybox"
+done
+
+# if /bin or /sbin are not symlinks, add /bin and /sbin to PATH
+if [ ! -L /tmp/debian-chroot/bin ] || [ ! -L /tmp/debian-chroot/sbin ]; then
+ export PATH="$PATH:/sbin:/bin"
+fi
+chroot /tmp/debian-chroot echo foobar \
+ | chroot /tmp/debian-chroot cat \
+ | chroot /tmp/debian-chroot sort \
+ | chroot /tmp/debian-chroot tee /dev/null \
+ | chroot /tmp/debian-chroot sed 's/foobar/blubber/' \
+ | chroot /tmp/debian-chroot grep blubber >/dev/null
diff --git a/tests/install-doc-debian b/tests/install-doc-debian
new file mode 100644
index 0000000..27d7f3e
--- /dev/null
+++ b/tests/install-doc-debian
@@ -0,0 +1,56 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+[ {{ VARIANT }} = "custom" ]
+[ {{ MODE }} = "chrootless" ]
+
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --include=doc-debian {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+tar -C /tmp/debian-chroot --owner=0 --group=0 --numeric-owner --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/debian-chroot.tar .
+tar tvf /tmp/debian-chroot.tar > doc-debian.tar.list
+rm /tmp/debian-chroot.tar
+# delete contents of doc-debian
+rm /tmp/debian-chroot/usr/share/doc-base/doc-debian.debian-*
+rm -r /tmp/debian-chroot/usr/share/doc/debian
+rm -r /tmp/debian-chroot/usr/share/doc/doc-debian
+# delete real files
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+rm /tmp/debian-chroot/var/cache/apt/archives/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock-frontend
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+## delete merged usr symlinks
+#rm /tmp/debian-chroot/libx32
+#rm /tmp/debian-chroot/lib64
+#rm /tmp/debian-chroot/lib32
+#rm /tmp/debian-chroot/sbin
+#rm /tmp/debian-chroot/bin
+#rm /tmp/debian-chroot/lib
+# in chrootless mode, there is more to remove
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Lock
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Unincorp
+rm /tmp/debian-chroot/var/lib/dpkg/status-old
+rm /tmp/debian-chroot/var/lib/dpkg/info/format
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.md5sums
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.list
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/install-doc-debian-and-output-tarball b/tests/install-doc-debian-and-output-tarball
new file mode 100644
index 0000000..118ae89
--- /dev/null
+++ b/tests/install-doc-debian-and-output-tarball
@@ -0,0 +1,23 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+[ {{ VARIANT }} = "custom" ]
+[ {{ MODE }} = "chrootless" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --include=doc-debian {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar tvf /tmp/debian-chroot.tar | grep -v ' ./dev' | diff -u doc-debian.tar.list -
+rm /tmp/debian-chroot.tar
diff --git a/tests/install-doc-debian-and-test-hooks b/tests/install-doc-debian-and-test-hooks
new file mode 100644
index 0000000..e69066c
--- /dev/null
+++ b/tests/install-doc-debian-and-test-hooks
@@ -0,0 +1,59 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+[ {{ VARIANT }} = "custom" ]
+[ {{ MODE }} = "chrootless" ]
+
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --skip=cleanup/tmp --variant={{ VARIANT }} --include=doc-debian --setup-hook='touch "$1/tmp/setup"' --customize-hook='touch "$1/tmp/customize"' {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+rm /tmp/debian-chroot/tmp/setup
+rm /tmp/debian-chroot/tmp/customize
+tar -C /tmp/debian-chroot --owner=0 --group=0 --numeric-owner --sort=name --clamp-mtime --mtime="$(date --utc --date=@{{ SOURCE_DATE_EPOCH }} --iso-8601=seconds)" -cf /tmp/debian-chroot.tar .
+tar tvf /tmp/debian-chroot.tar | grep -v ' ./dev' | diff -u doc-debian.tar.list -
+rm /tmp/debian-chroot.tar
+# delete contents of doc-debian
+rm /tmp/debian-chroot/usr/share/doc-base/doc-debian.debian-*
+rm -r /tmp/debian-chroot/usr/share/doc/debian
+rm -r /tmp/debian-chroot/usr/share/doc/doc-debian
+# delete real files
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+rm /tmp/debian-chroot/var/cache/apt/archives/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock-frontend
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+## delete merged usr symlinks
+#rm /tmp/debian-chroot/libx32
+#rm /tmp/debian-chroot/lib64
+#rm /tmp/debian-chroot/lib32
+#rm /tmp/debian-chroot/sbin
+#rm /tmp/debian-chroot/bin
+#rm /tmp/debian-chroot/lib
+# in chrootless mode, there is more to remove
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Lock
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Unincorp
+rm /tmp/debian-chroot/var/lib/dpkg/status-old
+rm /tmp/debian-chroot/var/lib/dpkg/info/format
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.md5sums
+rm /tmp/debian-chroot/var/lib/dpkg/info/doc-debian.list
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/install-libmagic-mgc-on-foreign b/tests/install-libmagic-mgc-on-foreign
new file mode 100644
index 0000000..918224b
--- /dev/null
+++ b/tests/install-libmagic-mgc-on-foreign
@@ -0,0 +1,69 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+[ {{ VARIANT }} = "custom" ]
+[ {{ MODE }} = "chrootless" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+case "$(dpkg --print-architecture)" in
+ arm64)
+ foreign_arch=amd64
+ ;;
+ amd64)
+ foreign_arch=arm64
+ ;;
+ *)
+ echo "unsupported native architecture" >&2
+ exit 1
+ ;;
+esac
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --architectures="$foreign_arch" --include=libmagic-mgc {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+# delete contents of libmagic-mgc
+rm /tmp/debian-chroot/usr/lib/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/README.Debian
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.Debian.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/changelog.gz
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/copyright
+rm /tmp/debian-chroot/usr/share/doc/libmagic-mgc/"changelog.Debian.$foreign_arch.gz"
+rm /tmp/debian-chroot/usr/share/file/magic.mgc
+rm /tmp/debian-chroot/usr/share/misc/magic.mgc
+# delete real files
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/cache/apt/archives/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock
+rm /tmp/debian-chroot/var/lib/dpkg/lock-frontend
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+## delete merged usr symlinks
+#rm /tmp/debian-chroot/libx32
+#rm /tmp/debian-chroot/lib64
+#rm /tmp/debian-chroot/lib32
+#rm /tmp/debian-chroot/sbin
+#rm /tmp/debian-chroot/bin
+#rm /tmp/debian-chroot/lib
+# in chrootless mode, there is more to remove
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Lock
+rm /tmp/debian-chroot/var/lib/dpkg/triggers/Unincorp
+rm /tmp/debian-chroot/var/lib/dpkg/status-old
+rm /tmp/debian-chroot/var/lib/dpkg/info/format
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.md5sums
+rm /tmp/debian-chroot/var/lib/dpkg/info/libmagic-mgc.list
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/invalid-mirror b/tests/invalid-mirror
new file mode 100644
index 0000000..97cde05
--- /dev/null
+++ b/tests/invalid-mirror
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}/invalid || ret=$?
+rm /tmp/debian-chroot.tar
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/jessie-or-older b/tests/jessie-or-older
new file mode 100644
index 0000000..a3a2ace
--- /dev/null
+++ b/tests/jessie-or-older
@@ -0,0 +1,42 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+trap "rm -f /tmp/debian-chroot-{{ MODE }}.tar /tmp/debian-chroot-root-normal.tar" EXIT INT TERM
+
+[ "$(id -u)" -eq 0 ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+MMTARFILTER=
+[ -x /usr/bin/mmtarfilter ] && MMTARFILTER=/usr/bin/mmtarfilter
+[ -x ./tarfilter ] && MMTARFILTER=./tarfilter
+
+filter() {
+ "$MMTARFILTER" \
+ --path-exclude=/usr/bin/uncompress \
+ --path-exclude=/var/cache/debconf/config.dat-old \
+ --path-exclude=/var/cache/debconf/templates.dat-old \
+ --path-exclude=/var/lib/dpkg/available \
+ --path-exclude=/var/lib/dpkg/diversions \
+ --path-exclude=/var/lib/dpkg/cmethopt \
+ --path-exclude=/var/lib/dpkg/status-old \
+ --path-exclude=/var/lib/shells.state
+}
+
+# base for comparison without jessie-or-older hook
+{{ CMD }} --mode=root --variant={{ VARIANT }} {{ DIST }} - {{ MIRROR }} > /tmp/debian-chroot-root-normal.tar
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --hook-dir=./hooks/jessie-or-older {{ DIST }} - {{ MIRROR }} | filter > /tmp/debian-chroot-{{ MODE }}.tar
+filter < /tmp/debian-chroot-root-normal.tar | cmp - /tmp/debian-chroot-{{ MODE }}.tar
diff --git a/tests/keyring b/tests/keyring
new file mode 100644
index 0000000..7308f0d
--- /dev/null
+++ b/tests/keyring
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+for f in /etc/apt/trusted.gpg.d/*.gpg /etc/apt/trusted.gpg.d/*.asc; do
+ [ -e "$f" ] || continue
+ rm "$f"
+done
+rmdir /etc/apt/trusted.gpg.d
+mkdir /etc/apt/trusted.gpg.d
+{{ CMD }} --mode=root --variant=apt --keyring=/usr/share/keyrings/debian-archive-keyring.gpg --keyring=/usr/share/keyrings/ {{ DIST }} /tmp/debian-chroot "deb {{ MIRROR }} {{ DIST }} main"
+# make sure that no [signedby=...] managed to make it into the sources.list
+echo "deb {{ MIRROR }} {{ DIST }} main" | cmp /tmp/debian-chroot/etc/apt/sources.list -
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/keyring-overwrites b/tests/keyring-overwrites
new file mode 100644
index 0000000..f070654
--- /dev/null
+++ b/tests/keyring-overwrites
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot; rmdir /tmp/emptydir; rm -f /tmp/emptyfile" EXIT INT TERM
+mkdir -p /tmp/emptydir
+touch /tmp/emptyfile
+# this overwrites the apt keyring options and should fail
+ret=0
+{{ CMD }} --mode=root --variant=apt --keyring=/tmp/emptydir --keyring=/tmp/emptyfile {{ DIST }} /tmp/debian-chroot "deb {{ MIRROR }} {{ DIST }} main" || ret=$?
+# make sure that no [signedby=...] managed to make it into the sources.list
+echo "deb {{ MIRROR }} {{ DIST }} main" | cmp /tmp/debian-chroot/etc/apt/sources.list -
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/logfile b/tests/logfile
new file mode 100644
index 0000000..5e2dbeb
--- /dev/null
+++ b/tests/logfile
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -rf /tmp/debian-chroot /tmp/log /tmp/trimmed" EXIT INT TERM
+
+# we check the full log to also prevent debug printfs to accidentally make it into a commit
+{{ CMD }} --mode=root --variant=apt --logfile=/tmp/log {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+# omit the last line which should contain the runtime
+head --lines=-1 /tmp/log > /tmp/trimmed
+cat << LOG | diff -u - /tmp/trimmed
+I: chroot architecture {{ HOSTARCH }} is equal to the host's architecture
+I: finding correct signed-by value...
+I: automatically chosen format: directory
+I: running apt-get update...
+I: downloading packages with apt...
+I: extracting archives...
+I: installing essential packages...
+I: cleaning package lists and apt cache...
+LOG
+tail --lines=1 /tmp/log | grep '^I: success in .* seconds$'
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/man b/tests/man
new file mode 100644
index 0000000..b5c38b9
--- /dev/null
+++ b/tests/man
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+# we redirect to /dev/null instead of using --quiet to not cause a broken pipe
+# when grep exits before mmdebstrap was able to write all its output
+{{ CMD }} --man | grep --fixed-strings 'mmdebstrap [OPTION...] [*SUITE* [*TARGET* [*MIRROR*...]]]' >/dev/null
diff --git a/tests/merged-fakechroot-inside-unmerged-chroot b/tests/merged-fakechroot-inside-unmerged-chroot
new file mode 100644
index 0000000..c05ada1
--- /dev/null
+++ b/tests/merged-fakechroot-inside-unmerged-chroot
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# make sure that the $FAKECHROOT_CMD_SUBST environment variable is set up
+# such that one can create a merged-/usr chroot from an unmerged-/usr system
+
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+trap "rm -f /tmp/chroot-fakechroot.tar /tmp/chroot-root.tar" EXIT INT TERM
+[ "$(whoami)" = "root" ]
+{{ CMD }} --mode=root --variant=apt --hook-dir=./hooks/merged-usr {{ DIST }} /tmp/chroot-root.tar {{ MIRROR }}
+cat << 'SCRIPT' > script.sh
+#!/bin/sh
+set -exu
+rootfs="$1"
+mkdir -p "$rootfs/mnt/hooks"
+[ -e /usr/libexec/mmdebstrap/ldconfig.fakechroot ] && cp -a /usr/libexec/mmdebstrap/ldconfig.fakechroot "$rootfs/mnt"
+[ -e ./ldconfig.fakechroot ] && cp -a ./ldconfig.fakechroot "$rootfs/mnt"
+[ -e /usr/share/mmdebstrap/hooks/merged-usr ] && cp -a /usr/share/mmdebstrap/hooks/merged-usr "$rootfs/mnt/hooks"
+[ -e ./hooks/merged-usr ] && cp -a ./hooks/merged-usr "$rootfs/mnt/hooks"
+[ -e /usr/bin/mmdebstrap ] && cp -aT /usr/bin/mmdebstrap "$rootfs/usr/bin/mmdebstrap"
+[ -e ./mmdebstrap ] && cp -aT ./mmdebstrap "$rootfs/mnt/mmdebstrap"
+chroot "$rootfs" env --chdir=/mnt \
+ runuser -u user -- \
+ {{ CMD }} --mode=fakechroot --variant=apt \
+ --hook-dir=./hooks/merged-usr \
+ --customize-hook='chroot "$1" echo "$FAKECHROOT_CMD_SUBST" | tr ":" "\n" | sort' \
+ --customize-hook='chroot "$1" sh -c "exec test \"\$(readlink /bin)\" = usr/bin"' \
+ --customize-hook='chroot "$1" sh -c "exec test \"\$(realpath -e /bin/ldd)\" = /usr/bin/ldd"' \
+ --customize-hook='chroot "$1" echo ":$FAKECHROOT_CMD_SUBST" | grep --quiet :/usr/bin/ldd=' \
+ --customize-hook='chroot "$1" echo ":$FAKECHROOT_CMD_SUBST" | grep --quiet :/bin/ldd=' \
+ --customize-hook='chroot "$1" env PATH=/bin ldd /bin/true 2>&1 | grep --quiet "fakeldd: objdump: command not found: install binutils package"' \
+ --customize-hook='chroot "$1" sh -c "exec test \"\$(readlink /sbin)\" = usr/sbin"' \
+ --customize-hook='chroot "$1" sh -c "exec test \"\$(realpath -e /sbin/ldconfig)\" = /usr/sbin/ldconfig"' \
+ --customize-hook='chroot "$1" echo ":$FAKECHROOT_CMD_SUBST" | grep --quiet :/usr/sbin/ldconfig=' \
+ --customize-hook='chroot "$1" echo ":$FAKECHROOT_CMD_SUBST" | grep --quiet :/sbin/ldconfig=' \
+ --customize-hook='chroot "$1" env PATH=/sbin ldconfig 2>&1 | grep --quiet "/usr/bin/env: ‘python3’: No such file or directory"' \
+ {{ DIST }} /tmp/chroot-fakechroot.tar {{ MIRROR }}
+SCRIPT
+chmod +x script.sh
+{{ CMD }} --mode=root --variant=apt --include=perl,python3,passwd,fakeroot,fakechroot \
+ --hook-dir=./hooks/no-merged-usr \
+ --customize-hook='chroot "$1" useradd --home-dir /home/user --create-home user' \
+ --customize-hook='chroot "$1" sh -c "exec test \"\$(realpath -e /usr/bin/ldd)\" = /usr/bin/ldd"' \
+ --customize-hook='chroot "$1" sh -c "exec test ! -e /usr/sbin/ldconfig"' \
+ --customize-hook=./script.sh \
+ --customize-hook="copy-out /tmp/chroot-fakechroot.tar /tmp" \
+ {{ DIST }} /dev/null {{ MIRROR }}
+cmp /tmp/chroot-fakechroot.tar /tmp/chroot-root.tar || diffoscope /tmp/chroot-fakechroot.tar /tmp/chroot-root.tar
diff --git a/tests/mirror-is-deb b/tests/mirror-is-deb
new file mode 100644
index 0000000..c751aad
--- /dev/null
+++ b/tests/mirror-is-deb
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar "deb {{ MIRROR }} {{ DIST }} main"
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/mirror-is-real-file b/tests/mirror-is-real-file
new file mode 100644
index 0000000..76f9efc
--- /dev/null
+++ b/tests/mirror-is-real-file
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar /tmp/sources.list" EXIT INT TERM
+echo "deb {{ MIRROR }} {{ DIST }} main" > /tmp/sources.list
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar /tmp/sources.list
+tar -tf /tmp/debian-chroot.tar \
+ | sed 's#^./etc/apt/sources.list.d/0000sources.list$#./etc/apt/sources.list#' \
+ | sort | diff -u tar1.txt -
diff --git a/tests/mirror-is-stdin b/tests/mirror-is-stdin
new file mode 100644
index 0000000..ba0e376
--- /dev/null
+++ b/tests/mirror-is-stdin
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+echo "deb {{ MIRROR }} {{ DIST }} main" | {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar -
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/missing-dev-sys-proc-inside-the-chroot b/tests/missing-dev-sys-proc-inside-the-chroot
new file mode 100644
index 0000000..d127911
--- /dev/null
+++ b/tests/missing-dev-sys-proc-inside-the-chroot
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+[ {{ MODE }} = "unshare" ]
+[ {{ VARIANT }} = "custom" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --include=dpkg,dash,diffutils,coreutils,libc-bin,sed {{ DIST }} /dev/null {{ MIRROR }}
diff --git a/tests/missing-device-nodes-outside-the-chroot b/tests/missing-device-nodes-outside-the-chroot
new file mode 100644
index 0000000..7f2fa27
--- /dev/null
+++ b/tests/missing-device-nodes-outside-the-chroot
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+rm /dev/console
+useradd --home-dir /home/user --create-home user
+runuser -u user -- {{ CMD }} --mode=unshare --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/mmdebstrap b/tests/mmdebstrap
new file mode 100644
index 0000000..3327fc6
--- /dev/null
+++ b/tests/mmdebstrap
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+[ "$(id -u)" -eq 0 ]
+[ {{ MODE }} = "root" ]
+case {{ FORMAT }} in tar|squashfs|ext2) : ;; *) exit 1;; esac
+
+{{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} {{ DIST }} ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.{{ FORMAT }} {{ MIRROR }}
+if [ "{{ FORMAT }}" = tar ]; then
+ printf 'ustar ' | cmp --bytes=6 --ignore-initial=257:0 ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.tar -
+elif [ "{{ FORMAT }}" = squashfs ]; then
+ printf 'hsqs' | cmp --bytes=4 ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.squashfs -
+elif [ "{{ FORMAT }}" = ext2 ]; then
+ printf '\123\357' | cmp --bytes=2 --ignore-initial=1080:0 ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.ext2 -
+else
+ echo "unknown format: {{ FORMAT }}" >&2
+ exit 1
+fi
diff --git a/tests/mount-is-missing b/tests/mount-is-missing
new file mode 100644
index 0000000..2e0c4b0
--- /dev/null
+++ b/tests/mount-is-missing
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+for p in /bin /usr/bin /sbin /usr/sbin; do
+ rm -f "$p/mount"
+done
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/multiple-include b/tests/multiple-include
new file mode 100644
index 0000000..36f53ec
--- /dev/null
+++ b/tests/multiple-include
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+{{ CMD }} --mode=root --variant=apt --include=doc-debian --include=tzdata {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+rm /tmp/debian-chroot/usr/share/doc-base/doc-debian.debian-*
+rm -r /tmp/debian-chroot/usr/share/doc/debian
+rm -r /tmp/debian-chroot/usr/share/doc/doc-debian
+rm /tmp/debian-chroot/usr/share/lintian/overrides/tzdata
+rm /tmp/debian-chroot/etc/localtime
+rm /tmp/debian-chroot/etc/timezone
+rm -r /tmp/debian-chroot/usr/share/doc/tzdata
+rm -r /tmp/debian-chroot/usr/share/zoneinfo
+rm /tmp/debian-chroot/var/lib/apt/extended_states
+for p in doc-debian tzdata; do
+ for f in list md5sums config postinst postrm templates preinst prerm; do
+ [ -e "/tmp/debian-chroot/var/lib/dpkg/info/$p.$f" ] || continue
+ rm "/tmp/debian-chroot/var/lib/dpkg/info/$p.$f"
+ done
+done
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/no-sbin-in-path b/tests/no-sbin-in-path
new file mode 100644
index 0000000..0cedc0b
--- /dev/null
+++ b/tests/no-sbin-in-path
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# If FAKECHROOT_CMD_SUBST sets up wrong substitutions, then binaries cannot be
+# found. For example if /usr/bin/chroot is listed in FAKECHROOT_CMD_SUBST but
+# /usr/sbin (the actual location of the chroot binary) is not in PATH, the
+# command fails
+
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+
+[ "{{ MODE }}" = "fakechroot" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix env PATH=/usr/bin:/bin fakechroot fakeroot {{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/not-having-to-install-apt-in-include-because-a-hook-did-it-before b/tests/not-having-to-install-apt-in-include-because-a-hook-did-it-before
new file mode 100644
index 0000000..9a36307
--- /dev/null
+++ b/tests/not-having-to-install-apt-in-include-because-a-hook-did-it-before
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=essential --include=apt \
+ --essential-hook='APT_CONFIG=$MMDEBSTRAP_APT_CONFIG apt-get update' \
+ --essential-hook='APT_CONFIG=$MMDEBSTRAP_APT_CONFIG apt-get --yes install apt' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | grep -v ./var/lib/apt/extended_states | diff -u tar1.txt -
diff --git a/tests/pass-distribution-but-implicitly-write-to-stdout b/tests/pass-distribution-but-implicitly-write-to-stdout
new file mode 100644
index 0000000..16d2243
--- /dev/null
+++ b/tests/pass-distribution-but-implicitly-write-to-stdout
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+cat << HOSTS >> /etc/hosts
+127.0.0.1 deb.debian.org
+127.0.0.1 security.debian.org
+HOSTS
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} > /tmp/debian-chroot.tar
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/pivot_root b/tests/pivot_root
new file mode 100644
index 0000000..860c41b
--- /dev/null
+++ b/tests/pivot_root
@@ -0,0 +1,54 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+trap "rm -f /tmp/chroot1.tar /tmp/chroot2.tar /tmp/chroot3.tar /tmp/mmdebstrap" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" 2>/dev/null; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --include=mount \
+ {{ DIST }} /tmp/chroot1.tar {{ MIRROR }}
+
+if [ {{ MODE }} = "unshare" ]; then
+ # calling pivot_root in root mode does not work for mysterious reasons:
+ # pivot_root: failed to change root from `.' to `mnt': Invalid argument
+ $prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include=mount \
+ --customize-hook='mkdir -p "$1/mnt" "$1/oldroot"' \
+ --customize-hook='[ ! -e /usr/bin/mmdebstrap ] || cp -aT /usr/bin/mmdebstrap "$1/usr/bin/mmdebstrap"' \
+ --customize-hook='[ ! -e ./mmdebstrap ] || cp -aT ./mmdebstrap "$1/mnt/mmdebstrap"' \
+ --customize-hook='mount -o rbind "$1" /mnt && cd /mnt && /sbin/pivot_root . oldroot' \
+ --customize-hook='unshare -U echo nested unprivileged unshare' \
+ --customize-hook='env --chdir=/mnt {{ CMD }} --mode=unshare --variant=apt --include=mount {{ DIST }} /tmp/chroot3.tar {{ MIRROR }}' \
+ --customize-hook='copy-out /tmp/chroot3.tar /tmp' \
+ --customize-hook='rm -f "/usr/bin/mmdebstrap" "/mnt/mmdebstrap"' \
+ --customize-hook='umount -l oldroot sys' \
+ --customize-hook='rmdir /oldroot' \
+ {{ DIST }} /tmp/chroot2.tar {{ MIRROR }}
+
+ cmp /tmp/chroot1.tar /tmp/chroot2.tar || diffoscope /tmp/chroot1.tar /tmp/chroot2.tar
+ cmp /tmp/chroot1.tar /tmp/chroot3.tar || diffoscope /tmp/chroot1.tar /tmp/chroot3.tar
+ rm /tmp/chroot2.tar /tmp/chroot3.tar
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include=mount \
+ --customize-hook='mkdir -p "$1/mnt"' \
+ --customize-hook='[ ! -e /usr/bin/mmdebstrap ] || cp -aT /usr/bin/mmdebstrap "$1/usr/bin/mmdebstrap"' \
+ --customize-hook='[ ! -e ./mmdebstrap ] || cp -aT ./mmdebstrap "$1/mnt/mmdebstrap"' \
+ --chrooted-customize-hook='env --chdir=/mnt {{ CMD }} --mode=unshare --variant=apt --include=mount {{ DIST }} /tmp/chroot3.tar {{ MIRROR }}' \
+ --customize-hook='copy-out /tmp/chroot3.tar /tmp' \
+ --customize-hook='rm -f "$1/usr/bin/mmdebstrap" "$1/mnt/mmdebstrap"' \
+ {{ DIST }} /tmp/chroot2.tar {{ MIRROR }}
+
+cmp /tmp/chroot1.tar /tmp/chroot2.tar || diffoscope /tmp/chroot1.tar /tmp/chroot2.tar
+cmp /tmp/chroot1.tar /tmp/chroot3.tar || diffoscope /tmp/chroot1.tar /tmp/chroot3.tar
diff --git a/tests/preserve-mode-of-etc-resolv-conf-and-etc-hostname b/tests/preserve-mode-of-etc-resolv-conf-and-etc-hostname
new file mode 100644
index 0000000..5e5f835
--- /dev/null
+++ b/tests/preserve-mode-of-etc-resolv-conf-and-etc-hostname
@@ -0,0 +1,102 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+for f in /etc/resolv.conf /etc/hostname; do
+ # preserve original content
+ cat "$f" > "$f.bak"
+ # in case $f is a symlink, we replace it by a real file
+ if [ -L "$f" ]; then
+ rm "$f"
+ cp "$f.bak" "$f"
+ fi
+ chmod 644 "$f"
+ [ "$(stat --format=%A "$f")" = "-rw-r--r--" ]
+done
+{{ CMD }} --variant=custom --mode={{ MODE }} {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+for f in /etc/resolv.conf /etc/hostname; do
+ [ "$(stat --format=%A "/tmp/debian-chroot/$f")" = "-rw-r--r--" ]
+done
+rm /tmp/debian-chroot/dev/console
+rm /tmp/debian-chroot/dev/fd
+rm /tmp/debian-chroot/dev/full
+rm /tmp/debian-chroot/dev/null
+rm /tmp/debian-chroot/dev/ptmx
+rm /tmp/debian-chroot/dev/random
+rm /tmp/debian-chroot/dev/stderr
+rm /tmp/debian-chroot/dev/stdin
+rm /tmp/debian-chroot/dev/stdout
+rm /tmp/debian-chroot/dev/tty
+rm /tmp/debian-chroot/dev/urandom
+rm /tmp/debian-chroot/dev/zero
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
+for f in /etc/resolv.conf /etc/hostname; do
+ chmod 755 "$f"
+ [ "$(stat --format=%A "$f")" = "-rwxr-xr-x" ]
+done
+{{ CMD }} --variant=custom --mode={{ MODE }} {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+for f in /etc/resolv.conf /etc/hostname; do
+ [ "$(stat --format=%A "/tmp/debian-chroot/$f")" = "-rwxr-xr-x" ]
+done
+rm /tmp/debian-chroot/dev/console
+rm /tmp/debian-chroot/dev/fd
+rm /tmp/debian-chroot/dev/full
+rm /tmp/debian-chroot/dev/null
+rm /tmp/debian-chroot/dev/ptmx
+rm /tmp/debian-chroot/dev/random
+rm /tmp/debian-chroot/dev/stderr
+rm /tmp/debian-chroot/dev/stdin
+rm /tmp/debian-chroot/dev/stdout
+rm /tmp/debian-chroot/dev/tty
+rm /tmp/debian-chroot/dev/urandom
+rm /tmp/debian-chroot/dev/zero
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
+for f in /etc/resolv.conf /etc/hostname; do
+ rm "$f"
+ ln -s "$f.bak" "$f"
+ [ "$(stat --format=%A "$f")" = "lrwxrwxrwx" ]
+done
+{{ CMD }} --variant=custom --mode={{ MODE }} {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+for f in /etc/resolv.conf /etc/hostname; do
+ [ "$(stat --format=%A "/tmp/debian-chroot/$f")" = "-rw-r--r--" ]
+done
+rm /tmp/debian-chroot/dev/console
+rm /tmp/debian-chroot/dev/fd
+rm /tmp/debian-chroot/dev/full
+rm /tmp/debian-chroot/dev/null
+rm /tmp/debian-chroot/dev/ptmx
+rm /tmp/debian-chroot/dev/random
+rm /tmp/debian-chroot/dev/stderr
+rm /tmp/debian-chroot/dev/stdin
+rm /tmp/debian-chroot/dev/stdout
+rm /tmp/debian-chroot/dev/tty
+rm /tmp/debian-chroot/dev/urandom
+rm /tmp/debian-chroot/dev/zero
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/progress-bars-on-fake-tty b/tests/progress-bars-on-fake-tty
new file mode 100644
index 0000000..e403111
--- /dev/null
+++ b/tests/progress-bars-on-fake-tty
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+script -qfec "{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}" /dev/null
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/quiet b/tests/quiet
new file mode 100644
index 0000000..d1cbb22
--- /dev/null
+++ b/tests/quiet
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+{{ CMD }} --mode=root --variant=apt --quiet {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/read-from-stdin-write-to-stdout b/tests/read-from-stdin-write-to-stdout
new file mode 100644
index 0000000..960cd3a
--- /dev/null
+++ b/tests/read-from-stdin-write-to-stdout
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm /tmp/debian-chroot.tar" EXIT INT TERM
+echo "deb {{ MIRROR }} {{ DIST }} main" | {{ CMD }} --mode={{ MODE }} --variant=apt > /tmp/debian-chroot.tar
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/remove-start-stop-daemon-and-policy-rc-d-in-hook b/tests/remove-start-stop-daemon-and-policy-rc-d-in-hook
new file mode 100644
index 0000000..d9c4be6
--- /dev/null
+++ b/tests/remove-start-stop-daemon-and-policy-rc-d-in-hook
@@ -0,0 +1,8 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt \
+ --customize-hook='rm "$1/usr/sbin/policy-rc.d"; rm "$1/usr/sbin/start-stop-daemon"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/root-mode-inside-chroot b/tests/root-mode-inside-chroot
new file mode 100644
index 0000000..0049c6b
--- /dev/null
+++ b/tests/root-mode-inside-chroot
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# Same as unshare-as-root-user-inside-chroot but this time we run mmdebstrap in
+# root mode from inside a chroot
+
+set -eu
+export LC_ALL=C.UTF-8
+[ "$(whoami)" = "root" ]
+
+trap "rm -f /tmp/debian-chroot.tar script.sh" EXIT INT TERM
+
+cat << 'SCRIPT' > script.sh
+#!/bin/sh
+set -exu
+rootfs="$1"
+mkdir -p "$rootfs/mnt"
+[ -e /usr/bin/mmdebstrap ] && cp -aT /usr/bin/mmdebstrap "$rootfs/usr/bin/mmdebstrap"
+[ -e ./mmdebstrap ] && cp -aT ./mmdebstrap "$rootfs/mnt/mmdebstrap"
+chroot "$rootfs" env --chdir=/mnt \
+ {{ CMD }} --mode=root --variant=apt \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+SCRIPT
+chmod +x script.sh
+{{ CMD }} --mode=root --variant=apt --include=perl,mount \
+ --customize-hook=./script.sh \
+ --customize-hook="download /tmp/debian-chroot.tar /tmp/debian-chroot.tar" \
+ {{ DIST }} /dev/null {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/root-mode-inside-unshare-chroot b/tests/root-mode-inside-unshare-chroot
new file mode 100644
index 0000000..e953c65
--- /dev/null
+++ b/tests/root-mode-inside-unshare-chroot
@@ -0,0 +1,40 @@
+#!/bin/sh
+#
+# Same as root-mode-inside-chroot but this time we run mmdebstrap in root mode
+# from inside an unshare chroot.
+
+set -eu
+export LC_ALL=C.UTF-8
+
+[ {{ MODE }} = "unshare" ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+cat << 'SCRIPT' > /tmp/script.sh
+#!/bin/sh
+set -eu
+rootfs="$1"
+mkdir -p "$rootfs/mnt"
+[ -e /usr/bin/mmdebstrap ] && cp -aT /usr/bin/mmdebstrap "$rootfs/usr/bin/mmdebstrap"
+[ -e ./mmdebstrap ] && cp -aT ./mmdebstrap "$rootfs/mnt/mmdebstrap"
+chroot "$rootfs" env --chdir=/mnt \
+ {{ CMD }} --mode=root --variant=apt \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+SCRIPT
+chmod +x /tmp/script.sh
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --include=perl,mount \
+ --customize-hook=/tmp/script.sh \
+ --customize-hook="download /tmp/debian-chroot.tar /tmp/debian-chroot.tar" \
+ {{ DIST }} /dev/null {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar /tmp/script.sh
diff --git a/tests/root-without-cap-sys-admin b/tests/root-without-cap-sys-admin
new file mode 100644
index 0000000..419f7b3
--- /dev/null
+++ b/tests/root-without-cap-sys-admin
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+[ "$(whoami)" = "root" ]
+
+if grep --null-data --quiet --no-messages '^container=lxc$' /proc/1/environ; then
+ # see https://stackoverflow.com/questions/65748254/
+ echo "cannot run under lxc -- Skipping test..." >&2
+ exit 0
+fi
+
+capsh --drop=cap_sys_admin -- -c 'exec "$@"' exec \
+ {{ CMD }} --mode=root --variant=apt \
+ --customize-hook='chroot "$1" sh -c "test ! -e /proc/self/fd"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar
diff --git a/tests/sigint-during-customize-hook b/tests/sigint-during-customize-hook
new file mode 100644
index 0000000..c8a4c94
--- /dev/null
+++ b/tests/sigint-during-customize-hook
@@ -0,0 +1,22 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+setsid --wait {{ CMD }} --mode=root --variant=apt --customize-hook='touch hookstarted && sleep 10 && touch fail' {{ DIST }} /tmp/debian-chroot {{ MIRROR }} &
+pid=$!
+while sleep 1; do [ -e hookstarted ] && break; done
+rm hookstarted
+# negative PID values choose the whole process group
+pgid=$((-1*$(ps -p "$pid" -o pgid=)))
+/bin/kill --signal INT -- "$pgid"
+ret=0
+wait $pid || ret=$?
+rm -r /tmp/debian-chroot
+if [ -e fail ]; then
+ echo customize hook was not interrupted >&2
+ rm fail
+ exit 1
+fi
+if [ "$ret" = 0 ]; then
+ echo expected failure but got exit $ret >&2
+ exit 1
+fi
diff --git a/tests/signed-by-with-host-keys b/tests/signed-by-with-host-keys
new file mode 100644
index 0000000..ef0d8c7
--- /dev/null
+++ b/tests/signed-by-with-host-keys
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf 'deb {{ MIRROR }} {{ DIST }} main\n' | cmp /tmp/debian-chroot/etc/apt/sources.list -
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/signed-by-without-host-keys b/tests/signed-by-without-host-keys
new file mode 100644
index 0000000..470a9de
--- /dev/null
+++ b/tests/signed-by-without-host-keys
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+for f in /etc/apt/trusted.gpg.d/*.gpg /etc/apt/trusted.gpg.d/*.asc; do
+ [ -e "$f" ] || continue
+ rm "$f"
+done
+rmdir /etc/apt/trusted.gpg.d
+mkdir /etc/apt/trusted.gpg.d
+{{ CMD }} --mode=root --variant=apt {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf 'deb [signed-by="/usr/share/keyrings/debian-archive-keyring.gpg"] {{ MIRROR }} {{ DIST }} main\n' | cmp /tmp/debian-chroot/etc/apt/sources.list -
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
+rm -r /tmp/debian-chroot
diff --git a/tests/skip-mount b/tests/skip-mount
new file mode 100644
index 0000000..e210d5c
--- /dev/null
+++ b/tests/skip-mount
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+[ "{{ MODE }}" = "unshare" ]
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode=unshare --variant=apt \
+ --skip=chroot/mount/proc,chroot/mount/sys \
+ --customize-hook='mountpoint "$1"/dev/null' \
+ --customize-hook='if mountpoint "$1"/sys; then exit 1; fi' \
+ --customize-hook='if mountpoint "$1"/proc; then exit 1; fi' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/skip-output-dev b/tests/skip-output-dev
new file mode 100644
index 0000000..0766a66
--- /dev/null
+++ b/tests/skip-output-dev
@@ -0,0 +1,35 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# test this for both unshare and root mode because the code paths creating
+# entries in /dev are different depending on whether mknod is available or not
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --skip=output/dev {{ DIST }} - {{ MIRROR }} | {
+ tar -t;
+ echo ./dev/console;
+ echo ./dev/fd;
+ echo ./dev/full;
+ echo ./dev/null;
+ echo ./dev/ptmx;
+ echo ./dev/pts/;
+ echo ./dev/random;
+ echo ./dev/shm/;
+ echo ./dev/stderr;
+ echo ./dev/stdin;
+ echo ./dev/stdout;
+ echo ./dev/tty;
+ echo ./dev/urandom;
+ echo ./dev/zero;
+} | sort | diff -u tar1.txt -
diff --git a/tests/skip-output-mknod b/tests/skip-output-mknod
new file mode 100644
index 0000000..8ccbfdf
--- /dev/null
+++ b/tests/skip-output-mknod
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# test this for both unshare and root mode because the code paths creating
+# entries in /dev are different depending on whether mknod is available or not
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt --skip=output/mknod \
+ {{ DIST }} - {{ MIRROR }} | {
+ tar -t;
+ echo ./dev/console;
+ echo ./dev/full;
+ echo ./dev/null;
+ echo ./dev/ptmx;
+ echo ./dev/random;
+ echo ./dev/tty;
+ echo ./dev/urandom;
+ echo ./dev/zero;
+} | sort | diff -u tar1.txt -
diff --git a/tests/skip-start-stop-daemon-policy-rc b/tests/skip-start-stop-daemon-policy-rc
new file mode 100644
index 0000000..bdf5469
--- /dev/null
+++ b/tests/skip-start-stop-daemon-policy-rc
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt \
+ --skip=chroot/start-stop-daemon,chroot/policy-rc.d \
+ --customize-hook='test ! -e "$1/sbin/start-stop-daemon.REAL"' \
+ --customize-hook='test ! -e "$1/usr/sbin/policy-rc.d"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/skip-tar-in-mknod b/tests/skip-tar-in-mknod
new file mode 100644
index 0000000..eb3027a
--- /dev/null
+++ b/tests/skip-tar-in-mknod
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+[ {{ MODE }} = "unshare" ]
+
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+$prefix {{ CMD }} --mode={{ MODE }} --variant=custom \
+ --skip=update,setup,cleanup,tar-in/mknod \
+ --setup-hook='tar-in ./cache/mmdebstrap-{{ DIST }}-apt.tar /' \
+ '' /tmp/debian-chroot.tar
+
+cmp ./cache/mmdebstrap-{{ DIST }}-apt.tar /tmp/debian-chroot.tar \
+ || diffoscope ./cache/mmdebstrap-{{ DIST }}-apt.tar /tmp/debian-chroot.tar
diff --git a/tests/special-hooks-using-helpers b/tests/special-hooks-using-helpers
new file mode 100644
index 0000000..a211746
--- /dev/null
+++ b/tests/special-hooks-using-helpers
@@ -0,0 +1,28 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+mkfifo /tmp/myfifo
+mkdir /tmp/root
+ln -s /real /tmp/root/link
+mkdir /tmp/root/real
+run_testA() {
+ echo content > /tmp/foo
+ # shellcheck disable=SC2094
+ { { { {{ CMD }} --hook-helper /tmp/root root setup '' 1 upload /tmp/foo "$1" < /tmp/myfifo 3>&-; echo $? >&3; printf "\\000\\000adios";
+ } | {{ CMD }} --hook-listener 1 3>&- >/tmp/myfifo; echo $?; } 3>&1;
+ } | { read -r xs1; [ "$xs1" -eq 0 ]; read -r xs2; [ "$xs2" -eq 0 ]; }
+ echo content | diff -u - /tmp/root/real/foo
+ rm /tmp/foo
+ rm /tmp/root/real/foo
+}
+run_testA link/foo
+run_testA /link/foo
+run_testA ///link///foo///
+run_testA /././link/././foo/././
+run_testA /link/../link/foo
+run_testA /link/../../link/foo
+run_testA /../../link/foo
+rmdir /tmp/root/real
+rm /tmp/root/link
+rmdir /tmp/root
+rm /tmp/myfifo
diff --git a/tests/special-hooks-using-helpers-and-env-vars b/tests/special-hooks-using-helpers-and-env-vars
new file mode 100644
index 0000000..7a1ffeb
--- /dev/null
+++ b/tests/special-hooks-using-helpers-and-env-vars
@@ -0,0 +1,31 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+cat << 'SCRIPT' > /tmp/script.sh
+#!/bin/sh
+set -eu
+echo "MMDEBSTRAP_APT_CONFIG $MMDEBSTRAP_APT_CONFIG"
+echo "$MMDEBSTRAP_HOOK" >> /tmp/hooks
+[ "$MMDEBSTRAP_MODE" = "root" ]
+echo test-content $MMDEBSTRAP_HOOK > test
+{{ CMD }} --hook-helper "$1" "$MMDEBSTRAP_MODE" "$MMDEBSTRAP_HOOK" '' 1 upload test /test <&$MMDEBSTRAP_HOOKSOCK >&$MMDEBSTRAP_HOOKSOCK
+rm test
+echo "content inside chroot:"
+cat "$1/test"
+[ "test-content $MMDEBSTRAP_HOOK" = "$(cat "$1/test")" ]
+{{ CMD }} --hook-helper "$1" "$MMDEBSTRAP_MODE" "$MMDEBSTRAP_HOOK" '' 1 download /test test <&$MMDEBSTRAP_HOOKSOCK >&$MMDEBSTRAP_HOOKSOCK
+echo "content outside chroot:"
+cat test
+[ "test-content $MMDEBSTRAP_HOOK" = "$(cat test)" ]
+rm test
+SCRIPT
+chmod +x /tmp/script.sh
+{{ CMD }} --mode=root --variant=apt \
+ --setup-hook=/tmp/script.sh \
+ --extract-hook=/tmp/script.sh \
+ --essential-hook=/tmp/script.sh \
+ --customize-hook=/tmp/script.sh \
+ {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+printf "setup\nextract\nessential\ncustomize\n" | diff -u - /tmp/hooks
+rm /tmp/script.sh /tmp/hooks
+rm -r /tmp/debian-chroot
diff --git a/tests/special-hooks-with-mode-mode b/tests/special-hooks-with-mode-mode
new file mode 100644
index 0000000..99e2a47
--- /dev/null
+++ b/tests/special-hooks-with-mode-mode
@@ -0,0 +1,148 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+[ "{{ MODE }}" = "fakechroot" ] && prefix="$prefix fakechroot fakeroot"
+symlinktarget=/real
+[ "{{ MODE }}" = "fakechroot" ] && symlinktarget='$1/real'
+echo copy-in-setup > /tmp/copy-in-setup
+echo copy-in-essential > /tmp/copy-in-essential
+echo copy-in-customize > /tmp/copy-in-customize
+echo tar-in-setup > /tmp/tar-in-setup
+echo tar-in-essential > /tmp/tar-in-essential
+echo tar-in-customize > /tmp/tar-in-customize
+tar --numeric-owner --format=pax --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime -C /tmp -cf /tmp/tar-in-setup.tar tar-in-setup
+tar --numeric-owner --format=pax --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime -C /tmp -cf /tmp/tar-in-essential.tar tar-in-essential
+tar --numeric-owner --format=pax --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime -C /tmp -cf /tmp/tar-in-customize.tar tar-in-customize
+rm /tmp/tar-in-setup
+rm /tmp/tar-in-essential
+rm /tmp/tar-in-customize
+echo upload-setup > /tmp/upload-setup
+echo upload-essential > /tmp/upload-essential
+echo upload-customize > /tmp/upload-customize
+mkdir /tmp/sync-in-setup
+mkdir /tmp/sync-in-essential
+mkdir /tmp/sync-in-customize
+echo sync-in-setup > /tmp/sync-in-setup/file
+echo sync-in-essential > /tmp/sync-in-essential/file
+echo sync-in-customize > /tmp/sync-in-customize/file
+$prefix {{ CMD }} --mode={{ MODE }} --variant=apt \
+ --setup-hook='mkdir "$1/real"' \
+ --setup-hook='copy-in /tmp/copy-in-setup /real' \
+ --setup-hook='echo copy-in-setup | cmp "$1/real/copy-in-setup" -' \
+ --setup-hook='rm "$1/real/copy-in-setup"' \
+ --setup-hook='echo copy-out-setup > "$1/real/copy-out-setup"' \
+ --setup-hook='copy-out /real/copy-out-setup /tmp' \
+ --setup-hook='rm "$1/real/copy-out-setup"' \
+ --setup-hook='tar-in /tmp/tar-in-setup.tar /real' \
+ --setup-hook='echo tar-in-setup | cmp "$1/real/tar-in-setup" -' \
+ --setup-hook='tar-out /real/tar-in-setup /tmp/tar-out-setup.tar' \
+ --setup-hook='rm "$1"/real/tar-in-setup' \
+ --setup-hook='upload /tmp/upload-setup /real/upload' \
+ --setup-hook='echo upload-setup | cmp "$1/real/upload" -' \
+ --setup-hook='download /real/upload /tmp/download-setup' \
+ --setup-hook='rm "$1/real/upload"' \
+ --setup-hook='sync-in /tmp/sync-in-setup /real' \
+ --setup-hook='echo sync-in-setup | cmp "$1/real/file" -' \
+ --setup-hook='sync-out /real /tmp/sync-out-setup' \
+ --setup-hook='rm "$1/real/file"' \
+ --essential-hook='ln -s "'"$symlinktarget"'" "$1/symlink"' \
+ --essential-hook='copy-in /tmp/copy-in-essential /symlink' \
+ --essential-hook='echo copy-in-essential | cmp "$1/real/copy-in-essential" -' \
+ --essential-hook='rm "$1/real/copy-in-essential"' \
+ --essential-hook='echo copy-out-essential > "$1/real/copy-out-essential"' \
+ --essential-hook='copy-out /symlink/copy-out-essential /tmp' \
+ --essential-hook='rm "$1/real/copy-out-essential"' \
+ --essential-hook='tar-in /tmp/tar-in-essential.tar /symlink' \
+ --essential-hook='echo tar-in-essential | cmp "$1/real/tar-in-essential" -' \
+ --essential-hook='tar-out /symlink/tar-in-essential /tmp/tar-out-essential.tar' \
+ --essential-hook='rm "$1"/real/tar-in-essential' \
+ --essential-hook='upload /tmp/upload-essential /symlink/upload' \
+ --essential-hook='echo upload-essential | cmp "$1/real/upload" -' \
+ --essential-hook='download /symlink/upload /tmp/download-essential' \
+ --essential-hook='rm "$1/real/upload"' \
+ --essential-hook='sync-in /tmp/sync-in-essential /symlink' \
+ --essential-hook='echo sync-in-essential | cmp "$1/real/file" -' \
+ --essential-hook='sync-out /real /tmp/sync-out-essential' \
+ --essential-hook='rm "$1/real/file"' \
+ --customize-hook='copy-in /tmp/copy-in-customize /symlink' \
+ --customize-hook='echo copy-in-customize | cmp "$1/real/copy-in-customize" -' \
+ --customize-hook='rm "$1/real/copy-in-customize"' \
+ --customize-hook='echo copy-out-customize > "$1/real/copy-out-customize"' \
+ --customize-hook='copy-out /symlink/copy-out-customize /tmp' \
+ --customize-hook='rm "$1/real/copy-out-customize"' \
+ --customize-hook='tar-in /tmp/tar-in-customize.tar /symlink' \
+ --customize-hook='echo tar-in-customize | cmp "$1/real/tar-in-customize" -' \
+ --customize-hook='tar-out /symlink/tar-in-customize /tmp/tar-out-customize.tar' \
+ --customize-hook='rm "$1"/real/tar-in-customize' \
+ --customize-hook='upload /tmp/upload-customize /symlink/upload' \
+ --customize-hook='echo upload-customize | cmp "$1/real/upload" -' \
+ --customize-hook='download /symlink/upload /tmp/download-customize' \
+ --customize-hook='rm "$1/real/upload"' \
+ --customize-hook='sync-in /tmp/sync-in-customize /symlink' \
+ --customize-hook='echo sync-in-customize | cmp "$1/real/file" -' \
+ --customize-hook='sync-out /real /tmp/sync-out-customize' \
+ --customize-hook='rm "$1/real/file"' \
+ --customize-hook='rmdir "$1/real"' \
+ --customize-hook='rm "$1/symlink"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+for n in setup essential customize; do
+ ret=0
+ cmp /tmp/tar-in-$n.tar /tmp/tar-out-$n.tar || ret=$?
+ if [ "$ret" -ne 0 ]; then
+ if type diffoscope >/dev/null; then
+ diffoscope /tmp/tar-in-$n.tar /tmp/tar-out-$n.tar
+ exit 1
+ else
+ echo "no diffoscope installed" >&2
+ fi
+ if type base64 >/dev/null; then
+ base64 /tmp/tar-in-$n.tar
+ base64 /tmp/tar-out-$n.tar
+ exit 1
+ else
+ echo "no base64 installed" >&2
+ fi
+ if type xxd >/dev/null; then
+ xxd /tmp/tar-in-$n.tar
+ xxd /tmp/tar-out-$n.tar
+ exit 1
+ else
+ echo "no xxd installed" >&2
+ fi
+ exit 1
+ fi
+done
+echo copy-out-setup | cmp /tmp/copy-out-setup -
+echo copy-out-essential | cmp /tmp/copy-out-essential -
+echo copy-out-customize | cmp /tmp/copy-out-customize -
+echo upload-setup | cmp /tmp/download-setup -
+echo upload-essential | cmp /tmp/download-essential -
+echo upload-customize | cmp /tmp/download-customize -
+echo sync-in-setup | cmp /tmp/sync-out-setup/file -
+echo sync-in-essential | cmp /tmp/sync-out-essential/file -
+echo sync-in-customize | cmp /tmp/sync-out-customize/file -
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
+rm /tmp/debian-chroot.tar \
+ /tmp/copy-in-setup /tmp/copy-in-essential /tmp/copy-in-customize \
+ /tmp/copy-out-setup /tmp/copy-out-essential /tmp/copy-out-customize \
+ /tmp/tar-in-setup.tar /tmp/tar-in-essential.tar /tmp/tar-in-customize.tar \
+ /tmp/tar-out-setup.tar /tmp/tar-out-essential.tar /tmp/tar-out-customize.tar \
+ /tmp/upload-setup /tmp/upload-essential /tmp/upload-customize \
+ /tmp/download-setup /tmp/download-essential /tmp/download-customize \
+ /tmp/sync-in-setup/file /tmp/sync-in-essential/file /tmp/sync-in-customize/file \
+ /tmp/sync-out-setup/file /tmp/sync-out-essential/file /tmp/sync-out-customize/file
+rmdir /tmp/sync-in-setup /tmp/sync-in-essential /tmp/sync-in-customize \
+ /tmp/sync-out-setup /tmp/sync-out-essential /tmp/sync-out-customize
diff --git a/tests/stable-default-mirror b/tests/stable-default-mirror
new file mode 100644
index 0000000..c79f9d6
--- /dev/null
+++ b/tests/stable-default-mirror
@@ -0,0 +1,20 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+cat << HOSTS >> /etc/hosts
+127.0.0.1 deb.debian.org
+127.0.0.1 security.debian.org
+HOSTS
+apt-cache policy
+cat /etc/apt/sources.list
+{{ CMD }} --mode=root --variant=apt stable /tmp/debian-chroot
+cat << SOURCES | cmp /tmp/debian-chroot/etc/apt/sources.list
+deb http://deb.debian.org/debian stable main
+deb http://deb.debian.org/debian stable-updates main
+deb http://security.debian.org/debian-security stable-security main
+SOURCES
+rm -r /tmp/debian-chroot
diff --git a/tests/supply-components-manually b/tests/supply-components-manually
new file mode 100644
index 0000000..038b5dc
--- /dev/null
+++ b/tests/supply-components-manually
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt --components="main main" --comp="main,main" {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+echo "deb {{ MIRROR }} {{ DIST }} main" | cmp /tmp/debian-chroot/etc/apt/sources.list
+tar -C /tmp/debian-chroot --one-file-system -c . | tar -t | sort | diff -u tar1.txt -
diff --git a/tests/tarfilter-idshift b/tests/tarfilter-idshift
new file mode 100644
index 0000000..731d40f
--- /dev/null
+++ b/tests/tarfilter-idshift
@@ -0,0 +1,58 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+trap "rm -f /tmp/debian-chroot.tar /tmp/debian-chroot-shifted.tar /tmp/debian-chroot.txt /tmp/debian-chroot-shiftedback.tar /tmp/expected; rm -rf /tmp/debian-chroot" EXIT INT TERM
+useradd --home-dir /home/user --create-home user
+echo user:100000:65536 | cmp /etc/subuid -
+echo user:100000:65536 | cmp /etc/subgid -
+# include iputils-ping so that we can verify that tarfilter does not remove
+# extended attributes
+# run through tarshift no-op to create a tarball that should be bit-by-bit
+# identical to a round trip through "tarfilter --idshift X" and "tarfilter --idshift -X"
+runuser -u user -- {{ CMD }} --mode=unshare --variant=apt --include=iputils-ping {{ DIST }} - {{ MIRROR }} \
+ | ./tarfilter --idshift 0 > /tmp/debian-chroot.tar
+# make sure that xattrs are set in the original tarball
+mkdir /tmp/debian-chroot
+tar --xattrs --xattrs-include='*' --directory /tmp/debian-chroot -xf /tmp/debian-chroot.tar ./usr/bin/ping
+echo "/tmp/debian-chroot/usr/bin/ping cap_net_raw=ep" > /tmp/expected
+getcap /tmp/debian-chroot/usr/bin/ping | diff -u /tmp/expected - >&2
+rm /tmp/debian-chroot/usr/bin/ping
+rmdir /tmp/debian-chroot/usr/bin
+rmdir /tmp/debian-chroot/usr
+rmdir /tmp/debian-chroot
+# shift the uid/gid forward by 100000 and backward by 100000
+./tarfilter --idshift 100000 < /tmp/debian-chroot.tar > /tmp/debian-chroot-shifted.tar
+./tarfilter --idshift -100000 < /tmp/debian-chroot-shifted.tar > /tmp/debian-chroot-shiftedback.tar
+# the tarball before and after the roundtrip through tarfilter should be bit
+# by bit identical
+cmp /tmp/debian-chroot.tar /tmp/debian-chroot-shiftedback.tar
+# manually adjust uid/gid and compare "tar -t" output
+tar --numeric-owner -tvf /tmp/debian-chroot.tar \
+ | sed 's# 42/0 # 100042/100000 #' \
+ | sed 's# 0/0 # 100000/100000 #' \
+ | sed 's# 0/5 # 100000/100005 #' \
+ | sed 's# 0/8 # 100000/100008 #' \
+ | sed 's# 0/42 # 100000/100042 #' \
+ | sed 's# 0/43 # 100000/100043 #' \
+ | sed 's# 0/50 # 100000/100050 #' \
+ | sed 's/ \+/ /g' \
+ > /tmp/debian-chroot.txt
+tar --numeric-owner -tvf /tmp/debian-chroot-shifted.tar \
+ | sed 's/ \+/ /g' \
+ | diff -u /tmp/debian-chroot.txt - >&2
+mkdir /tmp/debian-chroot
+tar --xattrs --xattrs-include='*' --directory /tmp/debian-chroot -xf /tmp/debian-chroot-shifted.tar
+echo "100000 100000" > /tmp/expected
+stat --format="%u %g" /tmp/debian-chroot/usr/bin/ping | diff -u /tmp/expected - >&2
+echo "/tmp/debian-chroot/usr/bin/ping cap_net_raw=ep" > /tmp/expected
+getcap /tmp/debian-chroot/usr/bin/ping | diff -u /tmp/expected - >&2
+echo "0 0" > /tmp/expected
+runuser -u user -- {{ CMD }} --unshare-helper /usr/sbin/chroot /tmp/debian-chroot stat --format="%u %g" /usr/bin/ping \
+ | diff -u /tmp/expected - >&2
+echo "/usr/bin/ping cap_net_raw=ep" > /tmp/expected
+runuser -u user -- {{ CMD }} --unshare-helper /usr/sbin/chroot /tmp/debian-chroot getcap /usr/bin/ping \
+ | diff -u /tmp/expected - >&2
diff --git a/tests/unpack-doc-debian b/tests/unpack-doc-debian
new file mode 100644
index 0000000..fe87d13
--- /dev/null
+++ b/tests/unpack-doc-debian
@@ -0,0 +1,57 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+trap "rm -rf /tmp/debian-chroot" EXIT INT TERM
+
+[ {{ VARIANT }} = extract ]
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+[ "{{ MODE }}" = "fakechroot" ] && prefix="$prefix fakechroot fakeroot"
+$prefix {{ CMD }} --mode={{ MODE }} --variant={{ VARIANT }} --include=doc-debian {{ DIST }} /tmp/debian-chroot {{ MIRROR }}
+# delete contents of doc-debian
+rm /tmp/debian-chroot/usr/share/doc-base/doc-debian.debian-*
+rm -r /tmp/debian-chroot/usr/share/doc/debian
+rm -r /tmp/debian-chroot/usr/share/doc/doc-debian
+# delete real files
+rm /tmp/debian-chroot/etc/apt/sources.list
+rm /tmp/debian-chroot/etc/fstab
+rm /tmp/debian-chroot/etc/hostname
+rm /tmp/debian-chroot/etc/resolv.conf
+rm /tmp/debian-chroot/var/lib/dpkg/status
+rm /tmp/debian-chroot/var/lib/dpkg/arch
+rm /tmp/debian-chroot/var/cache/apt/archives/lock
+rm /tmp/debian-chroot/var/lib/apt/lists/lock
+## delete merged usr symlinks
+#rm /tmp/debian-chroot/libx32
+#rm /tmp/debian-chroot/lib64
+#rm /tmp/debian-chroot/lib32
+#rm /tmp/debian-chroot/sbin
+#rm /tmp/debian-chroot/bin
+#rm /tmp/debian-chroot/lib
+# delete ./dev (files might exist or not depending on the mode)
+rm -f /tmp/debian-chroot/dev/console
+rm -f /tmp/debian-chroot/dev/fd
+rm -f /tmp/debian-chroot/dev/full
+rm -f /tmp/debian-chroot/dev/null
+rm -f /tmp/debian-chroot/dev/ptmx
+rm -f /tmp/debian-chroot/dev/random
+rm -f /tmp/debian-chroot/dev/stderr
+rm -f /tmp/debian-chroot/dev/stdin
+rm -f /tmp/debian-chroot/dev/stdout
+rm -f /tmp/debian-chroot/dev/tty
+rm -f /tmp/debian-chroot/dev/urandom
+rm -f /tmp/debian-chroot/dev/zero
+# the rest should be empty directories that we can rmdir recursively
+find /tmp/debian-chroot -depth -print0 | xargs -0 rmdir
diff --git a/tests/unshare-as-root-user b/tests/unshare-as-root-user
new file mode 100644
index 0000000..e1ba4d6
--- /dev/null
+++ b/tests/unshare-as-root-user
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+[ "$(whoami)" = "root" ]
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+{{ CMD }} --mode=unshare --variant=apt \
+ --customize-hook='chroot "$1" sh -c "test -e /proc/self/fd"' \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/unshare-as-root-user-inside-chroot b/tests/unshare-as-root-user-inside-chroot
new file mode 100644
index 0000000..9c0eb0d
--- /dev/null
+++ b/tests/unshare-as-root-user-inside-chroot
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# Before running unshare mode as root, we run "unshare --mount" but that fails
+# if mmdebstrap itself is executed from within a chroot:
+# unshare: cannot change root filesystem propagation: Invalid argument
+# This test tests the workaround in mmdebstrap using --propagation unchanged
+
+set -eu
+export LC_ALL=C.UTF-8
+[ "$(whoami)" = "root" ]
+trap "rm -f /tmp/debian-chroot.tar script.sh" EXIT INT TERM
+cat << 'SCRIPT' > script.sh
+#!/bin/sh
+set -eu
+rootfs="$1"
+mkdir -p "$rootfs/mnt"
+[ -e /usr/bin/mmdebstrap ] && cp -aT /usr/bin/mmdebstrap "$rootfs/usr/bin/mmdebstrap"
+[ -e ./mmdebstrap ] && cp -aT ./mmdebstrap "$rootfs/mnt/mmdebstrap"
+chroot "$rootfs" env --chdir=/mnt \
+ {{ CMD }} --mode=unshare --variant=apt \
+ {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+SCRIPT
+chmod +x script.sh
+{{ CMD }} --mode=root --variant=apt --include=perl,mount \
+ --customize-hook=./script.sh \
+ --customize-hook="download /tmp/debian-chroot.tar /tmp/debian-chroot.tar" \
+ {{ DIST }} /dev/null {{ MIRROR }}
+tar -tf /tmp/debian-chroot.tar | sort | diff -u tar1.txt -
diff --git a/tests/unshare-include-deb b/tests/unshare-include-deb
new file mode 100644
index 0000000..2b9c54b
--- /dev/null
+++ b/tests/unshare-include-deb
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+set -eu
+export LC_ALL=C.UTF-8
+
+[ "{{ MODE }}" = unshare ]
+
+trap "rm -rf /tmp/dummypkg.deb /tmp/dummypkg" EXIT INT TERM
+
+prefix=
+if [ "$(id -u)" -eq 0 ] && [ "{{ MODE }}" != "root" ] && [ "{{ MODE }}" != "auto" ]; then
+ if ! id "${SUDO_USER:-user}" >/dev/null 2>&1; then
+ if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+ fi
+ useradd --home-dir "/home/${SUDO_USER:-user}" --create-home "${SUDO_USER:-user}"
+ fi
+ prefix="runuser -u ${SUDO_USER:-user} --"
+fi
+
+# instead of obtaining a .deb from our cache, we create a new package because
+# otherwise apt might decide to download the package with the same name and
+# version from the cache instead of using the local .deb
+mkdir -p /tmp/dummypkg/DEBIAN
+cat << END > "/tmp/dummypkg/DEBIAN/control"
+Package: dummypkg
+Priority: optional
+Section: oldlibs
+Maintainer: Johannes Schauer Marin Rodrigues <josch@debian.org>
+Architecture: all
+Multi-Arch: foreign
+Source: dummypkg
+Version: 1
+Description: dummypkg
+END
+dpkg-deb --build "/tmp/dummypkg" "/tmp/dummypkg.deb"
+
+# make the .deb only redable by its owner which will exclude the unshared user
+chmod 600 /tmp/dummypkg.deb
+
+ret=0
+$prefix {{ CMD }} --variant=apt --mode={{ MODE }} --include="/tmp/dummypkg.deb" \
+ {{ DIST }} /dev/null {{ MIRROR }} || ret=$?
+
+if [ "$ret" -eq 0 ]; then
+ echo "expected failure but got exit $ret" >&2
+ exit 1
+fi
diff --git a/tests/variant-custom-timeout b/tests/variant-custom-timeout
new file mode 100644
index 0000000..06f0b42
--- /dev/null
+++ b/tests/variant-custom-timeout
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+
+# mmdebstrap used to hang forever if apt in custom mode failed to resolve
+# dependencies because a process died without cleaning up its children.
+# https://bugs.debian.org/1017795
+ret=0
+{{ CMD }} --mode={{ MODE }} --variant=custom \
+ --include=this-package-does-not-exist {{ DIST }} /dev/null {{ MIRROR }} || ret=1
+[ $ret -eq 1 ]
diff --git a/tests/verbose b/tests/verbose
new file mode 100644
index 0000000..b0b0fb9
--- /dev/null
+++ b/tests/verbose
@@ -0,0 +1,17 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+export SOURCE_DATE_EPOCH={{ SOURCE_DATE_EPOCH }}
+
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+
+# we use variant standard in verbose mode to see the maximum number of packages
+# that was chosen in case of USE_HOST_APT_CONFIG=yes
+# we use variant important on arches where variant standard is not bit-by-bit
+# reproducible due to #1031276
+case {{ VARIANT }} in standard|-) : ;; *) exit 1;; esac
+
+{{ CMD }} --variant={{ VARIANT }} --verbose {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+
+cmp ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.tar /tmp/debian-chroot.tar \
+ || diffoscope ./cache/mmdebstrap-{{ DIST }}-{{ VARIANT }}.tar /tmp/debian-chroot.tar
diff --git a/tests/version b/tests/version
new file mode 100644
index 0000000..cae04a6
--- /dev/null
+++ b/tests/version
@@ -0,0 +1,6 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+# we redirect to /dev/null instead of using --quiet to not cause a broken pipe
+# when grep exits before mmdebstrap was able to write all its output
+{{ CMD }} --version | grep -E '^mmdebstrap [0-9](\.[0-9])+$' >/dev/null
diff --git a/tests/without-etc-resolv-conf-and-etc-hostname b/tests/without-etc-resolv-conf-and-etc-hostname
new file mode 100644
index 0000000..1d3dfef
--- /dev/null
+++ b/tests/without-etc-resolv-conf-and-etc-hostname
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+if [ ! -e /mmdebstrap-testenv ]; then
+ echo "this test modifies the system and should only be run inside a container" >&2
+ exit 1
+fi
+trap "rm -f /tmp/debian-chroot.tar" EXIT INT TERM
+rm /etc/resolv.conf /etc/hostname
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar {{ MIRROR }}
+{ tar -tf /tmp/debian-chroot.tar;
+ printf "./etc/hostname\n";
+ printf "./etc/resolv.conf\n";
+} | sort | diff -u tar1.txt -
diff --git a/tests/xz-compressed-tarball b/tests/xz-compressed-tarball
new file mode 100644
index 0000000..6dc6a37
--- /dev/null
+++ b/tests/xz-compressed-tarball
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -eu
+export LC_ALL=C.UTF-8
+trap "rm -f /tmp/debian-chroot.tar.xz" EXIT INT TERM
+{{ CMD }} --mode={{ MODE }} --variant=apt {{ DIST }} /tmp/debian-chroot.tar.xz {{ MIRROR }}
+printf '\3757zXZ\0' | cmp --bytes=6 /tmp/debian-chroot.tar.xz -
+tar -tf /tmp/debian-chroot.tar.xz | sort | diff -u tar1.txt -