From c47bd946b8045417715cee06882e846f3193c062 Mon Sep 17 00:00:00 2001 From: entrepreneur-wake Date: Fri, 18 Sep 2026 06:17:28 -0400 Subject: [PATCH] appimage: bundle dlopen()ed GL libs, gate the AppImage, document the real install path The published cradicle-gui AppImage aborts on a clean host with SIGABRT (exit 134). WebKitGTK's libEGL dlopen()s libGLESv2.so.2; it is not in the bundle, ldd never reports it, and the build host has it in /usr/lib64, so nothing notices. - Makefile: copy the dlopen()ed GL set into APPDIR/usr/lib (via ldconfig), then run the new closure gate so an unstartable AppImage fails the build. - appimage/check-appimage-closure: resolves the ELF closure AND the sonames referenced as strings (dlopen candidates); accepts an AppDir or a published AppImage file. - appimage/AppRun: preflight printing the missing sonames plus the two remedies, instead of core-dumping after libEGL warnings. - README: doc/debian.md + deb.cradicle.xyz are the supported install path; the AppImage is experimental and must be built on the supported suite. Verified on Ubuntu 24.04: with libGLESv2.so.2 available the GUI starts and serves its UI (WebKit renders crad://app/cgi-bin/main, 2339 bytes); without it, exit 134. crad --version works inside the bundle: 'C-Radicle 0.1 (commit deadbeef1234, timestamp 1763053651)'. --- Makefile | 17 +++++ README.md | 26 +++++++ appimage/AppRun | 22 ++++++ appimage/check-appimage-closure | 120 ++++++++++++++++++++++++++++++++ 4 files changed, 185 insertions(+) create mode 100755 appimage/check-appimage-closure diff --git a/Makefile b/Makefile index e37888f..d4042d0 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,23 @@ appimage: all RAD_ARCH=$$(uname -m); \ tar xf $(CRAD_SRC)/bin/radicle-*-$${RAD_ARCH}-*-linux-musl.tar.xz --wildcards --strip-components=2 -C $(APPDIR)/usr/share/cradicle/rad-bin '*/bin/*' chmod 755 $(APPDIR)/usr/share/cradicle/rad-bin/* + # Libraries the bundled WebKitGTK / libEGL dlopen() at startup. `ldd` does + # not list them, so the dependency loops above miss them and the published + # AppImage aborts with SIGABRT (exit 134) on a clean host. libGLESv2.so.2 is + # the one that was actually hit in the wild. + for so in libGLESv2.so.2 libglapi.so.0 libEGL_mesa.so.0; do \ + src=$$(ldconfig -p 2>/dev/null | awk -v s="$$so" '$$1==s {print $$NF; exit}'); \ + if [ -e "$(APPDIR)/usr/lib/$$so" ]; then \ + echo "already bundled: $$so"; \ + elif [ -n "$$src" ]; then \ + cp -L "$$src" "$(APPDIR)/usr/lib/$$so"; \ + echo "bundled (dlopen): $$so <- $$src"; \ + else \ + echo "WARNING: dlopen()ed runtime library not found on this host: $$so"; \ + fi; \ + done + # Gate: never publish an AppImage that cannot start. + bash appimage/check-appimage-closure $(APPDIR) # AppImage metadata install -m 755 appimage/AppRun $(APPDIR)/AppRun install -m 644 appimage/cradicle-gui.desktop $(APPDIR)/cradicle-gui.desktop diff --git a/README.md b/README.md index 939816d..381f135 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,32 @@ Dependencies: Installation: `make install` +## Bundling, installing, diagnosing + +The supported install path outside Tails is the APT repository; the steps live in +[`doc/debian.md`](doc/debian.md) and are reachable over the clearnet at + — the same file is served from the Radicle node, so the +repository is easy to miss. + +`make appimage` produces `build/cradicle-gui-$(uname -m).AppImage`. Treat it as a +**community/experimental** artifact: it embeds the libraries of whatever host +builds it, so it must be built on the distribution family you intend to support +(Trixie), and the build now ends with a gate: + +``` +bash appimage/check-appimage-closure build/cradicle-gui.AppDir +``` + +That gate resolves the ELF closure **and** the libraries that are `dlopen()`ed at +runtime — the latter are why a shipped AppImage can abort with no useful message +(`libGLESv2.so.2` is the known case). Run it against an already published AppImage +by passing the file itself: + +``` +bash appimage/check-appimage-closure cradicle-gui-x86_64.AppImage +``` + + Usage: `crad-http` diff --git a/appimage/AppRun b/appimage/AppRun index db7957c..3edb3f1 100755 --- a/appimage/AppRun +++ b/appimage/AppRun @@ -87,4 +87,26 @@ cleanup() { } trap cleanup EXIT + +# --- runtime preflight ------------------------------------------------------- +# The bundled WebKitGTK/libEGL open these with dlopen() at startup. `ldd` does +# not list them, so a bundle built on a host that has them in /usr/lib64 ships +# without them, and the GUI dies with SIGABRT (exit 134) after only libEGL +# warnings. Fail loudly and tell the user what to do instead. +_missing="" +for _so in libGLESv2.so.2; do + [ -e "$SELF_DIR/usr/lib/$_so" ] || _missing="$_missing $_so" +done +if [ -n "$_missing" ]; then + echo "cradicle-gui: this AppImage is not self-contained." >&2 + echo " missing:$_missing" >&2 + echo " These are dlopen()ed by the bundled WebKitGTK/libEGL at startup;" >&2 + echo " ldd does not report them, so the failure looks like a crash." >&2 + echo " Either install them on the host:" >&2 + echo " sudo apt install libgles2 libgl1-mesa-dri # Debian/Ubuntu" >&2 + echo " or use the packaged build (recommended): doc/debian.md" >&2 + exit 1 +fi +# ----------------------------------------------------------------------------- + "$SELF_DIR/usr/bin/crad-gui" "$@" diff --git a/appimage/check-appimage-closure b/appimage/check-appimage-closure new file mode 100755 index 0000000..b575a81 --- /dev/null +++ b/appimage/check-appimage-closure @@ -0,0 +1,120 @@ +#!/bin/bash +# +# check-appimage-closure — fail the build when an AppImage cannot start. +# +# Why this exists: the published `cradicle-gui` AppImage aborts with SIGABRT +# (exit 134) on a clean host. The cause is NOT a missing package that `ldd` +# would list: it is `libGLESv2.so.2`, which the bundled libEGL/WebKitGTK opens +# with dlopen() at startup. `ldd` cannot see it, the build host has it in +# /usr/lib64, and the AppImage ships without it. Nothing in the build notices. +# +# This gate resolves two sets: +# 1. the ELF dependency closure (DT_NEEDED) of every bundled executable and .so +# 2. sonames referenced as strings, i.e. candidates for dlopen() +# and fails if any of them is not inside the bundle and not provided by the host +# C runtime (glibc/gcc, which every AppImage expects the host to supply). +# +# Usage: appimage/check-appimage-closure +# Exit: 0 = self-contained, 1 = at least one soname is unresolved + +set -u + +TARGET="${1:-}" +if [ -z "$TARGET" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +if [ -f "$TARGET" ]; then + # An AppImage file: unpack it without FUSE, the way a host without + # /dev/fuse has to. + ABS="$(cd "$(dirname "$TARGET")" && pwd)/$(basename "$TARGET")" + chmod +x "$ABS" + ( cd "$WORK" && "$ABS" --appimage-extract >/dev/null 2>&1 ) || { + echo "check-appimage-closure: cannot unpack $TARGET" >&2; exit 1; } + ROOT="$WORK/squashfs-root" +else + ROOT="$TARGET" +fi + +# Every directory in the bundle that actually holds a shared object -- derived +# from the files themselves, so subdirectories like usr/lib/torsocks are covered +# and no false positive is reported for a library that IS present. +BUNDLE_LIBS="$(find "$ROOT" -type f -name '*.so*' -printf '%h\n' 2>/dev/null | sort -u | tr '\n' ':')" + +# Provided by the host on every distribution an AppImage targets. +HOST_RE='^(libc|libm|libdl|libpthread|librt|libresolv|libgcc_s|libstdc\+\+|ld-linux|libnss_|libutil|libcrypt|libanl|libthread_db)[.-]' + +in_bundle() { + local s="$1" d + for d in ${BUNDLE_LIBS//:/ }; do + [ -e "$d/$s" ] && return 0 + done + return 1 +} + +echo "== bundled library dirs ==" +printf '%s\n' "$BUNDLE_LIBS" | tr ':' '\n' | sed '/^$/d' + +# ---------- 1. ELF closure ---------- +declare -A MISSING=() +CHECKED=0 +while IFS= read -r elf; do + head -c4 "$elf" 2>/dev/null | grep -q $'\x7fELF' || continue + CHECKED=$((CHECKED + 1)) + while read -r s; do + [ -n "$s" ] || continue + [[ "$s" =~ $HOST_RE ]] && continue + in_bundle "$s" || MISSING["$s|${elf#$ROOT/}"]=1 + done < <(LD_LIBRARY_PATH="${BUNDLE_LIBS%:}" ldd "$elf" 2>/dev/null | + awk '/not[ \t]+found/ {print $1}') +done < <(find "$ROOT" -type f \( -perm -u+x -o -name '*.so*' \) 2>/dev/null) + +# ---------- 2. dlopen() candidates ---------- +DLOPEN=0 +while IFS= read -r elf; do + head -c4 "$elf" 2>/dev/null | grep -q $'\x7fELF' || continue + while read -r s; do + [ -n "$s" ] || continue + [[ "$s" =~ $HOST_RE ]] && continue + in_bundle "$s" || DLOPEN=1 + done < <(strings -a "$elf" 2>/dev/null | + grep -oE 'lib[A-Za-z0-9_+.-]+\.so(\.[0-9]+)+' | sort -u) +done < <(find "$ROOT" -type f \( -perm -u+x -o -name '*.so*' \) 2>/dev/null) + +# ---------- report ---------- +echo "== ELF objects checked: $CHECKED ==" + +if [ "${#MISSING[@]}" -eq 0 ] && [ "$DLOPEN" -eq 0 ]; then + echo "OK: bundle is self-contained" + exit 0 +fi + +if [ "${#MISSING[@]}" -gt 0 ]; then + echo "FAIL: ${#MISSING[@]} unresolved DT_NEEDED soname(s)" + printf '%s\n' "${!MISSING[@]}" | sort -t'|' -k1,1 | + awk -F'|' '{printf " %-34s <- %s\n", $1, $2}' +fi + +if [ "$DLOPEN" -eq 1 ]; then + echo "FAIL: soname(s) referenced as strings but absent from the bundle" + echo " (these are dlopen()ed at runtime — ldd does NOT show them, and" + echo " one of them, libGLESv2.so.2, is what makes the GUI abort)" + while IFS= read -r elf; do + head -c4 "$elf" 2>/dev/null | grep -q $'\x7fELF' || continue + while read -r s; do + [ -n "$s" ] || continue + [[ "$s" =~ $HOST_RE ]] && continue + in_bundle "$s" || printf ' %-34s <- %s\n' "$s" "${elf#$ROOT/}" + done < <(strings -a "$elf" 2>/dev/null | + grep -oE 'lib[A-Za-z0-9_+.-]+\.so(\.[0-9]+)+' | sort -u) + done < <(find "$ROOT" -type f \( -perm -u+x -o -name '*.so*' \) 2>/dev/null) | sort -u +fi + +echo +echo "Add the missing libraries to \$(APPDIR)/usr/lib in the 'appimage' target," +echo "or declare them as runtime dependencies in doc/debian.md and the homepage." +exit 1 -- 2.43.0