ELBE: automated building of Ubuntu images for a Raspberry Pi 3B

Building embedded Linux systems

ELBETypical embedded Linux systems include a wide number of software components, which all need to be compiled and integrated together. Two main approaches are used in the industry to integrate such embedded Linux systems: build systems such as Yocto/OpenEmbedded, Buildroot or OpenWrt, and binary distributions such as Debian, Ubuntu or Fedora. Of course, both options have their own advantages and drawbacks.

One of the benefits of using standard binary distributions such as Debian or Ubuntu is their widespread use, their serious and long-term security maintenance and their large number of packages. However, they often lack appropriate tools to automate the process of creating a complete Linux system image that combines existing binary packages and custom packages.

In this blog post, we introduce ELBE (Embedded Linux Build Environment), which is a build system designed to build Debian distributions and images for the embedded world. While ELBE was initially focused on Debian only, Bootlin contributed support for building Ubuntu images with ELBE, and this blog post will show as an example how to build an Ubuntu image with ELBE for a Raspberry Pi 3B.

ELBE base principle

When you first run ELBE, it creates a Virtual Machine (VM) for building root filesystems. This VM is called initvm. The process of building the root filesystem for your image is to submit and XML file to the initvm, which triggers the building of an image.

The ELBE XML file can contain an archive, which can contain configuration files, and additional software. It uses pre-built software in the form of Debian/Ubuntu packages (.deb). It is also possible to use custom repositories to get special packages into the root filesystem. The resulting root file system (a customized Debian or Ubuntu distribution) can still be upgraded and maintained through Debian’s tools such as APT (Advanced Package Tool). This is the biggest difference between ELBE and other build systems like the Yocto Project and Buildroot.

Bootlin contributions

As mentioned in this blog post introduction, Bootlin contributed support for building Ubuntu images to ELBE, which led to the following upstream commits:

Build an Ubuntu image for the Raspberry Pi 3B

We are now going to illustrate how to use ELBE by showing how to build an image for the popular RaspberryPi 3B platform.

Add required packages

This was tested on Ubuntu 20.04. Install the below packages if needed, and make sure you are in the libvirt, libvirt-qemu and kvm groups:

$ sudo apt install python3 python3-debian python3-mako \
  python3-lxml python3-apt python3-gpg python3-suds \
  python3-libvirt qemu-utils qemu-kvm p7zip-full \
  make libvirt-daemon libvirt-daemon-system \
  libvirt-clients python3-urwid
$ sudo adduser youruser libvirt 
$ sudo adduser youruser libvirt-qemu
$ sudo adduser youruser kvm
$ newgrp libvirt
$ newgrp libvirt-qemu
$ newgrp kvm

Prepare ELBE initvm

First, you need to clone ELBE’s git reposority:

git clone https://github.com/Linutronix/elbe.git

We need to use the v13.2 version because our latest contributions for Ubuntu support made it to 13.2:

$ cd elbe
$ git checkout v13.2

To create the initvm:

$ PATH=$PATH:$(pwd)
$ elbe initvm create --devel

The --devel parameter allows to use ELBE from the current working directory into the initvm.

If the command fails with the Signature with unknown key: message you need to add these keys to apt. Use the following command where XXX is the key to be added:

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys XXX

Creating your initvm should take at least 10 to 20 minutes.

In case you rebooted your computer or stopped the VM, you will need to start it:

$ elbe initvm start

Create an ELBE project for our Ubuntu image.

To begin with, we will base our image on the armhf-ubuntu example. We create an ELBE pbuilder project and not a simple ELBE project because we later want to build our own Linux kernel package for our board:

$ elbe pbuilder create --xmlfile=examples/armhf-ubuntu.xml \
  --writeproject rpi.prj --cross

The project identifier is written to rpi.prj. We save the identifier to a shell variable to simplify the next ELBE commands:

$ PRJ=$(cat rpi.prj)

Build the Linux package

