Skip to content
251 changes: 243 additions & 8 deletions build_appimage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,42 @@ set -euo pipefail

# Parse command line options
FORCE_CPU_FLAG=false
NO_MOONSHINE=false
VERBOSE_FLAG=false
# Shadow-library handling is ON by default: if a third-party app (e.g. Insync)
# has copies of the GLib/GTK libraries under /usr/lib, linuxdeploy sweeps them in
# and the AppImage build fails. By default we move them aside for the build and
# restore them afterwards (needs sudo). Pass --keep-shadow-libs to disable that
# and only get a warning instead.
HIDE_SHADOW_LIBS=true
for arg in "$@"; do
case "$arg" in
--cpu) FORCE_CPU_FLAG=true ;;
# Build the whisper-cpp-only AppImage (the pre-Moonshine build). This
# drops the `moonshine` cargo feature so the compile no longer downloads
# a prebuilt ONNX Runtime from cdn.pyke.io — useful for offline builds or
# when that download host is unreachable/blocked.
--no-moonshine) NO_MOONSHINE=true ;;
# Pass --verbose to `tauri build` and crank up linuxdeploy's own
# logging. Tauri otherwise collapses a bundling failure into a bare
# "failed to run linuxdeploy"; this surfaces linuxdeploy's real error.
--verbose|-v) VERBOSE_FLAG=true ;;
# Explicit form of the default behaviour (kept for compatibility).
--hide-shadow-libs|--fix-shadow-libs) HIDE_SHADOW_LIBS=true ;;
# Opt out: do NOT touch third-party shadow libraries; only warn. The
# build will likely fail at linuxdeploy if any are present.
--keep-shadow-libs|--no-hide-shadow-libs) HIDE_SHADOW_LIBS=false ;;
esac
done

# Env overrides.
if [ "${MOONSHINE:-1}" = "0" ]; then
NO_MOONSHINE=true
fi
if [ "${KEEP_SHADOW_LIBS:-0}" = "1" ]; then
HIDE_SHADOW_LIBS=false
fi

# ── Colors ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; BOLD='\033[1m'; NC='\033[0m'
Expand All @@ -24,6 +54,27 @@ info() { echo -e " ${BLUE}[*]${NC} $*"; }
fail() { echo -e " ${RED}[FAIL]${NC} $*"; }
step() { echo -e "\n${BOLD}── $* ──────────────────────────────────────────${NC}"; }

# Shadow-library hiding (see --hide-shadow-libs). SHADOW_HIDE_MAP holds
# "original|hidden" entries for directories we moved out of linuxdeploy's search
# path; restore_shadow_libs() puts them back and is wired to run on any exit
# (normal, error, or Ctrl-C) so a third-party app like Insync is never left
# displaced if the build fails midway.
SHADOW_HIDE_MAP=()
restore_shadow_libs() {
local entry orig hidden
for entry in "${SHADOW_HIDE_MAP[@]}"; do
orig="${entry%%|*}"; hidden="${entry#*|}"
if [ -e "$hidden" ] && [ ! -e "$orig" ]; then
if sudo mv "$hidden" "$orig"; then
info "Restored $orig"
else
warn "Could not restore $orig — run manually: sudo mv \"$hidden\" \"$orig\""
fi
fi
done
}
trap restore_shadow_libs EXIT INT TERM

# ══════════════════════════════════════════════════════════════════════════════
# 1. Verification of appimagetool Wrapper
# ══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -127,6 +178,124 @@ if ! command -v cmake &>/dev/null; then
exit 1
fi

# Verify the GTK/WebKit development libraries Tauri links against are present.
# Without these, the Rust compile fails deep in the build with a cryptic
# pkg-config error (e.g. "Package gdk-3.0 was not found") rather than a clear
# up-front message. We check via pkg-config: gtk+-3.0 covers the overlay's gdk
# dependency, webkit2gtk-4.1 covers the Tauri webview.
if ! command -v pkg-config &>/dev/null; then
fail "'pkg-config' is not installed on your system!"
info "Building the Tauri application requires pkg-config to locate GTK/WebKit."
info "👉 Please install it via your package manager:"
info " - Arch: sudo pacman -S pkgconf"
info " - Ubuntu: sudo apt install pkg-config"
info " - Fedora: sudo dnf install pkgconf-pkg-config"
echo ""
exit 1
fi

