{"id":1051,"date":"2025-10-03T21:53:58","date_gmt":"2025-10-03T19:53:58","guid":{"rendered":"https:\/\/seriyps.com\/blog\/?p=1051"},"modified":"2025-11-15T22:51:35","modified_gmt":"2025-11-15T21:51:35","slug":"making-a-deb-package-for-cmake-c-project","status":"publish","type":"post","link":"https:\/\/seriyps.com\/blog\/2025\/10\/03\/making-a-deb-package-for-cmake-c-project\/","title":{"rendered":"Making a .deb package for CMake C\/C++ project"},"content":{"rendered":"\n<p>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 &#8220;large-scale&#8221; debian package maintainers who maintain a lot of packages for the Debian project, but is not very friendly for &#8220;indie&#8221; 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 <code>debian<\/code> directory into the project.<\/p>\n\n\n\n<!--more-->\n\n\n\n<p>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.<\/p>\n\n\n\n<p>The application is <a href=\"https:\/\/github.com\/OpenIPC\/PixelPilot_rk\/tree\/4b79ef5358430a9d64f52b12b42cab9d0c978b7a\">PixelPilot<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Preparation<\/h2>\n\n\n\n<p>For the reference: <a href=\"https:\/\/www.debian.org\/doc\/manuals\/debmake-doc\/index.en.html\">official Debian packaging manual<\/a>.<\/p>\n\n\n\n<p>Let&#8217;s clone the source code<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">git clone https:\/\/github.com\/OpenIPC\/PixelPilot_rk.git<\/code><\/pre>\n\n\n\n<p>The app repo is named using camel-case notation, so let&#8217;s assume that our DEB package going to be named <code>pixelpilot-rk<\/code>.<\/p>\n\n\n\n<p>And let&#8217;s assume we are building the git tag 1.3.0 and will build a DEB package with version 1.3.0. Let&#8217;s define a variable for that<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">export PKG_VERSION=1.3.0<\/code><\/pre>\n\n\n\n<p>If the project we are building doesn&#8217;t have proper versioning culture, we can use artificial version based on the last commit date and sha:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">PKG_VERSION_PREFIX=1.0.0\nPKG_VERSION=`git log --date=format:%Y%m%d --pretty=${PKG_VERSION_PREFIX}~git%cd.%h | head -n 1`<\/code><\/pre>\n\n\n\n<p>It would generate a version number like <code>1.0.0~git20250607.2ec19e1<\/code>, where the commit date would guarantee that it would be increasing over time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Generate the template files<\/h2>\n\n\n\n<p>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&#8217;s use the <code>dh_make<\/code> tool from <code>debhelper<\/code> package:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">cd PixelPilot_rk\ndh_make --packagename pixelpilot-rk_${PKG_VERSION} --single<\/code><\/pre>\n\n\n\n<p>This will generate the <code>debian\/<\/code> 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&#8217;d need to create them from scratch.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>control<\/code> &#8211; is the file that contains the metadata of the package: its short description, list of dependencies. When you type <code>apt show &lt;package&gt;<\/code>, this is more or less what you would see in this file. However one <code>control<\/code> 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.<\/li>\n\n\n\n<li><code>rules<\/code> &#8211; 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 <code>debhelper<\/code> tools. For now you can leave it as is, but we&#8217;d need to make a few changes later.<\/li>\n\n\n\n<li><code>changelog<\/code> &#8211; is the list of changes between package versions. Normally it would contain the same info as what you have in the &#8220;releases&#8221; 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&#8217;s much easier to do it using <code>dch<\/code> \/ <code>debchange<\/code> tool: either <code>dch -i<\/code> to +1 the version or <code>dch -v ${PKG_VERSION}<\/code> to generate an entry for a specific version. The version number from this file would be used to build the actual DEB package, so keep it up-to-date.<\/li>\n\n\n\n<li><code>pixelpilot.1<\/code> and <code>pixelpilot-rk.manpages<\/code> 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&#8217;s better to use some tools to generate this file. The simplest one is <code>help2man<\/code> tool that tries to generate a basic man page from the <code>--help<\/code> output, but please don&#8217;t commit it as-is, add at least some information and examples! Or use <code>lowdown<\/code> to convert markdown file to man page.<\/li>\n\n\n\n<li><code>copyright<\/code>, <code>watch<\/code>, <code>source<\/code> etc &#8211; are less important boilerplate<\/li>\n<\/ul>\n\n\n\n<p>A few more files we&#8217;d need to add, because <code>pixelpilot<\/code> needs to be started as a systemd service, those files name starts with package name &#8220;pixelpilot-rk&#8221; because when you&#8217;d want to build several packages from the same repo, they&#8217;d going to have different name and it prevents name clashes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>pixelpilot-rk.default<\/code> (however we&#8217;d name it <code>pixelpilot-rk.pixelpilot.default<\/code>) &#8211; is the file that is going to be placed to <code>\/etc\/default\/pixelpilot<\/code> &#8211; it is the list of environment variables which can be used by systemd unit file and overwritten by the enduser. It looks like a &#8220;KEY=value&#8221; per-line with comments starting with <code>#<\/code>.<\/li>\n\n\n\n<li><code>pixelpilot-rk.service<\/code> (we will name it <code>pixelpilot-rk.pixelpilot.service<\/code>) &#8211; is the actual systemd service config. It is the standard <a href=\"https:\/\/www.freedesktop.org\/software\/systemd\/man\/latest\/systemd.service.html\">systemd service configuration file<\/a>.<\/li>\n<\/ul>\n\n\n\n<p>We add <code>.pixelpilot.<\/code> in the middle of the name because we want user to use <code>systemctl start pixelpilot<\/code> and not <code>systemctl start pixelpilot-rk<\/code> just because it makes a bit more sense. But it&#8217;s also fine to leave them as just <code>pixelpilot-rk.service<\/code> if you are fine with service name being the same as package name. You can also create a <code>pixelpilot-rk@.service<\/code> if you need to create a service-template.<\/p>\n\n\n\n<p>Since we use <code>cmake<\/code> in this project and because we use non-default systemd service name, we need to make a few changes to <code>rules<\/code> file:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-makefile\">%:\n\tdh $@ --buildsystem=cmake\n\noverride_dh_auto_configure:\n\tdh_auto_configure -- \\\n\t-DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH)\n\noverride_dh_installsystemd:\n\tdh_installsystemd --restart-after-upgrade --name=pixelpilot\n\noverride_dh_installinit:\n\tdh_installinit --name=pixelpilot<\/code><\/pre>\n\n\n\n<p>Note the <code>--buildsystem=cmake<\/code> &#8211; it says debhelper that the project have to be build with cmake. And <code>override_dh_install*<\/code> rules tell debhelper to look for <code>pixelpilot-rk.pixelpilot.{service,default}<\/code> files.<\/p>\n\n\n\n<p>Now you can commit the contents of <code>debian\/<\/code> directory, just don&#8217;t forget to remove all the unused <code>.ex<\/code> files.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Building the package<\/h2>\n\n\n\n<p>building the package could be as simple as just typing <code>dpkg-buildpackage -uc -us -b<\/code>. However it might be beneficial to build the project in a clean directory. Also, some tools expect that your work directory is named as <code>&lt;project name&gt;_&lt;version&gt;<\/code> 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&#8217;s create such a directory and export our source code there:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">mkdir  pixelpilot-rk_${PKG_VERSION}\ngit archive master | tar -x -C pixelpilot-rk_${PKG_VERSION}<\/code><\/pre>\n\n\n\n<p>If your project contains submodules, then <code>git archive<\/code> won&#8217;t work, but you can use (there are probably ways to skip the tar stage):<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">git ls-files --recurse-submodules | tar -c -T- | tar -x -C pixelpilot-rk_${PKG_VERSION}<\/code><\/pre>\n\n\n\n<p>You may actually want to also generate the &#8220;orig&#8221; archive along with sources directory:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">ORIG_ARCHIVE=pixelpilot-rk_${PKG_VERSION}.orig.tar.gz\nSRCDIR=pixelpilot-rk_${PKG_VERSION}\n\ngit ls-files --recurse-submodules | tar -caf $ORIG_ARCHIVE -T-\nrm -rf $SRCDIR\nmkdir $SRCDIR\ntar -axf $ORIG_ARCHIVE -C $SRCDIR<\/code><\/pre>\n\n\n\n<p>Then step into the <code>&lt;name&gt;_&lt;version&gt;<\/code> directory and start the build. Keep in mind that it assumes you have the <code>&lt;name&gt;_&lt;version&gt;\/debian\/<\/code> directory! And worth noting that it is expected that <code>CMakeLists.txt<\/code> should contain proper <code>install<\/code> target that ideally installs not just binary, but also all the necessary assets like images \/ data-files \/ configs etc.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">cd pixelpilot-rk_${PKG_VERSION}\ndpkg-buildpackage -uc -us -b<\/code><\/pre>\n\n\n\n<p>After it finishes, your DEB package is going to be in the parent directory:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">ls pixelpilot-rk*.deb\n&gt; pixelpilot-rk_1.3.0-1_arm64.deb  pixelpilot-rk-dbgsym_1.3.0-1_arm64.deb<\/code><\/pre>\n\n\n\n<p>1st one is the actual software package and the <code>dbgsym<\/code> contains information that is useful if one needs to, mainly, run <code>gdb<\/code> on PixelPilot.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cross-compiling to arm64 architecture<\/h2>\n\n\n\n<p>What if we are working on regular <code>amd64<\/code> computer, but we need to build a package for <code>arm<\/code>? Well, for C\/C++ projects the easiest way is to either build on a separate ARM host or use <code>qemu<\/code> emulator\/virtual machine. There are likely a lot of ways to build in a container, here I&#8217;ll describe just one possibility.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">DEBIAN_HOST=https:\/\/cloud.debian.org\/images\/cloud\/bullseye\nDEBIAN_RELEASE=latest\nDEBIAN_SYSTEM=debian-11-generic-arm64.tar\n\nwget -nv $(DEBIAN_HOST)\/$(DEBIAN_RELEASE)\/$(DEBIAN_SYSTEM).xz\ntar -xf $(DEBIAN_SYSTEM).xz<\/code><\/pre>\n\n\n\n<p>This way we download the latest ARM64 Debian bullseye image (not .iso, but installed disk image). Now we mount the filesystem from this image:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">mkdir -p $OUTPUT\nLOOP=`sudo losetup -P --show -f disk.raw`\nsudo mount ${LOOP}p1 $OUTPUT\nsudo mkdir -p $OUTPUT\/usr\/src\/PixelPilot_rk\nsudo mount -o bind `pwd` $OUTPUT\/usr\/src\/PixelPilot_rk\nsudo rm $OUTPUT\/etc\/resolv.conf\necho nameserver 1.1.1.1 | sudo tee -a $OUTPUT\/etc\/resolv.conf<\/code><\/pre>\n\n\n\n<p>The way it&#8217;s done is a bit messy: first we mount the root partition (p1) of the image as <code>$(OUTPUT)<\/code> directory but then we also just bind-mount the project directory to <code>\/usr\/src<\/code> 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.<\/p>\n\n\n\n<p>After that we need to create an entry-point script for the container that&#8217;s going to be executed and will build the package. It needs to perform all the steps from the &#8220;building the deb package&#8221; section above, but from within a container: install all the necessary packages, create the directories and call <code>dpkg-buildpackage<\/code>. Since we mounted the current working directory to container, the final packages would appear in the current directory in the end.<\/p>\n\n\n\n<p>There are definitely better ways to do the container part, I&#8217;m just not that well skilled in Docker \/ porman etc, so did it in the &#8220;manual&#8221; way.<\/p>\n\n\n\n<p>Don&#8217;t forget to unmount what you mounted:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">sudo umount $OUTPUT\/usr\/src\/PixelPilot_rk\nsudo umount $OUTPUT\nsudo losetup --detach $LOOP<\/code><\/pre>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[3,199,198,4,251],"class_list":["post-1051","post","type-post","status-publish","format-standard","hentry","category-linux","tag-apt","tag-deb","tag-debian","tag-dpkg","tag-linux-2"],"_links":{"self":[{"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/posts\/1051","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/comments?post=1051"}],"version-history":[{"count":10,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/posts\/1051\/revisions"}],"predecessor-version":[{"id":1085,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/posts\/1051\/revisions\/1085"}],"wp:attachment":[{"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/media?parent=1051"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/categories?post=1051"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/seriyps.com\/blog\/wp-json\/wp\/v2\/tags?post=1051"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}