As explained earlier we want to use ELBE to build our package for the Linux kernel. ELBE uses the standard Debian tool pbuilder to build packages. Therefore, we need to have debianized sources (i.e sources with the appropriate Debian metadata in a debian/ subfolder) to build a package with pbuilder.

First clone the Linux repositories:

$ git clone -b rpi-5.10.y https://github.com/raspberrypi/linux.git
$ cd linux

Debianize the Linux repositories. We use the elbe debianize command to simplify the generation of the debian folder:

$ elbe debianize

Fill the settings in the UI as follows (make sure you reduce the font size if you don’t see the Confirm button):

Make sure you set Name to rpi. Otherwise, you won’t get the output file names we use in the upcoming instructions.

The debianize command helps to create the skeleton of the debian folder in the sources. It has been pre-configured for a few packages like bootloaders or the Linux kernel, to create the rules to build these packages. It may need further modifications to finish the packaging process. Take a look a the manual to have more information on debianization. In our case, we need to tweak the debian/ folder with the two following steps to cross-build the Raspberry Linux kernel without error.

Append the below lines to the debian/rules file (use tabs instead of spaces):

	dh_strip -Xscripts

	dh_shlibdeps -Xscripts

Remove the following line from the debian/linux-image-5.10-rpi.install file:


Update the source format:

$ echo "1.0" > debian/source/format

The Linux kernel sources are now ready, we can run elbe pbuilder to compile them:

$ mkdir ../out
$ elbe pbuilder build --project $PRJ --cross --out ../out

According to how fast your system is, this can run for hours!

If everything ends well without error the out/ directory has been filled with output files:

$ ls ../out

Update the Ubuntu XML image description

Now we have our Linux kernel packaged we can move on to the image generation. Since we started from examples/armhf-ubuntu.xml, we will modify this file to fit our needs.

We begin by adding the Linux kernel package to the XML image description in the pkg-list node:


We also have to add the Device Tree to the boot/ directory because the Linux kernel package installs all the Device Trees into the /usr/lib directory.

This change is part of the rootfs modifications, therefore it is described under the finetuning XML node. We also rename the kernel image to kernel.img:

	<cp path="/usr/lib/linux-image-5.10-rpi/bcm2710-rpi-3-b.dtb">/boot/bcm2710-rpi-3-b.dtb</cp>
	<cp path="/usr/lib/linux-image-5.10-rpi/overlays">/boot/overlays</cp>
	<mv path="/boot/vmlinuz-5.10-rpi">/boot/kernel.img</mv>

We want to use an SD card on our Raspberry Pi, so we have to describe the partitioning of our image. For this purpose, we add the images and the fstab XML nodes to the target XML node:


The Raspberry Pi board also needs firmware binaries and configurations file to boot properly. We will use the overlay directory to add these Raspberry firmware files to the image:

$ mkdir -p overlay/boot
$ cd overlay/boot
$ wget https://github.com/raspberrypi/firmware/raw/1.20210201/boot/bootcode.bin
$ wget https://github.com/raspberrypi/firmware/raw/1.20210201/boot/start.elf
$ wget https://github.com/raspberrypi/firmware/raw/1.20210201/boot/fixup.dat
$ echo "console=ttyAMA0,115200 console=tty1 root=/dev/mmcblk0p2 rootwait" > cmdline.txt
$ echo "dtoverlay=miniuart-bt" > config.txt

ELBE stores the overlay uuencoded in the XML file using the chg_archive command:

$ elbe chg_archive examples/armhf-ubuntu.xml overlay

The archive node got created in the XML file.

To tell ELBE that the XML file has changed, you need to send it to the initvm:

$ elbe control set_xml $PRJ examples/armhf-ubuntu.xml

Then build the image with ELBE:

$ elbe control build $PRJ
$ elbe control wait_busy $PRJ

Finally, if the build completes successfully, you can retrieve the image file from the initvm:

$ elbe control get_files $PRJ
$ elbe control get_file $PRJ sdcard.img.tar.gz

