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 (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.
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:
#ifndef HAVE_STRLCPY strncpy(buf, input, sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; #else strlcpy(buf, input, sizeof(buf)); #endif
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. 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!
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 redshift-1.7p4:libelf-0.8.13p1: ok redshift-1.7p4:libffi-3.0.9p3: ok redshift-1.7p4:lzo2-2.06p0: ok redshift-1.7p4:libf2c-3.3.6p1: ok redshift-1.7p4:blas-1.0p6: ok redshift-1.7p4:lapack-3.1.1p4: ok redshift-1.7p4:libdaemon-0.14: ok redshift-1.7p4: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.
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.
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__) #endif
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.