Building ARM Debian packages with pbuilder

The goal of this post is to help myself reproduce this pbuilder setup in the future. If you find it useful too, then that’s great.

Building ARM packages on older ARM devices like the Raspberry Pi 1 is slow, so I want to use a single amd64 machine to build Debian packages for all my target platforms:

  • amd64,
  • i386,
  • armel/ARMv4+ (e.g. Debian on Raspberry Pi 1),
  • armhf/ARMv6+ (Raspbian), and
  • armhf/ARMv7+ (e.g. Debian/Ubuntu on Raspberry Pi 2, Odroid C1, etc.).

Preferably, I want to build the packages for the current Debian stable, which at the time of writing is wheezy, so that they’ll work for as many users as possible.

Instead of managing a bunch of virtual machines by hand, we can use pbuilder. pbuilder creates chroots with debootstrap, keeps them updated with changes to the base system, and keeps the chroots clean of dependencies installed when you build packages within them.

There is an introduction to pbuilder and lots of information on various ways of using it in the Ubuntu wiki.

I’m doing the following on an amd64 system running Ubuntu 14.10.

I start out with a ~/.pbuilderrc to help simplify the later commands:

#!/bin/sh

set -e

if [ "$OS" == "debian" ]; then
    MIRRORSITE="http://ftp.no.debian.org/debian/"
    COMPONENTS="main contrib non-free"
    DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}"
        "--keyring=/usr/share/keyrings/debian-archive-keyring.gpg")
    : ${DIST:="wheezy"}
    : ${ARCH:="amd64"}
    if [ "$DIST" == "wheezy" ]; then
        #EXTRAPACKAGES="$EXTRAPACKAGES debian-backports-keyring"
        OTHERMIRROR="$OTHERMIRROR | deb $MIRRORSITE wheezy-backports $COMPONENTS"
    fi
elif [ "$OS" == "raspbian" ]; then
    MIRRORSITE="http://ftp.acc.umu.se/mirror/raspbian/raspbian/"
    COMPONENTS="main contrib non-free"
    DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}"
        "--keyring=/usr/share/keyrings/raspbian-archive-keyring.gpg")
    : ${DIST:="wheezy"}
    : ${ARCH:="armhf"}
elif [ "$OS" == "ubuntu" ]; then
    MIRRORSITE="http://no.archive.ubuntu.com/ubuntu/"
    COMPONENTS="main restricted universe multiverse"
    DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}"
        "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg")
else
    echo "Unknown OS: $OS"
    exit 1
fi

if [ "$DIST" == "" ]; then
    echo "DIST is not set"
    exit 1
fi

if [ "$ARCH" == "" ]; then
    echo "ARCH is not set"
    exit 1
fi

NAME="$OS-$DIST-$ARCH"

if [ "$ARCH" == "armel" ] && [ "$(dpkg --print-architecture)" != "armel" ]; then
    DEBOOTSTRAP="qemu-debootstrap"
fi
if [ "$ARCH" == "armhf" ] && [ "$(dpkg --print-architecture)" != "armhf" ]; then
    DEBOOTSTRAP="qemu-debootstrap"
fi

DEBOOTSTRAPOPTS=("${DEBOOTSTRAPOPTS[@]}" "--arch=$ARCH")
BASETGZ="/var/cache/pbuilder/$NAME-base.tgz"
DISTRIBUTION="$DIST"
BUILDRESULT="/var/cache/pbuilder/$NAME/result/"
APTCACHE="/var/cache/pbuilder/$NAME/aptcache/"
BUILDPLACE="/var/cache/pbuilder/build"
HOOKDIR="/var/cache/pbuilder/hook.d/"

The .pbuilderrc file cares about three environment variables:

  • OS: Either debian, raspbian, or ubuntu
  • DIST: For example wheezy, jessie, or trusty
  • ARCH: For example amd64, i386, armel, or armhf

Based on these three variables, the .pbuilderrc script selects the proper APT mirror, distribution components, and keyring to verify the downloaded packages. If the architecture is armel or armhf, and we’re not currently on that architecture, it uses qemu-debootstrap to bootstrap the chroot and copies a static qemu emulator for ARM into the chroot.

For more details on the qemu part, see the Ports page in the Ubuntu wiki.

