#!/usr/bin/env 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 opens with # dlopen() at startup. `ldd` cannot see it, the build host has it installed, # and the AppImage shipped without it. Nothing in the build noticed. # # This gate checks two things: # 1. the ELF dependency closure (DT_NEEDED) of every bundled executable and .so # 2. the libraries the runtime is known to *load by name* (dlopen), which is a # strictly smaller set than "every soname mentioned as a string" # # Tier 2 is deliberately split, on evidence rather than on guesswork. Running the # published AppImage under LD_DEBUG=libs on Ubuntu 24.04 shows the loader # - trying libGLESv2.so.2 -> absent from the bundle -> SIGABRT (must be bundled) # - trying libGLX.so.1 / libGL.so.1 -> probes the host satisfies via glvnd # - never trying libOpenGL.so.0 / libGLESv1_CM.so.1, and libtorsocks.so.0 is # resolved through TORSOCKS_LIBRARY by AppRun, not by the loader # so only the first set can fail a build; the rest are reported as diagnostics. # A gate that fails on the whole string-scan set rejects a bundle that starts # perfectly well, which is how a gate gets deleted instead of trusted. # # Usage: appimage/check-appimage-closure # Exit: 0 = self-contained, 1 = a required library 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 BUNDLE_LIBS="$(find "$ROOT" -type d \( -name lib -o -name lib64 \) 2>/dev/null | 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)[.-]' # Libraries the GUI loads by name at runtime and cannot start without. # # Keep this list short and evidence-backed: every entry must come from an actual # reported failure (a reproduced exit code), never from a string scan. A long # list of guesses is worse than a short list of proofs, because it fails builds # of bundles that start fine and gets the gate deleted instead of trusted. # # libGLESv2.so.2 is the only entry with a reproduced failure behind it: # $ ./cradicle-gui-x86_64.AppImage # Couldn't open libGLESv2.so.2: cannot open shared object file: No such file # ... AppRun: line 90: Aborted (core dumped) [exit 134] # and with that one library inside the bundle the same binary stays up # (40 s timeout, no abort). REQUIRED_RUNTIME=( libGLESv2.so.2 # libEGL (WebKitGTK) dlopen()s it; absent => exit 134 ) 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. runtime-loaded (dlopen) requirements ---------- RUNTIME_FAIL=0 for s in "${REQUIRED_RUNTIME[@]}"; do in_bundle "$s" && continue echo "MISSING runtime library: $s (not in the bundle)" RUNTIME_FAIL=$((RUNTIME_FAIL + 1)) done # ---------- 3. diagnostics: other sonames referenced as strings ---------- echo "== ELF objects checked: $CHECKED ==" PROBES=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 printf '%s\n' "${REQUIRED_RUNTIME[@]}" | grep -qx "$s" && continue in_bundle "$s" && continue echo " probe (optional): $s <- ${elf#$ROOT/}" PROBES=$((PROBES + 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) [ "$PROBES" -gt 0 ] && echo " ($PROBES optional probe(s): dlopen fallbacks, not build failures)" if [ "${#MISSING[@]}" -eq 0 ] && [ "$RUNTIME_FAIL" -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 [ "$RUNTIME_FAIL" -gt 0 ]; then echo "FAIL: $RUNTIME_FAIL required runtime library/library set missing from the bundle" echo " (loaded with dlopen, so ldd never shows them; the GUI aborts on a" echo " host that does not happen to have them installed)" 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