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

After switching to Debian for a bit (and bootstrapping it from scratch!) I opted to switch to Fedora ‘rawhide’, and did so broadly non-destructively to my original Arch and Debian environment. How did I do it?

Short answer: ZFS boot environments. (And pain, and SQLite3.)

Long answer:

Create a boot environment.

This looks almost exactly the same as the way I did this for Debian.

Install a release identity.

For starters, I needed to install rpm on my host. (I suspect this probably could be worked around.) Like most distros that package cross-distro package managers, Debian have gone out of their way to make sure you don’t break your real system. Once you’ve done that:

rpm --root /lisbon/fedora --dbpath /var/lib/rpm --rebuilddb
alias rpm='rpm --force-debian --root /lisbon/fedora --dbpath /var/lib/rpm'

Next, you need to work out what packages to install.

The first bit is easy: you need to tell the nascent system that it’ll be a Fedora system, which you do by stuffing in the release packages, and the things that depends upon. Because Fedora 35 is the current stable, I’ll start with exactly it, as released.

These paths are all relative to /fedora/linux/releases/35/Everything/x86_64/os/ on whatever nearby mirror floats your boat and I dump all these RPMs into a directory stage0.

Packages/f/fedora-release-35-33.noarch.rpm
Packages/f/fedora-release-identity-basic-35-33.noarch.rpm
Packages/f/fedora-release-common-35-33.noarch.rpm
Packages/f/fedora-repos-35-1.noarch.rpm
Packages/f/fedora-repos-rawhide-35-1.noarch.rpm
Packages/f/fedora-gpg-keys-35-1.noarch.rpm

And that’s super easy to install.