Now you can flash the SD card image:

$ tar xf sdcard.img.tar.gz
$ dd if=sdcard.img of=/dev/sdX bs=1M

And boot the board with root and foo as login and password:

Ubuntu 18.04.1 LTS myUbuntu ttyAMA0

myUbuntu login: root
Welcome to Ubuntu 18.04.1 LTS (GNU/Linux 5.10-rpi armv7l)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage

The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.


Note: Ubuntu cannot be built for Raspberry A, B, B+, 0 and 0W according to https://wiki.ubuntu.com/ARM/RaspberryPi, as Ubuntu targets the ARMv7-A architecture, while the older RaspberryPi use an ARMv6 processor.

Further details

HOWTO – Mailing lists with Mailman on Ubuntu 10.04

GNU mailman logoBootlin is not in the system administration business (we offer free and open-source solutions for embedded systems), but we do our best to share whatever experience we acquire, and whatever code we produce.

We configured a KVM virtual machine to run our mailing lists, and we used Ubuntu 10.04, the long term support (LTS) version. Here are instructions based on this experience. You could also use the same instructions to install Ubuntu 10.04 on a tiny, low power ARM board. And if you wish to use a more recent version of Ubuntu, I expect the steps to be very similar.

Here we assume that your domain is example.com and that you install your mailing list software on lists.example.com. It is indeed a good idea to install your mailing list software on a different server. This way, you won’t mess up with your main web and mail servers. Not having CGI scripts running on it will also keep your main mail server more secure.

Install packages

Install the Apache web server. It will allow administrators to configure and manage the lists, and users to subscribe, unsubscribe and tune their subscription settings.

sudo apt-get install apache2

For e-mail delivery, I chose the Postfix MTA (Mail Transport Agent). Exim would have been a good solution too, but I am more familiar with Postfix, which is already in use on our main mail server.

sudo apt-get install postfix

Choose the Internet site option.

Now install the mailman package:

sudo apt-get install mailman

Select the languages that you want your mailing list interfaces to support. I chose English and French for the moment. To add more languages later, run:

sudo dpkg-reconfigure mailman

Configure the Apache web server

First copy the sample Apache configuration file provided by the mailman package:

cp /etc/mailman/apache.conf /etc/apache2/sites-available/mailman

Now, in /etc/apache2/sites-available/mailman, enable short URLs by enabling:

ScriptAlias /mailman/ /usr/lib/cgi-bin/mailman/

Also modify /etc/mailman/mm_cfg.py:

DEFAULT_URL_PATTERN = 'http://%s/mailman/'

The last step is to enable your mailman site in Apache:

sudo a2ensite mailman
sudo /etc/init.d/apache2 restart

You should now have a new symbolic link in /etc/apache2/sites-enabled/.

Check that the Mailman website works by opening your mailing lists home page: http://lists.example.com/mailman/listinfo. It should look like https://lists.bootlin.com/mailman/listinfo.

Configure postfix

Enable the following line in /etc/mailman/mm_cfg.py:


Once the MTA is configured, generate Mailman specific aliases for Postfix:

sudo /usr/lib/mailman/bin/genaliases

Now, you need to configure Postfix through its main.cf file. A convenient way to do this is to run the below commands:

sudo postconf -e 'relay_domains = lists.example.com'
sudo postconf -e 'transport_maps = hash:/etc/postfix/transport'
sudo postconf -e 'mailman_destination_recipient_limit = 1'
sudo postconf -e 'alias_maps = hash:/etc/aliases, hash:/var/lib/mailman/data/aliases'

Also add the following line to /etc/postfix/transport:

lists.bootlin.com      mailman:

and run:

sudo postmap -v /etc/postfix/transport

You won’t have any mail delivery if you forget. I struggled for a few hours before I realized I forgot this setting.

Now, set correct file ownership:

sudo chown root:list /var/lib/mailman/data/aliases
sudo chown root:list /etc/aliases

