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.