MISSING_DEV_LIBS=()
for pc in gtk+-3.0 webkit2gtk-4.1; do
if ! pkg-config --exists "$pc" 2>/dev/null; then
MISSING_DEV_LIBS+=("$pc")
fi
done
if [ ${#MISSING_DEV_LIBS[@]} -gt 0 ]; then
fail "Missing GTK/WebKit development libraries: ${MISSING_DEV_LIBS[*]}"
info "Tauri links against these at build time; the Rust compile will fail without them."
info "👉 Please install the development packages for your distribution:"
info " - Arch: sudo pacman -S gtk3 webkit2gtk-4.1"
info " - Ubuntu: sudo apt install libgtk-3-dev libwebkit2gtk-4.1-dev"
info " - Fedora: sudo dnf install gtk3-devel webkit2gtk4.1-devel"
echo ""
exit 1
fi

# Verify the tools Tauri's AppImage bundler shells out to. Tauri downloads and
# runs `linuxdeploy` for the final packaging step; linuxdeploy in turn requires
# `patchelf` (to rewrite library rpaths) and `file` (to classify binaries). When
# either is missing the Rust compile succeeds but bundling dies late with a
# generic "failed to run linuxdeploy" and no obvious cause. Check them up front.
MISSING_BUNDLE_TOOLS=()
for tool in patchelf file; do
if ! command -v "$tool" &>/dev/null; then
MISSING_BUNDLE_TOOLS+=("$tool")
fi
done
if [ ${#MISSING_BUNDLE_TOOLS[@]} -gt 0 ]; then
fail "Missing AppImage bundling tools: ${MISSING_BUNDLE_TOOLS[*]}"
info "Tauri's AppImage packager runs 'linuxdeploy', which needs these to succeed."
info "Without them the build fails late with 'failed to run linuxdeploy'."
info "👉 Please install them via your package manager:"
info " - Arch: sudo pacman -S patchelf file"
info " - Ubuntu: sudo apt install patchelf file"
info " - Fedora: sudo dnf install patchelf file"
echo ""
exit 1
fi

# Warn about third-party directories under /usr/lib (or /lib) that ship their own
# copies of the core GLib/GTK libraries. linuxdeploy's GTK plugin deploys that
# stack by globbing library names across these prefixes and *their subdirectories*
# — not via the loader, the ld.so cache, or the environment — so it sweeps in any
# stray copy it finds and then fails to bundle it when it needs a library the host
# lacks. The classic case is Insync: /usr/lib/insync/libgio-2.0.so.0 needs
# libselinux.so.1, absent on Arch/CachyOS. That surfaces late as a cryptic
# "failed to run linuxdeploy", so flag it up front with the correct remediation.
#
# Note: the shadow directory must be moved *out* of the scanned prefix entirely —
# renaming it in place (e.g. to insync.disabled) or renaming the files (…so.0.bak)
# does NOT help, because the glob still matches anything under /usr/lib.
# Note: /usr/lib64 and /lib are commonly symlinks to /usr/lib (Arch/CachyOS), so
# the same physical directory can be reached under several prefixes. Canonicalize
# each candidate and dedupe, otherwise the same shadow dir is processed twice and
# the second hide collides with the first.
SHADOW_LIB_DIRS=()
declare -A _shadow_seen=()
shopt -s nullglob
for _prefix in /usr/lib /usr/lib64 /lib /lib64; do
[ -d "$_prefix" ] || continue
for _sub in "$_prefix"/*/; do
_sub="${_sub%/}"
# Skip the standard multiarch dir (e.g. /usr/lib/x86_64-linux-gnu).
case "$_sub" in */*-linux-gnu) continue ;; esac
if compgen -G "$_sub/libgio-2.0.so*" >/dev/null 2>&1 \
|| compgen -G "$_sub/libgobject-2.0.so*" >/dev/null 2>&1 \
|| compgen -G "$_sub/libglib-2.0.so*" >/dev/null 2>&1 \
|| compgen -G "$_sub/libgdk_pixbuf-2.0.so*" >/dev/null 2>&1 \
|| compgen -G "$_sub/libgtk-3.so*" >/dev/null 2>&1; then
_canon="$(readlink -f "$_sub" 2>/dev/null || echo "$_sub")"
if [ -z "${_shadow_seen[$_canon]:-}" ]; then
_shadow_seen[$_canon]=1
SHADOW_LIB_DIRS+=("$_canon")
fi
fi
done
done
shopt -u nullglob
if [ ${#SHADOW_LIB_DIRS[@]} -gt 0 ]; then
warn "Found third-party copies of the GLib/GTK libraries under /usr/lib:"
for _d in "${SHADOW_LIB_DIRS[@]}"; do warn " $_d"; done
warn "linuxdeploy's GTK plugin globs those library names across /usr/lib and will"
warn "try to bundle these instead of the system copies, then fail on their extra"
warn "dependencies (e.g. Insync's libgio-2.0.so.0 needs libselinux.so.1) — the"
warn "late, cryptic 'failed to run linuxdeploy'."
if [ "$HIDE_SHADOW_LIBS" = true ]; then
info "These will be moved aside for the build and restored automatically when it"
info "finishes (needs sudo). Pass --keep-shadow-libs to disable and build as-is."
else
info "--keep-shadow-libs is set, so they are left in place and the build will most"
info "likely fail at linuxdeploy. To let the script handle them, drop that flag."
info "To do it by hand, move each directory OUT of /usr/lib (a sibling path, not a"
info "rename in place; the glob still matches anything under /usr/lib), e.g.:"
for _d in "${SHADOW_LIB_DIRS[@]}"; do
_hide="$(dirname "$(dirname "$_d")")/$(basename "$_d").buildhide"
info " sudo mv \"$_d\" \"$_hide\" # …build… then: sudo mv \"$_hide\" \"$_d\""
done
fi
echo ""
fi

ok "AppImage toolchain wrapper is verified and ready."

# ══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -206,16 +375,82 @@ if [ "$CUDA_FOUND" = false ] && [ "$HAS_NVIDIA_GPU" = true ]; then
fi
fi

info "Running Tauri release compiler with headless PATH and CUDA injection..."
# The Moonshine ONNX backend is always compiled in so both speech engines
# (whisper-cpp and Moonshine) are selectable in every AppImage. It links ONNX
# Runtime, fetched at build time, so this step needs network access.
# Assemble the cargo feature list for the release build.
# - cuda : GPU acceleration (only when a CUDA toolkit was found).
# - moonshine : the Moonshine ONNX speech engine (default on; opt out with
# --no-moonshine / MOONSHINE=0). This feature makes the compile
# download a prebuilt ONNX Runtime from cdn.pyke.io, so it is the
# one part of the build that requires network access.
BUILD_FEATURES=""
if [ "$CUDA_FOUND" = true ]; then
info "CUDA detected. Compiling with GPU support (whisper-cpp + Moonshine)..."
npx tauri build -- --features cuda,moonshine
BUILD_FEATURES="cuda"
fi
if [ "$NO_MOONSHINE" = false ]; then
BUILD_FEATURES="${BUILD_FEATURES:+$BUILD_FEATURES,}moonshine"
fi

# The Moonshine backend fetches a prebuilt ONNX Runtime from cdn.pyke.io at
# compile time. If that host is unreachable the build only fails after a long
# compile, so probe it up front and point users at the offline escape hatch.
if [ "$NO_MOONSHINE" = false ] && command -v curl &>/dev/null; then
info "Checking reachability of the ONNX Runtime download host (cdn.pyke.io)..."
# Without -f, curl succeeds as long as it *connected* (even on an HTTP 4xx),
# and only fails on connection-level problems: DNS, connect timeout, or a
# proxy/egress policy rejecting the CONNECT (403/407). That is exactly the
# condition that dooms the ONNX Runtime download, so key off the exit status.
if ! curl -s -o /dev/null --connect-timeout 10 --max-time 20 https://cdn.pyke.io/ 2>/dev/null; then
warn "Could not reach cdn.pyke.io."
warn "The Moonshine build downloads a prebuilt ONNX Runtime from there at compile"
warn "time; if it stays unreachable the build will fail after a long compile."
info "To build the whisper-cpp-only AppImage without this download, re-run with:"
info " ./build_appimage.sh --no-moonshine (or MOONSHINE=0 ./build_appimage.sh )"
echo ""
fi
fi

# In verbose mode, ask Tauri and linuxdeploy to print what they are doing so a
# bundling failure shows linuxdeploy's real error instead of just Tauri's
# generic "failed to run linuxdeploy" wrapper.
TAURI_VERBOSE=()
if [ "$VERBOSE_FLAG" = true ]; then
TAURI_VERBOSE=(--verbose)
export VERBOSE=2 # linuxdeploy log verbosity (0=error … 2=debug)
info "Verbose mode enabled: streaming Tauri + linuxdeploy output."
fi

# With --hide-shadow-libs, move any detected third-party GLib/GTK directories out
# of linuxdeploy's search path now, recording each move so the EXIT trap restores
# them once the build finishes (or fails). They are staged in a sibling of the
# scanned prefix (e.g. /usr/lib/insync -> /usr/insync.buildhide) — outside
# /usr/lib so the plugin's glob can't reach them, and on the same filesystem so
# the move is an instant rename.
if [ "$HIDE_SHADOW_LIBS" = true ] && [ ${#SHADOW_LIB_DIRS[@]} -gt 0 ]; then
step "Hiding third-party shadow libraries from linuxdeploy"
for _d in "${SHADOW_LIB_DIRS[@]}"; do
_hide="$(dirname "$(dirname "$_d")")/$(basename "$_d").buildhide"
if [ -e "$_hide" ]; then
fail "Staging path $_hide already exists; refusing to overwrite it."
info "Move it out of the way and re-run, or restore it manually."
exit 1
fi
info "Moving $_d -> $_hide (restored automatically when the build finishes)..."
if sudo mv "$_d" "$_hide"; then
SHADOW_HIDE_MAP+=("$_d|$_hide")
else
fail "Could not move $_d out of the way (sudo required)."
exit 1
fi
done
ok "Shadow libraries hidden; they will be restored automatically on exit."
fi

info "Running Tauri release compiler with headless PATH and CUDA injection..."
if [ -n "$BUILD_FEATURES" ]; then
info "Compiling with features: ${BUILD_FEATURES}"
npx tauri build "${TAURI_VERBOSE[@]}" -- --features "$BUILD_FEATURES"
else
info "CUDA not detected. Compiling for CPU only (whisper-cpp + Moonshine)..."
npx tauri build -- --features moonshine
info "Compiling with default features (whisper-cpp only)..."
npx tauri build "${TAURI_VERBOSE[@]}"
fi

ok "Compilation finished successfully."
Expand Down
19 changes: 11 additions & 8 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ else
if ! command -v cargo &>/dev/null || ! cargo --version &>/dev/null; then MISSING_BUILD_TOOLS=1; fi
if ! command -v npm &>/dev/null; then MISSING_BUILD_TOOLS=1; fi
if ! command -v cmake &>/dev/null; then MISSING_BUILD_TOOLS=1; fi
# patchelf/file are needed by linuxdeploy for the AppImage packaging step.
if ! command -v patchelf &>/dev/null; then MISSING_BUILD_TOOLS=1; fi
if ! command -v file &>/dev/null; then MISSING_BUILD_TOOLS=1; fi

# Check if an NVIDIA GPU is present
HAS_NVIDIA_GPU=false
Expand All @@ -173,35 +176,35 @@ else
pacman)
if [ "$INSTALL_CUDA" = true ]; then
info "NVIDIA GPU detected. Adding 'cuda' toolkit to installation..."
sudo pacman -S --noconfirm --needed base-devel rustup nodejs npm pkgconf cuda cmake
sudo pacman -S --noconfirm --needed base-devel rustup nodejs npm pkgconf cuda cmake patchelf file
else
sudo pacman -S --noconfirm --needed base-devel rustup nodejs npm pkgconf cmake
sudo pacman -S --noconfirm --needed base-devel rustup nodejs npm pkgconf cmake patchelf file
fi
;;
apt)
if [ "$INSTALL_CUDA" = true ]; then
info "NVIDIA GPU detected. Adding 'nvidia-cuda-toolkit' to installation..."
sudo apt-get install -y build-essential curl nodejs npm pkg-config nvidia-cuda-toolkit cmake
sudo apt-get install -y build-essential curl nodejs npm pkg-config nvidia-cuda-toolkit cmake patchelf file
else
sudo apt-get install -y build-essential curl nodejs npm pkg-config cmake
sudo apt-get install -y build-essential curl nodejs npm pkg-config cmake patchelf file
fi
;;
dnf)
sudo dnf groupinstall -y "Development Tools"
if [ "$INSTALL_CUDA" = true ]; then
info "NVIDIA GPU detected. Adding 'cuda-toolkit' to installation..."
sudo dnf install -y curl nodejs npm pkgconf-pkg-config cuda-toolkit cmake
sudo dnf install -y curl nodejs npm pkgconf-pkg-config cuda-toolkit cmake patchelf file
else
sudo dnf install -y curl nodejs npm pkgconf-pkg-config cmake
sudo dnf install -y curl nodejs npm pkgconf-pkg-config cmake patchelf file
fi
;;
zypper)
sudo zypper install -t pattern -y devel_basis
if [ "$INSTALL_CUDA" = true ]; then
info "NVIDIA GPU detected. Adding 'cuda' toolkit to installation..."
sudo zypper install -y curl nodejs npm pkg-config cuda cmake
sudo zypper install -y curl nodejs npm pkg-config cuda cmake patchelf file
else
sudo zypper install -y curl nodejs npm pkg-config cmake
sudo zypper install -y curl nodejs npm pkg-config cmake patchelf file
fi
;;
esac
Expand Down
Loading