Mental model
An AppImage isn't a black box — it's a squashfs filesystem plus a launcher script chain that decides, at runtime, which copy of a library the app actually loads. Most "works on my machine, broken on theirs" Linux bugs live in that decision, not in the app's own code.
screenpipe's AppImage is built with linuxdeploy, which wraps the real binary in three layers. Each layer can override where libraries get loaded from, so the bug hunt is really: find which layer is pointing at the wrong copy.
flowchart TD
A["screenpipe.appimage
(squashfs, mounted or extracted)"] --> B["AppRun
screenpipe's own wrapper script"]
B -->|"sets SPA_PLUGIN_DIR → bundled usr/lib/spa-0.2"| C["AppRun.screenpipe-original
linuxdeploy launcher script"]
C -->|"sources apprun-hooks/linuxdeploy-plugin-gtk.sh
forces GDK_BACKEND, GTK env"| D["AppRun.wrapped
linuxdeploy ELF binary"]
D -->|"prepends bundled usr/lib/ onto LD_LIBRARY_PATH"| E["usr/bin/screenpipe-app
the actual binary"]
Two env vars matter most: LD_LIBRARY_PATH (general shared-library search path) and any *_PLUGIN_DIR variable a library defines for its own plugin discovery (PipeWire's SPA, GStreamer, GTK modules, etc). Both get force-pointed at the bundle before your system libraries are ever considered — that's normally correct AppImage behavior, but it means a stale or incomplete bundle can't be silently rescued by a good system install sitting right next to it.
01 Extract it
Turn the single-file AppImage into a real, inspectable, editable directory tree.
# from wherever the AppImage lives ./screenpipe.appimage --appimage-extract # → creates ./squashfs-root/ next to it, untouched original stays intact
Do this in a scratch directory, not on top of the release file — you'll be diffing and possibly patching this copy, and you want the original left alone as a known-good fallback.
02 Trace the launcher chain
Before touching any library, find every place the launcher scripts set a path variable — that tells you which directory is actually authoritative at runtime.
grep -nE "LD_LIBRARY_PATH|PLUGIN_DIR|_PATH=" squashfs-root/AppRun \ squashfs-root/AppRun.* squashfs-root/apprun-hooks/*.sh 2>/dev/null
Anything assigned here — SPA_PLUGIN_DIR, GST_PLUGIN_PATH, GDK_PIXBUF_MODULE_FILE, whatever — is a candidate for the same bug class: a bundled directory that's stale, sparse, or simply wrong, silently shadowing a correct system copy.
03 Inventory what's actually bundled
Two different failure shapes to check for, separately.
Shape A — a stale version of a library that is present
# what does the running binary actually need? ldd squashfs-root/usr/bin/screenpipe-app 2>&1 | grep "not found" # for any given bundled .so, read its embedded version string strings squashfs-root/usr/lib/libpipewire-0.3.so.0 \ | grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" | sort -u
Shape B — a plugin/module directory that's incomplete
Libraries like PipeWire, GStreamer, and GTK don't just link a .so — they load a directory of backend plugins at runtime. A library can be the right version and still be useless if its plugin directory only shipped one file out of thirty.
find squashfs-root -iname "*plugin*" -maxdepth 4 -type d find squashfs-root/usr/lib/<suspect-dir> -name "*.so" | wc -l
04 Diff bundled vs. system
The version/plugin-count comparison only means something next to what your distro actually ships.
| Distro family | Query installed version |
|---|---|
| Arch | pacman -Q <pkg> |
| Debian / Ubuntu | dpkg -l | grep <pkg> |
| Fedora / RHEL | rpm -q <pkg> |
| Any (file-level) | strings /usr/lib/<lib>.so | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' |
A version gap of a patch release is rarely the cause of anything. A gap of multiple minor versions on a library with an active plugin ABI (PipeWire, GStreamer) is worth treating as a lead, not a coincidence.
05 Let the runtime logs confirm it
screenpipe logs through tracing-subscriber. A few line shapes reliably mean "the library layer, not the app logic."
| Log signature | Reads as |
|---|---|
Creation failed right after a manager/backend "started" line | library layer the handle came up but the backend it needs couldn't init — classic sparse-plugin-dir symptom |
| A restart / retry loop on the same subsystem | library layer app logic is fine, it's being told the resource doesn't exist and keeps trying |
error while loading shared libraries: ... cannot open shared object file | library layer literal — LD_LIBRARY_PATH doesn't include something the binary needs |
| Feature silently no-ops with no error at all | library layer often a plugin that failed to load logs at debug level only — rerun with more verbosity before assuming it's app logic |
| A specific, named panic inside screenpipe's own crates | app logic this guide doesn't help — that's a real code bug |
06 Patch and repackage
Once you know which file or directory is the culprit, replacing it in the extracted tree is just cp.
# -L is not optional: system libs are often symlinks, # and appimagetool needs real regular files, not dangling links cp -L /usr/lib/<the-stale-lib>.so.0 squashfs-root/usr/lib/<the-stale-lib>.so.0 # for a whole plugin directory, replace it wholesale rather than merging rm -rf squashfs-root/usr/lib/<plugin-dir> cp -rL /usr/lib/<plugin-dir> squashfs-root/usr/lib/<plugin-dir> # repackage into a normal, drop-in-replacement single-file AppImage ARCH=x86_64 appimagetool squashfs-root screenpipe-fixed.appimage
rsync isn't installed, rsync -a src/ dst/ fails with "command not found" but a wrapping script can swallow that and continue — leaving you with a directory that looks copied but is actually still the old, incomplete one. Verify file counts after any bulk copy; don't trust exit codes alone.
07 Verify against a real profile — not a scratch one
This is the step most likely to give a false negative.
A fresh, isolated SCREENPIPE_DATA_DIR — even with onboarding explicitly skipped — often never reaches the state where capture actually starts within a short manual test window. Permission grants, prior setup, and other startup gates beyond just "onboarding complete" seem to matter. If you test the patch against throwaway data and see nothing happen, that's inconclusive, not a failed fix.
HOME="$HOME" \
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" \
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" \
./screenpipe-fixed.appimage 2>&1 | tee ~/screenpipe-verify.log
Running with your real, already-onboarded profile is what actually exercises the code path you're trying to fix. Grep the log for the signal you identified in step 5 — its absence, sustained over the test window, is your actual confirmation.
SCREENPIPE_SKIP_ONBOARDING (checked in apps/screenpipe-app-tauri/src-tauri/src/main.rs) to bypass the GUI wizard — helpful for cutting steps, but not a substitute for real profile data per above.
Process hygiene gotchas
Small things that waste time if you don't expect them.
pkill -f <appimage-path>usually won't match. The AppRun chain execs into a differently-named final binary (screenpipe-app), so pattern-matching on the AppImage's own filename misses the live process. Find the PID withps aux | grep screenpipe-appandkillit directly.- A repackaged, launched AppImage self-mounts via FUSE at something like
/tmp/.mount_screen<random>. After killing the process, checkmount | grep fuse.screenpipeand runfusermount -u <path>— it'll report "busy" for a second or two right after the kill, that's normal, just retry. - Keep the original AppImage untouched. Always extract into a separate scratch copy. It's your known-good fallback and your basis for the diff.
Cheat sheet
The whole loop, back to back.
# 1. extract ./screenpipe.appimage --appimage-extract # 2. find the path-override points grep -nE "LD_LIBRARY_PATH|PLUGIN_DIR" squashfs-root/AppRun* # 3. inventory + diff a suspect lib strings squashfs-root/usr/lib/<lib>.so.0 | grep -E "^[0-9]+\.[0-9]+\.[0-9]+$" pacman -Q <pkg> # or dpkg -l / rpm -q # 4. inventory + diff a suspect plugin dir find squashfs-root/usr/lib/<dir> -name "*.so" | wc -l find /usr/lib/<dir> -name "*.so" | wc -l # 5. patch cp -L /usr/lib/<lib>.so.0 squashfs-root/usr/lib/<lib>.so.0 rm -rf squashfs-root/usr/lib/<dir> && cp -rL /usr/lib/<dir> squashfs-root/usr/lib/<dir> # 6. repackage ARCH=x86_64 appimagetool squashfs-root screenpipe-fixed.appimage # 7. verify against REAL data, not a scratch dir HOME="$HOME" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" \ XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" \ ./screenpipe-fixed.appimage 2>&1 | tee ~/screenpipe-verify.log # cleanup ps aux | grep screenpipe-app # find the real PID kill <pid> mount | grep fuse.screenpipe && fusermount -u <mount-path>
Automate it: screenpipe-appimage-debug
Every step above, wired into four subcommands. It won't tell you a mismatch is the bug — it flags candidates and leaves the call to you — but extraction, inventory, diffing, patching, repackaging, and a scripted verify pass are each one command away.
What each subcommand covers
| Subcommand | Maps to | Does |
|---|---|---|
inspect | steps 1–5 | extracts, prints the AppRun path overrides, diffs bundled libs vs. system (allowlisted libs only — see below), runs ldd for genuinely missing deps, flags sparse plugin dirs. Read-only. |
patch | step 6 | --auto applies everything inspect flagged; --lib NAME / --dir NAME fixes one thing at a time. |
package | step 6 | repackages the patched tree into a new standalone .appimage via appimagetool. |
verify | step 7 | launches the build and greps for the pass/fail log signatures from step 5. Isolated data by default (fast, inconclusive); --real-data runs it against your actual profile instead — nothing touches real data unless you pass that flag. |
all | the whole loop | runs inspect always; add --auto to also patch + package, and --real-data to also verify against your real profile. Without those flags it stops after the report. |
Install
- bash
- coreutils
- binutils (strings)
- glibc (ldconfig)
- appimagetool (package/all)
- fuse2 (verify/all)
# 1. pick a directory on your PATH mkdir -p ~/.local/bin # 2. save the script below as ~/.local/bin/screenpipe-appimage-debug # (expand "full source" further down and paste it in) $EDITOR ~/.local/bin/screenpipe-appimage-debug # 3. make it executable chmod +x ~/.local/bin/screenpipe-appimage-debug # 4. confirm the directory is actually on PATH echo "$PATH" | tr ':' '\n' | grep -qx "$HOME/.local/bin" \ || echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc which screenpipe-appimage-debug
Run it
# look, don't touch — just the report screenpipe-appimage-debug inspect ~/AppImages/screenpipe.appimage # apply what it flagged, then repackage screenpipe-appimage-debug patch /tmp/screenpipe-appimage-debug/screenpipe --auto screenpipe-appimage-debug package /tmp/screenpipe-appimage-debug/screenpipe \ ~/AppImages/screenpipe-fixed.appimage # confirm it actually works, against your real profile screenpipe-appimage-debug verify ~/AppImages/screenpipe-fixed.appimage \ --real-data --seconds 30 # or the whole loop in one line screenpipe-appimage-debug all ~/AppImages/screenpipe.appimage --auto --real-data
libpipewire-0.3.so.0 by default) — an earlier version of this script diffed every bundled .so blindly and produced a wall of false positives, because most libraries embed unrelated version-shaped numbers (vendored sub-deps, Unicode table versions) rather than their own release version. Extend the list with export SCREENPIPE_DEBUG_VERSION_LIBS="libfoo.so.1" only after checking that library's strings output by hand. The sparse-plugin-dir check has no such caveat — file counting is reliable.
verify never touches your real ~/.screenpipe data unless you pass --real-data. Without it, capture may never start within the test window (step 7) — that's an inconclusive result, not a failed one.
full source — screenpipe-appimage-debug (~330 lines)
#!/usr/bin/env bash
# screenpipe-appimage-debug — automates the AppImage library-debugging method
# from the "AppImage Library Debugging" guide: extract, inventory bundled
# libs, diff against the system, flag suspects, optionally patch/repackage,
# optionally verify.
#
# Subcommands:
# inspect <appimage> [workdir] extract (if needed) + report suspects
# patch <workdir> [--auto|--lib X|--dir Y] apply fixes into squashfs-root
# package <workdir> <output.appimage> repackage squashfs-root
# verify <appimage> [--real-data] [--seconds N] run it, grep for signal
# all <appimage> [workdir] [--auto] [--real-data] run the full loop
#
# Nothing here auto-launches the app against your real profile unless you
# pass --real-data explicitly to `verify` (or `all --real-data`).
set -uo pipefail
MIN_MINOR_GAP=2 # flag a version diff at or above this many minor versions
SPARSE_RATIO=3 # flag a plugin dir if system has >= this many times the bundled file count
SPARSE_MIN_SYSTEM=4 # only consider a dir "plugin-shaped" if system side has at least this many files
# Version strings sniffed via `strings` on an arbitrary .so are unreliable —
# most libraries embed unrelated version-shaped numbers (vendored sub-deps,
# Unicode table versions, font-format specs, etc), not their own release
# version. Only compare versions for libraries confirmed (by hand, once) to
# embed a clean, unambiguous self-version string. Extend by exporting
# SCREENPIPE_DEBUG_VERSION_LIBS="libfoo.so.1 libbar.so.2" after you've
# manually verified a library's `strings` output is trustworthy.
VERSION_CHECKED_LIBS="libpipewire-0.3.so.0 ${SCREENPIPE_DEBUG_VERSION_LIBS:-}"
c_bold=$'\033[1m'; c_red=$'\033[31m'; c_grn=$'\033[32m'; c_yel=$'\033[33m'; c_dim=$'\033[2m'; c_off=$'\033[0m'
say() { printf '%s\n' "$*" >&2; }
info() { say "${c_dim}$*${c_off}"; }
warn() { say "${c_yel}$*${c_off}"; }
good() { say "${c_grn}$*${c_off}"; }
bad() { say "${c_red}$*${c_off}"; }
usage() {
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
exit 1
}
version_of() {
# extract the first plausible X.Y.Z version string embedded in a binary
strings -a "$1" 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -Vu | head -1
}
system_path_for() {
# find the system copy of a bundled soname, via ldconfig cache first
local soname="$1" p
p=$(ldconfig -p 2>/dev/null | awk -v s="$soname" '$1==s {print $NF; exit}')
if [ -n "${p:-}" ] && [ -e "$p" ]; then printf '%s\n' "$p"; return 0; fi
for cand in "/usr/lib/$soname" "/usr/lib64/$soname" "/usr/lib/x86_64-linux-gnu/$soname"; do
[ -e "$cand" ] && { printf '%s\n' "$cand"; return 0; }
done
return 1
}
system_dir_for() {
# given a bundled dir's path relative to usr/lib, find a same-shaped system dir
local rel="$1"
for cand in "/usr/lib/$rel" "/usr/lib64/$rel" "/usr/lib/x86_64-linux-gnu/$rel"; do
[ -d "$cand" ] && { printf '%s\n' "$cand"; return 0; }
done
return 1
}
minor_gap() {
# prints the |minor_a - minor_b| gap, or 99 if majors differ
local va="$1" vb="$2"
local maja min_a majb min_b
maja=${va%%.*}; min_a=$(printf '%s' "$va" | cut -d. -f2)
majb=${vb%%.*}; min_b=$(printf '%s' "$vb" | cut -d. -f2)
if [ "$maja" != "$majb" ]; then printf '99\n'; return; fi
printf '%s\n' "$(( min_a > min_b ? min_a - min_b : min_b - min_a ))"
}
cmd_extract() {
local appimage="$1" workdir="$2"
mkdir -p "$workdir"
if [ -d "$workdir/squashfs-root" ]; then
info "reusing existing extraction at $workdir/squashfs-root"
return 0
fi
info "extracting $appimage -> $workdir/squashfs-root"
( cd "$workdir" && "$appimage" --appimage-extract >/dev/null )
}
cmd_inspect() {
local appimage="${1:?usage: inspect <appimage> [workdir]}"
local workdir="${2:-/tmp/screenpipe-appimage-debug/$(basename "$appimage" .appimage)}"
appimage="$(readlink -f "$appimage")"
cmd_extract "$appimage" "$workdir"
local root="$workdir/squashfs-root"
say ""
say "${c_bold}== launcher chain: path overrides ==${c_off}"
grep -nE 'LD_LIBRARY_PATH|PLUGIN_DIR|_PATH=' "$root"/AppRun "$root"/AppRun.* "$root"/apprun-hooks/*.sh 2>/dev/null \
| sed "s|$root/||" || info "(none found — check AppRun manually, launcher structure may differ)"
local libs_report="$workdir/inspect-libs.tsv"
local dirs_report="$workdir/inspect-dirs.tsv"
: > "$libs_report"; : > "$dirs_report"
say ""
say "${c_bold}== bundled libraries: version check (allowlisted libs only) ==${c_off}"
info "everything else only gets the missing-deps + plugin-dir checks below — see VERSION_CHECKED_LIBS."
printf '%-32s %-12s %-12s %s\n' "LIBRARY" "BUNDLED" "SYSTEM" "VERDICT"
while IFS= read -r -d '' f; do
local soname bver sver gap verdict spath
soname=$(basename "$f")
case " $VERSION_CHECKED_LIBS " in *" $soname "*) : ;; *) continue ;; esac
bver=$(version_of "$f")
if spath=$(system_path_for "$soname"); then
sver=$(version_of "$spath")
else
spath=""; sver=""
fi
if [ -z "$bver" ] || [ -z "$sver" ]; then
verdict="no-version-string"
else
gap=$(minor_gap "$bver" "$sver")
if [ "$gap" -ge "$MIN_MINOR_GAP" ]; then verdict="MISMATCH"; else verdict="ok"; fi
fi
printf '%-32s %-12s %-12s %s\n' "$soname" "${bver:--}" "${sver:--}" "$verdict"
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$f" "$spath" "${bver:-}" "${sver:-}" "$verdict" "$soname" >> "$libs_report"
done < <(find "$root/usr/lib" -maxdepth 1 -name '*.so*' -type f -print0 2>/dev/null)
say ""
say "${c_bold}== missing dependencies (ldd, with the bundle's own LD_LIBRARY_PATH) ==${c_off}"
local main_bin missing
main_bin=$(find "$root/usr/bin" -maxdepth 1 -type f | head -1)
if [ -n "${main_bin:-}" ]; then
missing=$(LD_LIBRARY_PATH="$root/usr/lib" ldd "$main_bin" 2>&1 | grep -i "not found" || true)
if [ -n "$missing" ]; then
bad "$missing"
warn "these are genuinely unresolved at link time — highest-confidence signal in this whole report."
else
good "none — everything the binary links against resolves within the bundle."
fi
else
warn "couldn't find a binary under usr/bin to check"
fi
say ""
say "${c_bold}== plugin-shaped directories vs. system ==${c_off}"
printf '%-40s %-10s %-10s %s\n' "DIR (relative to usr/lib)" "BUNDLED" "SYSTEM" "VERDICT"
while IFS= read -r -d '' d; do
local rel bcount scount sdir verdict
rel=${d#"$root"/usr/lib/}
[ "$rel" = "$d" ] && continue
sdir=$(system_dir_for "$rel") || continue
bcount=$(find "$d" -name '*.so' -type f 2>/dev/null | wc -l)
scount=$(find "$sdir" -name '*.so' -type f 2>/dev/null | wc -l)
[ "$scount" -lt "$SPARSE_MIN_SYSTEM" ] && continue
if [ "$bcount" -gt 0 ] && [ $(( scount / bcount )) -ge "$SPARSE_RATIO" ]; then
verdict="SPARSE"
elif [ "$bcount" -eq 0 ]; then
verdict="SPARSE"
else
verdict="ok"
fi
printf '%-40s %-10s %-10s %s\n' "$rel" "$bcount" "$scount" "$verdict"
printf '%s\t%s\t%s\t%s\t%s\n' "$d" "$sdir" "$bcount" "$scount" "$verdict" >> "$dirs_report"
done < <(find "$root/usr/lib" -mindepth 1 -maxdepth 3 -type d -print0 2>/dev/null)
say ""
local n_mismatch n_sparse
n_mismatch=$(awk -F'\t' '$5=="MISMATCH"' "$libs_report" | wc -l)
n_sparse=$(awk -F'\t' '$5=="SPARSE"' "$dirs_report" | wc -l)
if [ "$n_mismatch" -gt 0 ] || [ "$n_sparse" -gt 0 ]; then
warn "${n_mismatch} library version mismatch(es), ${n_sparse} sparse plugin dir(s) flagged."
info "review: $libs_report / $dirs_report"
info "apply fixes: screenpipe-appimage-debug patch $workdir --auto"
else
good "nothing flagged — bundled libs look current relative to this system."
fi
say "${c_dim}workdir: $workdir${c_off}"
}
cmd_patch() {
local workdir="${1:?usage: patch <workdir> [--auto|--lib NAME|--dir NAME]}"; shift
local root="$workdir/squashfs-root"
local mode="${1:---auto}"
[ -d "$root" ] || { bad "no extraction at $root — run inspect first"; exit 1; }
apply_lib() {
local bpath="$1" spath="$2"
if [ -z "$spath" ]; then warn " skip $bpath — no system match recorded"; return; fi
cp -L "$spath" "$bpath" && good " patched $(basename "$bpath")"
}
apply_dir() {
local bdir="$1" sdir="$2"
if [ -z "$sdir" ]; then warn " skip $bdir — no system match recorded"; return; fi
rm -rf "$bdir" && cp -rL "$sdir" "$bdir" && good " patched dir $(basename "$bdir") ($(find "$bdir" -name '*.so' | wc -l) files)"
}
case "$mode" in
--auto)
say "${c_bold}patching all MISMATCH/SPARSE findings from last inspect${c_off}"
[ -f "$workdir/inspect-libs.tsv" ] || { bad "no $workdir/inspect-libs.tsv — run inspect first"; exit 1; }
while IFS=$'\t' read -r bpath spath bver sver verdict soname; do
[ "$verdict" = "MISMATCH" ] && apply_lib "$bpath" "$spath"
done < "$workdir/inspect-libs.tsv"
while IFS=$'\t' read -r bdir sdir bcount scount verdict; do
[ "$verdict" = "SPARSE" ] && apply_dir "$bdir" "$sdir"
done < "$workdir/inspect-dirs.tsv"
;;
--lib)
local name="${2:?--lib needs a soname, e.g. libpipewire-0.3.so.0}"
local bpath="$root/usr/lib/$name"
local spath; spath=$(system_path_for "$name") || { bad "no system copy of $name found"; exit 1; }
apply_lib "$bpath" "$spath"
;;
--dir)
local rel="${2:?--dir needs a path relative to usr/lib, e.g. spa-0.2}"
local bdir="$root/usr/lib/$rel"
local sdir; sdir=$(system_dir_for "$rel") || { bad "no system copy of $rel found"; exit 1; }
apply_dir "$bdir" "$sdir"
;;
*) usage ;;
esac
}
cmd_package() {
local workdir="${1:?usage: package <workdir> <output.appimage>}"
local out="${2:?usage: package <workdir> <output.appimage>}"
local root="$workdir/squashfs-root"
[ -d "$root" ] || { bad "no extraction at $root"; exit 1; }
command -v appimagetool >/dev/null || { bad "appimagetool not found on PATH"; exit 1; }
ARCH=x86_64 appimagetool "$root" "$out"
good "built $out"
}
cmd_verify() {
local appimage="${1:?usage: verify <appimage> [--real-data] [--seconds N]}"; shift
local real_data=0 seconds=30
while [ $# -gt 0 ]; do
case "$1" in
--real-data) real_data=1 ;;
--seconds) seconds="$2"; shift ;;
esac
shift
done
appimage="$(readlink -f "$appimage")"
local log; log=$(mktemp /tmp/screenpipe-verify.XXXXXX.log)
if [ "$real_data" -eq 1 ]; then
info "launching against your real profile (HOME/XDG_CONFIG_HOME/XDG_DATA_HOME) for up to ${seconds}s..."
HOME="$HOME" XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}" \
"$appimage" > "$log" 2>&1 &
else
warn "launching against an isolated scratch profile — this often never reaches the capture code path;"
warn "pass --real-data for a conclusive check (see the debugging guide, step 7)."
local scratch; scratch=$(mktemp -d /tmp/screenpipe-verify-data.XXXXXX)
SCREENPIPE_DATA_DIR="$scratch" SCREENPIPE_SKIP_ONBOARDING=1 "$appimage" > "$log" 2>&1 &
fi
local pid=$!
local waited=0 result="" pattern_ok='started for|capture started'
local pattern_bad='Creation failed|cannot open shared object file|panicked at'
while [ "$waited" -lt "$seconds" ]; do
if grep -qEi "$pattern_bad" "$log" 2>/dev/null; then result="fail"; break; fi
if grep -qEi "$pattern_ok" "$log" 2>/dev/null; then result="pass"; break; fi
sleep 1; waited=$((waited + 1))
done
# find the real exec'd binary (AppRun chain), not the launcher pid, and kill it
sleep 1
local realpid
realpid=$(pgrep -f 'screenpipe-app$' | head -1)
[ -n "${realpid:-}" ] && kill "$realpid" 2>/dev/null
kill "$pid" 2>/dev/null
sleep 1
local mnt
mnt=$(mount | grep 'fuse.screenpipe' | awk '{print $3}' | head -1)
if [ -n "${mnt:-}" ]; then
for _ in 1 2 3 4 5; do
fusermount -u "$mnt" 2>/dev/null && break
sleep 1
done
mount | grep -q 'fuse.screenpipe' && warn "FUSE mount still busy after retries: $mnt (clean up manually with fusermount -u)"
fi
case "$result" in
pass) good "PASS — capture-started signal seen, no failure signature. log: $log" ;;
fail) bad "FAIL — failure signature matched:"; grep -Ei "$pattern_bad" "$log" | head -5; info "log: $log" ;;
*) warn "INCONCLUSIVE — neither signal seen in ${seconds}s. log: $log"
[ "$real_data" -eq 0 ] && warn " (expected with an isolated profile — retry with --real-data)" ;;
esac
}
cmd_all() {
local appimage="${1:?usage: all <appimage> [workdir] [--auto] [--real-data]}"; shift
local workdir="${1:-/tmp/screenpipe-appimage-debug/$(basename "$appimage" .appimage)}"
[[ "$workdir" == --* ]] && workdir="/tmp/screenpipe-appimage-debug/$(basename "$appimage" .appimage)" || shift
local auto=0 real_data=0
for a in "$@"; do
[ "$a" = "--auto" ] && auto=1
[ "$a" = "--real-data" ] && real_data=1
done
cmd_inspect "$appimage" "$workdir"
if [ "$auto" -eq 1 ]; then
say ""
cmd_patch "$workdir" --auto
say ""
cmd_package "$workdir" "$workdir/screenpipe-fixed.appimage"
say ""
cmd_verify "$workdir/screenpipe-fixed.appimage" $( [ "$real_data" -eq 1 ] && echo --real-data )
else
info "review the report above, then: patch --auto / patch --lib / patch --dir, then package, then verify."
fi
}
main() {
local cmd="${1:-}"; shift || true
case "$cmd" in
inspect) cmd_inspect "$@" ;;
patch) cmd_patch "$@" ;;
package) cmd_package "$@" ;;
verify) cmd_verify "$@" ;;
all) cmd_all "$@" ;;
*) usage ;;
esac
}
main "$@"
Worked example
This method, applied end to end — the case that prompted writing it down.
Wayland screen capture was failing on an Arch system with a fully current PipeWire install. Tracing the AppRun chain showed SPA_PLUGIN_DIR forced onto the bundled directory; inventorying it found only support/libspa-support.so — one file, versus 35 on the system. The bundled libpipewire-0.3.so.0 itself read as v1.0.5 against the system's v1.6.8. Logs showed the exact "started, then Creation failed" pattern from step 5. Swapping both in and repackaging resolved it — confirmed via real-profile verification, no restart loop, capture stayed up.
recurred across at least two releases — the bundled PipeWire hasn't been updated in the build pipeline, so this exact recipe is worth rerunning on every new release until that's fixed upstream.
reproduced by the script — screenpipe-appimage-debug all <appimage> --auto --real-data found the same two findings unassisted and reported PASS after patch + repackage + real-profile verify.