fix(release): bundle sharun AppImage loader

## Summary

- Bundles the glibc dynamic linker that `sharun` needs inside the Linux AppImage when the post-build AppImage cleanup runs.
- Repackages and re-signs AppImage artifacts when the only mutation is adding the missing loader, not just when graphics libraries were stripped.
- Keeps the existing Mesa/libdrm/libva stripping behavior intact and adds a release smoke item for the Ubuntu 24.04 `Interpreter not found!` regression.

## Problem

- `OpenHuman_0.54.0_amd64.AppImage` can exit immediately on Ubuntu 24.04 with `Interpreter not found!` because the AppImage contains a `sharun` launcher but no `lib/ld-linux-x86-64.so.2`.
- The existing post-processing script only repacked when graphics libraries were removed, so it had no guard for a missing `sharun` interpreter.

## Solution

- Detect `sharun`-style launchers by checking the extracted AppDir launcher binaries for the `Interpreter not found!` marker.
- Resolve the expected loader from the build target architecture and copy it into `squashfs-root/lib/` when missing.
- Fail the post-processing step if the AppImage uses `sharun` but the build host cannot provide the required loader.
- Track all modified AppImages, whether modified by stripping graphics libs or by adding the loader, so updater tarballs and signatures stay in sync.

## Submission Checklist

