Making a .deb package for CMake C/C++ project

Each time I need to package some software as .deb, I am not able to do it in one go. Because it is always a quite complex procedure that requires quite a lot of knowledge and context and you have to learn a lot of tools (million of debhelper tools, dpkg, debuild etc). It is designed to be comfortable for “large-scale” debian package maintainers who maintain a lot of packages for the Debian project, but is not very friendly for “indie” developers working on their own projects. And the process may vary a lot depending on what kind of software you need to package (what language, is it a library or shell tool or GUI etc). Also it strictly recommends that debian packaging files are stored separately from the software repo which might be somewhat inconvenient for small projects. We are going to include the debian directory into the project.

So in this article we are going to build a very specific kind of package: binary package for CMake project. However we are going to cross-compile it for arm64 from amd64 machine.

The application is PixelPilot.

Preparation

Let’s clone the source code

git clone https://github.com/OpenIPC/PixelPilot_rk.git

The app repo is named using camel-case notation, so let’s assume that our DEB package going to be named pixelpilot-rk.

Generate the template files

We can ofcourse create all the files by hand, however there are quite a lot of them and some have rather complex structure. So, let’s use the dh_make tool from debhelper package:

cd PixelPilot_rk
dh_make --packagename pixelpilot-rk_1.3.0 --single

This will generate the debian/ directory with a lot of files in it (most have .ex extension because they are examples and if you plan to use it, the .ex extension needs to be removed). Most of those files have names that follow a special convention and debhelper tools would look for those files and change the way the final package is built depending on the existance of various files and their contents. Some of the files are not generated and we’d need to create them from scratch.

  • control – is the file that contains the metadata of the package: its short description, list of dependencies. When you type apt show <package>, this is more or less what you would see in this file. However one control file can contain definitions of several DEB packages, if you want to build several packages from the same repo. Make sure to add at least the description and the list of build- and runtime-dependencies.
  • rules – is a makefile that is going to be used to build your project and various its artifacts. By-default it is just a dummy makefile that forwards all its targets to debhelper tools. For now you can leave it as is, but we’d need to make a few changes later.
  • changelog – is the list of changes between package versions. Normally it would contain the same info as what you have in the “releases” section on GitHub, but in a special format. Make sure to set the correct version number and write the sensible list of changes. While new entries can be added manually, it’s much easier to do it using dch / debchange tool: either dch -i to +1 the version or dch -v X.Y.Z to generate an entry for a specific version.
  • pixelpilot.1 and pixelpilot-rk.manpages are the actual manual page and the listing of all man pages included in this package. The syntax of man pages is rather strange, so it’s better to use some tools to generate this file. The simplest one is help2man tool that tries to generate a basic man page from the --help output, but please don’t commit it as-is, add at least some information and examples! Or use lowdown to convert markdown file to man page.
  • copyright, watch, source etc – are less important boilerplate

A few more files we’d need to add, because pixelpilot needs to be started as a systemd service, those files name starts with package name “pixelpilot-rk” because when you’d want to build several packages from the same repo, they’d going to have different name and it prevents name clashes:

  • pixelpilot-rk.default (however we’d name it pixelpilot-rk.pixelpilot.default) – is the file that is going to be placed to /etc/default/pixelpilot – it is the list of environment variables which can be used by systemd unit file and overwritten by the enduser. It looks like a “KEY=value” per-line with comments starting with #.
  • pixelpilot-rk.service (we will name it pixelpilot-rk.pixelpilot.service) – is the actual systemd service config. It is the standard systemd service configuration file.

We add .pixelpilot in the middle of the name because we want user to use systemctl start pixelpilot and not systemctl start pixelpilot-rk just because it makes a bit more sense. But it’s also fine to leave them as just pixelpilot-rk.service if you are fine with service name being the same as package name. You can also create a pixelpilot-rk@.service if you need to create a service-template.

Since we use cmake in this project and because we use non-default systemd service name, we need to make a few changes to rules file:

%:
	dh $@ --buildsystem=cmake

override_dh_auto_configure:
	dh_auto_configure -- \
	-DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH)

override_dh_installsystemd:
	dh_installsystemd --restart-after-upgrade --name=pixelpilot

override_dh_installinit:
	dh_installinit --name=pixelpilot

