Photo by mockupbee on Unsplash

Development isn’t over until it’s packaged

Most software development I’ve done has been utilities for highly specific workflows. I’ve written code to ensure that metadata for a company’s custom file format gets copied along with the rest of the data when the file gets archived, code that ensures a search field doesn’t mangle input, lots of Git hooks, file converters, parsers, and of course my fair share of dirty hacks. Because most software projects I work on are designed for a specific task, very few of them have required packaging. My utilities have been either integrated into a larger code base I’m not responsible for, or else distributed across an infrastructure by an admin. It’s like a magic trick, which has made my life conveniently easier but, as magic does, it has also tricked me into thinking that my development work is done once I can prove that my code does its job. The reality is that code development isn’t actually done until you can deliver it to your users in a format they can install.

I don’t think I’m alone in forgetting that software delivery is the real final product. There are many reasons some developers stop short of providing an installable package for the code they’ve worked on for weeks or months or years. First of all, packaging is work, and after writing and troubleshooting code for months, sometimes you just want your work to be over just as soon as everything functions as expected. Secondly, there are a lot of software package formats out there, regardless of what platform you’re delivering to. However, I view packaging as part of quality assurance. There are lots of benefits you gain by packaging your code into an installer, and you don’t have to target every package format. In fact, you get the benefits of packaging by creating just one package.

Checking for consistency

When you package your code as an installable file, whether it’s an RPM file or a Bash script or a Flatpak or AppImage or EXE or MSI or anything else, you are checking your code base for consistency. Pick whatever package format you’re most comfortable with, or the one you think represents the bulk of your target audience, and you’re sure to find that the package tooling expects to be automated. Nobody wants to start packaging from scratch every time they update code, so naturally packaging tools are designed to be configured once for a specific code base and then to create updated packages each time the code base is updated. If you’re building a package for your project and discover that you have to manually intervene, then you’ve discovered a bug in your code.

Imagine that you’ve got a project repository with a name in camel-case. You hadn’t noticed before, but your code refers to itself in a mix of lowercase and camel-case. Your package build grinds to a halt because a variable used by the packaging tools suddenly can’t find your code base because it was set to a lowercase title but the archive of your code uses camel-case. If this happens to you, it’s also going to happen for every software packager trying to help you deliver your project to their users. Fix it for yourself, and you’ve fixed it for everyone.

Discover surprise dependencies

For decades, one of the most common problems of software troubleshooting has been the phrase “well, it works on my machine.” No matter how many tools we developers have at our disposal to make it easy to build and run software on a clean system, it’s still common to accidentally deliver software with surprise dependencies. It’s easy to forget to revert to a clean snapshot in a virtual machine, or to use a container that just happens to have a more recent version of a library than you’d realised, or to get the path of an important executable wrong in a script, or to forget that not all computers ship with a thing you take for granted.

Not all packaging tools are immune to this problem, but very robust ones (like RPM and DEB, Flatpak, and AppImage) are. I can’t count the times I’ve tried to deliver an RPM only to be reminded by rpmbuild that I haven’t included the -devel version of a dependency (many Linux distributions separate development libraries from binaries.)

You may not literally fix every problem with dependency management by building a single package, but you can clearly identify what your code requires. It only takes a single warning from your packaging tool for you to add a note to other packagers about what they must include in their own builds.

As an additional bonus, it’s also a good reminder to double check the licenses your project is using. In the haze of desperate hacking to get something to just-work-already, it’s helpful to get a gentle reminder that you’ve linked to a library with a different license than everything else. Few packaging tools (if any?) detect licensing requirements directly, but sometimes all it takes is a reminder that you’re using a library that comes from a non-standard repo for you to remember to review licensing.

Every package is an example package

Once you’ve packaged your code once, you create an example for everyone coming to your project to turn it into a package of their own. It doesn’t matter whether your example package is an RPM or a DEB or just a TGZ for a front-end like SlackBuild or Arch’s AUR, it’s the interaction between a packaging system and the input script that counts. Even a novice package maintainer is likely to be able to reverse engineer a packaging script enough to reuse the same logic for their own package.

Here’s the build and install section of the RPM for GNU Hello:

%prep
%autosetup

%build
%configure
make %{?_smp_mflags}

%install
%make_install
%find_lang %{name}
rm -f %{buildroot}/%{_infodir}/dir

%post
/sbin/install-info %{_infodir}/%{name}.info %{_infodir}/dir || :

Here’s the GNU Hello build script for Arch Linux:

source=(https://ftp.gnu.org/gnu/hello/$pkgname-$pkgver.tar.gz)
md5sums=('5cf598783b9541527e17c9b5e525b7eb')

build(){
cd "$pkgname-$pkgver"
./configure --prefix=/usr
make
}
package(){
cd "$pkgname-$pkgver"
make DESTDIR="$pkgdir/" install
}

There are differences, but you can see the shared logic. There are macros or functions that abstract some common steps of the build process, there are variables to ensure consistency, and they both benefit from using automake as provided by the source code. Armed with these examples, you could probably write a DEB package or Flatpak ref for GNU Hello in an afternoon.

Package your code at least once

Packaging is quality assurance. Even though a packaging system is really just a front-end for whatever build system your code uses anyway, the rigour of creating a repeatable and automated process for delivering your project is a helpful exercise. It benefits your project, and it benefits the people eager to deliver your project to other users. Software development isn’t over until it’s packaged.

Leave a Reply