Portable binaries

From FreeGameDevWiki
Jump to: navigation, search

Preamble

This article describes how to build dynamic binary packages for Linux that work out of the box regardless of what distribution you're using and what libraries you have installed.

The described method differs to static linking in the following ways:

  • Easy to invoke manually since it is enabled with a single linker flag.
  • Requires no changes to your existing build system.
  • Gives the user an option to easily override libraries.
  • Allows your application to use packages that have no static library targets.
  • Generates slightly larger packages.
  • Requires you to manually copy the required shared libraries to the package when making a release.

The following differences to other dynamic linking techniques apply:

  • No need for startup scripts of any kind.
  • Does not require modifications to library paths.
  • Does not require scattering unmanaged files around.

Introduction

Relative dynamic libraries

In the context of this article, relative dynamic libraries refer to libraries that are addressed relative to the directory where the run executable lies, as opposed to absolute paths. The directory in which the executable lies is referred to as $ORIGIN by the dynamic loader and the way how it's used is explained in the ld.so(8) manual page. For this article, it is sufficient to understand these three things:

  • Additional library lookup paths can be embedded to executables with the -rpath linker flag.
  • The dynamic loader may substitute certain special character sequences in these lookup paths with special information.
  • $ORIGIN is one of these special cases and is substituted with the path to the directory in which the executable lies.

Build environment

A build environment is a collection of applications and libraries that allow compilation of executables or other libraries. An example of a build environment is any modern Linux distribution with a compiler and other related tools installed. A modern distribution isn't, however, a build environment that generates portable binaries because to be portable you need to target the lowest common denominator.

In other words, the rule of thumb of building portable binaries is that you need to link against system libraries that are no more recent than in the oldest distribution you intend to target because libraries are, in general, backwards compatible but not forwards compatible. As an example, a binary compiled on a Debian Lenny system with glibc 2.7 won't run on a Debian Sarge system with glibc 2.3 because the older glibc version doesn't have all the features found and used when the binary was compiled on a newer system. A binary compiled on Debian Sarge will run on Debian Lenny, however, thanks to backwards compatibility.

In practice, all this means is that you need to compile your application in a sufficiently old distribution. Examples of distributions that have been used successfully for making portable binaries include old glibc 2.2.5 based Red Hat Linux versions, Debian Sarge, and Debian Etch. For modern games targeting modern distributions, simply compiling your binaries in Debian oldstable has been found to be an easy and reliable approach.

System libraries that cannot be bundled

Glibc

Distributions and glibc versions used by them
Year Distro Glibc
2005 Debian Sarge 2.3.2
2008 Debian Etch 2.3.6
2004 Fedora Core 3 2.3.3
2006 Ubuntu Dapper 2.3.6
2004 SuSE Linux 9.1 2.3.3
2003 Slackware 9.0 2.3.1
2004 Mandrake 10.0 2.3.3

Glibc is an integral part of any distribution and generally cannot be replaced with a custom version due to its dependencies on many system files. Generally, if you bundle glibc with your application, it will likely break when run on a system with a different version of glibc, and even if you're lucky enough to get it to work now, it will probably break in the future.

Simply put, you should never statically link against glibc or bundle it as a shared library with your binary. Instead, the way to go is to take advantage of the principle of backwards compatibility of glibc by dynamically linking against a sufficiently old version and then simply using the version the user has.

As shown in the table to the left, basically all distributions that use a glibc version older than 2.3 have been unsupported for a good while. Thus, when targeting at least remotely new distributions, it's a good starting point to link against glibc 2.3.x or older.

Libasound

Bundling ALSA is not very future proof due to its use of plug-ins to support sound system or distribution specific features. For example, when distributions began switching to PulseAudio, all binaries that used their own ALSA libraries broke due to not being able to load the version incompatible plug-in provided by the distribution and not having support of their own for PulseAudio.

Xlib

Xlib has remained binary compatible for a long time but it has undergone some internal changes that may create dependencies to X protocols not available in older X servers. To avoid problems, simply link against the X libraries dynamically and use the libraries present in user's system.

OpenGL

Not using the OpenGL library version present in the user's system will break things horribly for sure since the library internals are completely different for different video cards. The binary interface is standardizes to the finest detail, though, so dynamically loading the version the user has is safe, and actually the only sensible option since it's a hardware acceleration library.

Installing the base system

Basically, any method that allows you to install and run an older distribution is valid for getting your build system up and running. Some ways to achieve this are outlined below:

Chroot jail