Note the --buildsystem=cmake – it says debhelper that the project have to be build with cmake. And override_dh_install* rules tell debhelper to look for pixelpilot-rk.pixelpilot.{service,default} files.

Now you can commit the contents of debian/ directory, just don’t forget to remove all the unused .ex files.

Building the package

building the package could be as simple as just typing dpkg-buildpackage -uc -us -b. However it might be beneficial to build the project in a clean directory. Also, some tools expect that your work directory is named as <project name>_<version> and they would put the final DEB package (and a few extra files) not into the current directory, but at one level up. So let’s create such a directory and export our source code there:

mkdir  pixelpilot-rk_1.3.0
git archive master | tar -x -C pixelpilot-rk_1.3.0

If your project contains submodules, then git archive won’t work, but you can use (there are probably ways to skip the tar stage):

git ls-files --recurse-submodules | tar -c -T- | tar -x -C pixelpilot-rk_1.3.0

You may actually want to also generate the “orig” archive along with sources directory:

ORIG_ARCHIVE=pixelpilot-rk_${PKG_VERSION}.orig.tar.gz
SRCDIR=pixelpilot-rk_${PKG_VERSION}

git ls-files --recurse-submodules | tar -caf $ORIG_ARCHIVE -T-
rm -rf $SRCDIR
mkdir $SRCDIR
tar -axf $ORIG_ARCHIVE -C $SRCDIR

Then step into the <name>_<version> directory and start the build. Keep in mind that it assumes you have the <name>_<version>/debian/ directory! And worth noting that it is expected that CMakeLists.txt should contain proper install target that ideally installs not just binary, but also all the necessary assets like images / data-files / configs etc.

cd pixelpilot-rk_1.3.0
dpkg-buildpackage -uc -us -b

After it finishes, your DEB package is goint to be in the parent directory:

ls pixelpilot-rk*.deb
> pixelpilot-rk_1.3.0-1_arm64.deb  pixelpilot-rk-dbgsym_1.3.0-1_arm64.deb

1st one is the actual software package and the dbgsym contains information that is useful if one needs to, mainly, run gdb on PixelPilot.

Cross-compiling to arm64 architecture

What if we are working on regular amd64 computer, but we need to build a package for arm? Well, for C/C++ projects the easiest way is to either build on a separate ARM host or use qemu emulator/virtual machine. There are likely a lot of ways to build in a container, here I’ll describe just one possibility.

DEBIAN_HOST=https://cloud.debian.org/images/cloud/bullseye
DEBIAN_RELEASE=latest
DEBIAN_SYSTEM=debian-11-generic-arm64.tar

wget -nv $(DEBIAN_HOST)/$(DEBIAN_RELEASE)/$(DEBIAN_SYSTEM).xz
tar -xf $(DEBIAN_SYSTEM).xz

This way we download the latest ARM64 Debian bullseye image (not .iso, but installed disk image). Now we mount the filesystem from this image:

mkdir -p $OUTPUT
LOOP=`sudo losetup -P --show -f disk.raw`
sudo mount ${LOOP}p1 $OUTPUT
sudo mkdir -p $OUTPUT/usr/src/PixelPilot_rk
sudo mount -o bind `pwd` $OUTPUT/usr/src/PixelPilot_rk
sudo rm $OUTPUT/etc/resolv.conf
echo nameserver 1.1.1.1 | sudo tee -a $OUTPUT/etc/resolv.conf

The way it’s done is a bit messy: first we mount the root partition (p1) of the image as $(OUTPUT) directory but then we also just bind-mount the project directory to /usr/src of the mounted image. It can be just copied there ofcourse. And we do a few hacks to make sure DNS works fine inside the container.

After that we need to create an entry-point script for the container that’s going to be executed and will build the package. It needs to perform all the steps from the “building the deb package” section above, but from within a container: install all the necessary packages, create the directories and call dpkg-buildpackage. Since we mounted the current working directory to container, the final packages would appear in the current directory in the end.

There are definitely better ways to do the container part, I’m just not that well skilled in Docker / porman etc, so did it in the “manual” way.

Don’t forget to unmount what you mounted:

sudo umount $OUTPUT/usr/src/PixelPilot_rk
sudo umount $OUTPUT
sudo losetup --detach $LOOP

Leave a Reply

Your email address will not be published. Required fields are marked *