- [x] Tests added or updated (happy path + at least one failure / edge case) per [Testing Strategy](../gitbooks/developing/testing-strategy.md#failure-path-requirement) — release smoke checklist updated; local function smoke covers loader injection path.
- [x] **Diff coverage >= 80%** — N/A: shell release packaging script and docs are not covered by Vitest/cargo diff coverage.
- [x] Coverage matrix updated — N/A: release packaging behavior, not a product feature row.
- [x] All affected feature IDs from the matrix are listed in the PR description under `## Related` — N/A: no feature matrix row.
- [x] No new external network dependencies introduced (mock backend used per [Testing Strategy](../gitbooks/developing/testing-strategy.md#mock-policy))
- [x] Manual smoke checklist updated if this touches release-cut surfaces ([`docs/RELEASE-MANUAL-SMOKE.md`](../docs/RELEASE-MANUAL-SMOKE.md))
- [x] Linked issue closed via `Closes #NNN` in the `## Related` section

## Impact

- Linux AppImage release artifacts should launch on clean Ubuntu 24.04 hosts without requiring users to extract the AppImage and manually copy `ld-linux-x86-64.so.2`.
- Packaging fails earlier if a future build cannot locate the required loader, preventing a known-broken AppImage from shipping.

## Related

- Closes #2297
- Follow-up PR(s)/TODOs: N/A

---

## AI Authored PR Metadata (required for Codex/Linear PRs)

### Linear Issue
- Key: N/A
- URL: N/A

### Commit & Branch
- Branch: `codex/2297-appimage-sharun-loader`
- Commit SHA: `4260df24f5355a63df02e748e276586ebed0c53a`

### Validation Run
- [x] `pnpm --filter openhuman-app format:check` — N/A: no frontend files changed.
- [x] `pnpm typecheck` — N/A: no TypeScript files changed.
- [x] Focused tests: Git Bash smoke test for `ensure_sharun_interpreter` copies `ld-linux-x86-64.so.2` into a synthetic sharun AppDir.
- [x] Rust fmt/check (if changed): N/A: no Rust files changed.
- [x] Tauri fmt/check (if changed): N/A: no Tauri Rust files changed.
- [x] Shell syntax/check: `C:\Program Files\Git\bin\bash.exe -n scripts/release/strip-appimage-graphics-libs.sh`; `git diff --check`.

### Validation Blocked
- `command:` Full AppImage rebuild and launch on Ubuntu 24.04.
- `error:` No produced Linux AppImage artifact is available in this local Windows workspace.
- `impact:` CI release packaging will exercise the changed script; manual release smoke checklist now covers the Ubuntu 24.04 launch regression.

### Behavior Changes
- Intended behavior change: AppImage post-processing now bundles the missing `sharun` dynamic linker when needed.
- User-visible effect: Linux users should no longer hit `Interpreter not found!` on affected AppImages.

### Parity Contract
- Legacy behavior preserved: existing graphics-library stripping, re-signing, and updater tarball rebuild behavior remain intact.
- Guard/fallback/dispatch parity checks: AppImages unchanged by stripping or loader injection are still left untouched; missing host loader now fails packaging instead of shipping a broken artifact.

### Duplicate / Superseded PR Handling
- Duplicate PR(s): N/A
- Canonical PR: this PR
- Resolution (closed/superseded/updated): N/A


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **Bug Fixes**
  * AppImages now remove incompatible host graphics libraries and auto-include a missing bundled dynamic loader when needed, preventing "Interpreter not found" failures on clean Ubuntu 24.04 hosts.

* **Chores**
  * Added a smoke-test checklist item to validate AppImage launches on Ubuntu 24.04.
  * Release tooling now only repacks and re-signs AppImages that were actually modified.

<!-- review_stack_entry_start -->

[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/tinyhumansai/openhuman/pull/2307?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: aqilaziz <gonzes7@gmail.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aqil Aziz
2026-05-20 16:53:09 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent c9f293d8ec
commit ff918f030d
2 changed files with 181 additions and 18 deletions
+1
View File
@@ -47,6 +47,7 @@ Applies to every release, all platforms.
### Linux
- [ ] **`.deb` and/or `.AppImage` install on a clean Ubuntu 22.04** — `sudo dpkg -i openhuman_*.deb` or `chmod +x openhuman-*.AppImage && ./openhuman-*.AppImage`. Expected: no missing-dependency errors; app launches.
- [ ] **`.AppImage` launches on a clean Ubuntu 24.04 host without a sibling extracted tree** — Run the downloaded AppImage directly from an empty directory. Expected: no `Interpreter not found!` error; `sharun` finds its bundled dynamic linker and the app reaches the first window.
- [ ] **OS-native notification toasts fire** — Trigger a notification from inside the app (e.g. memory captured, agent finished). Expected: a libnotify-style toast appears outside the app window. (CI Linux sees only Xvfb; this surface verifies on a real desktop.)
- [ ] **Headless supervisor update stages without self-exit** — On a Linux service deployment with `[update] restart_strategy = "supervisor"` and `rpc_mutations_enabled = false`, stage a new core binary through the documented operator flow. Expected: the running process stays up until the supervisor restart, the staged binary is present on disk, and `systemctl restart openhuman` (or equivalent) picks up the new version.
+180 -18
View File
@@ -77,6 +77,165 @@ ensure_appimagetool() {
APPIMAGETOOL_BIN="$tool"
}
appimage_loader_name() {
local target_arch="${APPIMAGE_TARGET_ARCH:-${MATRIX_TARGET:-$(uname -m)}}"
case "$target_arch" in
x86_64*|amd64*)
echo "ld-linux-x86-64.so.2"
;;
*)
return 1
;;
esac
}
host_dynamic_loader() {
local loader_name="$1"
local candidates=()
case "$loader_name" in
ld-linux-x86-64.so.2)
candidates=(
"/lib64/$loader_name"
"/lib/x86_64-linux-gnu/$loader_name"
"/usr/lib64/$loader_name"
"/usr/lib/$loader_name"
)
;;
ld-linux-aarch64.so.1)
candidates=(
"/lib/$loader_name"
"/lib/aarch64-linux-gnu/$loader_name"
"/usr/lib/aarch64-linux-gnu/$loader_name"
)
;;
esac
local candidate
for candidate in "${candidates[@]}"; do
if [ -f "$candidate" ]; then
echo "$candidate"
return 0
fi
done
return 1
}
is_executable_elf() {
local candidate
candidate="$1"
[ -f "$candidate" ] || return 1
[ -x "$candidate" ] || return 1
[ "$(LC_ALL=C head -c 4 "$candidate" 2>/dev/null || true)" = $'\177ELF' ]
}
emit_entry_if_elf() {
local candidate="$1"
if is_executable_elf "$candidate"; then
printf '%s\0' "$candidate"
fi
}
emit_desktop_exec_candidate() {
local appdir="$1"
local command="$2"
local candidate
[ -n "$command" ] || return 0
case "$command" in
/*)
emit_entry_if_elf "$appdir$command"
;;
*/*)
emit_entry_if_elf "$appdir/$command"
;;
*)
for candidate in "$appdir/$command" "$appdir/bin/$command" "$appdir/usr/bin/$command"; do
emit_entry_if_elf "$candidate"
done
;;
esac
}
discover_appimage_entry_binaries() {
local appdir="$1"
local desktop line exec_line command root candidate
emit_entry_if_elf "$appdir/AppRun"
emit_entry_if_elf "$appdir/sharun"
while IFS= read -r -d '' desktop; do
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
Exec=*)
exec_line="${line#Exec=}"
case "$exec_line" in
\"*\")
command="${exec_line#\"}"
command="${command%%\"*}"
;;
\'*\')
command="${exec_line#\'}"
command="${command%%\'*}"
;;
*)
command="${exec_line%%[[:space:]]*}"
;;
esac
emit_desktop_exec_candidate "$appdir" "$command"
;;
esac
done < "$desktop"
done < <(find "$appdir" -maxdepth 1 -type f -name '*.desktop' -print0)
for root in "$appdir" "$appdir/bin" "$appdir/usr/bin"; do
[ -d "$root" ] || continue
while IFS= read -r -d '' candidate; do
emit_entry_if_elf "$candidate"
done < <(find "$root" -maxdepth 1 -type f -perm /111 -print0)
done
}
uses_sharun_launcher() {
local appdir="$1"
local candidate
while IFS= read -r -d '' candidate; do
if grep -a -q "Interpreter not found!" "$candidate" 2>/dev/null; then
return 0
fi
done < <(discover_appimage_entry_binaries "$appdir")
return 1
}
ensure_sharun_interpreter() {
local appdir="$1"
if ! uses_sharun_launcher "$appdir"; then
return 1
fi
local loader_name
if ! loader_name="$(appimage_loader_name)"; then
echo "[strip-libs] ERROR: AppImage uses sharun but architecture is unsupported; cannot determine required loader" >&2
exit 1
fi
local target="$appdir/lib/$loader_name"
if [ -e "$target" ]; then
return 1
fi
local source
if ! source="$(host_dynamic_loader "$loader_name")"; then
echo "[strip-libs] ERROR: AppImage uses sharun but host loader $loader_name was not found; refusing to ship an AppImage that exits with 'Interpreter not found!'" >&2
exit 1
fi
mkdir -p "$appdir/lib"
cp -L "$source" "$target"
chmod 755 "$target"
echo "[strip-libs] bundling sharun interpreter ${target#"$appdir"/} from $source"
return 0
}
strip_one_appimage() {
local img="$1"
local original
@@ -98,6 +257,7 @@ strip_one_appimage() {
local appdir="$workdir/squashfs-root"
local removed=0
local added_loader=0
local lib_roots=()
for candidate in \
"$appdir/usr/lib" \
@@ -111,26 +271,28 @@ strip_one_appimage() {
if [ "${#lib_roots[@]}" -eq 0 ]; then
echo "[strip-libs] WARNING: no known lib roots inside $original — layout changed?" >&2
rm -rf "$workdir"
return
fi
for root in "${lib_roots[@]}"; do
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
while IFS= read -r -d '' f; do
echo "[strip-libs] removing ${f#"$appdir"/}"
rm -f "$f"
removed=$((removed + 1))
done < <(find "$root" -maxdepth 1 -name "$pattern" -print0)
else
for root in "${lib_roots[@]}"; do
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
while IFS= read -r -d '' f; do
echo "[strip-libs] removing ${f#"$appdir"/}"
rm -f "$f"
removed=$((removed + 1))
done < <(find "$root" -maxdepth 1 -name "$pattern" -print0)
done
done
done
fi
if [ "$removed" -eq 0 ]; then
echo "[strip-libs] No graphics libs found in $original — leaving unchanged."
if ensure_sharun_interpreter "$appdir"; then
added_loader=1
fi
if [ "$removed" -eq 0 ] && [ "$added_loader" -eq 0 ]; then
echo "[strip-libs] No graphics libs or missing sharun interpreter found in $original; leaving unchanged."
rm -rf "$workdir"
return
fi
echo "[strip-libs] Removed $removed file(s); repacking AppImage."
echo "[strip-libs] Removed $removed file(s), added $added_loader loader file(s); repacking AppImage."
local rebuilt="$workdir/$name"
(
@@ -140,7 +302,7 @@ strip_one_appimage() {
)
mv "$rebuilt" "$original"
rm -rf "$workdir"
STRIPPED_PATHS+=("$original")
MODIFIED_PATHS+=("$original")
}
resign_artifact() {
@@ -167,7 +329,7 @@ main() {
fi
ensure_appimagetool
shopt -s nullglob
STRIPPED_PATHS=()
MODIFIED_PATHS=()
local found_any=0
for root in "$@"; do
[ -d "$root/appimage" ] || continue
@@ -184,7 +346,7 @@ main() {
# Re-sign each modified .AppImage and rebuild its updater tarball + sig.
# The updater tarball is just a gzipped tar of the .AppImage (Tauri convention),
# so its contents are stale the moment we mutate the AppImage.
for original in "${STRIPPED_PATHS[@]:-}"; do
for original in "${MODIFIED_PATHS[@]:-}"; do
[ -n "$original" ] || continue
resign_artifact "$original"