Discovering AppImages
As explained in my previous post, application sandboxing doesn’t need to be relegated to the cloud and server space, desktop users can also enjoy a pain-free and transparent local app experience thanks to technologies like Flatpak and Snappy. Today I’ll show you how this fantastic concept can come to fruition on your own desktop with the simplest packaging framework I know: AppImage. Let’s talk a little about it before getting our hands dirty.
Why AppImage?
The aim behind packaged apps is to provide simplicity. Obtaining and using a packaged application should be equally simple tasks. All you should have to worry about is downloading a file from the vendor’s website or a package from a repository, launching it et voilà. Major existing implementations such as Canonical’s Snappy and Red Hat’s favourite, Flatpak, provide a decent level of simplicity to the end user, although it is not a wholly no-frills experience. At the very least, you’ll need to install a few packages on your system. And with Flatpak, you’ll also need to install one or more runtimes before you can get any apps running. Equally, building and distributing Snappy and Flatpak packages is not as simple as it could be, in my humble opinion. AppImage, on the other hand, does promise the simplest achievable process for everyone, and it does get it right.
AppImage files are very easy to produce: all you need to do is to whack all your binaries, libraries and assets into a single directory and run a bundling application on them. This will give you a single, self-contained binary which you can directly upload to a public software repository for anyone to download and run. There is no installation process, no configuration steps, no installing appimage
packages or dependencies for your end user. Just double-click the file and you’re cooking. It’s really that simple.
What does AppImage exactly bring to the table that other packaged app implementations don’t? Well, glad you asked. Here’s a list!3:
- Works out of the box, no installation of packages (unlike Snappy) or runtimes needed (unlike Flatpak)
- No root needed, as no packages or system config files need to be changed
- Binary delta updates, e.g., for continuous builds (only download the binary diff) using AppImageUpdate
- Can GPG2-sign your AppImages (inside the file)
With all this in mind I want to commence this tutorial. We’ll be sandboxing a simple C application with some library dependencies to make it a bit more interesting. Note these steps easily translate to C++, Fortran or any language that scatters -or not- dependencies all over your filesystem. I’ve decided to use one of my own projects, which is a basic C instant messaging application, although complex enough to include its own libraries, so you also get to see how that’s done. The only things you need to get started are a working Linux distribution with GCC installed, a shell and your trusty text editor1. Right, let’s get to it then.
Sandboxing an Application with AppImage
Firstly, let’s clone the repository of the application we’re going to be building for this tutorial
git clone git@github.com:dvejmz/cim.git
Now cd
into the repository you just downloaded and make a new directory, which we’ll use as a build workspace for our packaged app. By convention it should be called like the application you want to package and include the .AppDir
suffix.
mkdir -p cimx/cimx.AppDir
cd cimx/cimx.AppDir
Let’s copy the application’s source files and assets into it
cp -r ../../cim-client/ ./src
cp -r ../../libcim/ ./libcim
cp -r ../../assets ./assets
Run this couple of sed
commands to make the application’s Makefiles aware of the AppImage directory structure.
sed -i -e 's#^LIB_DIR=/usr/local/lib/cim#LIB_DIR=../libcim/bin -L../usr/lib#' src/Makefile
sed -i -e 's#-lcrypto#lib/libcrypto.so.1.1#' libcim/Makefile
On to the build stage, starting with libcim, which provides all the code shared between the client and the server executables of our instant messaging application,
cd libcim
make
Create the directories where the library and binary files will live in the AppDir.
cd ../
mkdir -p ./usr/lib ./usr/bin
cp libcim/bin/libcim.so ./usr/lib
cp libcim/lib/*.so* ./usr/lib
We need to set the linker library path so that it can find our libraries when we compile the binary in the upcoming steps
export LD_LIBRARY_PATH=$(realpath ./usr/lib):$(realpath ./bin):$(realpath ./lib)
On to building the main application
cd ./src
make
Copy the resulting executable into the AppImage directory
cp ./bin/cimx ../usr/bin
cd
back into the AppDir root dir and copy all the header files and assets
cd ../
mkdir -p ./usr/include
cp -r libcim/include ./usr/include/libcim
cp -r libcim/lib/cson-core ./usr/include/libcim/lib
cp -r libcim/lib/uthash ./usr/include/libcim/lib
Every AppImage is required to ship with an icon asset so that a shortcut for the application can be created in desktop environments such as GNOME or KDE. Let’s copy ours to our AppDir
cp ./assets/cimx.png ./
We have now built the application, linked in all the libraries and shuffled all output files around so that they can be readily bundled up.
The entrypoint to an AppImage packaged application is usually a shell script which sets up the environment in which it will run. You could also point it directly at the application executable but shell scripts are more flexible as they let you set environment variables for your application and perform other platform-dependent preparatory steps if required. This should, in practice, be a fairly small file. In our case, we’ll just be defining the search path ld
needs to follow to locate the dynamically linked libraries our app uses. Now let’s configure the AppImage file.
Create a shell script called AppRun
inside cimx/cimx.AppDir/
with the following contents
#!/usr/bin/env sh
HERE=$(dirname $(readlink -f "${0}"))
export LD_LIBRARY_PATH="${HERE}"/usr/lib
"${HERE}"/usr/bin/cimx
In this file we simply instruct our AppImage where to retrieve its dynamic libraries from and then invoke the application executable that we produced earlier on, just like we’d do with a normal application we had built from source.
The last step before creating the AppImage file is writing a desktop entry for it so that the packaged app can be run like any other installed application would do. Create a file in the following path: cimx/cimx.AppDir/cimx.desktop
;; cimx/cimx.AppDir/cimx.desktop
[Desktop Entry]
Name=cimx
Exec=AppRun
Icon=cimx
This is as basic as it gets. Define the display name of your app with Name
, the path to its icon with Icon
. More importantly, the entrypoint is set with the Exec
key. Note in this case we’re pointing our AppImage to an executable shell script but you can also point it directly at your application main executable if you don’t need to perform any set-up steps beforehand, as I said before.
The (hard?) work is finally over and all that’s left to do is build the AppImage. AppImage provides a straightforward application called AppImageKit, surprisingly distributed in AppImage format, which you can use to generate your package.
curl -L --progress-bar https://github.com/probonopd/AppImageKit/releases/download/7/appimagetool-x86_64.AppImage > ./appimagetool-x86_64.AppImage
Make it executable and run it against the AppDir folder
chmod +x ./appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage -n $(realpath ./cimx/cimx.AppDir) ./cimx.AppImage
And that’s it! Take your flaming new AppImage for a spin by just running it as any other binary2
./cimx.AppImage
You can now distribute this file (don’t zip it up!) to any fellow Linux user, knowing with a 100% certainty it will run on their machine just the same as it did on yours.
Needless to say the process we’ve described in this tutorial can -and should- be entirely automated with shell scripts, known as recipes. You can find the recipe for cimx in the repository you downloaded for this tutorial, under the packaging
directory. If you want more examples of recipes for well-known applications, check out this list compiled by the authors of AppImage themselves.
You can learn more about AppImage on their documentation page and read about AppImageKit and building AppImages here.
Armed with the resources above and what you (hopefully) learned in this tutorial, you should be quite able to produce distributable versions of relatively complex applications that will readily run on any Linux distribution out there. So get packaging!
A Final Note
I’ve intentionally picked a C application which is a bit involved to build for this tutorial with the aim of giving you a broader understanding of how a normal compiled application can be made into a working package, including the complexities inherent to using dynamic libraries and all. If your application is even simpler than this, it might take half the time and effort to produce a valid AppImage for it.
1: I might be lying a bit here, as I am assuming the Linux installation you use to follow this tutorial has FUSE already, which comes pre-installed fairly often with some Linux distros. If it doesn’t, you’ll need to install it: the package is usually called simply fuse
.
2: Unless you have the companion cimd
server application running on your machine before launching this, you won’t get the greatest of satisfactions seeing the output that this executable will produce for you. It most certainly will error out as soon as you launch it as it won’t be able to find a server to connect to.
3: This list contains a few important items cited in AppImageKit’s wiki. Refer to this article for a full list.