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:
apply_noobs_os_config.service
, which applies system setup instructions from the NOOBS installerregenerate_ssh_host_keys.service
, which generates random ssh host keys on first boot. We can do it at rootfs setup time, and it would probably come in handy to be able to install well-known host keys.sshswitch.service
, which enables ssh ifboot/ssh
is present. We can do it at rootfs setup time.
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:
- configuring keyboard and timezone
- setting a CPU scaling governor at boot cpufrequtils seems to do it automatically
sed -i 's| init=/usr/lib/raspi-config/init_resize\.sh||' /boot/cmdline.txt
, or boot will fail!
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.