For logging and debugging e-mail delivery, I recommend to install the sysklogd package. Without it, you won’t have any mail.info, mail.warn and mail.err files in /var/mail/.

To install this package, enable the universe repository if needed (uncomment the lines with universe in the /etc/apt/sources.list file), and run:

sudo apt-get update
sudo apt-get install sysklogd

To avoid having two mail.info and mail.log files with identical contents, edit /etc/syslog.conf and remove the below line:

mail.*                          -/var/log/mail.log

Also create a /etc/logrotate.d/mail file to rotate logs, as in the below example:

/var/log/mail.* {
        size 10M
        rotate 4
        create 640 root adm

Last but not least, restart Postfix:

/etc/init.d/postfix restart

At this point, a good idea is to check that mail delivery works:

sudo apt-get install bsd-mailx
mailx alice@example.com
Subject: test

Note: that’s the line containing only a dot character that allows to terminate the message.

If the receipient doesn’t receive this message, there is an issue in the way your mail server is configured. This could be because the firewall doesn’t allow connections to outside machines through tcp port 25. Anyway, look at the logs in /var/log/mail.* to get a clue. There is no point going on in this howto until you get this fixed.

Creating the mailman site list

Mailman needs a so-called “site list”, which is the list from which password reminders and such are sent out from. The default name for this list list mailman, though you can change this through the MAILMAN_SITE_LIST setting in /etc/mailman/mm_cfg.py.

To create this list, run:

sudo newlist mailman

You will have to answer a few questions like:

Enter the email of the person running the list: postmaster@example.com
Initial newsletter password: xxx
Hit enter to notify newsletter owner...

Choose the password carefully, as crackers will be able to highjack your mailing list if it is too easy to guess.

The next required step is to add the list aliases to /etc/aliases:

# mailman mailing list
mailman:              "|/var/lib/mailman/mail/mailman post mailman"
mailman-admin:        "|/var/lib/mailman/mail/mailman admin mailman"
mailman-bounces:      "|/var/lib/mailman/mail/mailman bounces mailman"
mailman-confirm:      "|/var/lib/mailman/mail/mailman confirm mailman"
mailman-join:         "|/var/lib/mailman/mail/mailman join mailman"
mailman-leave:        "|/var/lib/mailman/mail/mailman leave mailman"
mailman-owner:        "|/var/lib/mailman/mail/mailman owner mailman"
mailman-request:      "|/var/lib/mailman/mail/mailman request mailman"
mailman-subscribe:    "|/var/lib/mailman/mail/mailman subscribe mailman"
mailman-unsubscribe:  "|/var/lib/mailman/mail/mailman unsubscribe mailman"

In addition to delivering e-mail to the mailing lists, these aliases also allow to subscribe and unsubscribe by writing to special e-mail addresses.

You also need to run the newaliases command, without which there is no e-mail delivery:

sudo newaliases
sudo /etc/init.d/postfix restart
sudo /etc/init.d/mailman restart

Look at the /var/log/mailman/error file for potential issues.

Create regular mailing lists

Regular mailing lists are created in the same way as above: Assuming you want to create a newsletter mailing list. You will need run:

sudo newlist newsletter

Add your new mailing list to /etc/aliases:

# newletter mailing list
newsletter:              "|/var/lib/mailman/mail/mailman post newsletter"
newsletter-admin:        "|/var/lib/mailman/mail/mailman admin newsletter"
newsletter-bounces:      "|/var/lib/mailman/mail/mailman bounces newsletter"
newsletter-confirm:      "|/var/lib/mailman/mail/mailman confirm newsletter"
newsletter-join:         "|/var/lib/mailman/mail/mailman join newsletter"
newsletter-leave:        "|/var/lib/mailman/mail/mailman leave newsletter"
newsletter-owner:        "|/var/lib/mailman/mail/mailman owner newsletter"
newsletter-request:      "|/var/lib/mailman/mail/mailman request newsletter"
newsletter-subscribe:    "|/var/lib/mailman/mail/mailman subscribe newsletter"
newsletter-unsubscribe:  "|/var/lib/mailman/mail/mailman unsubscribe newsletter"

Then, run the usual commands:

sudo newaliases
sudo /etc/init.d/postfix restart
sudo /etc/init.d/mailman restart

Configuring your lists

The easiest way to configure your lists and add members is to open the http://lists.example.com.com/mailman/listinfo URL with a browser.

A few things are also possible from the command line. For example, you can add a member as follows:

echo "alice@example.com" > /tmp/foo
sudo add_members -r /tmp/foo newsletter

Enabling archives

Default file permissions are not completely ready to support mailing list archives:

sudo chown -R root:list /var/lib/mailman/archives
sudo chmod o+rX /var/lib/mailman/archives/private

The second line allows the webserver to access the archives. Note that these settings were forgotten in the official Ubuntu documentation.

Settings for newsletter mailing lists

At Bootlin, we also use Mailman to deliver our newsletters. Mailman provides recipients with an easy mechanism to subscribe by themselves and unsubscribe whenever they want.

To make it even easier to unsubscribe from a newsletter, Mailman can add a special footer to each recipient, with a custom URL that allows to unsubscribe without having to remember one’s password.

To enable this feature, you have to enable the following line in /etc/mailman/mm_cfg.py:

# Extra options
# Allow to personalize each message
# (useful to provide a password-less unsubscribe link)

Restart Mailman (/etc/init.d/mailman restart) and go to the administrative interface for your list. Under Non digest options, you will then be able to set the Should Mailman personalize each non-digest delivery? option to Full Personalization.

Then, you can set a custom footer for each recipient in the Footer added to mail sent to regular list members option. Here is an example:

Bootlin quarterly newsletter
Unsubscribe: %(user_optionsurl)s?password=%(user_password)s
Archives: https://lists.bootlin.com/pipermail/newsletter/

Beware that sending custom e-mails to each recipient will increase the load on your server. You may not want to do this on mailing lists with great numbers of subscribers.

Useful resources

The below ressources were useful to prepare this HOWTO document:

Don’t hesitate to ask questions and give feedback by leaving a reply below.

Issues with changing Ethernet device names

Beagle boardNo eth0 interface in your system!

If you are using Ubuntu or Debian on ARM, as I am doing at the moment, you may be surprised to see that your system has an eth1 interface (or even eth5), but no eth0!.

This also happens on x86, of course, and probably with other distributions. I faced and understood this problem when I migrated a KVM virtual machine from a regular network interface card to a virtio one. eth0 was gone, and because my network settings were bound to this interface, I couldn’t connect to my virtual machine anymore.

In recent releases of Ubuntu, this happens because of some udev rules, which try to make sure that each network card always gets bound to the same ethx device. So, every time the system see a new network card, the /etc/udev/rules.d/70-persistent-net.rules file gets updated as follows:

# This file was automatically generated by the /lib/udev/write_net_rules
# program, run by the persistent-net-generator.rules rules file.
# You can modify it, as long as you keep each rule on a single
# line, and change only the value of the NAME= key.

# PCI device 0x14e4:0x167a (tg3)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:18:8b:87:52:12", ATTR{type}=="1",
KERNEL=="eth*", NAME="eth0"

# USB device 0x:0x (asix)
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="00:25:00:eb:eb:26", ATTR{type}=="1", KERNEL=="eth*",  NAME="eth1"

This can happen if you replace a USB to Ethernet dongle by another one with a different MAC address, or if you reuse a filesystem built on another board. Even if the board and Ethernet device are the same, you will get eth1 instead of eth0. This can be annoying if you configured your network through /etc/network/interfaces or if you bring up your network with your own scripts.

A clean solution for this problem is simply to remove the /etc/udev/rules.d/70-persistent-net.rules file. The next time you boot, you will get back to a normal eth0 network device.

I checked that these persistent net naming rules are not part of udev releases. In other distributions, you could have udev without such rules.