#!/usr/bin/env bash
# Strip host graphics libraries from AppImage bundles so they load the user's
# system Mesa/libdrm/libva at launch instead of the older versions baked in by
# lib4bin's ldd-walk on the ubuntu-22.04 build runner.
#
# Without this, AppImages built on Mesa 22.x fail to initialize on systems
# with newer GPUs (RDNA3, Intel Arc, Lovelace) because the bundled drivers
# can't talk to the host kernel/driver stack. AppImage convention is to never
# ship graphics drivers — they must come from the host. See:
# https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist
#
# Only top-level lib directories are swept. CEF's own subdirs (swiftshader/,
# locales/, libcef.so neighbors) are left alone — CEF ships its own
# GLES/EGL implementation that must stay bundled.
#
# Usage: strip-appimage-graphics-libs.sh <bundle-root> [bundle-root...]
#   where <bundle-root> contains an `appimage/` subdir with *.AppImage files.
#
# Env:
#   TAURI_SIGNING_PRIVATE_KEY            — re-sign modified artifacts when set
#   TAURI_SIGNING_PRIVATE_KEY_PASSWORD   — passphrase for the key (may be empty)
#   APPIMAGETOOL_URL                     — override appimagetool download URL
#   APPIMAGETOOL_SHA256                  — expected SHA256 of the download
#                                          (verified before use when set; rotate
#                                          alongside APPIMAGETOOL_URL)

set -euo pipefail

EXCLUDE_PATTERNS=(
  'libGL.so.*'
  'libGLX.so.*'
  'libGLdispatch.so.*'
  'libGLESv1_CM.so.*'
  'libGLESv2.so.*'
  'libEGL.so.*'
  'libgbm.so.*'
  'libdrm.so.*'
  'libdrm_*.so.*'
  'libva.so.*'
  'libva-drm.so.*'
  'libva-glx.so.*'
  'libva-x11.so.*'
  'libvdpau.so.*'
  'libxcb-dri2.so.*'
  'libxcb-dri3.so.*'
  'libxcb-glx.so.*'
  'libxcb-present.so.*'
)

# Default to a pinned release tag rather than the mutable `continuous` asset so
# CI builds are reproducible and resistant to upstream replacement. Override via
# APPIMAGETOOL_URL (and bump APPIMAGETOOL_SHA256 alongside it).
default_appimagetool_url() {
  local target_arch="${APPIMAGE_TARGET_ARCH:-${MATRIX_TARGET:-$(uname -m)}}"
  case "$target_arch" in
    x86_64*|amd64*)
      echo "https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage"
      ;;
    aarch64*|arm64*)
      echo "https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-aarch64.AppImage"
      ;;
    *)
      echo "[strip-libs] ERROR: unsupported appimagetool architecture: $target_arch" >&2
      return 1
      ;;
  esac
}

APPIMAGETOOL_URL="${APPIMAGETOOL_URL:-$(default_appimagetool_url)}"
APPIMAGETOOL_SHA256="${APPIMAGETOOL_SHA256:-}"

ensure_appimagetool() {
  if command -v appimagetool >/dev/null 2>&1; then
    APPIMAGETOOL_BIN="$(command -v appimagetool)"
    return
  fi
  local tool=/tmp/appimagetool.AppImage
  if [ ! -x "$tool" ]; then
    echo "[strip-libs] Downloading appimagetool from $APPIMAGETOOL_URL"
    curl -fsSL "$APPIMAGETOOL_URL" -o "$tool"
    if [ -n "$APPIMAGETOOL_SHA256" ]; then
      echo "[strip-libs] Verifying appimagetool sha256"
      if ! echo "${APPIMAGETOOL_SHA256}  ${tool}" | sha256sum -c -; then
        echo "[strip-libs] ERROR: appimagetool sha256 mismatch — refusing to run" >&2
        rm -f "$tool"
        exit 1
      fi
    else
      echo "[strip-libs] WARNING: APPIMAGETOOL_SHA256 not set — skipping integrity check" >&2
    fi
    chmod +x "$tool"
  fi
  APPIMAGETOOL_BIN="$tool"
}

ensure_desktop_file_validate() {
  if command -v desktop-file-validate >/dev/null 2>&1; then
    return
  fi
  local shim="/tmp/desktop-file-validate"
  printf '#!/bin/sh\nexit 0\n' > "$shim"
  chmod +x "$shim"
  export PATH="/tmp:$PATH"
  echo "[strip-libs] desktop-file-validate not found; installed no-op shim"
}

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"
      ;;
    aarch64*|arm64*)
      echo "ld-linux-aarch64.so.1"
      ;;
    *)
      return 1
      ;;
  esac
}

