Guidelines for portable software
Last updated on 2024-09-03.
To me, portability is one of the most important attributes of good software.
Portable software is software you can build and run with minimal dependencies
and in many different kinds of environments, including highly
resource-constrained environments. The more portable your software is, the
greater the number of people who will be able to use and enjoy it.
Software that is highly portable tends to have other good qualities as well,
like being simple in implementation, polite, and
respectful.
This list comes from my personal experience with writing and using software,
and especially from the frustration I have experienced when building and
packaging software for use in environments that are not the typical GNU/Linux
desktop. The list written mostly from a Unix perspective, but many of the
guidelines apply to software in general, whether or not it's for a Unix-like
platform. There are even a few nods to non-Unix environments specifically.
The list begins with the most general principles, and ends in the weird and
obscure, things that might be called "tips" more than principles. Now, without
further ado...
- Write software in C, because that is the most portable language.
C is also formally standardized.
- For C programs, conform to the ISO standard, preferably an older
version like C99 or even C89/C90.
- If ISO C does not have the features you need, conform to
POSIX.
- If you must use Linux-specific features, make it explicit which ones you
are using. Make it easy to turn off Linux-specific features at build
time.
- In general, be reluctant to add new dependencies, and consider transitive
dependencies (dependencies of dependencies) when doing so. Be more inclined
to implement things yourself.
- Don't use bloated languages like C++, Python, Perl, Rust, JavaScript, Go.
For simple things that are best done in a scripting language, write shell
scripts and test them using a strict POSIX-conforming shell, not Bash. Use
standard tools like grep, sed, and awk for text processing.
- For more complex scripts, and for even greater portability than that
provided by shell scripts, consider
Lua. Lua is
implemented in ISO standard C, so it runs everywhere. It integrates nicely
with C code and is about 100x smaller than the more mainstream scripting
languages like Python. When statically linked, it fits into a single binary
that's no more than a few hundred kilobytes.
- For building your software, use portable Makefiles. Test them using a
strict POSIX make, not GNU make. Use GNU make only if you really need
specific extra features that it provides, and in that case, name your
Makefile "GNUmakefile": GNU make will recognize it, other make
implementations will not, and it is an obvious indication that your
Makefile requires GNU make.
- If your project is too complex for even GNU make, only then should you
consider weird complicated build systems like GNU Autotools and cmake.
Prefer Autotools over cmake, because Autotools does not introduce build
dependencies. If you use cmake then you have added an extra huge build
dependency to your project. Another hint: you do not need Autotools just
for feature detection. Document your assumptions about what features the
platform needs to support, and let the user ensure himself that his system
meets the requirements.
- Learn which functionality is standardized (for example, as part of ISO C or
POSIX), and which are GNU extensions. Don't use GNU extensions. Refer
to the
POSIX specification
and the
C99 specification.
- Support building and running your software offline as a first-class use
case.
- Do not depend on the presence of configuration files at runtime. Build sane
defaults into the program.
- Allow running the program as any user, including root. Do not try to
discourage running as root. If you insist on adding stern warning messages
about running as root, then please make them easy to disable. Some systems
do not have any user accounts other than root.
- In shell scripts, do not depend on "sudo". If the script requires
privileges, then note that in your documentation. Consider putting
privileged functionality in a separate script.
- Test with multiple C compilers, not just gcc and clang. Try building with a
minimal compiler like
chibicc.
- Do not assume that glibc is the C library that will be used on Linux
systems. Test your software with musl,
which is more standards-compliant than glibc.
- Do not assume that executables will be dynamically linked. Test your builds
with static linking. On Linux, glibc is pretty broken with static linking,
so test with musl.
- For C and C++ programs, test at compiler optimization level -Os,
which may be used when compiling for resource-constrained
systems.
- When using the GNU C toolchain (GCC and binutils), test your software with
the compiler flags -ffunction-sections -fdata-sections and linker
flag --gc-sections. This combination of options is sometimes used
to discard all unused functions and data definitions from the final
executable, making it smaller. It can also expose bugs. Some software does
not play nice with it. Make sure yours does, so that people compiling
for systems where space is at a premium can take advantage of
these options.
- For libraries, make it easy to build only a static version of the library.
Do not assume that whoever is building the software will always want to
build the shared library or even be able to do so. If you are using
libtool, then do not use it when building only the static library -- it's
totally unnecessary.
- Do not hardcode any absolute paths.
- Allow native language support (NLS) to be disabled at build time. NLS is
often unneeded, and the message files and non-English documentation just
end up wasting storage space, which may be highly limited.
- Distribute fully-compiled documentation in source code releases. Do not
make me have Perl (or whatever else) installed so I can compile a man page
from some other format. Ideally, include documentation in plain-text format
for maximum portability and usability.
- For command line programs, don't generate color output by default. Not
all terminals support color. Also, it's just rude.
- Don't overthink compression. Stick with gzip and zlib. Distribute source
code in .tar.gz format. Or, if you are distributing software mainly for
non-Unix platforms, use .zip files. Forget about trendy stuff like zstd
(aka "Zstandard", arrogantly named, as it is in no way "standard").
- For all files in your source distribution, as well as any files generated
by your program whose filename is not provided by the user, use only the
most portable character set: uppercase and lowercase letters A-Z a-z of the
English alphabet, digits 0-9, the hyphen (-), the underscore (_), and the
dot (.).
- If anyone could conceivably compile and/or run your software on a DOS-based
OS (including MS-DOS itself, DOS clones such as FreeDOS, or versions of
Windows from before the year 2001), or could conceivably put your software
on a floppy disk, use "8.3" filenames only. Use them for all files in your
source distribution and for any files generated by your program at runtime
(except when the filename is specified by the user). That is, no more than
eight ASCII characters before the extension, and no more than three in the
extension. It may sound silly to go out of your way to accomodate such old
software, but really, how hard is it to limit yourself to short filenames
for your source code?
- Do not use file names "aux", "con", "nul", or "prn", with or without a file
extension, in any combination of uppercase and lowercase. These are reserved
file names on Windows systems.
- Stick with ASCII in your source code and documentation. Especially don't
use frivolous Unicode characters like fancy checkmarks and quote marks,
emojis, and their ilk. The exception is if you need UTF-8 because you are
actually writing things in natural languages that ASCII cannot
represent.
- Do not rely on case-sensitivity of file names, because some commonly used
filesystems (most notably, FAT) are not case-sensitive. That is, don't
include files in your software that are the same name except for some
letters having a different case. You might wonder who would do something
that dumb, but in fact, the OpenBSD source code repository is guilty of
this.
Good examples of these guidelines:
- All of the software hosted at this site. (With some imperfections, but
I'm working on it.)
- Lua, a small and elegant scripting
language written in ISO standard C.
- Most suckless software.
Programs that violate these guidelines:
- Linux -- the kernel's build process is polluted by GNUisms: nonstandard
options, options mixed with positional arguments, totally unnecessary
dependency on Bison rather than yacc. Also, scripts gratuitously
written in Perl when they could have been written using purely POSIX
tools or Lua.
- git -- its configure script lies, saying that "you cannot use git
without perl". In fact, git can be used without Perl with only
minimal loss of functionality.
- GNU tar -- its configure script will refuse to run as root unless you
set an environment variable called "FORCE_UNSAFE_CONFIGURE".
- Lynx -- will not even start up if its configuration file is not
present in /etc.
- Many, many programs that include a "Makefile" that should properly
be named "GNUmakefile". Please, use the name "Makefile" only for
truly portable Makefiles.
33mph.com