From ff918f030d27010b300a3c17c14f691dc08b30e4 Mon Sep 17 00:00:00 2001 From: Aqil Aziz Date: Thu, 21 May 2026 06:53:09 +0700 Subject: [PATCH] fix(release): bundle sharun AppImage loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ## 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 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) Co-authored-by: aqilaziz Co-authored-by: Steven Enamakel --- docs/RELEASE-MANUAL-SMOKE.md | 1 + .../release/strip-appimage-graphics-libs.sh | 198 ++++++++++++++++-- 2 files changed, 181 insertions(+), 18 deletions(-) diff --git a/docs/RELEASE-MANUAL-SMOKE.md b/docs/RELEASE-MANUAL-SMOKE.md index 65ef87a08..3c7b124f7 100644 --- a/docs/RELEASE-MANUAL-SMOKE.md +++ b/docs/RELEASE-MANUAL-SMOKE.md @@ -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. diff --git a/scripts/release/strip-appimage-graphics-libs.sh b/scripts/release/strip-appimage-graphics-libs.sh index 05f448200..0ac8d3eaa 100755 --- a/scripts/release/strip-appimage-graphics-libs.sh +++ b/scripts/release/strip-appimage-graphics-libs.sh @@ -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"