Advantages:

  • Native performance.
  • Easy to move files in and out.
  • Easy to install on some systems.

Disadvantages:

  • Cannot target different architectures.
  • Requires root permissions.

Example of installing a Debian Etch environment on Ubuntu:

sudo debootstrap --arch=i386 --variant=buildd etch ./debian-etch-i386 http://ftp.us.debian.org/debian

Note: It is possible to install an x86 chroot on an x86_64 system and build 32-bit binaries this way, but you need to run the chroot using linux32 or your machine will be reported as 64-bit by uname and friends in the build environment.

Virtual machine

Advantages:

  • Support for different architectures.
  • No root permissions required.

Disadvantages:

  • Poor performance.
  • Moving files in and out with SSH or FTP is cumbersome.

Example of installing a Debian oldstable environment using Qemu:

wget http://ftp.us.debian.org/debian/dists/oldstable/main/installer-i386/current/images/netboot/mini.iso
qemu-img create debian-oldstable-i386.img 2000M
qemu -boot d -cdrom mini.iso -hda debian-oldstable-i386.img

Virtualization

TODO

Installing dependencies

After you have installed your minimal build environment, it's time to install the dependencies required by your application. Generally, it makes no difference whether you compile the dependencies manually or install them using the package manager of the build environment. As long as the libraries are compiled in the same build environment with your final executable, they should not introduce compatibility problems. You might, however, want to compile some dependencies yourself to get security updates or new features not present in the old packages of your build environment.

Modifying your application

You might want to add support for loading data files relative to the directory where the executable lies to achieve a truly self-contained package that doesn't require any installation at all. It takes some extra effort since you need to abstract your file loading calls but it's totally worth it since your users will love it. There are a couple of packages that will help you make this reality without all that much pain:

Compiling your application

This is where the relative library path magic happens. The whole trick is to pass the following flags appropriately escaped to your compiler that will further pass them to the linker when your binaries are linked:

-Wl,-rpath -Wl,$ORIGIN/lib

If you're using autotools in your project, you can simply configure the package as follows:

./configure LDFLAGS="-Wl,-rpath -Wl,\\\$\$ORIGIN/lib"

If you don't want to memorize the cryptic sequence, you can also add it as a switch to your configure.in script:

AC_ARG_ENABLE([relpath],
  AC_HELP_STRING([--enable-relpath], [Data and libraries are searched relative to the executable]),
  [want_relative=yes], [want_relative=no])
if test "$want_relative" = yes; then
  LDFLAGS="-Wl,-rpath -Wl,\\\$\$ORIGIN/lib $LDFLAGS"
  CFLAGS="$CFLAGS -DUSE_RELATIVE_PATHS"
fi

If you decide to take this route, you can also include the macro controlling your data file loading mode to the same test, as is done above. Use the USE_RELATIVE_PATHS macro in your file loading code, configure with --enable-relpath flags in your build environment, compile and the result should be a portable binary that doesn't require installation or a startup script to work.

Packaging

Package layout

Make sure that the final layout of your final package matches the library path passed to the compiler and data path conventions of your code. This is probably obvious but is stressed here just in case since build systems typically create the traditional UNIX style installation tree for the package, which is not what you want in this case. You have to either modify your configure script to override all the different paths when building a relative binary or manually copy the files to their appropriate places.

About the package layout in general, it has been relatively popular among other applications to place the binaries to the root of the package directory and libraries and data files in their respective subdirectories. Unless there is a compelling reason to do otherwise, this layout is recommended since it makes it easy for the user to find the executables and it's consistent with most of the other binary packages.

Bundling libraries

You need to manually copy all the libraries your application depends on, except those mentioned earlier in the list of libraries that cannot be bundled, to the relative library lookup directory under your package root. The following command should be useful for determining what libraries your package depends on, but beware since none of the libraries loaded with dlopen and friends are shown:

ldd ./mybinary

Verification

Glibc version

strings ./mybinary | grep GLIBC

should print at most

GLIBC_2.3

Relative libraries

strings ./mybinary | grep ORIGIN

should produce something along the lines of

$ORIGIN/lib

Bundled libraries

LD_DEBUG=libs ./mybinary

should indicate that all the intended libraries are found from /path/to/binary/lib (RPATH from file /path/to/binary/mybinary).

Relative data files

(strace ./mybinary 2>&1) | grep "^open"

should indicate that all the data files are searched from the right places.

Examples

All the commands and scripts used for compiling binary releases for Lips of Suna and Hex-a-hop have been released into public domain. You can use these as a basis for your own build environment.

See also