---
title: Basic Ubuntu packaging
snippets_root_directory: snippets/ubuntu_packaging
snippets_language: plain
snippets:
basic:
- basic/changelog
- basic/control
- basic/copyright
- basic/rules
install:
- install/test.install
gbp:
- gbp/gbp.conf
---
It took me about an hour to make a PKGBUILD for my simple, non-compiled
piece of software to be published on [AUR].
In contrast, it took me a few days to figure out how to build suitable .deb
packages for publishing in a PPA on [Launchpad].
In this post, I'll try to describe some of the initial pain points of mine.
[AUR]: https://aur.archlinux.org/
[Launchpad]: https://launchpad.net/
Basics
------
The Debian package format is really old, and it shows.
There's a billion of metadata files to take care of, and barely any suitable
tutorials for beginners.
At best, you'll learn how to build _binary_ packages, not suitable for
publishing in a PPA (which only accept _source_ packages and builds the
binaries itself).
First, you need to realize that there are source packages and binary packages.
Binary packages are the .deb files that actually contain the software.
A source package is, confusingly, multiple files, and you need to submit them
all to Launchpad.
You can distribute binary packages directly to your users, but they would have
to fetch & install the new version manually every time there's an upgrade.
If you could set up a repository and just point the users to it, they would get
the new versions naturally via the package manager (`apt`).
Canonical's Launchpad provides a very handy PPA (Personal Package Archive)
service so that anyone can set up a repository.
Users could then use `add-apt-repository ppa:...` and get the packages in a
standard and convenient way.
Tools
-----
There's a myriad of tools to build and maintain Debian packages.
The [Debian New Maintainers' Guide] provides a [short summary] of how these
tools interact.
This tutorial assumes that your software lives in a Git repository and you'd
like to use Git to maintain the packaging metadata in the same repository.
This process is greatly aided by the [git-buildpackage] tool.
We still need to install a bunch of other stuff though; the complete command
line to install the required tools would be something like
{% include jekyll-theme/shell.html cmd='sudo apt install -y build-essential devscripts dh-make git-buildpackage' %}
Many of the tools pick up particular metadata (like the maintainer name and
email address) from environment variables.
You can put something like
export DEBFULLNAME='John Doe'
export DEBEMAIL='John.Doe@example.com'
in your .bashrc to set them globally.
[Debian New Maintainers' Guide]: https://www.debian.org/doc/manuals/maint-guide
[short summary]: https://www.debian.org/doc/manuals/maint-guide/build.en.html#hierarchy
[git-buildpackage]: http://honk.sigxcpu.org/projects/git-buildpackage/manual-html/gbp.html
Getting started
---------------
Let's create a repository to try things out.
It'll contain a single executable shell script test.sh, which only outputs the
string "test".
{% include jekyll-theme/shell.html cmd='mkdir test' %}
{% include jekyll-theme/shell.html cmd='cd test' %}
{% include jekyll-theme/shell.html cmd='git init' %}
{% capture cmd1 %}
cat <<'EOF' > test.sh
#!/usr/bin/env bash
echo test
EOF
{% endcapture %}
{% include jekyll-theme/shell.html cmd=cmd1 %}
{% include jekyll-theme/shell.html cmd='chmod +x test.sh' %}
{% include jekyll-theme/shell.html cmd='git add .' %}
{% include jekyll-theme/shell.html cmd='git commit -m \'initial commit\'' %}
This is going to be version 1.0 of our project, let's tag it as such.
{% include jekyll-theme/shell.html cmd='git tag -a -m \'Release 1.0\' v1.0' %}
All of the Debian packaging tools are tailored to the following use-case.
1. There's an upstream distribution, which releases the software in tarballs.
2. There's a maintainer (who's not the software author), who takes care of
packaging and is disconnected from the development.
This disconnect means that maintaining the Debian packaging files in the
`master` branch is inconvenient using the existing tools.
At the very least, you should create a separate branch for doing packaging
work.
In addition, Debian (and hence, Ubuntu) is not a rolling-release distribution.
That means that there're regular releases, and the software version shouldn't
change too much during a lifetime of a single release.
Once Debian makes a release, the software version is more or less fixed, and
security fixes from future versions should be backported separately for each of
the supported Debian/Ubuntu releases.
Except there _is_ a rolling-release distribution of Debian, and it's called
"unstable" or "sid".
The bleeding-edge packaging work should target the "unstable" distribution.
So, let's create a new branch `debian` for our packaging work:
{% include jekyll-theme/shell.html cmd='git checkout -b debian' %}
All the packaging tools assume there's a separate folder "debian" that contains
the package metadata files.
There's a handy tool `dh_make` that creates the directory and populates it with
a number of template metadata files.
Using it is not so simple though.
First of all, it assumes that there's a properly named tarball with the project
sources available in the parent directory.
Why?
Who knows.
Let's create said tarball:
{% include jekyll-theme/shell.html cmd='git archive --format=tar --prefix=test_1.0/ v1.0 | gzip -c > ../test_1.0.orig.tar.gz' %}
The tarball name should follow the NAME_VERSION.orig.tar.gz pattern exactly!
Anyway, now is the time to run `dh_make`:
{% include jekyll-theme/shell.html cmd='dh_make --indep --copyright mit --packagename test_1.0 --yes' %}
I'm using the MIT License for our little script, hence the `--copyright mit`
argument.
In addition, every package in Debian is either "single", "arch-independent",
"library" or "python".
I'm not sure what the exact differences between those are, but a shell script
is clearly CPU architecture-independent, hence the `--indep` argument.
If it was a compiled executable, it would be a "single" (`--single`) package.
`dh_make` created the "debian" directory for us, filled with all kinds of
files.
The only required ones are "changelog", "control", "source", "rules" and the
"source" directory.
Let's remove every other file for now:
{% include jekyll-theme/shell.html cmd='rm -f -- debian/*.ex debian/*.EX debian/README.* debian/*.docs' %}
You can study the exact format of the metadata files in the [Debian New
Maintainers' Guide], but for now let's keep it simple:
{% include jekyll-theme/snippets/section.html section_id='basic' %}
The "control" and "copyright" files are fairly straighforward.
The "changelog" file has a strict format and is supposed to be maintained using
the `dch` tool (luckily, git-buildpackage helps with that; more on that later).
The "rules" file is an _executable_ Makefile, and actually controls how the
software is built.
Building a package involves invoking many predefined targets in this Makefile;
for now, we'll resort to delegating everything to the `dh` tool.
It's the Debhelper tool; it's a magic set of scripts that contain an
unbelievable amount of hidden logic that's supposed to aid package maintainers
in building the software.
For example, if the package is supposed to be built using the standard
`./configure && make && make install` sequence, it'll do this automatically.
If it's a Python package with setup.py, it'll use the Python package-building
utilities, etc.
We don't want any of that, we just want to copy test.sh to /usr/bin.
It can be taken care of using the `dh_install` script.
While building the package, it'll get executed by `dh`, read the
"debian/test.install" file and copy the files listed there to the specified
directories.
Our test.install should look like this:
{% include jekyll-theme/snippets/section.html section_id='install' %}
At this point, we can actually build a proper Debian package!
{% include jekyll-theme/shell.html cmd='dpkg-buildpackage -uc -us' %}
This command will generate a bunch of files in the parent directory.
The one of interest to us is "test_1.0-1_all.deb".
We can install it using `dpkg`:
{% include jekyll-theme/shell.html cmd='sudo dpkg -i ../test_1.0-1_all.deb' %}
We can now execute `test.sh`, and it'll hopefully print the string "test".
This .deb file can be distributed to other users, but is no good for uploading
to Launchpad.
For one, it's a binary package, and we need source packages for Launchpad to
build itself.
Second, it's unsigned, which is also a no-no.
I'm not going to describe how to set up a GnuPG key and upload it to the Ubuntu
keyserver (keyserver.ubuntu.com), but it's pretty straightforward once you know
the basics of GnuPG key handling.
One disadvantage of the `dpkg-buildpackage` tool is that it creates a lot of
files in the "debian" directory; their purpose is unclear to me.
For now, you can delete them, leaving only the original "changelog", "control",
"copyright", "rules", "test.install" and the "source" directory.
git-buildpackage
----------------
git-buildpackage is a wonderful tool that helps with managing the packaging
work in the upstream repository.
Please refer to its manual to learn how to use it properly.
We need to configure it so that it knows how the release tags look like
(`vVERSION`), how the packaging branch is called (`debian`) and where to put
the generated files.
Create "debian/gbp.conf" with the following contents:
{% include jekyll-theme/snippets/section.html section_id='gbp' %}
One unclear line here is `pristine-tar = False`.
It turns out, a lot of Debian package maintainers use the `pristine-tar` tool
to create "pristine", byte-for-byte reproducible tarballs of the upstream
software.
This is just more headache for us, so we're not going to use that;
git-buildpackage will just use the normal `git archive` to create tarballs.
First, commit the packaging work we just made:
{% include jekyll-theme/shell.html cmd='git add debian/' %}
{% include jekyll-theme/shell.html cmd='git commit -m \'initial Debian release\'' %}
We can now build the package using git-buildpackage:
{% include jekyll-theme/shell.html cmd='gbp buildpackage' %}
The tool will try to sign the packages, so this assumes that you have your
GnuPG key set up!
If all went right, it just built the packages in the ../build-area directory.
And it hasn't crapped all over the working directory too!
Similar to `dpkg-buildpackage`, it builds binary packages by default.
To build _source_ packages, it needs to be invoked with the `-S` argument:
{% include jekyll-theme/shell.html cmd='gbp buildpackage -S' %}
It'll build the source package in the same directory (you'll notice a lot of
files having the "_source" suffix).
If all is well, we can tag the packaging work we've just completed:
{% include jekyll-theme/shell.html cmd='gbp buildpackage --git-tag-only' %}
This will create the `debian/1.0-1` tag in the repository.
We are now ready to upload the source package to Launchpad.
It's done using the `dput` tool.
The naive way would fail:
{% include jekyll-theme/shell.html cmd='dput ppa:john-doe/test ../build-area/test_1.0-1_source.changes' %}
This is due to the fact that we've specified that we're targetting the
"unstable" distribution in debian/changelog.
There's no "unstable" distribution of Ubuntu though; we need to manually
specify the minimal-supported version (e.g. "bionic"):
{% include jekyll-theme/shell.html cmd='dput ppa:john-doe/test/ubuntu/bionic ../build-area/test_1.0-1_source.changes' %}
What about other distributions?
Well, if the binary package doesn't need recompiling, we can use Launchpad's
"Copy packages" feature; this is well-described in this [Ask Ubuntu question].
[Ask Ubuntu question]: https://askubuntu.com/q/23227/844205
New versions
------------
When a new version is released, git-buildpackage helps to integrate it to the
packaging branch.
Let's say the new version is tagged `v1.1`:
{% include jekyll-theme/shell.html cmd='git checkout debian' %}
{% include jekyll-theme/shell.html cmd='git merge v1.1' %}
{% include jekyll-theme/shell.html cmd='gbp dch' %}
The above command will update debian/changelog; modify it manually to target
the usual "unstable" distribution instead of "UNRELEASED" and update the
version to something like "1.1-1".
{% include jekyll-theme/shell.html cmd='git add debian/' %}
{% include jekyll-theme/shell.html cmd='git commit -m \'Debian release 1.1\'' %}
{% include jekyll-theme/shell.html cmd='gbp buildpackage -S' %}
This will build the source package for the new version in the ../build-area
directory; you can then upload it Launchpad and copy the built binary packages.
Aftermath
---------
This fucking sucks.
What's the way to sanely manage the repository if the build/runtime
dependencies are different for different Ubuntu versions?
I have no idea.
Some pointers to help you understand what's going on in this tutorial more
deeply:
* [When upstream uses Git: Building Debian Packages with git-buildpackage](https://honk.sigxcpu.org/projects/git-buildpackage/manual-html/gbp.import.upstream-git.html)
* [Using Git for Debian packaging](https://www.eyrie.org/~eagle/notes/debian/git.html)
Good luck with this, because I'm definitely overwhelmed.