Building an AppImage for StepMania
StepMania is a fairly popular rhythm game, in the same vein as Dance Dance Revolution or In The Groove, and I wanted to have a portable executable on Linux that “just works” so I decided to make my first AppImage executable with this game. Let’s get to business.
Building StepMania
Build System
AppImage suggest the executable must be able to run on the oldest still supported Ubuntu LTS release and, at the writing of this post, that is Ubuntu 18.04 “Bionic Beaver” so I spin up a VM with it and install the bare minimum development niceties:
~/
$ apt-get install build-essential cmake git
Installing dependencies
For stepmania to build successfully, a few development libraries will be needed, these can be installed with the command below:
~/
$ apt-get install \
libmad0-dev libvorbis-dev libbz2-dev \
libx11-dev zlib1g-dev libjpeg62-dev \
libxinerama-dev libasound-dev libpulse-dev \
libva-dev libglew-dev libxrandr-dev \
libudev-dev libxtst-dev yasm
Getting & compiling the code
StepMania source code is available on GitHub so we clone the repo
~/
$ git clone https://github.com/stepmania/stepmania.git
And head to the Build
directory to run cmake
and prepare for the build.
~/stepmania/Build
$ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr ..
AppImage requires the install prefix to be /usr
for programs to be packaged.
If all dependencies are installed, camke
should end with success and we can
compile it by running make
after that.
~/stepmania/Build
$ make -j5
After a few minutes, compilation should complete and we can now test if the
game works by running the stepmania
executable on the project root directory.
~/stepmania
$ ./stepmania
If the game works, we can now move onto making an AppImage with it.
Creating an AppImage
Following AppImage’s “Packaging native binaries” guide, all the
compiled binaries will now be installed to an AppImage
directory using the
make install
on the Build
directory with the DESTDIR
variable set.
~/stepmania/Build
$ make install DESTDIR=AppDir
Now there should be an AppDir
directory with all the StepMania files installed
on it. In this case, executable and game files lie on
AppDir/usr/stepmania-5.1/
To create the AppImage we will be using Linux Deploy
Getting Linux Deploy
Linux Deploy is distributed as an AppImage itself, we can download the latest release on GitHub. On console it can be downloaded, made executable and test if it works with these commands:
~/stepmania/Build
$ wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
$ chmod +x linuxdeploy-x86_64.AppImage
$ ./linuxdeploy-x86_64.AppImage --version
That last command should give an output similar to this
linuxdeploy version 1-alpha (git commit ID 56760df), GitHub actions build 89 built on 2022-07-01 01:50:07 UTC
Now I will describe all the attempts at making an AppImage, with all the
pitfalls I encountered along the way. Keep in mind *all* attempts are
performed on a fresh make && make install DESTDIR=AppDir
execution with an
empty or deleted AppDir
.
make clean
or a new build from scratch were not needed at any point.
AppImage creation (Failed attempt)
Following the native binaries manual, running the command:
~/stepmania/build
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--output=appimage
Ends with the error:
[appimage/stderr] Desktop file not found, aborting
ERROR: Failed to run plugin: appimage (exit code: 1)
So to fix that, we pass the provided stepmania.desktop
from the repo to
linuxdeploy
~/stepmania/build
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--output=appimage
This execution also ends up with an error, only this time it doesn’t find the icon files.
ERROR: Could not find icon executable for Icon entry: stepmania-ssc
Icons for AppImage must be under AppDir/usr/share/icons/
and it seems
make install
has not copied them into its place, so we do that ourselves
before calling linuxdeploy
~/stepmania/build
$ cp -r ../icons/* AppDir/usr/share/icons/
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--output=appimage
And again, it ends with error, this time it seems linuxdeploy
is unable to
find the stepmania
executable which instead of being installed on the expected
AppDir/usr/bin
directory, it has been installed on AppDir/usr/stepmania-5.1/
instead, so we explicitly pass it to linuxdeploy
~/stepmania/build
$ cp -rv ../icons/* AppDir/usr/share/icons/
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--executable=AppDir/usr/stepmania-5.1/stepmania \
--output=appimage
And now, finally, linuxdeploy
succeeds and we are left with a shiny new
AppImage for StepMania, so we try to run it like we would any other AppImage:
~/stepmania/build
$ ./StepMania-675c752e9e-x86_64.AppImage
And we are greeted with an error.
//////////////////////////////////////////////////////
Exception: Couldn't find 'Songs'
//////////////////////////////////////////////////////
Error: Couldn't find 'Songs'
Time for the next step.
Debugging the AppImage
Since the regular not packaged executable works, and the AppImage throws an
error regarding being unable to find the Songs
directory, let’s check if
the AppImage added the all the files correctly by mounting it. Running the
command:
~/stepmania/Build
$ ./StepMania-675c752e9e-x86_64.AppImage --appimage-mount
Yields the following output and waits for the user to exit the process by
pressing Ctrl+C
/tmp/.mount_StepMaF7YAXz
By exploring this directory, we can check that the Songs
directory is
indeed available, and contains a song pack, so the song files are there.
$ ls -l /tmp/.mount_StepMaF7YAXz/usr/stepmania-5.1/Songs
total 1
-rw-r--r-- 1 root root 527 Jul 8 18:20 instructions.txt
drwxr-xr-x 5 root root 0 Jul 8 18:54 'StepMania 5'
The StepMania executable it is also available on both usr/bin/
and on
usr/stepmania-5.1
so besides having a duplicate file, the executables
are there and, if we execute usr/bin/stepmania
we get the same error:
$ /tmp/.mount_StepMaF7YAXz/usr/bin/stepmania
//////////////////////////////////////////////////////
Exception: Couldn't find 'Songs'
//////////////////////////////////////////////////////
Error: Couldn't find 'Songs'
But if we execute usr/stepmania-5.1/stepmania
the game launches. Better yet
if we execute usr/bin/stepmania
from the usr/stepmania-5.1/
directory
it will work as well, pointing towards a work directory related issue. So we
kill the ./StepMania-675c752e9e-x86_64.AppImage --appimage-mount
process and
let’s check where is the program attempting to get its working directory,
which should be usr/stepmania-5.1/
relative to the AppImage mount point,
and how can we fix it.
For this, we will be using the strace
command, which will give us an insight
at most file open/close calls and related functions that the AppImage does
during its execution, so without further ado, we run:
~/stepmania/Build
$ strace ./StepMania-675c752e9e-x86_64.AppImage
And a very long trace output reports on every thing the program tried
to open, and every output it generated. So we search for our error
line containing Couldn't find 'Songs'
and see what happened just before it.
The suspicious lines are:
stat("/home/builduser/dev/stepmania/Build/./Songs", 0x7ffcb512bfa0) = -1 ENOENT (No such file or directory)
stat("/home/builduser/dev/stepmania/Build/Songs", 0x7ffcb512bfa0) = -1 ENOENT (No such file or directory)
Where the program attempted to find the Songs
directory on the directory
we launched ./StepMania-675c752e9e-x86_64.AppImage
. This makes sense since,
as far as we are concerned, the AppImage file is the executable, even though
we know it is mounted somewhere else an run. If we look a few more lines above
the stat
calls, we find a call to getcwd
that returns the “incorrect”
current working directory:
getcwd("/home/builduser/dev/stepmania/Build", 4096) = 36
So somewhere in the code, something is calling getcwd
and getting an incorrect
response. To check it is indeed the current working directory, executing the
AppImage from the StepMania repository root like this:
~/stepmania
$ ./Build/StepMania-675c752e9e-x86_64.AppImage
Actually works, since it will get the resource files from the repository files even though it is ignoring the resources from where it should read them on the AppImage mount point.
The CWD issue
Looking getcwd
and AppImage, a bug report and its responses seem to
indicate that getcwd
and AppImage don’t get along too well, these are the
bad news, but it mentions the $APPIMAGE
environment variable that
among other variables are available for executables running from
an AppImage. This variables are accessible through getenv
and its presence
or lack thereof can be used to detect if we are running inside AppImage or not.
In particular, these 2 variables are interesting:
Var | Description |
---|---|
APPIMAGE |
(Absolute) path to AppImage file (with symlinks resolved) |
APPDIR |
Path of mount point of the SquashFS image contained in the AppImage |
So if APPIMAGE
and APPDIR
environment variables are present, our current
working directory must be $APPDIR/usr/stepmania-5.1
and with this info, we
shall modify the StepMania source code. We have it within reach and it shouldn’t
be too hard, right?
Fixing the CWD issue
Using my favourite IDE I searched for any call to getcwd
within the StepMania
source code and, lucky me, the developers decided to wrap the getcwd
call
into their own RString GetCwd()
method on the RageUtil.cpp
file.
The original RString GetCwd()
looks like this:
RString GetCwd()
{
char buf[PATH_MAX];
bool ret = getcwd(buf, PATH_MAX) != nullptr;
ASSERT(ret);
return buf;
}
Just before the return
statement we can add the AppImage detection code
such that, if APPIMAGE
and APPDIR
environment variables exist, we
return the correct path instead.
RString GetCwd()
{
char buf[PATH_MAX];
bool ret = getcwd(buf, PATH_MAX) != nullptr;
ASSERT(ret);
//If running on AppImage, return the correct cwd
if( getenv("APPIMAGE") && getenv("APPDIR") )
{
snprintf(buf, PATH_MAX, "%s/%s", getenv("APPDIR"), "usr/stepmania-5.1");
printf("Appimage detected; CWD at: %s\n", buf);
}
return buf;
}
Changes also available as a git patch file
Then we run make
and after testing the StepMania executable still works
outside AppImage, we perform a clean install into the AppDir
directory.
AppImage creation
Again, we run the latest commands that worked for the creation of the previous
AppImage, just adding the directory creation before linuxdeploy
is
called.
~/stepmania/build
$ mkdir -p AppDir/usr/share/icons/
$ cp -r ../icons/* AppDir/usr/share/icons/
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--executable=AppDir/usr/stepmania-5.1/stepmania \
--output=appimage
And after it finishes successfully, we run the AppImage
~/stepmania/build
$ ./StepMania-675c752e9e-x86_64.AppImage
And the firs output lines are the expected appimage detection.
Appimage detected; CWD at: /tmp/.mount_StepMadPMo4g/usr/stepmania-5.1
Appimage detected; CWD at: /tmp/.mount_StepMadPMo4g/usr/stepmania-5.1
After which the game loads and runs flawlessly! But the AppImage looks… Fat.
It is 88MB in size and, if you recall, there are two copies of the stepmania
executable which is not particularly small, so as optional next steps…
Improving AppImage size
We can remove the redundant stepmania
executable by removing it from the
AppDir
before calling linuxdeploy
with the executable from the
repository instead of the AppDir
like this:
~/stepmania/build
$ mkdir -p AppDir/usr/share/icons/
$ cp -r ../icons/* AppDir/usr/share/icons/
$ rm AppDir/usr/stepmania-5.1/stepmania
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--executable=../stepmania \
--output=appimage
From 88MB to 61MB in one step.
Adding extra libraries
Some libraries are loaded dynamically, so they need to be explicitly added, otherwise the AppImage will have an extra dependency on the target system. The libraries added here are the same that were installed during compilation, with the exception of X11 libraries, which were already added or expected to be available anyway.
~/stepmania/build
$ mkdir -p AppDir/usr/share/icons/
$ cp -r ../icons/* AppDir/usr/share/icons/
$ rm AppDir/usr/stepmania-5.1/stepmania
$ ./linuxdeploy-x86_64.AppImage \
--appdir=AppDir \
--desktop-file=../stepmania.desktop \
--executable=../stepmania \
--library=/usr/lib/x86_64-linux-gnu/libmad.so.0 \
--library=/usr/lib/x86_64-linux-gnu/libvorbis.so.0 \
--library=/usr/lib/x86_64-linux-gnu/libbz2.so \
--library=/usr/lib/x86_64-linux-gnu/libjpeg.so.62 \
--output=appimage
Afterthoughts
AppImage it is very convenient for distributing software for linux distributions that do not ship a program, and even large projects may be packaged into AppImage with minor to no modifications.
I still think distribution packaging is better in any case, but by its simplicity, AppImage is a close second.
Flatpack or Snap weren’t even an option since these packaging solutions require the target system to be aware of what a Flatpack or a Snap are, while AppImage doesn’t.