Cleanup Raspbian

This is part of a series of posts on the design and technical steps of creating Himblick, a digital signage box based on the Raspberry Pi 4.

Rapsbian is designed to be an interactive system, but we want to build a noninteractive black box out of it, which should never ever get a keyboard plug into it. See the "Museum ceiling" use case.

Ideally we should use a plain Debian as a base, but the Raspberry Pi model 4 is not supported yet for that.

Instead, we start from Raspbian Lite, and remove the bits that get in the way.

Review of raspbian's customizations

Here is a review of the Raspbian customizations that we've found, and how we chose to keep them or remove them.

raspberrypi-bootloader, raspberrypi-kernel

It's the code in /boot, and I guess also how it can get updated: keep.

raspbian-archive-keyring

This makes it possible to use the raspbian apt repositories: keep.

raspberrypi-net-mods

Source: https://github.com/RPi-Distro/raspberrypi-net-mods

It's the part that copies /boot/wpa_supplicant.conf to /etc/wpa_supplicant and does other system tweaks.

This we need to remove, to do our own customization.

raspberrypi-sys-mods

Source: https://github.com/RPi-Distro/raspberrypi-sys-mods

It contains a lot of hardware-specific setups and udev rules that should probably be kept.

It also contains the sudo rule that allows pi to sudo without password.

It does have a number of services that we need to disable:

What is the purpose of rpi-display-backlight.service? I could not find an explanation on why it is needed in the file or in the git logs.

raspi-config

Source: https://github.com/RPi-Distro/raspi-config

It's the core of Raspbian's interactive configuration, which we need to remove, to avoid interactive prompts, and replace with doing the configuration we need at rootfs setup time.

It's still useful as a reference on what is the standard way in Raspbian to do things like changing keyboard and timezone, or setting up graphical autologin.

Removing this leaves various things to be done:

The last one is important: on first boot, Raspbian won't boot the standard system, but run a script to resize the root partition, remove itself from the kernel command line, and reboot into the system proper.

We took care of partitioning ourselves and we do not need this: it would actually fail leaving the boot stuck in an interactive prompt, since it will not expect to find our media partition after the rootfs.

raspi-copies-and-fills

Partial source: https://github.com/bavison/arm-mem, it misses the .deb packaging.

This installs a ld.preload library with hardware accelerated replacements for common functions.

Since Raspbian is supposed to run unmodified on all RaspberryPi hardwares, the base libc is not optimized, and preloads are applied according to platform.

The package installs a /etc/ld.so.preload configuration which contains:

/usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so

In my case, ${PLATFORM} is not getting replaced inside the chroot environment, giving slower execution and filling the console with linker warnings.

Since we know we're running on the RaspberryPi 4, we can replace ${PLATFORM} with aarch64 in the rootfs setup.

triggerhappy

It does no harm, but it's a running service that we aren't needing yet, and it makes sense to remove it.

dhcpcd5

dhcpcd5 is a network configurator.

We would rather use systemd-networkd, which is somehow more standard and should play well with a read only root filesystem.

Replace Raspbian's customizations

For the boot partition:

    def cleanup_raspbian_boot(self):
        """
        Remove the interactive raspbian customizations from the boot partition
        """
        # Remove ' init=/usr/lib/raspi-config/init_resize.sh' from cmdline.txt
        # This is present by default in raspbian to perform partition
        # resize on the first boot, and it removes itself and reboots after
        # running. We do not need it, as we do our own partition resizing.
        # Also, we can't keep it, since we remove raspi-config and the
        # init_resize.sh script would break without it
        self.file_contents_replace(
                relpath="cmdline.txt",
                search=" init=/usr/lib/raspi-config/init_resize.sh",
                replace="")

For the rootfs:

    def cleanup_raspbian_rootfs(self):
        """
        Remove the interactive raspbian customizations from the rootfs
        partition
        """
        # To support multiple arm systems, ld.so.preload tends to contain something like:
        # /usr/lib/arm-linux-gnueabihf/libarmmem-${PLATFORM}.so
        # I'm not sure where that ${PLATFORM} would be expanded, but it
        # does not happen in a chroot/nspawn. Since we know we're working
        # on the 4B, we can expand it ourselves.
        self.file_contents_replace(
                relpath="/etc/ld.so.preload",
                search="${PLATFORM}",
                replace="aarch64")

        # Deinstall unneeded Raspbian packages
        self.dpkg_purge(["raspberrypi-net-mods", "raspi-config", "triggerhappy", "dhcpcd5", "ifupdown"])

        # Disable services we do not need
        self.systemctl_disable("apply_noobs_os_config")
        self.systemctl_disable("regenerate_ssh_host_keys")
        self.systemctl_disable("sshswitch")

        # Enable systemd-network and systemd-resolvd
        self.systemctl_disable("wpa_supplicant")
        self.systemctl_enable("wpa_supplicant@wlan0")
        self.systemctl_enable("systemd-networkd")
        self.write_symlink("/etc/resolv.conf", "/run/systemd/resolve/stub-resolv.conf")
        self.systemctl_enable("systemd-resolved")
        self.write_file("/etc/systemd/network/wlan0.network", """[Match]
Name=wlan0

[Network]
DHCP=ipv4

[DHCP]
RouteMetric=20
""")
        self.write_file("/etc/systemd/network/eth0.network", """[Match]
Name=eth0

[Network]
DHCP=all

[DHCP]
RouteMetric=10
""")

After this point, /etc/resolf.conf in the chroot will point to a broken symlink unless resolved is running. To continue working in the chroot and have internet access, we can temporarily replace it with the host's resolv.conf:

    @contextmanager
    def working_resolvconf(self, relpath: str):
        """
        Temporarily replace /etc/resolv.conf in the chroot with the current
        system one
        """
        abspath = self.abspath(relpath)
        if os.path.lexists(abspath):
            fd, tmppath = tempfile.mkstemp(dir=os.path.dirname(abspath))
            os.close(fd)
            os.rename(abspath, tmppath)
            shutil.copy("/etc/resolv.conf", os.path.join(self.root, "etc/resolv.conf"))
        else:
            tmppath = None
        try:
            yield
        finally:
            if os.path.lexists(abspath):
                os.unlink(abspath)
            if tmppath is not None:
                os.rename(tmppath, abspath)

This leaves keyboard, timezone, wifi, ssh, and autologin, still to be configured. We'll do it in the next step.