Eric Radman : a Journal

Write Portable Software

Many utilities and applications are designed around languages and frameworks that claim to make the programmer's job easy. It's time to reverse these priorities by choosing a strategy that is capable of providing a smooth experience for the user.

The following principles represent some of the lessons learned while developing entr(1), a utility for BSD and Linux that executes commands or writes to a FIFO when files change.

Provide Static Binaries

Nearly all Linux distributions insist that all utilities be built with shared libraries. This viewpoint is not concerned in the least with the experience of the common user or the task of system administrators.

It's not unusual to have NFS-mounted home directories that are accessed from a mix of OS releases (Cent6 and Cent7 for example). Static builds make running applications on multiple versions easy. Some users will similarly benefit from a portable app that they can copy using Dropbox or rsync.

Static binaries not only give users a smooth experience, they also greatly simplify the task of securing environments using chroots.

Choose an Interface

Sometimes an attempt at supporting multiple platforms is made by sprinkling the source with compile-time or runtime conditional checks. If not used carefully they can easily create software that doesn't have a clear design and is ultimately less portable. Consider the following:

    strncpy(buf, input, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';
    strlcpy(buf, input, sizeof(buf));

It's usually much better to pick one interface and emulate it's behavior on platforms without the native capability. In this way code paths will also be less susceptible to brittle or stale code paths.

From it's inception entr was designed around kqueue(2) since it was already very well designed for the purpose of this utility.

Eschew Compatibility Libraries

The first and most obvious reason to bundle functionality is to eliminate obstacles for the common user.

In other cases the facilities provided by a compatibility library are incomplete or wrong. The software builds and runs this may be acceptable, but taking ownership for the end-to-end behavior of the system requires that you bundle or statically link functionality that is not provided by the target platform.

This is the strategy followed by a great deal of software that you may be familiar with, such as OpenSSH and PostgreSQL. The following directory listing is from ruby-2.0.0-p247/missing/

acosh.c         erf.c           isinf.c         setproctitle.c  strtol.c
alloca.c        ffs.c           isnan.c         signbit.c       tgamma.c
cbrt.c          file.h          langinfo.c      strchr.c        x86_64-chkstk.s
close.c         fileblocks.c    lgamma_r.c      strerror.c
crt_externs.h   finite.c        memcmp.c        strlcat.c
crypt.c         flock.c         memmove.c       strlcpy.c
dup2.c          hypot.c         os2.c           strstr.c

Linking against a compatibility library such as libbsd may be an order of magnitude harder because as the developer you may be forced to deal with up with distributions and packing systems that use an old or broken version. For the benefit of your users, anticipate this circumstance and attempt to bundle the behavior your application requires.

Employ Separate Scripts for Each Platform

One of problems with Autoconf/CMake/SCons is they tend to be full of cross-cutting concerns and conditional parameters. What is the alternative? Separate builds for each platform!

Makefile.bsd Makefile.linux Makefile.macos

Writing separate makefiles makes it possible to experiment with a new platform without complicating the entire build. In this case we simply clone an existing build and adapt accordingly


Carrying too Much Baggage

It's comical how many dependencies simple utilities sometimes carry

$ pkg_add -n redshift
libelf-0.8.13p1: ok
libffi-3.0.9p3: ok
lzo2-2.06p0: ok
libf2c-3.3.6p1: ok
blas-1.0p6: ok
lapack-3.1.1p4: ok
libdaemon-0.14: ok
hicolor-icon-theme-0.12p2: ok

Users are not serviced well by such simple utilities that carry a large dependency chain. This otherwise nifty utility is hampered by too much baggage.

Respect the User's Build Environment

Another very common practice that hampers portability is the inclusion of GCC-specific options in makefiles. Some of these options are useful, but instead of overwriting the user's environment provide, consider adding an alternate build option to exercise these additional checks and optimizations.

gcc-lint: clean
        @CFLAGS="-pedantic -Wall -Wpointer-arith -Wbad-function-cast" make test

Improve Conditions Upstream

Once you've assembled something that works you have an opportunity to leave the world in better shape than when you started. Vendors (Apple, Redhat) may or may not pay attention to a bug report, but you don't know if you don't try. Provide links to code with a license they can use and maybe somebody there will agree to support the best standards available.

Let The Configure Stage Pick the Platform

Here is another pattern that is not portable:

#if defined(__FreeBSD__) || defined (__DragonflyBSD__) || defined(__OpenBSD__)

If what is meant is that "this code runs on BSD", then say so


Get User Feedback Early

Building software that works requires some user feedback because it's the only way to understand what the system is supposed to do. The first iteration of a utility or service is successful if it provides a means of discovering what is important and what is unimportant to the people who will use it.

Another way to get this feedback is to be the first user. For example if you mainly use a utility interactively, try scripting with it. You'll quickly discover the problems that would prevent an early adopter from giving you valuable feedback.

Last updated on May 09, 2019