#!/usr/bin/env bash # Builds the desktop app (Tauri) and places the resulting bundles in dist-gui/. # # scripts/build-gui.sh linux # AppImage and .deb (native architecture) # scripts/build-gui.sh linux-aarch64 # AppImage and .deb for aarch64 # scripts/build-gui.sh linux-x86_64 # AppImage and .deb for x86_64 (native or cross-built from aarch64) # scripts/build-gui.sh windows # NSIS installer, cross-built from Linux via cargo-xwin # scripts/build-gui.sh windows-docker # Same as above inside an isolated container # # Windows builds use cargo-xwin and NSIS (requires: nsis, lld, llvm, clang, and # `cargo install cargo-xwin`). set -euo pipefail cd "$(dirname "$0")/.." platform=${1:?usage: build-gui.sh } tauri() { npx --yes @tauri-apps/cli@2 "$@"; } if [ "$platform" = windows-docker ]; then docker volume create guara-cargo >/dev/null docker volume create guara-rustup >/dev/null mkdir -p dist-gui exec docker run --rm -v "$PWD":/src -v guara-cargo:/root/.cargo -v guara-rustup:/root/.rustup \ -w /src -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \ ubuntu:24.04 bash -c ' set -e export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq curl ca-certificates build-essential pkg-config nsis lld llvm clang nodejs npm unzip >/dev/null [ -x /root/.cargo/bin/rustup ] || curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null export PATH=/root/.cargo/bin:$PATH command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked scripts/build-gui.sh windows chown -R "$HOST_UID:$HOST_GID" dist-gui apps/gui/src-tauri/target 2>/dev/null || true' fi rm -rf dist-gui mkdir -p dist-gui cd apps/gui/src-tauri [ -f icons/icon.ico ] || tauri icon icons/source.png -o icons tauri_blob_cache="$HOME/.cache/tauri" if [ -d /opt/hostedtoolcache ] && [ -w /opt/hostedtoolcache ]; then tauri_blob_cache="/opt/hostedtoolcache/tauri-tools" fi mkdir -p "$tauri_blob_cache" "$HOME/.cache/tauri" fetch_cached_tool() { local name=$1 url=$2 patch_elf=${3:-0} local cached="$tauri_blob_cache/$name" if [ ! -s "$cached" ]; then local tmp="$cached.tmp.$$" curl -fsSL "$url" -o "$tmp" if [ "$patch_elf" = 1 ]; then dd if=/dev/zero of="$tmp" bs=1 count=3 seek=8 conv=notrunc 2>/dev/null fi chmod +x "$tmp" mv -f "$tmp" "$cached" fi if [ "$cached" != "$HOME/.cache/tauri/$name" ]; then cp -f "$cached" "$HOME/.cache/tauri/$name" chmod +x "$HOME/.cache/tauri/$name" fi } case "$platform" in linux|linux-aarch64) bundle="${CARGO_TARGET_DIR:-target}/release/bundle" rm -rf "$bundle/appimage" "$bundle/deb" tauri build --bundles appimage,deb cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/ ;; linux-x86_64) if [ "$(uname -m)" = x86_64 ]; then bundle="${CARGO_TARGET_DIR:-target}/release/bundle" rm -rf "$bundle/appimage" "$bundle/deb" tauri build --bundles appimage,deb else rustup target add x86_64-unknown-linux-gnu >/dev/null mkdir -p /tmp/x86_64-cross-tools "$HOME/.cache/tauri" cat >/tmp/x86_64-cross-tools/ldd <<'EOF' #!/usr/bin/env bash exec qemu-x86_64 /lib64/ld-linux-x86-64.so.2 --list "$@" EOF chmod +x /tmp/x86_64-cross-tools/ldd ln -sf /usr/bin/x86_64-linux-gnu-strip /tmp/x86_64-cross-tools/strip for tool_rel in glib-2.0/gio-querymodules gdk-pixbuf-2.0/gdk-pixbuf-query-loaders libgtk-3-0t64/gtk-query-immodules-3.0 libgtk-3-0/gtk-query-immodules-3.0; do if [ -x "/usr/lib/x86_64-linux-gnu/$tool_rel" ]; then tname=$(basename "$tool_rel") printf '#!/usr/bin/env bash\nexec qemu-x86_64 "/usr/lib/x86_64-linux-gnu/%s" "$@"\n' "$tool_rel" >"/tmp/x86_64-cross-tools/$tname" chmod +x "/tmp/x86_64-cross-tools/$tname" fi done fetch_cached_tool "linuxdeploy-07333c6-aarch64.AppImage" \ "https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy-07333c6/linuxdeploy-aarch64.AppImage" 1 # tauri-bundler looks for this exact cache name (no arch suffix); the # architecture only appears in the download URL. fetch_cached_tool "linuxdeploy-plugin-appimage.AppImage" \ "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-aarch64.AppImage" 1 fetch_cached_tool "AppRun-x86_64" \ "https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-x86_64" 0 fetch_cached_tool "runtime-x86_64" \ "https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" 0 # tauri-bundler runs `dd ... seek=8` on this path to hide linuxdeploy from # AppImage integration tools. A shell-script wrapper loses its shebang and # then fails with ENOENT, so use a native ELF launcher: the dd writes into # ELF padding and the launcher stays executable. launcher_src=/tmp/x86_64-cross-tools/linuxdeploy-x86_64.c launcher_bin="$HOME/.cache/tauri/linuxdeploy-07333c6-x86_64.AppImage" cat >"$launcher_src" <<'EOF' #define _POSIX_C_SOURCE 200809L #include #include #include int main(int argc, char **argv) { const char *home = getenv("HOME"); if (home == NULL) { fputs("HOME is not set\n", stderr); return 127; } char target[4096]; char runtime[4096]; char path[8192]; const char *old_path = getenv("PATH"); snprintf(target, sizeof target, "%s/.cache/tauri/linuxdeploy-07333c6-aarch64.AppImage", home); snprintf(runtime, sizeof runtime, "%s/.cache/tauri/runtime-x86_64", home); snprintf(path, sizeof path, "/tmp/x86_64-cross-tools:%s", old_path ? old_path : "/usr/local/bin:/usr/bin:/bin"); if (setenv("ARCH", "x86_64", 1) || setenv("NO_STRIP", "true", 1) || setenv("APPIMAGE_EXTRACT_AND_RUN", "1", 1) || setenv("LDAI_RUNTIME_FILE", runtime, 1) || setenv("LD_GTK_LIBRARY_PATH", "/usr/lib/x86_64-linux-gnu", 1) || setenv("DEB_HOST_MULTIARCH", "x86_64-linux-gnu", 1) || setenv("PKG_CONFIG_ALLOW_CROSS", "1", 1) || setenv("PKG_CONFIG_LIBDIR", "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig", 1) || setenv("PATH", path, 1)) { perror("setenv"); return 127; } char **args = calloc((size_t)argc + 1, sizeof *args); if (args == NULL) { perror("calloc"); return 127; } args[0] = target; for (int i = 1; i < argc; i++) args[i] = argv[i]; execv(target, args); perror(target); return 127; } EOF cc -O2 -Wall -Wextra -o "$launcher_bin" "$launcher_src" export PKG_CONFIG_ALLOW_CROSS=1 export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc export CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++ bundle="${CARGO_TARGET_DIR:-target}/x86_64-unknown-linux-gnu/release/bundle" rm -rf "$bundle/appimage" "$bundle/deb" tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb fi cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/ ;; windows) rustup target add x86_64-pc-windows-msvc >/dev/null bundle="${CARGO_TARGET_DIR:-target}/x86_64-pc-windows-msvc/release/bundle/nsis" rm -rf "$bundle" tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis cp "$bundle"/*-setup.exe ../../../dist-gui/ ;; *) echo "unknown platform: $platform" >&2; exit 2 ;; esac ( cd ../../../dist-gui for asset in *; do [ -f "$asset" ] || continue case "$asset" in *.sha256) ;; *) sha256sum "$asset" > "$asset.sha256" ;; esac done ) ls -1 ../../../dist-gui