If AppImage, Flatpak and Snap don’t cut it, you can roll your own
Creating a universal open-source Linux installer (the hard way)
As part of the work I do on Genymotion Android Emulator, me and my team have to make sure we provide a seamless installation experience.
A while back, we decided to improve our Linux installer a bit to make it work out of the box on as many Linux distributions as possible. With this in mind, we evaluated Appimage, Flatpak and Snap… but none of them suited all our needs, so we came up with a new solution.
This blog post explains how to build your own universal Linux installer and introduces Copydeps, a tool we created to help us build Genymotion installer.
We needed our installer to satisfy the following conditions:
- Compatible with every Linux distributions we officially support (i.e. Ubuntu latest stable release, Ubuntu latest LTS, Debian stable and latest Fedora). It also had to work as much as possible on non-officially supported distributions.
- Support for multiple executables. Genymotion comes bundled with several executable files. In addition to Genymotion itself, we also have gmtool and genyshell – our command line tools to manipulate devices.
- Integrate correctly with the distribution (e.g. entry in application menus and Gmtool shell completion for bash & zsh).
- Reasonable size. We set the limit to 50 MB.
- VirtualBox friendly. Genymotion relies on VirtualBox, but we did not want to ship it with the installer (see previous requirement). However, of course, it had to be compatible with it.
We first took a look at existing solutions
Packaging software on Linux is not a new topic. There are already a lot of existing solutions, and even new ones that have been popping up lately.
Distribution packaging tools
Linux users usually install software through their distribution package management tool. This solution gives them the best integration of all, but it requires maintaining distribution-specific scripts, and even distribution-version specific scripts.
We wanted a more universal solution so this was not for us.
AppImage is a universal packaging solution for Linux. An application packaged via AppImage is a single file. This file is an ISO9660 file system bundling the executable with all its dependencies. A minimal Linux system in other words.
We evaluated this solution, but it did not fit our needs because:
- AppImage does not support applications composed of multiple executables ;
- The generated image was too big, around 80MB.
Flatpak and Snap
Theses solutions were promising, but we did not choose them for the following reasons:
- Both solutions are young, we don’t know which one is going to win or if both are going to stay ;
- At the time we evaluated them, we supported Debian 8, which was not supported by Flatpak and Snap ;
- Both solutions tend to produce large images.
Doing it the hard way
Since we were not happy with existing solutions, we decided to go with the good-old solution of creating an auto-extractible archive. This gives us more control on what we ship and makes it possible to:
- Interact with VirtualBox ;
- Make all our executables accessible to users ;
- Give us more control on what we bundle in our archive.
Our initial install builder scripts simply hardcoded a list of libraries, but this was error-prone. On some distributions, some libraries were missing or using a wrong version, other libraries had to be removed for Genymotion to work.
To avoid this we developed copydeps, a tool to analyze and copy library dependencies of our executables. Copydeps is a Python-based command-line tool which uses a combination of readelf and ldd to create a directed graph of the dependencies of an executable. It then traverses the graph and copies the dependencies to the destination directory.
It can also be used to generate Graphviz diagrams of the dependencies, to produce fancy images like these:
To avoid shipping libraries we consider to be part of the system, copydeps uses a blacklist file. The grayed-out items in the diagrams are excluded libraries.
We carefully created this blacklist from the Linux Standard Base documentation, augmented by a few rounds of QA. copydeps comes with a sample blacklist, which is actually the one we use to ship Genymotion.
Learnings we made along the way (common traps to avoid)
Shipping the required libraries is good, but ensuring your executables use them is better. One way to easily force your executables to load your libraries is to create a wrapper script which sets the LD_LIBRARY_PATH environment variable to point to your libraries directories, but it has a few drawbacks:
- You must create one wrapper script for each executable ;
- The LD_LIBRARY_PATH applies to any process started by your executable. In our case this means it is applied when calling VirtualBox tools, which we do not ship. Forcing VirtualBox tools to use library versions they were not built with can lead to difficult-to-debug crashes.
A better way to ensure your executables uses your copies of the dependent libraries is to define their RPATH. RPATH is a tag in ELF binaries which contains a list of directories where the Linux library loader should look for libraries before looking in system directories.
Our installer script iterates on all our executables and libraries and adjust their RPATH using chrpath.
Another trap is plugins: plugins are libraries loaded at runtime, so they are not discovered by copydeps. Genymotion has been built with the Qt framework. To force Qt to load plugins from our installation directory, we defined a qt.conf file. Amusingly, an empty qt.conf is enough to let Qt know that it should load plugins from the “plugins” directory inside our installation directory.
SSL and QtNetwork
SSL libraries are another tricky part. The OpenSSL project produces two libraries: libcrypto and libssl. Due to lack of API stability, QtNetwork loads them dynamically, and will only do so if it finds them both. So be sure to always ship both.
The final trap is the build machine itself. Mature Linux libraries provide backward compatibility: an executable built with version N of libfoo will work with version N+1. The other way is not true: an executable built with version N+1 of libfoo might not work with version N. This is not a problem for libraries you ship, but it is for the libraries you assume to be installed, such as libc or libstdc++.
To ensure the executable produced works on every Linux versions you plan to support, build the shipped executable on the oldest supported version. This way expected libraries are always newer, never older than the ones on your build machine.
Appimage, Flatpak and Snap sounded promising, but they did not support all our needs, so we rolled our own solution: Copydeps. This is not written in stone though, we keep an eye on these tools and might make the switch if these evolve to fit our needs.
If you need to build a universal Linux installer, you should first look at these tools. If for some reason you decide to create an installer by hand, give copydeps a try to copy your libraries.
Just make sure RPATH is correctly set on your binaries and that your executables load dynamically loaded libraries from the right place. Finally, check if your build machine uses the oldest distribution you want to support.
Thank you for reading. Hope you learned a few things! Make sure to check Copydeps in Genymobile Github repository and let me know your thoughts in comments. See you soon!