diff --git a/build_appimage.sh b/build_appimage.sh index c46d4a2..5652262 100755 --- a/build_appimage.sh +++ b/build_appimage.sh @@ -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' @@ -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 # ══════════════════════════════════════════════════════════════════════════════ @@ -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." # ══════════════════════════════════════════════════════════════════════════════ @@ -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." diff --git a/install.sh b/install.sh index 9a05e3b..6b5182b 100755 --- a/install.sh +++ b/install.sh @@ -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 @@ -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