Privacy-first AI infrastructure.

Engineering

The AppImage that launched and rendered nothing

Sep 9, 20265 min read

Velum's Linux build shipped on 19 August as an AppImage and a .deb, verified on a real desktop the same day. Two weeks later it turned out to be broken on a machine we had not tested: current Arch, Hyprland, AMD graphics. The app launched. A window appeared. The window was empty, and stayed empty.

This is the worst shape a bug can take. There was no crash dialog, no log line in the user's face, nothing to search for. From the outside it looked like the app had started and decided to do nothing.

What the terminal said

Launching the same AppImage from a terminal produced the line that the GUI never showed:

Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...

"Aborting" is literal. Velum's desktop app is a Tauri app, so its UI is a WebKit web process. That process calls abort() and dies. GTK, meanwhile, has already mapped the window. So the frame is real, it is on your screen, and there is nothing behind it.

Why a bundled library broke the host's graphics

An AppImage is one file that carries the app and its dependencies, mounted at runtime. The point is that it runs on distributions the build machine has never heard of. The catch is that a small set of libraries must not travel with it, because they are the ones that talk to hardware and to the running compositor. Ship your own copy of those and you force the host's drivers to work with a stranger.

AppImageKit publishes an excludelist for exactly this, and libwayland-client, libwayland-cursor, libwayland-egl and libwayland-server are on it. Our build tool, linuxdeploy-plugin-gtk, deployed all four into the AppDir anyway.

On Ubuntu 24.04, where the image is built, nothing happens: the bundled copies match the host closely enough. On a host whose Wayland and Mesa are newer, the bundled copies win the lookup and shadow the host's. Mesa's libEGL then fails to initialise against a Wayland client library older than the compositor it is speaking to, and gives up.

The failure is therefore invisible to CI and invisible to any test on the build platform. It only appears on the distributions that update fastest, which is a good description of the Linux desktop audience for a developer tool.

Narrowing it

The AppDir can be unpacked, edited and repacked, which makes this testable by hand:

  • Remove the four libraries. The app renders.
  • Put them back. The window is empty again.
  • Remove them again. The app renders.

Present, absent, present. Deleting exactly those four files is both necessary and sufficient. No environment variable was a substitute: WEBKIT_DISABLE_DMABUF_RENDERER, WEBKIT_DISABLE_COMPOSITING_MODE and LIBGL_ALWAYS_SOFTWARE all still abort, because the process dies inside EGL initialisation, before any of those switches change a decision.

The fix that would have been worse

Stripping the files is easy. Putting the AppImage back together is where the trap is.

The obvious move is to repack with appimagetool. That produces a working file, and it quietly replaces the AppImage runtime, the small executable header that mounts the payload when you run it. The runtime linuxdeploy ships is a static-pie build that mounts through fusermount3. The one appimagetool bundles is the old FUSE 2 runtime, and Ubuntu has not shipped libfuse2 by default since 22.04. Every user on a modern Ubuntu would double-click the file and get nothing.

Trading a blank window on Arch for a dead download on Ubuntu is not a fix. So the repack splices the original runtime back on instead:

offset=$(./Velum.AppImage --appimage-offset)
params=$(unsquashfs -o "$offset" -s ./Velum.AppImage)
comp=$(awk '/^Compression/ {print $2}' <<<"$params")
bs=$(awk '/^Block size/ {print $3}' <<<"$params")

head -c "$offset" ./Velum.AppImage > runtime.bin
mksquashfs Velum.AppDir payload.sqfs -root-owned -noappend -comp "$comp" -b "$bs"
cat runtime.bin payload.sqfs > repacked

Compression and block size are read back off the original build rather than assumed, so the artifact is otherwise equivalent to what Tauri produced. The build step then re-extracts the result and fails if any libwayland file survived, and it also fails if it finds none to remove in the first place, because that would mean the tool's behaviour changed and the step is now protecting nothing.

We republished 0.14.0 rather than bumping it

The fixed build went out as 0.14.0 again, same version number, same filenames.

That looks sloppy and it was deliberate. Our update manifest carries a single top-level version, so publishing Linux as 0.14.1 would have set the manifest to 0.14.1 and dropped the macOS entry, taking macOS updates dark until a matching Mac build shipped. Republishing in place keeps every platform's entry alive.

The cost is that existing Linux 0.14.0 installs get no update prompt, since the manifest version equals theirs. That is acceptable here only because the broken installs render nothing and could not have used an update prompt anyway. It is not a habit, and a version number is normally the one thing you do not reuse.

The rebuild also picked up a change that had landed on main in the meantime: the bundle no longer stages the four foreign-platform ONNX Runtime binaries it never loads. The AppImage went from 401 MB to 336 MB in a release that was supposed to change nothing. A republish is not a rebuild of the same tree, which is worth knowing before you tell someone the bytes are identical.

If you ship a GTK or WebKit app as an AppImage

Three things carry over from this:

  1. Test on a distribution newer than your build image. Ubuntu LTS building for Ubuntu LTS proves very little. The bug lives in the gap between the build image's system libraries and the host's, so a current Arch or Fedora box is the test that matters.
  2. Check what your packaging tool actually deployed. find your AppDir for anything on the AppImageKit excludelist. Being on the list is not enforcement.
  3. Treat a mapped window with no content as a dead subprocess, not a rendering bug. Run from a terminal first. The message is usually there, addressed to nobody.

Velum for Linux is a single AppImage, no install, no account, and a seven-day trial with no key. Get it here. If you run Ubuntu 22.04 or Debian 12 it will not start: the build has a glibc floor of Ubuntu 24.04, which is a different post.

Share this article
XLinkedIn

Keep reading