CI on a headless ubuntu-24.04-arm runner caught what a desktop Pi could not:
the AppImage only started on a machine that already had a full desktop stack
installed. Three distinct causes, all from bundling Debian's builds of
libraries that Debian builds for a co-versioned system, which is the opposite
of an AppImage's situation.
1. Hard-linked backends. Debian's libSDL2 lists libpulse, libasound, libX11
and libwayland-client as DT_NEEDED rather than dlopening them, so the
loader demanded all four at startup; the CI job failed with
"libpulse.so.0 => not found". Debian's OpenAL does the same through
libsndio, which itself hard-links libasound. Built from source with
--enable-*-shared and ALSOFT_DLOPEN, both dlopen their backends, so the
image now runs on a Wayland-only session, a KMSDRM handheld with no X
server, or a box with ALSA and no PulseAudio.
2. A stray link. Debian's libtheoradec is linked against libcairo, which
drags in X11, xcb, fontconfig and freetype for a video decoder.
--disable-examples leaves it needing only libogg.
3. SONAME collision with the host. OpenAL dlopens ALSA, ALSA's config loads
its PulseAudio hook plugin, and that plugin pulls the host's libsndfile
into the process. libsndfile links libogg, libvorbis and libmpg123 -- the
same three we bundle -- and since the loader resolves a SONAME once per
process it bound to our bullseye copies. A bullseye libmpg123 has no
mpg123_info2 (added in 1.32), so the plugin failed to relocate, ALSA
config collapsed, and the game ran with no audio device at all. Building
them current means our copies satisfy the host's libsndfile instead of
starving it.
The general rule, now stated as an assertion instead of a comment: never
bundle a library the host's own stack may also load unless ours is at least
as new as theirs. build_appimage.sh fails if any shipped object hard-requires
anything beyond glibc, libstdc++ and the font stack, and CI re-checks it on
the extracted artifact.
Host requirements drop from "a working desktop" to glibc 2.29+, libstdc++,
libfreetype6 and zlib. Bundled libraries drop from 13 to 10: libcairo,
libpixman and libsndio are gone entirely.
Verified on a Raspberry Pi 5 (trixie, Wayland): boots, imports, plays, and
audio works -- SDL 2.30 now picks the native Wayland backend rather than
falling back to XWayland as bullseye's 2.0.14 did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scripts/build.sh's `linux` target only ever produces x86_64: it unpacks
LOVE's official love-11.5-x86_64.AppImage and re-fuses game.love into it.
There is no aarch64 equivalent to unpack -- LOVE 11.5 publishes win32,
win64, macOS, Android, iOS and exactly one x86_64 AppImage -- so arm64
desktop Linux (Raspberry Pi 4/5, Armbian, arm64 VMs on Apple Silicon) had
no artifact at all.
Compile LOVE 11.5 from the official linux-src tarball instead, inside a
Debian bullseye arm64 container, and assemble the AppImage from scratch.
Both pinned inputs (the LOVE source tarball and the AppImage type-2
runtime, on a dated tag rather than `continuous`) are SHA-256 verified on
the host, so the container runs with no network access.
Bullseye is the compile environment, not a claim about where the artifact
runs: glibc is backward but not forward compatible, so linking against the
oldest supported glibc is the only thing that makes one artifact work
everywhere. The binaries come out needing only glibc 2.29 / GLIBCXX_3.4.21,
covering Raspberry Pi OS bullseye through trixie and Ubuntu 20.04 onward.
The dependency walker copies in LOVE's own libraries and leaves the
driver-coupled, loader-coupled and font-stack libraries to the host. That
last category is not cosmetic: Debian's libtheoradec is linked against
libcairo, so a host cairo gets loaded into the process, and because the
loader resolves one SONAME once per process it then binds to whatever
libfreetype we bundled -- bullseye's 2.10.4 has no FT_Get_Transform, which
cairo 1.18 needs, and the game died at startup with a symbol lookup error.
Excluding the whole font stack makes the process self-consistent.
CI gets three path-gated jobs: an offline selftest on ubuntu-latest (pins,
the host-arch guard, the exclude list, the AppRun fusion contract), a real
build on ubuntu-24.04-arm that asserts the layout, that every bundled
object resolves under AppRun's LD_LIBRARY_PATH, and that the glibc floor is
still <= 2.31, and a release job that reuses the shared game.love payload.
None of it needs secrets or self-hosted hardware, so it runs on fork PRs.
Verified end to end on a Raspberry Pi 5 (Debian trixie, Wayland): the
launcher boots from the AppImage and renders correctly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>