(Debian users who know what they’re doing: there’s probably nothing hugely amazing here, and instead just lots of horrors!)

I tried switching to Debian for a month or so, and did so broadly non-destructively to my original Arch environment. How did I do it?

Short answer: ZFS boot environments and debootstrap.

Long answer:

Create a boot environment.

With a function or script something like:

create() {
	local dataset="$1"; shift;
	local mountpoint="${dataset#lisbon/ROOT/debian}"
	zfs create "$@" "$dataset";
	mkdir -p "/lisbon/debian/${mountpoint}";
	mount -t zfs "$dataset" "/lisbon/debian/${mountpoint}";
	printf '%s %s zfs rw,xattr,posixacl 0 0\n' \
		"${dataset}" \
		"/lisbon/debian/${mountpoint%/}" \
		>> /etc/fstab
}

I then do:

create lisbon/ROOT/debian                       -o mountpoint=legacy
create lisbon/ROOT/debian/usr
create lisbon/ROOT/debian/usr/local
create lisbon/ROOT/debian/var
create lisbon/ROOT/debian/var/cache             -o com.sun:auto-snapshot=false
create lisbon/ROOT/debian/var/lib
create lisbon/ROOT/debian/var/log
create lisbon/ROOT/debian/var/obj
create lisbon/ROOT/debian/var/spool
create lisbon/ROOT/debian/var/tmp

I’m not at all sure that the constellation of datasets in the OpenZFS install guide are needed at all: whilst that suggests their creation to make rollbacks easier, I’m not yet convinced, and I’ve got a fairly stripped down set. (I suspect this could go further.)

I initially also created datasets for /opt and /srv, which broke things a little; but I backed them out.

I’m sure I shouldn’t need to futz around with explicitly mounting datasets, either. If someone’s got a better idea, let me know!

Run debootstrap.

Now we’ve got our system mounted, grab a copy of both debootstrap and debian-archive-keyring, and unroll them. (I’m not sure the latter is necessary: I’d also been trying cdebootstrap, which did need it, and my final debootstrap invocation looks to have included it.)

I use tar(1) from libarchive. You could use dpkg-deb -x, if you can summon up dpkg from your local package manager.

POOL=https://mirror.aarnet.edu.au/debian/pool
wget \
$POOL/main/d/debian-archive-keyring/debian-archive-keyring_2021.1.1_all.deb \
$POOL/main/d/debootstrap/debootstrap_1.0.126+nmu1_all.deb
                  [... wget output elided ...]
for i (*.tar); tar xOf $i data.tar.xz | tar xvf -
                  [... tar output elided ...]

Now, we run the bootstrap. Surprise! It’s super simple.

doas \
env DEBOOTSTRAP_DIR=$PWD/usr/share/debootstrap \
bash usr/sbin/debootstrap \
--keyring=usr/share/keyrings/debian-archive-keyring.gpg \
--arch=amd64 \
--components=main,contrib,non-free \
testing \
/lisbon/debian \
https://deb.debian.org/debian/
[...]

Now for the fun bits:

Do post-installation configuration.

Shovelling files from the host environment seems like a reasonable fit at times. For things like keys and network/device configuration, these are definitely easiest to just slurp from the host.

cd /lisbon/debian
              [Copy in the fstab(5) and fix it up.]
cp -p /etc/fstab               etc/fstab
mg etc/fstab
              [Copy in a bunch of local configuration:]
cp -p /etc/doas.conf           etc/doas.conf
cp -p /etc/sudoers             etc/sudoers
cp -p /etc/hosts               etc/hosts
cp -p /etc/localtime*          etc/
cp -p /etc/ssh/*_key*          etc/ssh
cp -p /etc/zfs/*               etc/zfs/
              [Copy Bluetooth configuration and state:]
rsync -Pai /etc/bluetooth/          etc/bluetooth
rsync -Pai /var/lib/bluetooth/      var/lib/bluetooth
rsync -Pai /var/lib/blueman/        var/lib/blueman
              [Copy print configuration:]
rsync -Pai /etc/cups/               etc/cups
              [Copy firewall configuration:]
rsync -Pai /etc/firewalld/          etc/firewalld
              [Copy network configuration and state:]
rsync -Pai /etc/NetworkManager/     etc/NetworkManager
rsync -Pai /var/lib/NetworkManager/ var/lib/NetworkManager
              [Copy sound system state:]
rsync -Pai /var/lib/alsa/           var/lib/alsa

I then push in the rest of my configuration. This gives me a whole stack of stuff for free, including hostid, hostname, krb5.conf, locale.gen, modprobe.d, ntp.conf, systemd configuration, and some ZFS support scripts.

rsync -Pai --chown=root:root ~jashank/.system/_etc/ etc

(There’s almost definitely stuff missing that I’ve forgotten entirely, and which I’m sure I’ll remember eventually.)

Do more configuration; install packages.

This is the first time we get to actually see whether our system even works! I’m excited. (Well, I was the first time. After about the tenth, it wasn’t nearly as much fun.)

                 [strip comments from the package list.]
ccat ~/etc/packages.debian.lisbon > /lisbon/debian/var/tmp/pkgs
                 [drop into a new systemd slice]
sudo systemd-nspawn --quiet --directory=/lisbon/debian --register=yes --as-pid2

apt update
apt install $(cat /var/tmp/pkgs)
        [download the Internet...]

In particular, packages on this list include zfs-initramfs and zfsutils-linux for ZFS, grub-efi-amd64 so we can have some bootloader support glue, a linux-image-amd64 to run, and some firmware. There’s lots of other stuff too!

Futz with GRUB. (Optionally, break UEFI.)

I don’t use grub-mkconfig(8) so this is usually a fairly manual process.

At some point in the past, I wrote a GRUB function that looks like:

function load_linux {
	set	root=($zpool)/ROOT/$1/@

	set	kernel_img=$root/$2
	set	initrd_img=$root/$3
	set	rootparam=ZFS=lisbon/ROOT/$1
	shift; shift; shift

	echo	":: loading $kernel_img ... "
	linux	$kernel_img root=$rootparam rw resume=$l_resume "$@"

	if [ -f "$root/boot/amd-ucode.img" ]
	then
		echo	":: loading $root/boot/amd-ucode.img ... "
		initrd	$root/boot/amd-ucode.img
	fi

	echo	":: loading $initrd_img ... "
	initrd	$initrd_img

	echo	":: booting."
	echo	""
}

I can now add invocations like:

menuentry	'[lisbon] Debian Linux testing (linux)' \
	--class debian --class gnu-linux --class gnu --class os \
{
	load_linux debian vmlinuz initrd.img \
		security=selinux
}

menuentry	'[lisbon] Debian Linux testing (linux), fallback' \
	--class debian --class gnu-linux --class gnu --class os \
{
	load_linux debian vmlinuz.old initrd.img.old \
		security=selinux
}

I also had a brief interlude where I accidentally wiped my UEFI boot sequence — efibootmgr(8) sure is easy to use! — and ended up needing the UEFI Shell to recover. That’s always a great adventure, and I’m very glad that the UEFI shell exists. Once I got back into my system, I ended up doing:

efibootmgr \
--bootnum 0000 \
--create \
--disk /dev/nvme0n1p1 --part 1 \
--loader /EFI/GRUB/shimx64.efi \
--label 'shimx64/grubx64'

Enjoy.

Oh.