Generating an OS from a static-QEMU Raspbian chroot¶
This page is a work in progress.
This page describes a setup where a Pi Zero OS image is generated from a Raspbian system that runs through QEMU on an x86_64 host, allowing for package installation/compilation/etc outside of the Pi, then installed on the Pi’s SD card and boot up from the real hardware.
The OS image produced in this tutorial is just the minimum working OS. To install a desktop environment and Symbolibre applications, follow with Setting up the Symbolibre OS and applications.
Basic Raspbian chroot¶
First install a statically-linked version of QEMU that can be used to transparently execute ARM executables on your x86_64 Linux install.
On Arch specifically
qemu-user-static-bin to avoid compiling QEMU).
binfmt-qemu-static will set up executable formats to use the
statically-linked QEMU in the binfmt service; but at the time it is
installed at a dependency, that QEMU is not available so it fails.
Restart the service manually once the static QEMU is installed.
(host)% pacman -S binfmt-qemu-static qemu-user-static-bin (host)% systemctl restart systemd-binfmt
On Debian see . The QEMU binaries might need to copied or mounted to the chroot (see the appendix on the wiki page) which can be done elegantly with a bind-mount (see later).
Create a starter Raspbian install with
debootstrap on a new directory.
Because this is for a foreign architecture, only the first stage can be
executed natively. You can use a mounted separate partition or a plain
--variant=minbase might provide a smaller image.
(host)% debootstrap --foreign --arch=armhf bullseye symbolibre-os/ https://archive.raspbian.org/raspbian/
The second stage of debootstrap must be executed from the
/debootstrap/debootstrap executable left in the new system. This can
be done through transparent QEMU emulation. However, the shared
armhf libraries are also in the new system, so the root must be
changed for dynamic loading to succeed.
Before chrooting, on some distros the QEMU binaries should be mounted into the new system.
(host)% mount --bind /usr/bin/qemu-arm-static symbolibre-os/usr/bin
Then log into Raspbian.
(host)% sudo chroot symbolibre-os /bin/env -i TERM=$TERM /bin/bash
env -i clears the host environment variables. The
variable is preserved as it allows the capabilities of your terminal emulator
to be properly identified. If your terminal is not supported by the
newly-installed OS, you can always set
Once in the Raspbian chroot, complete the system boostrap.
% debootstrap/debootstrap --second-stage
Add a new standard user (
symbolibre with password
% adduser symbolibre
That’s it for the base system.
Adding some detail¶
Because the guest system is not fully booted up from scratch with
systemd, a few things don’t work out of the box (for instance there is no
/proc). On Arch, one can use arch-chroot or manually bind the required
filesystems into the chroot. See
 for both
options. Accessing dbus in this setup will affect your host system, so be
For now, set the hostname in
/etc/hostname and update
sudo doesn’t complain about not being able to resolve it. Don’t use
hostnamectl tools as it will affect the host system.
The name should already be set properly when booting from the Pi Zero.
% echo slcalc | tee /etc/hostname
/etc/hosts should look like this.
127.0.0.1 localhost 127.0.0.1 slcalc (...)
I suggest using a script to start the chroot (launch with
#! /usr/bin/env bash # usage: sudo ./symbolibre-os.sh [--mount-qemu] [--arch-chroot] mount_qemu= use_arch_chroot= for arg in "$@"; do case "$arg" in "--mount-qemu") mount_qemu=1;; "--arch-chroot") use_arch_chroot=1;; *) echo "unrecognized argument $arg" >&2; exit 1 esac done # Use a basic standard terminal export TERM=linux # Set the proper host name export HOSTNAME="slcalc" # Mount QEMU executables into the chroot if needed if [[ ! -z "$mount_qemu" ]]; then mount --bind /usr/bin/qemu-arm-static symbolibre-os/usr/bin fi # arch-chroot version: best to have the target directory a mount point, so if # it's not make it one artifically if [[ ! -z "$use_arch_chroot" ]]; then mount --bind symbolibre-os/ symbolibre-os/ env -i TERM="$TERM" HOSTNAME="$HOSTNAME" arch-chroot symbolibre-os/ umount symbolibre-os/ exit 0 fi # Normal chroot. Debian will supply a default PATH env -i TERM="$TERM" HOSTNAME="$HOSTNAME" chroot symbolibre-os/
Now before we can install packages we need to keep the system up-to-date with the repositories.
% apt update % apt upgrade
If the upgrade step complains of unmet dependencies, run
apt --fix-broken install as advertised before upgrading again.
Kernel and boot from Pi Zero¶
The system is now mostly complete; we still need to install a kernel, an fstab, and a couple other files to get it to boot for real instead of relying on the host’s fully-booted environment. All this section is run from the host x86_64 Linux.
Option 1: Leeching from Raspberry Pi OS¶
This option grabs the kernel and modules of Raspberry Pi OS. Download a Raspberry Pi OS Lite image from the official download page and copy it the SD card so that we can snatch the kernel and some files out of it.
% sudo dd if=2020-05-27-raspios-buster-lite-armhf.img of=<device> bs=4M
Then copy out the stuff we need.
rsync is used here to keep the file
hierarchy. We save the following files:
/lib/modules, which contains the kernel modules;
raspi-config, which is used as init script during the first boot.
The first include
*/ makes sure every directory is traversed, which
is needed to find subdirectories.
--prune-empty-dirs removes unused
directories at the end. Paths with interesting files are included and
everything else is excluded at the end with
# Set MOUNTPOINT to the path of the mounted root partition of Raspberry Pi OS % MOUNTPOINT="/path/to/rootfs" % mkdir rpios-root % sudo rsync -a --prune-empty-dirs --include='*/' --include='lib/modules/***' --include=etc/fstab -r --include='usr/lib/raspi-config/***' --include=usr/bin/raspi-config --exclude='*' $MOUNTPOINT/ rpios-root/
It seems that of the subfolders of
lib/modules, only the one named
<x>.<y>.<z>+ is used. Not copying the others saves about 200 MB.
With that saved, reset the root partition with a copy of the chrooted
Raspbian install and restore the saved files. The
-K option to
rsync ensures that
/lib/modules is copied correctly with respect
/lib being a symlink to
/usr/lib in Raspbian (ie.
is copied into
/usr/lib; the default behavior replaces the symlink
with a new directory).
% sudo rm -rf $MOUNTPOINT/* % sudo cp -ra ../symbolibre-os/* $MOUNTPOINT/ % sudo rsync -avK rpios-root/ $MOUNTPOINT/
Then check that the fstab has correct partition UUIDs and modify it
accordingly if needed (this will be needed in particular if getting the
fstab file from another SD card). Look up the UUIDs with
% lsblk -o +PARTUUID <device> NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT PARTUUID sdc 8:32 1 7.4G 0 disk ├─sdc1 8:33 1 256M 0 part 2fed7fee-01 └─sdc2 8:34 1 1.5G 0 part /path/to/rootfs 2fed7fee-02
Then the fstab should look like this.
proc /proc proc defaults 0 0 PARTUUID=2fed7fee-01 /boot vfat defaults 0 2 PARTUUID=2fed7fee-02 / ext4 defaults,noatime 0 1 # a swapfile is not a swap partition, no line here # use dphys-swapfile swap[on|off] for that
Finally, put the SD card into the Raspberry Pi and boot.
Option 2: From official firmware with pre-built kernel¶
Start from a blank microSD card and make a new partition table.
All existing data on the SD card will be lost.
A boot partition of at least 100 Mo, type
W95 FAT32 (LBA)(type ID is
A root partition of at least 1.5 Go, type
Linux(type ID is
0x83), preferably with an ext4 filesystem.
The instructions that follow use the command-line tool
parted for partitioning,
but more user-friendly partitioning tool can also be used:
The exact partition sizes do not matter.
The first partition should not start at sector 0 because space is needed for the partition table. We start at sector 8192, which is what Raspberry Pi OS does. The first partition then stops at sector 532479 and the second partition starts at 532480, going all the way to 100% of the card size.
% sudo parted <device> (parted) mklabel msdos (parted) mkpart primary fat32 8192s 532479s (parted) mkpart primary ext4 532480s 100%
The disk identifier changes when the partition table is changed, so make
sure to update the partition UUIDs if you’re going to reuse a previous
Create the file systems, for instance here with
/dev/mmcblk0 as a
device, and give useful names at the same time.
% sudo mkfs.fat -F 32 -n slboot /dev/mmcblk0p1 % sudo mkfs.ext4 -L slroot /dev/mmcblk0p2
Now get the repository with the Raspberry Pi boot files and kernel modules from Github (~400M once uncompressed).
Mount both partitions and copy the boot folder to /boot. Add in a
cmdline.txt. It can be derived from the
Raspberry Pi OS iso, though
config.txt is almost empty. Remember to
change the PARTUUID to the correct ID in
% MOUNT_BOOT="/path/to/slboot" % MOUNT_ROOT="/path/to/slroot" % sudo cp -r firmware/boot/* $MOUNT_BOOT % echo "console=serial0,115200 console=tty1 root=PARTUUID=$(sudo blkid -s PARTUUID -o value /dev/mmcblk0p2) rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait" | sudo tee $MOUNT_BOOT/cmdline.txt
The boot partition is now complete. For the root partition, combine the
chrooted Raspbian install with the kernel modules from the
directory and a copy of
/etc/fstab from the previous section.
% sudo cp -ra ../symbolibre-os/* $MOUNT_ROOT/ % sudo cp -r firmware/modules $MOUNT_ROOT/usr/lib/ # also copy the fstab
That’s it. Plug the SD card into the Raspberry Pi and boot.
Updating the OS from now on¶
The boot partition that we just created is independent from the OS root. The OS can be updated from the chroot by just synchronizing the contents of the root partition.
% rsync -a --progress --delete --exclude etc/fstab --exclude usr/lib/modules --exclude var/cache/apt symbolibre-os/ $MOUNT_ROOT/
This method can be used even with different OS variants and chroots as long as they all agree on the kernel version and boot settings.