rpm --install --verbose --hash stage0/*.rpm

Reinvent a dnf-based bootstrap.

This was probably unnecessary. It took me ages to discover microdnf, and I suspect that might be a better solution.

Now for the hard bit: because dnf is written in Python, it’s not as simple as grabbing a static binary and running it. Instead, you get to walk the dependency tree yourself! (I spent several minutes swearing after discovering microdnf.)

After some time, I eventually wound up with the following (where each indentation step is a new level of dependency resolution.)

Packages/d/dnf-4.9.0-1.fc35.noarch.rpm
 Packages/b/bash-5.1.8-2.fc35.x86_64.rpm
 Packages/p/python3-dnf-4.9.0-1.fc35.noarch.rpm
  Packages/d/dnf-data-4.9.0-1.fc35.noarch.rpm
  Packages/f/filesystem-3.14-7.fc35.x86_64.rpm
  Packages/g/glibc-2.34-7.fc35.x86_64.rpm
  Packages/l/libmodulemd-2.13.0-3.fc35.x86_64.rpm
  Packages/n/ncurses-libs-6.2-8.20210508.fc35.x86_64.rpm
  Packages/p/python3-3.10.0-1.fc35.x86_64.rpm
  Packages/p/python3-gpg-1.15.1-4.fc35.x86_64.rpm
  Packages/p/python3-hawkey-0.64.0-1.fc35.x86_64.rpm
  Packages/p/python3-libcomps-0.1.18-1.fc35.x86_64.rpm
  Packages/p/python3-libdnf-0.64.0-1.fc35.x86_64.rpm
  Packages/p/python3-rpm-4.17.0-1.fc35.x86_64.rpm
   Packages/a/audit-libs-3.0.6-1.fc35.x86_64.rpm
   Packages/b/basesystem-11-12.fc35.noarch.rpm
   Packages/b/bzip2-libs-1.0.8-9.fc35.x86_64.rpm
   Packages/e/elfutils-libelf-0.185-5.fc35.x86_64.rpm
   Packages/e/elfutils-libs-0.185-5.fc35.x86_64.rpm
   Packages/f/file-libs-5.40-9.fc35.x86_64.rpm
   Packages/g/glib2-2.70.0-5.fc35.x86_64.rpm
   Packages/g/glibc-common-2.34-7.fc35.x86_64.rpm
   Packages/g/glibc-langpack-en-2.34-7.fc35.x86_64.rpm
   Packages/g/gpgme-1.15.1-4.fc35.x86_64.rpm
   Packages/i/ima-evm-utils-1.3.2-3.fc35.x86_64.rpm
   Packages/l/libacl-2.3.1-2.fc35.x86_64.rpm
   Packages/l/libcap-2.48-3.fc35.x86_64.rpm
   Packages/l/libcomps-0.1.18-1.fc35.x86_64.rpm
   Packages/l/libdnf-0.64.0-1.fc35.x86_64.rpm
   Packages/l/libfsverity-1.4-4.fc35.x86_64.rpm
   Packages/l/libgcc-11.2.1-1.fc35.x86_64.rpm
   Packages/l/libreport-filesystem-2.15.2-6.fc35.noarch.rpm
   Packages/l/libsmartcols-2.37.2-1.fc35.x86_64.rpm
   Packages/l/libsolv-0.7.19-3.fc35.x86_64.rpm
   Packages/l/libstdc++-11.2.1-1.fc35.x86_64.rpm
   Packages/l/libyaml-0.2.5-6.fc35.x86_64.rpm
   Packages/l/libzstd-1.5.0-2.fc35.x86_64.rpm
   Packages/l/lua-libs-5.4.3-2.fc35.x86_64.rpm
   Packages/n/ncurses-base-6.2-8.20210508.fc35.noarch.rpm
   Packages/o/openssl-libs-1.1.1l-2.fc35.x86_64.rpm
   Packages/p/popt-1.18-6.fc35.x86_64.rpm
   Packages/p/python3-libs-3.10.0-1.fc35.x86_64.rpm
   Packages/r/rpm-build-libs-4.17.0-1.fc35.x86_64.rpm
   Packages/r/rpm-libs-4.17.0-1.fc35.x86_64.rpm
   Packages/r/rpm-sign-libs-4.17.0-1.fc35.x86_64.rpm
   Packages/s/setup-2.13.9.1-2.fc35.noarch.rpm
   Packages/s/sqlite-libs-3.36.0-3.fc35.x86_64.rpm
   Packages/x/xz-libs-5.2.5-7.fc35.x86_64.rpm
   Packages/z/zlib-1.2.11-30.fc35.x86_64.rpm
    Packages/c/ca-certificates-2021.2.50-3.fc35.noarch.rpm
    Packages/c/crypto-policies-20210819-1.gitd0fdcfb.fc35.noarch.rpm
    Packages/e/elfutils-default-yama-scope-0.185-5.fc35.noarch.rpm
    Packages/e/expat-2.4.1-2.fc35.x86_64.rpm
    Packages/g/gdbm-libs-1.20-2.fc35.x86_64.rpm
    Packages/g/gnupg2-2.3.2-2.fc35.x86_64.rpm
    Packages/g/gnutls-3.7.2-2.fc35.x86_64.rpm
    Packages/j/json-c-0.15-2.fc35.x86_64.rpm
    Packages/k/keyutils-libs-1.6.1-3.fc35.x86_64.rpm
    Packages/l/libassuan-2.5.5-3.fc35.x86_64.rpm
    Packages/l/libattr-2.5.1-3.fc35.x86_64.rpm
    Packages/l/libcap-ng-0.8.2-6.fc35.x86_64.rpm
    Packages/l/libffi-3.1-29.fc35.x86_64.rpm
    Packages/l/libgomp-11.2.1-1.fc35.x86_64.rpm
    Packages/l/libgpg-error-1.42-3.fc35.x86_64.rpm
    Packages/l/libmount-2.37.2-1.fc35.x86_64.rpm
    Packages/l/libnsl2-1.3.0-4.fc35.x86_64.rpm
    Packages/l/librepo-1.14.2-1.fc35.x86_64.rpm
    Packages/l/libselinux-3.2-4.fc35.x86_64.rpm
    Packages/l/libtirpc-1.3.2-1.fc35.x86_64.rpm
    Packages/l/libuuid-2.37.2-1.fc35.x86_64.rpm
    Packages/l/libxcrypt-4.4.26-4.fc35.x86_64.rpm
    Packages/l/libxml2-2.9.12-6.fc35.x86_64.rpm
    Packages/m/mpdecimal-2.5.1-2.fc35.x86_64.rpm
    Packages/p/pcre-8.45-1.fc35.x86_64.rpm
    Packages/p/python-pip-wheel-21.2.3-2.fc35.noarch.rpm
    Packages/p/python-setuptools-wheel-57.4.0-1.fc35.noarch.rpm
    Packages/r/readline-8.1-3.fc35.x86_64.rpm
    Packages/r/rpm-4.17.0-1.fc35.x86_64.rpm
    Packages/t/tpm2-tss-3.1.0-3.fc35.x86_64.rpm
    Packages/t/tzdata-2021b-1.fc35.noarch.rpm
    Packages/z/zchunk-libs-1.1.15-2.fc35.x86_64.rpm
     Packages/c/coreutils-8.32-31.fc35.x86_64.rpm
     Packages/c/curl-7.78.0-3.fc35.x86_64.rpm
     Packages/g/gmp-6.2.0-7.fc35.x86_64.rpm
     Packages/g/grep-3.6-4.fc35.x86_64.rpm
     Packages/k/krb5-libs-1.19.2-2.fc35.x86_64.rpm
     Packages/l/libarchive-3.5.2-2.fc35.x86_64.rpm
     Packages/l/libblkid-2.37.2-1.fc35.x86_64.rpm
     Packages/l/libcom_err-1.46.3-1.fc35.x86_64.rpm
     Packages/l/libcurl-7.78.0-3.fc35.x86_64.rpm
     Packages/l/libgcrypt-1.9.4-1.fc35.x86_64.rpm
     Packages/l/libidn2-2.3.2-3.fc35.x86_64.rpm
     Packages/l/libksba-1.6.0-2.fc35.x86_64.rpm
     Packages/l/libsepol-3.2-3.fc35.x86_64.rpm
     Packages/l/libtasn1-4.16.0-6.fc35.x86_64.rpm
     Packages/l/libunistring-0.9.10-14.fc35.x86_64.rpm
     Packages/n/nettle-3.7.3-2.fc35.x86_64.rpm
     Packages/n/npth-1.6-7.fc35.x86_64.rpm
     Packages/o/openldap-2.4.59-3.fc35.x86_64.rpm
     Packages/p/p11-kit-0.23.22-4.fc35.x86_64.rpm
     Packages/p/p11-kit-trust-0.23.22-4.fc35.x86_64.rpm
     Packages/p/pcre2-10.37-4.fc35.x86_64.rpm
     Packages/s/sed-4.8-8.fc35.x86_64.rpm
     Packages/s/shadow-utils-4.9-3.fc35.x86_64.rpm
      Packages/a/alternatives-1.19-1.fc35.x86_64.rpm
      Packages/c/coreutils-common-8.32-31.fc35.x86_64.rpm
      Packages/c/cyrus-sasl-lib-2.1.27-13.fc35.x86_64.rpm
      Packages/g/gawk-5.1.0-4.fc35.x86_64.rpm
      Packages/l/libbrotli-1.0.9-6.fc35.x86_64.rpm
      Packages/l/libnghttp2-1.45.1-1.fc35.x86_64.rpm
      Packages/l/libpsl-0.21.1-4.fc35.x86_64.rpm
      Packages/l/libsemanage-3.2-4.fc35.x86_64.rpm
      Packages/l/libsigsegv-2.13-3.fc35.x86_64.rpm
      Packages/l/libssh-0.9.6-1.fc35.x86_64.rpm
      Packages/l/libverto-0.3.2-2.fc35.x86_64.rpm
      Packages/l/lz4-libs-1.9.3-3.fc35.x86_64.rpm
      Packages/p/pcre2-syntax-10.37-4.fc35.noarch.rpm
       Packages/l/libssh-config-0.9.6-1.fc35.noarch.rpm
       Packages/m/mpfr-4.1.0-8.fc35.x86_64.rpm
       Packages/p/publicsuffix-list-dafsa-20210518-2.fc35.noarch.rpm

I started doing this by hand, and eventually wrote some scripts, because it started getting tricky — one useful (to me) fact is that the repository metadata is all usable via a SQLite database, so I downloaded that, and hacked together:

#!/usr/bin/env zsh

primary=core/9103df16446e4e0b7eb02fe94936b7ec20c2caac436acedf9a8ba19f52c5af51-primary.sqlite

QUERY()   { sqlite3 -readonly $primary "select packages.location_href from packages $@" }
name()    { QUERY "where packages.name like '$@';" }
provide() { QUERY "join provides on (packages.pkgKey = provides.pkgKey) where provides.name like '$@';" }

op="$1"; shift
case "$op" in
	(name)
		name "$@"
		;;
	(provide)
		provide "$@"
		;;
	(*)
		exit 1
		;;
esac

# >>> ./match name libssh
# Packages/l/libssh-0.9.6-1.fc35.i686.rpm
# Packages/l/libssh-0.9.6-1.fc35.x86_64.rpm

With that package list in hand, ,and then downloaded into stage1:

rpm --install --verbose --hash stage1/*.rpm

It turns out this package set is a non-trivial portion of a basic Fedora system: remarkably little is needed afterwards to be able to step through more of the installation process.

Finish the bootstrap of Fedora 35.

The next thing to do is to step into our new system, and start the process of lifting it from Fedora 35 to Fedora rawhide — presently, rawhide is Fedora 37 — which is surprisingly painless.

systemd-nspawn --quiet --directory=/lisbon/fedora --register=yes --as-pid2

dnf shell
install dnf-command(system-upgrade)
update
run

There’s two important things happening here: first, we update to the newest packages, which is absolutely a prerequisite to any sort of release-hopping operations; and second, we summon up the release-hopping tool. It’s important to note here that I’ve not installed anything beyond the bootstrapping package set: I’m going to install more only once I’ve arrived on rawhide. I use dnf shell to run the whole thing in one transaction.

Next, we need to do some weird key juggling. I’m not quite sure why this isn’t running automatically.

rpm -K /etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-3[567]-x86_64

Upgrade to Fedora rawhide.

This is the fun bit: download and install the new system!

dnf system-upgrade download --refresh --releasever=rawhide
export DNF_SYSTEM_UPGRADE_NO_REBOOT=True
dnf system-upgrade reboot --releasever=rawhide
dnf system-upgrade upgrade --releasever=rawhide

Now, we “reboot”.

exit

systemd-nspawn --quiet --directory=/lisbon/fedora --register=yes --as-pid2


Set up additional repositories.

At the moment, the OpenZFS folks don’t have any Fedora 36 or later repositories. I’m therefore assuming that the Fedora 35 packages will work, and this so far has been true.

dnf install https://zfsonlinux.org/fedora/zfs-release.fc35.noarch.rpm
rpm -K /etc/pki/rpm-gpg/RPM-GPG-KEY-zfsonlinux

I’d also like RPM Fusion.

dnf install \
https://mirrors.rpmfusion.org/free/fedora/rpmfusion-free-release-rawhide.noarch.rpm \
https://mirrors.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-rawhide.noarch.rpm
rpm -K /etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-{,non}free-fedora-rawhide

For completeness, I’m doing the key juggling myself again. These do, it turns out, happen automatically.

Install ZFS bits.

We need to install everything except ZFS here —

dnf shell
install @core
install grub2-efi-x64 grub2-efi-x64-modules
install grub2-common grub2-tools grub2-tools-efi
install shim-x64
install efibootmgr
install kernel kernel-headers kernel-devel
install python3-dnf-plugin-post-transaction-actions
update
run

— because the next thing I do is this extremely evil, cursed, horrible thing that you should absolutely not ever do at all under any circumstances:

dnf remove kernel{,-core,-devel,-modules}
dnf install \
--disablerepo=rawhide \
--enablerepo=fedora,updates --setopt=releasever=36 \
kernel{,-core,-devel,-modules}

Woah, why am I throwing away the rawhide kernel? Because, it turns out, ZFS collides with it: OpenZFS isn’t quite ready for the 5.18 kernels, and after some cajoling I found that the Fedora 36-Beta kernels work fine — why yes, I’m doing this during the pointy end of the Fedora 36 release cycle, so everything’s full of exciting weirdness! — so I’m opting into them over rawhide. I suspect I’ll continue to track released kernels, but run the bleeding-edge user-lands. (I sure wish the Modularity work was possible here!)

With all that dealt with, at last:

dnf install zfs zfs-dracut

Install the rest of the universe.

ccat ~/etc/packages.fedora.lisbon > /lisbon/fedora/var/tmp/pkgs

I also wound up doing some group marking too.

dnf install $(cat /var/tmp/pkgs)

Interlude: fix passwd, shadow, group, gshadow.

Here’s a horrible quirk to deal with: I can’t directly reuse the system credentials databases (i.e., passwd(5), shadow(5), group(5), gshadow(5)) between these separate system roots, because the base set of identities between the systems differs; and then the identities generated by systemd’s sysusers.d(5) mechanism tend to have no fixed UIDs or GIDs specified, so I now have three separate UID/GID tables.

I started trying to unify them by hand, spent some time tearing my hair out, and eventually reached the point of wanting to build a tool to do it. This caused more tearing of hair (and lots of grumbling at z3 — that should be the ideal tool for the job, except trying to express my problem to it proved to be beyond my capabilities).

I eventually hacked up generate-sys-cred-tables, which let me define users and groups using a single configuration file, and then generates a matching set of passwd, shadow, group, and gshadow files.

I then hacked up some tools to deal with rewriting UIDs and GIDs in a system: generate-uid-gid-deltas takes current and new credentials tables, and generates a mapping of old-to-new identifiers; and translate-mtree-uids-gids, takes mtree(8) on standard input, and applies the generated mappings from generate-uid-gid-deltas; and install-new-sys-cred-tables swaps from the current to the new tables within that particular sysroot.

(translate-mtree-uids-gids probably looks like it should be really slow — but because mtree(8) was more CPU-bound, the performance overhead here was effectively negligible.)

Now I change directories and run this mess. I previewed the changes, and then ran the rewrite.

generate-uid-gid-deltas /lisbon/fedora
cd /lisbon/fedora
sudo mtree -c | translate-uids-gids ~/ch.{passwd,shadow}.fedora | sudo mtree
                  [... lots of output elided; check it makes sense!]
                  [commit by making the final command `mtree -u']

And I install the tables using install-new-sys-cred-tables.

cd /lisbon/fedora/etc
sudo install-new-sys-cred-tables

(I also ran this in my Arch install. I’ll eventually run it in my Debian install, but I’m not quite mad enough to change the creds tables this substantially on a running *nix system…)

Do post-installation configuration.

This looks almost exactly the same as the way I did this for Debian.

One notable addition: sort out SELinux. I’m going to run permissive for a bit, until I get used to dealing with it again — whilst, yes, I do run SELinux in my current Debian environment, the default Fedora policy set is different, and definitely needs more consciousness.

sed -i -e 's/^SELINUX=.*/SELINUX=permissive/' etc/selinux/config
systemd-nspawn --quiet --directory=/lisbon/fedora --register=yes --as-pid2
              [force a full relabel.]
restorecon -T64 -p -F -r /

Beware, though, that doing a normal mass relabel will clobber any labels on the other system-roots. You’ll need to exercise some care about how to apply labels to things like home directories or any other content to be shared between systems. In particular, I had to do a bit of experimenting to work out how/when/what to relabel.

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

Enjoy.

This is, of course, the interesting bit.