-
Notifications
You must be signed in to change notification settings - Fork 3
appimage_build_process
This document provides complete instructions and design details for compiling, packaging, and deploying the VoxCtrl desktop application into a portable, standalone Linux AppImage. It serves as an authoritative playbook for developers and automated agents to execute rapid, error-free production releases.
Below is the workflow showing how the frontend, backend compiler, local system wrappers, and write-protected platform tools assemble into a portable AppImage:
graph TD
subgraph Frontend Build [1. Svelte Frontend]
A[Svelte 5 / TypeScript] -->|npm run build| B[Vite Assets /dist]
end
subgraph Backend Build [2. Rust Tauri Release]
B -->|npx tauri build| C[Tauri Bundler CLI]
D[Cargo / crates] -->|release target| C
end
subgraph Build Environment [3. Toolchain & FUSE Bypasses]
C -->|Prepends PATH| E[Root appimagetool wrapper]
E -->|Executes| F[appimagetool.bin --appimage-extract-and-run]
C -->|Spawns linuxdeploy| G[~/.cache/tauri/linuxdeploy-x86_64.AppImage]
G -->|Read-only Wrapper| H[linuxdeploy-x86_64.AppImage.real]
end
subgraph Output Bundles [4. Standalone Packages]
H -->|Assembles| I[target/release/bundle/appimage/VoxCtrl_*.AppImage]
I -->|Exposed by build_appimage.sh| J[Root: VoxCtrl-VERSION-x86_64.AppImage]
end
classDef primary fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#fff;
classDef secondary fill:#1e293b,stroke:#0284c7,stroke-width:1px,color:#cbd5e1;
class A,D,F,H,J primary;
class B,C,E,G,I secondary;
Before executing a build, ensure the host machine has the core compilation packages and AppImage extraction tools installed.
| Package | Purpose | Package Name (Arch) | Package Name (Debian/Ubuntu) |
|---|---|---|---|
| Squashfs Extraction | Allows FUSE-less extraction of packaging AppImages. | squashfs-tools |
squashfs-tools |
| Rust Toolchain | Rust compiler and Cargo bundle tools. |
rustup / rust
|
rustc / cargo
|
| Node.js Environment | Package manager and Vite bundling. |
nodejs npm
|
nodejs npm
|
| System Webview | Tauri interface runtime dependency. | webkit2gtk-4.1 |
libwebkit2gtk-4.1-dev |
| Audio Library | Low-latency audio capture via CPAL/ALSA. | alsa-lib |
libasound2-dev |
| Desktop Integrations | System tray application icons support. | libayatana-appindicator |
libayatana-appindicator3-dev |
-
Arch Linux / CachyOS:
sudo pacman -S --needed base-devel rustup nodejs npm webkit2gtk-4.1 alsa-lib libayatana-appindicator squashfs-tools
-
Ubuntu / Debian:
sudo apt-get update sudo apt-get install -y build-essential curl nodejs npm pkg-config libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev libasound2-dev squashfs-tools
To build a fresh deployment AppImage, run the automated compile script from the project root:
chmod +x build_appimage.sh
./build_appimage.sh-
Toolchain Check: Validates that
unsquashfsis present to support FUSE-less unpacking. -
Frontend Compiles: Compiles all visual assets and generates the optimized production build (
/dist). -
Environment Setup: Prepends the workspace root to the shell
$PATHand exportsAPPIMAGE_EXTRACT_AND_RUN=1andQT_QPA_PLATFORM=offscreento allow FUSE-less head-free compilation. -
Tauri Releases: Runs
npx tauri buildto compile the optimized release binary and bundles it using the FUSE-bypass tools. -
Relocation: Copies the completed executable dynamically using the
productNameandversionfromtauri.conf.json(e.g.VoxCtrl-0.1.0-x86_64.AppImage) directly to the project root, creating a convenientVoxCtrl-latest-x86_64.AppImagesymlink.
If the bundling process fails at the AppImage packaging stage with a generic system error:
failed to bundle project `No such file or directory (os error 2)`
The IDE/container sandbox environment features an automated background file-watching daemon that intercepts files inside ~/.cache/tauri/ to sanitize environment paths.
However, due to a byte-truncation bug in the platform's wrapper generator, the daemon automatically rewrites ~/.cache/tauri/linuxdeploy-x86_64.AppImage to utilize a corrupted interpreter shebang:
#!/bin/b Because the Linux kernel cannot find /bin/b to execute the wrapper, it fails with a low-level ENOENT (os error 2) instantly.
To resolve this, we bypass the platform's editor hooks by writing the correct bash script directly via shell redirection and revoking write permissions to make it read-only. Run these commands:
# 1. Re-write the correct wrapper script directly via the shell
# NOTE: If your IDE/container injects a custom PATH entry that is being
# written into the shebang, adjust the sed pattern below to match
# your environment's injected bin path.
cat << 'EOF' > ~/.cache/tauri/linuxdeploy-x86_64.AppImage
#!/bin/bash
export APPIMAGE_EXTRACT_AND_RUN=1
export NO_STRIP=1
export QT_QPA_PLATFORM=offscreen
# Remove any IDE-injected PATH entries that corrupt the linuxdeploy wrapper.
# Adjust the pattern below to match your environment if needed.
export PATH=$(echo $PATH | sed 's|<YOUR_IDE_BIN_PATH>:||g')
exec "$HOME/.cache/tauri/linuxdeploy-x86_64.AppImage.real" "$@" --exclude-library=libselinux* --exclude-library=libgio* --exclude-library=*gdk_pixbuf*
EOF
# 2. REVOKE write permissions to make the file immutable to the broken platform hook
chmod 555 ~/.cache/tauri/linuxdeploy-x86_64.AppImage
# 3. Make sure the helper plugin is also protected
chmod 555 ~/.cache/tauri/linuxdeploy-plugin-appimage.AppImageOnce locked, the platform is physically blocked from corrupting the shebang, and ./build_appimage.sh will bundle successfully.
To run the completed portable application on a fresh host system, follow these deployment steps:
You only need to transfer one file to the new machine:
-
VoxCtrl-*-x86_64.AppImage(the versioned executable)
Run the built-in setup command once on the new machine to automatically pull in host runtime dependencies, install desktop icons, and write a menu launcher. Global shortcuts need no permissions — they go through the desktop portal:
chmod +x VoxCtrl-*-x86_64.AppImage
./VoxCtrl-*-x86_64.AppImage --installAlternatively, you can just double-click or run the AppImage directly; VoxCtrl's setup window detects anything missing at startup and offers to install it.
Global shortcuts work immediately. VoxCtrl registers them with your desktop
through the XDG GlobalShortcuts portal, so there is no permission to grant, no
udev rule, no group membership, no logout and no reboot — and VoxCtrl never
reads your keyboard.
If your desktop does not implement the portal, the setup window says so at launch and explains the options. VoxCtrl will not grant itself keyboard access to work around it.
The version number is declared independently in three places: package.json,
src-tauri/tauri.conf.json (the one that actually drives getVersion(), the
About tab, and the AppImage filename), and Cargo.toml's
[workspace.package]. They've drifted before, and a release has shipped
under a git tag that didn't match what the built app actually reported.
Always bump with the script, never by hand-editing one file:
./scripts/bump_version.sh 0.2.8
cargo check # refreshes Cargo.lock's recorded crate versions
git add -A && git commit -m "Bump version to 0.2.8"CI (.github/workflows/release.yml) runs a check-version-sync job before
every release build that fails immediately if the three files disagree, and
rejects a manually-dispatched release tag that doesn't match the version in
the files — bump the files instead of overriding the tag.
The AppImage is deliberately not self-contained. The slimming step in
build_appimage.sh and .github/workflows/release.yml (keep the two in sync)
deletes bundled copies of libraries that must come from the host at runtime,
and ldd -r of every bundled object under the AppImage's own library path must
stay clean on a newer host. The rules, each learned from a startup crash:
-
Graphics, Wayland and input libraries come from the host (Mesa, EGL, libdrm, libwayland, libxkbcommon). Bundled copies from the Ubuntu 22.04 build host break Wayland frame callbacks and xkbcommon.
-
Once a library comes from the host, everything it links against must come from the host too. The host's copy resolves its symbols against whatever is first on
LD_LIBRARY_PATH; a stale bundled dependency makes it abort withundefined symbol/version ... not found. This is why the GLib family, GStreamer, the GIO TLS module and gnutls stack, and their dependencies (pcre2, libffi, libmount, libblkid, libselinux, liborc, libunwind, libdw, libelf, bz2, lzma, zstd) are all stripped together. Everything still bundled only needs the Ubuntu 22.04 versions of these, so any newer host satisfies it. -
Only strip a library whose soname is stable across distributions.
libxml2(.so.16on Arch) andlibunistring(.so.5on newer hosts) stay bundled for that reason. -
Some libraries are host-first with a bundled fallback. They live in
usr/lib/fallback/, and thescripts/appimage-hooks/host-first-fallback.shAppRun hook exposes each one only when the host has no library of that soname — so a host that has its own copy always wins, and a host that has none still starts.-
libsystemd/libudev: Arch'slibmountlinkslibsystemd, so a bundled copy must not shadow the host's; but non-systemd distributions may not have them at all and the bundled WebKitGTK needs them. -
libgstgl-1.0/libwayland-server: the bundled WebKitGTK links both directly, and rules 1 and 2 would otherwise delete them.libgstgl-1.0.so.0ships inlibgstreamer-gl1.0-0, whichgstreamer1.0-plugins-basedoes not depend on, so a desktop with no WebKit of its own can be missing it and the app dies witherror while loading shared libraries: libgstgl-1.0.so.0. -
libwebkit2gtk-4.1/libjavascriptcoregtk-4.1: the build host's WebKitGTK renders a transparent window's compositing layers without their alpha channel, so an overlay animating as it closes left an opaque, blurry copy of its last frame on screen until the window was destroyed, while anything static drew correctly. Newer WebKitGTK is fine, which is why a localbuild_appimage.shbuild — bundling the developer's own newer WebKitGTK — never reproduced it and released builds always did. Preferring the host's copy fixes it for every desktop that has one (all rolling releases, and most others), and the bundled copy still starts on a desktop that has none, so rule 4's "always self-contained enough to run" property holds.
-
-
WebKitGTK finds its helper processes relative to the working directory. The Tauri bundler rewrites the compiled-in
/usr/lib/x86_64-linux-gnu/webkit2gtk-4.1to././/lib/x86_64-linux-gnu/webkit2gtk-4.1, and linuxdeploy's AppRun starts the app in$APPDIR/usrso that resolves. Release builds of WebKitGTK honour no environment override for this (WEBKIT_EXEC_PATHonly exists in developer-mode builds), so the helpers must sit at exactly that relative path, and nothing in the app may change the process's working directory. An earlier Pocket-TTS implementation used to require a bundledusr/config/for the same reason; the current audio.cpp-backed engines take a--model <dir>argument directly and no longer touch the process's working directory at all.The library and its helpers are one unit: the bundled
libwebkit2gtkhas the bundled helper path compiled into it, so removing only the helpers makes it abort at startup withFailed to spawn child process "././/lib/.../WebKitNetworkProcess". Since rule 4 now prefers the host's WebKitGTK, the AppRun hook also unsetsWEBKIT_EXEC_PATHandWEBKIT_INJECTED_BUNDLE_PATHwhenever the host's copy wins — those point into this bundle, and the injected bundle is version-locked to the library it shipped with, so leaving them set would pair the host's library with helpers it was never built against. The bundled helpers stay in place regardless, since they are only ever reached when the bundled library is. -
The AppImage runtime must not need FUSE 2. appimagetool's built-in type-2 runtime statically links libfuse 2 and looks for a
fusermountbinary; Ubuntu 22.04 / Linux Mint 21 and newer ship fuse3 (fusermount3) and nolibfuse2, so such an AppImage aborts withError: No suitable fusermount binary found on the $PATHbefore it ever reachesAppRun.scripts/appimage-pack.shtherefore packages with uruntime, which speaks FUSE 3 and extracts-and-runs itself when nofusermountis usable. If that runtime cannot be fetched, or the AppImage built with it does not unpack again, packaging falls back to appimagetool's own runtime, so a build is never left worse off than before.
The release AppImages are built on the ubuntu-22.04 runner, which sets the
floor for what they run on: glibc 2.35 (hypot@GLIBC_2.35 in the overlay,
WebKitGTK and cairo) and GLIBCXX_3.4.30 (GCC 12's libstdc++, needed by
usr/bin/voxctrl and WebKitGTK). Ubuntu 22.04+, Linux Mint 21+, Debian 12+ and
Fedora 36+ satisfy both; Ubuntu 20.04, Mint 20, Debian 11 and RHEL 9 do not.
Building locally with build_appimage.sh produces an AppImage with your
distribution's baseline — on a rolling distro that is far higher than 22.04's,
so ship CI artifacts, not local builds.
-
Tauri Config (
src-tauri/tauri.conf.json):-
"targets": ["appimage"]: Isolated to bundle exclusively the AppImage target. Can be set back to"all"to compile.deband.rpmfiles if production repositories require standard package formats.
-
-
Environment Variables:
-
APPIMAGE_EXTRACT_AND_RUN=1: Directs packaging and runtime binaries to extract themselves into/tmprather than attempting FUSE mounts. -
QT_QPA_PLATFORM=offscreen: Prevents Qt platform errors inside display-less or restricted terminal workspaces.
-