Before running pbuilder, we have to install pbuilder and qemu-debootstrap:

sudo apt install pbuilder qemu-user-static

And we also need the keyrings. Ubuntu includes packages with the Ubuntu and Debian keyrings:

sudo apt install ubuntu-keyring debian-archive-keyring

While we have to download the Raspbian keyring package and install it by hand:

wget http://archive.raspbian.org/raspbian/pool/main/r/raspbian-archive-keyring/raspbian-archive-keyring_20120528.2_all.deb
sudo dpkg -i raspbian-archive-keyring_20120528.2_all.deb

You should now have all keyrings referenced from the .pbuilderrc:

$ ls /usr/share/keyrings
...
/usr/share/keyrings/debian-archive-keyring.gpg
/usr/share/keyrings/raspbian-archive-keyring.gpg
/usr/share/keyrings/ubuntu-archive-keyring.gpg
...
$

Then we can bootstrap the five chroots. This takes a bit of time as it downloads all packages needed to bootstrap a fully functional OS. Five times.

sudo OS=debian DIST=wheezy ARCH=amd64 pbuilder --create
sudo OS=debian DIST=wheezy ARCH=i386 pbuilder --create
sudo OS=debian DIST=wheezy ARCH=armel pbuilder --create
sudo OS=raspbian DIST=wheezy ARCH=armhf pbuilder --create
sudo OS=debian DIST=wheezy ARCH=armhf pbuilder --create

This will create tarballs with the pristine chroots:

$ ls /var/cache/pbuilder
...
/var/cache/pbuilder/debian-wheezy-amd64-base.tgz
/var/cache/pbuilder/debian-wheezy-armel-base.tgz
/var/cache/pbuilder/debian-wheezy-armhf-base.tgz
/var/cache/pbuilder/debian-wheezy-i386-base.tgz
/var/cache/pbuilder/raspbian-wheezy-armhf-base.tgz
...

Now we’re ready to build packages. Basically, you just get the source of a Debian package, change into its directory, and run:

OS=raspbian DIST=wheezy ARCH=armhf pdebuild

If the pdebuild command completes successfully, you’ll find the resulting Debian source and binary packages for armhf in e.g. /var/cache/pbuilder/raspbian-wheezy-armhf/result. All without using the rather slow Raspberry Pi 1 I used to build packages with.

Appendix: Depending on packages from an unofficial APT repo

Since I build packages for Mopidy, to be hosted at apt.mopidy.com, they often depend on other packages hosted there, and not just packages from the official repos. Thus I need to add an extra APT repository to the chroots. As I already have five chroots, I don’t want to change each chroot manually with pbuilder --login --save-after-login.

The solution is to add a hook script to be executed every time the chroot is being set up, before the Debian packaging begins, which adds apt.mopidy.com as an APT repo and runs apt-get update.

Create the directory /var/cache/pbuilder/hook.d and make a file /var/cache/pbuilder/hook.d/D10add-apt.mopidy.com:

#!/bin/sh

cat << EOF | apt-key add -
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.10 (GNU/Linux)

mQINBEzXBRYBEADGWsXEGa+WcoVMv6SevKPJQsFNbp/CbK6J8xJ0wVdf5LdfWF46
rNFMUa6DPwVOjp7wdY6mwTnW7OkpHLakP9DzaUXLGZ/9Bku7O+Aw+NDLC2W4IbdJ
... key data removed for brevity ...
0ax2VWX0487MYQcU8e2fEPOi3QkPc7zMr7wWDeB2JNBf2y/FnJxcbTLS6CEWr8mf
nXSGfezg+nzdC8eags3zCmQ5dlIKFLi7QautytYDA32vl09JcLl7mA8cxST8LyGw
rNNPwkCckaxcrWfSUso6saWj5j8y+OVn
=kcoa
-----END PGP PUBLIC KEY BLOCK-----
EOF

echo "deb http://apt.mopidy.com/ stable main contrib non-free" >> /etc/apt/sources.list
apt-get update

Remember to make the hook script executable, or else it will have no effect:

sudo chmod a+x /var/cache/pbuilder/hook.d/D10add-apt.mopidy.com

And that should be it. The hook script will now be executed before any package build and all packages from the APT repo will be available to pdebuild.