%h1 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.
%h2 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 (Cent5 and Cent6 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.
%h2 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.
%h2 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
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. In some
cases a vendor will refuse to package a library (libkqueue suffered this
fate). 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!
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 to Much Bagage
It's comical how many dependencies simple utilities sometimes carry
$ pkg_add -n redshift
Users are not serviced well by such simple utilities that carry a large
dependency chain. This otherwise nifty utility is hampered by too much
Among source control systems Subversion made the same mistake by useful,
because it carries a cart-load of requirements.
Respect the User's Build Environment
Another very common practice that hampers portability is the inclusion of
GCC-specific options in makefiles. Some compilers such as pcc will
ignore -W options it doesn't recognize while gcc will abort.
Some of these options are useful, but instead of overwriting the user's
environment provide an alternate build option to exercise these additional
checks and optimizations.
@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