appimagetool_arch() {
  local target_arch="${APPIMAGE_TARGET_ARCH:-${MATRIX_TARGET:-$(uname -m)}}"
  case "$target_arch" in
    x86_64*|amd64*)
      echo "x86_64"
      ;;
    aarch64*|arm64*)
      echo "aarch64"
      ;;
    *)
      echo "[strip-libs] ERROR: unsupported AppImage repack architecture: $target_arch" >&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" 2>/dev/null || true
  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"
  # Always replace — lib4bin may bundle an ld-linux from the CI runner
  # that is incompatible with newer host glibc (#3224, #3099).
  # The host_dynamic_loader source is the CI runner's own system ld-linux,
  # which is guaranteed compatible with the binary compiled on the same runner.
  rm -f "$target"

  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
}

rewrite_sharun_lib_path() {
  local appdir="$1"
  if ! uses_sharun_launcher "$appdir"; then
    return 1
  fi

  local lib_path="$appdir/shared/lib/lib.path"
  [ -s "$lib_path" ] || return 1

  if ! grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
    return 1
  fi

  echo "[strip-libs]   rewriting CI runner paths in shared/lib/lib.path"

  local raw
  raw="$(cat "$lib_path")"

  local -a entries=()
  local entry
  while IFS= read -r entry; do
    [ -n "$entry" ] || continue
    entries+=("$entry")
  done < <(printf '%s' "$raw" | tr '+:' '\n\n')

  local -a cleaned=()
  local rel seen_set=""
  for entry in "${entries[@]}"; do
    case "$entry" in
      /home/runner/*|/__w/*)
        rel="${entry##*/squashfs-root/}"
        if [ "$rel" = "$entry" ]; then
          rel="${entry##*/data/}"
          [ "$rel" != "$entry" ] || continue
        fi
        ;;
      /*)
        continue
        ;;
      *)
        rel="$entry"
        ;;
    esac

    [ -d "$appdir/$rel" ] || continue

    case "+${seen_set}+" in
      *"+${rel}+"*) continue ;;
    esac
    seen_set="${seen_set}+${rel}"
    cleaned+=("$rel")
  done

  if [ "${#cleaned[@]}" -eq 0 ]; then
    cleaned=("shared/lib")
  fi

  local joined
  joined="$(IFS='+'; echo "${cleaned[*]}")"
  printf '%s' "$joined" > "$lib_path"
  echo "[strip-libs]   lib.path rewritten to: $joined"
}

validate_sharun_lib_path() {
  local appdir="$1"
  if ! uses_sharun_launcher "$appdir"; then
    return 0
  fi

  local lib_path="$appdir/shared/lib/lib.path"
  if [ ! -s "$lib_path" ]; then
    echo "[strip-libs] ERROR: sharun AppImage is missing shared/lib/lib.path; refusing to ship an AppImage that exits with 'Interpreter not found!'" >&2
    exit 1
  fi

  if grep -E '(^|[+:])/home/runner/|(^|[+:])/__w/' "$lib_path" >/dev/null; then
    echo "[strip-libs] ERROR: shared/lib/lib.path contains CI runner paths; regenerate it with bundle-relative entries before release." >&2
    exit 1
  fi
}

# is_elf — true if the file begins with the ELF magic, regardless of the +x
# bit. Shared objects (.so) are not executable but still carry the
# DT_RPATH/DT_RUNPATH we need to sanitize, so the executable-only check in
# is_executable_elf is too narrow here.
is_elf() {
  local candidate="$1"
  [ -f "$candidate" ] || return 1
  [ "$(LC_ALL=C head -c 4 "$candidate" 2>/dev/null || true)" = $'\177ELF' ]
}

# sanitize_elf_rpaths — strip absolute CI build-machine paths (/home/runner,
# /__w) from the DT_RPATH/DT_RUNPATH of every bundled ELF and rewrite them to
# $ORIGIN-relative entries.
#
# Problem (issue #3224): libcef.so is copied verbatim into usr/lib by the
# vendored sharun_cef bundler and is never ldd-processed by lib4bin, so any
# absolute RUNPATH baked in on the build runner (e.g.
# `/home/runner/.cache/tauri-cef/.../shared/lib`) survives into the shipped
# AppImage. rewrite_sharun_lib_path() above only sanitizes the sharun lib.path
# TEXT file — it never touches ELF headers. Those absolute, non-existent search
# dirs leak the build-machine layout and, on a host that happens to have a
# matching path, could shadow a bundled library.
#
# Fix: patchelf every ELF whose RPATH/RUNPATH contains a CI marker, dropping the
# absolute CI components and keeping $ORIGIN-relative ones. Falls back to a
# conservative `$ORIGIN:$ORIGIN/../shared/lib` when nothing relative remains.
# ELFs whose RPATH is already clean ($ORIGIN-only or empty) are left untouched,
# so the pass is idempotent.
#
# Returns 0 if any ELF was rewritten, 1 otherwise.
sanitize_elf_rpaths() {
  local appdir="$1"
  if ! command -v patchelf >/dev/null 2>&1; then
    echo "[strip-libs] ERROR: patchelf not found; cannot sanitize build-machine RPATHs from bundled ELFs (issue #3224). Install patchelf on the build runner." >&2
    exit 1
  fi

  # Inverted truthiness: 1 == "no change" (shell-false), flips to 0 (shell-true)
  # the first time an ELF is rewritten.
  local rewrote=1
  local search_roots=(
    "$appdir/usr/lib"
    "$appdir/shared/lib"
    "$appdir/lib"
    "$appdir/usr/bin"
    "$appdir/bin"
  )

  local root f cur cleaned entry
  for root in "${search_roots[@]}"; do
    [ -d "$root" ] || continue
    while IFS= read -r -d '' f; do
      is_elf "$f" || continue
      cur="$(patchelf --print-rpath "$f" 2>/dev/null || true)"
      [ -n "$cur" ] || continue
      case "$cur" in
        *"/home/runner/"*|*"/__w/"*) ;;
        *) continue ;; # already clean — leave it untouched (idempotent)
      esac

      # Keep only $ORIGIN-relative entries; drop absolute / CI-marked ones.
      local -a kept=()
      local seen=""
      local old_ifs="$IFS"
      IFS=':'
      for entry in $cur; do
        IFS="$old_ifs"
        [ -n "$entry" ] || { IFS=':'; continue; }
        case "$entry" in
          '$ORIGIN'*) ;;     # relative — keep
          *) IFS=':'; continue ;; # absolute — drop
        esac
        case "+${seen}+" in
          *"+${entry}+"*) IFS=':'; continue ;;
        esac
        seen="${seen}+${entry}"
        kept+=("$entry")
        IFS=':'
      done
      IFS="$old_ifs"

      if [ "${#kept[@]}" -eq 0 ]; then
        # No relative entry survived — synthesize a fallback that reaches the
        # bundle's top-level shared/lib FROM THIS ELF's own directory. $ORIGIN is
        # the ELF's dir, so the number of `../` hops equals the ELF's directory
        # depth below the AppDir root: a file in usr/lib needs
        # `$ORIGIN/../../shared/lib`, one in lib needs `$ORIGIN/../shared/lib`. A
        # flat `$ORIGIN/../shared/lib` (the naive form) resolves to
        # `usr/shared/lib` for usr/* files and would still miss libs at runtime.
        local rel_dir comp up=""
        rel_dir="$(dirname "${f#"$appdir"/}")"
        local ifs_save="$IFS"
        IFS='/'
        for comp in $rel_dir; do
          [ -n "$comp" ] && [ "$comp" != "." ] && up="../$up"
        done
        IFS="$ifs_save"
        cleaned="\$ORIGIN:\$ORIGIN/${up}shared/lib"
      else
        cleaned="$(IFS=':'; echo "${kept[*]}")"
      fi

      patchelf --set-rpath "$cleaned" "$f"
      echo "[strip-libs]   patchelf rpath ${f#"$appdir"/}: '$cur' -> '$cleaned'"
      rewrote=0
    done < <(find "$root" -type f -print0)
  done

  return $rewrote
}

# validate_appimage_required_libs — fail the build loudly if a library the app
# binary hard-links (NEEDED) but which MUST travel inside the AppImage is absent
# from the bundle.
#
# Problem (issue #3224): the app links libxdo.so.3 via enigo
# (`#[link(name = "xdo")]`, used by src/openhuman/tools/impl/computer for Linux
# mouse/keyboard control). lib4bin's ldd-walk normally bundles it into
# shared/lib, but if a future runner image drops libxdo-dev or bumps its soname,
# the lib silently vanishes from the AppImage and the binary segfaults on launch
# on any host lacking the legacy soname (e.g. Arch, which ships libxdo.so.4). The
# .deb path already guards this via its `depends` (libxdo3) +
# linux_cef_deb_runtime_e2e; the AppImage path had no equivalent. This turns a
# silent runtime segfault into a loud build failure.
validate_appimage_required_libs() {
  local appdir="$1"
  if ! uses_sharun_launcher "$appdir"; then
    return 0
  fi

  local root
  for root in "$appdir/shared/lib" "$appdir/usr/lib" "$appdir/lib"; do
    [ -d "$root" ] || continue
    if [ -n "$(find "$root" -name 'libxdo.so*' -print -quit 2>/dev/null)" ]; then
      return 0
    fi
  done

  echo "[strip-libs] ERROR: AppImage is missing libxdo.so.* — the enigo NEEDED dependency was not bundled (issue #3224). The app would segfault on launch on hosts without the legacy libxdo soname (e.g. Arch). Ensure libxdo-dev is installed on the build runner so lib4bin's ldd-walk bundles it." >&2
  exit 1
}

# patch_apprun_sharun_cwd — inject `cd "$APPDIR"` into AppRun before the final
# exec call so sharun resolves preload/library paths relative to the AppDir
# rather than the caller's CWD.
#
# Problem (issue #2822): sharun reads its `.preload` entry and library search
# paths relative to the process CWD.  When a user launches the AppImage from
# any directory other than the AppDir itself (e.g. double-click from ~/Downloads)
# CWD != AppDir, so ld.so can't find `anylinux.so` (preload) or `libcef.so`
# (LD_LIBRARY_PATH entry).  SHARUN_DIR is set correctly — the AppDir IS known —
# but sharun doesn't use it to anchor the preload/library arguments it hands to
# ld.so.
#
# Fix: prepend `cd "$APPDIR"` to the exec line in AppRun so the working
# directory is always the AppDir by the time sharun/the binary runs.  This
# mirrors the verified manual workaround from the bug report.
#
# Returns 0 (true) if the AppRun was modified, 1 if no change was needed.
patch_apprun_sharun_cwd() {
  local appdir="$1"
  if ! uses_sharun_launcher "$appdir"; then
    return 1
  fi

  local apprun="$appdir/AppRun"
  if [ ! -f "$apprun" ]; then
    # Some sharun bundles use the sharun binary directly as the AppDir entry
    # point without a separate shell AppRun.  Nothing to patch in that case.
    return 1
  fi

  # Check if the file is a shell script (not an ELF binary).
  local first_bytes
  first_bytes="$(LC_ALL=C head -c 2 "$apprun" 2>/dev/null || true)"
  if [ "$first_bytes" = $'\x7fE' ]; then
    # AppRun is an ELF binary — cannot patch with sed.
    return 1
  fi

  # Idempotency guard: skip if we already patched this AppRun.
  # Match only the exact patched line — a loose substring (e.g. 'cd.*"$APPDIR"')
  # would false-positive on comments like '# cd "$APPDIR"' or unrelated lines
  # and leave the real `exec "$@"` unpatched.
  local patched_line_re='^[[:space:]]*cd[[:space:]]+"\$APPDIR"[[:space:]]*&&[[:space:]]*exec[[:space:]]+"\$@"[[:space:]]*$'
  if grep -Eq "$patched_line_re" "$apprun" 2>/dev/null; then
    return 1
  fi

  # Locate the exec line.  AppRun scripts generated by lib4bin / sharun
  # typically have a line of the form (possibly with leading whitespace):
  #   exec "$@"
  # Patch it to:
  #   cd "$APPDIR" && exec "$@"
  #
  # The sed pattern is anchored to end-of-line ($) so trailing content (extra
  # args, comments, redirections) doesn't get silently absorbed into the cd &&
  # exec sequence.
  #
  # Use a temp file + mv to avoid truncating AppRun mid-write on failure.
  local tmp_apprun
  tmp_apprun="$(mktemp)"
  if sed 's|^\([[:space:]]*\)exec "\$@"[[:space:]]*$|\1cd "$APPDIR" \&\& exec "$@"|' \
       "$apprun" > "$tmp_apprun" \
     && grep -Eq "$patched_line_re" "$tmp_apprun"; then
    chmod --reference="$apprun" "$tmp_apprun"
    mv "$tmp_apprun" "$apprun"
    echo "[strip-libs]   patched AppRun: added 'cd \"\$APPDIR\"' before exec to fix sharun CWD preload resolution (issue #2822)"
    return 0
  else
    rm -f "$tmp_apprun"
    echo "[strip-libs] WARNING: could not locate 'exec \"\$@\"' in AppRun — sharun CWD fix not applied; AppImage may fail to launch from non-AppDir CWDs" >&2
    return 1
  fi
}

strip_one_appimage() {
  local img="$1"
  local original
  original="$(realpath "$img")"
  local name
  name="$(basename "$original")"
  local workdir
  workdir="$(mktemp -d)"

  echo "[strip-libs] Processing $original"
  (
    cd "$workdir"
    chmod +x "$original"
    if ! "$original" --appimage-extract >/dev/null; then
      echo "[strip-libs] ERROR: --appimage-extract failed for $original" >&2
      exit 1
    fi
  )

  local appdir="$workdir/squashfs-root"
  local removed=0
  local added_loader=0
  local rewrote_libpath=0
  local patched_apprun=0
  local rewrote_rpaths=0
  local lib_roots=()
  for candidate in \
    "$appdir/usr/lib" \
    "$appdir/usr/lib/x86_64-linux-gnu" \
    "$appdir/usr/lib/aarch64-linux-gnu" \
    "$appdir/shared/lib" \
    "$appdir/shared/lib/x86_64-linux-gnu" \
    "$appdir/shared/lib/aarch64-linux-gnu" \
    "$appdir/lib" \
    "$appdir/lib/x86_64-linux-gnu" \
    "$appdir/lib/aarch64-linux-gnu"; do
    [ -d "$candidate" ] && lib_roots+=("$candidate")
  done

  if [ "${#lib_roots[@]}" -eq 0 ]; then
    echo "[strip-libs] WARNING: no known lib roots inside $original — layout changed?" >&2
  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
  fi

  if ensure_sharun_interpreter "$appdir"; then
    added_loader=1
  fi
  if rewrite_sharun_lib_path "$appdir"; then
    rewrote_libpath=1
  fi
  if patch_apprun_sharun_cwd "$appdir"; then
    patched_apprun=1
  fi
  if sanitize_elf_rpaths "$appdir"; then
    rewrote_rpaths=1
  fi
  validate_sharun_lib_path "$appdir"
  validate_appimage_required_libs "$appdir"

  if [ "$removed" -eq 0 ] && [ "$added_loader" -eq 0 ] && [ "$rewrote_libpath" -eq 0 ] && [ "$patched_apprun" -eq 0 ] && [ "$rewrote_rpaths" -eq 0 ]; then
    echo "[strip-libs] No graphics libs, missing sharun interpreter, or build-machine RPATHs found in $original; leaving unchanged."
    rm -rf "$workdir"
    return
  fi
  echo "[strip-libs] Removed $removed file(s), added $added_loader loader file(s), patched AppRun=$patched_apprun, rewrote RPATHs=$rewrote_rpaths; repacking AppImage."

  local rebuilt="$workdir/$name"
  local appimage_arch
  appimage_arch="$(appimagetool_arch)"
  (
    cd "$workdir"
    ARCH="$appimage_arch" "$APPIMAGETOOL_BIN" --appimage-extract-and-run \
      --no-appstream squashfs-root "$rebuilt" >/dev/null
  )
  mv "$rebuilt" "$original"
  rm -rf "$workdir"
  MODIFIED_PATHS+=("$original")
}

resign_artifact() {
  local file="$1"
  if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
    return
  fi
  if ! command -v cargo-tauri >/dev/null 2>&1; then
    echo "[strip-libs] WARNING: cargo-tauri not on PATH; cannot re-sign $file" >&2
    return
  fi
  echo "[strip-libs] Re-signing $file"
  rm -f "$file.sig"
  cargo tauri signer sign \
    --private-key "$TAURI_SIGNING_PRIVATE_KEY" \
    --password "${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}" \
    "$file" >/dev/null
}

main() {
  if [ $# -lt 1 ]; then
    echo "Usage: $0 <bundle-root> [bundle-root...]" >&2
    exit 2
  fi
  ensure_appimagetool
  ensure_desktop_file_validate
  shopt -s nullglob
  MODIFIED_PATHS=()
  local found_any=0
  for root in "$@"; do
    [ -d "$root/appimage" ] || continue
    for img in "$root/appimage"/*.AppImage; do
      found_any=1
      strip_one_appimage "$img"
    done
  done
  if [ "$found_any" -eq 0 ]; then
    echo "[strip-libs] No AppImages found under any provided bundle root." >&2
    return
  fi

  # 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 "${MODIFIED_PATHS[@]:-}"; do
    [ -n "$original" ] || continue
    resign_artifact "$original"

    local tar="$original.tar.gz"
    if [ -e "$tar" ]; then
      echo "[strip-libs] Rebuilding $(basename "$tar")"
      tar -C "$(dirname "$original")" -czf "$tar" "$(basename "$original")"
      resign_artifact "$tar"
    fi
  done
}

# Run main only when executed directly, not when sourced (e.g. by the
# scripts/release/test-strip-appimage-rpaths.sh regression test, which exercises
# sanitize_elf_rpaths / validate_appimage_required_libs in isolation).
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
  main "$@"
fi
