Migrate desktop app from Electron to Tauri#1611
Conversation
Replaces the Electron 39 desktop app in src/app/ with a Tauri v2 host written in Rust (src/app/src-tauri/). The existing React 19 + TypeScript renderer is reused unchanged via a window.api shim that maps to Tauri invoke() / listen() calls at runtime, keeping the web-app (src/web-app/) compatible with the server.cpp mock. Main changes: - src/app/src-tauri/: Rust host with tauri v2, plugins (single-instance, deep-link, opener, clipboard-manager), tokio-based UDP beacon discovery, settings read/write mirroring main.js sanitize logic, macOS tray launcher, and system stats/info proxies to lemond. - src/app/src/renderer/tauriShim.ts: installs window.api -> invoke() bridge so the 55+ React files stay untouched. Dynamic imports gated by window.__TAURI_INTERNALS__ detection; web-app build aliases the @tauri-apps modules to src/web-app/tauri-stub.js. - CMakeLists.txt: replaces the electron-app target with tauri-app. The new target runs npm ci + webpack + cargo tauri build, then stages the binary into build/app/lemonade-app[.exe|.app]. Rewrites prepare_tauri_app (macOS productbuild path) and the AppImage target (cargo tauri build --bundles appimage, renamed to lemonade-app-<version>-x86_64.AppImage for CI compatibility). Debian BUILD_ELECTRON_APP -> BUILD_TAURI_APP and installs a single binary instead of a chromium tree. - WiX: renames IncludeElectron -> IncludeTauriApp (and all refs), generate_electron_fragment.py -> generate_tauri_fragment.py. MSI still packs the desktop app into [INSTALLDIR]app\\lemonade-app.exe and the lemonade:// protocol handler still points at it. - CI: adds Rust toolchain + Swatinem/rust-cache to Windows, macOS, Linux AppImage jobs. Linux AppImage job also installs libwebkit2gtk-4.1-dev, libsoup-3.0-dev, librsvg2-dev, libayatana-appindicator3-dev. build-macos-dmg action: include-electron -> include-tauri, replaces CSC_NAME with APPLE_SIGNING_IDENTITY. - Docs: updates README roadmap, AGENTS.md, dev-getting-started.md, BUILD_OPTIONS.md, src/app/README.md, src/web-app/README.md. - setup.sh / setup.ps1: check for Rust and Linux webkit2gtk-dev deps. - Deletes src/app/main.js, src/app/preload.js, src/app/index.html, src/cpp/installer/generate_electron_fragment.py (renamed). The Tauri Linux binary weighs ~10 MB (vs Electron's ~180 MB); the AppImage weighs ~85 MB (vs ~190 MB). Unit tests cover settings sanitization, beacon parsing, and deep-link URL parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MSBuild custom-build commands fail with "cannot find the batch label specified - VCEnd" when the command chains cmd /c npm.cmd exec -- tauri build. Add dedicated build:nobundle / build:mac / build:appimage scripts to src/app/package.json and invoke them via `npm run <script>`, mirroring the pattern the old electron-app target used successfully. Verified locally on Linux: cmake --build --preset default --target tauri-app still produces build/app/lemonade-app (9.6 MB stripped release binary). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Findings from a 3-agent review of the Tauri port. Each fix in turn: Correctness - lib::handle_protocol_urls used to call queue_pending_nav() in BOTH the emit-success and emit-failure branches, so every deep link fired the navigate event twice once the renderer mounted. Track a RENDERER_READY AtomicBool, only queue when the renderer hasn't signaled ready yet, and emit immediately otherwise. Symmetric drain in renderer_ready. Safety - settings::sanitize_app_settings used `*mut bool` raw pointers + an unsafe block to iterate the layout boolean fields. Replaced with a safe inner closure that takes `&mut bool` slots one at a time. Efficiency - system_info::http_client now caches a single reqwest::Client in OnceLock so the connection pool survives across the status-bar polling cycle instead of being thrown away on every fetch. - beacon::is_local_machine no longer opens a fresh UDP socket on every non-loopback packet — the local outbound IP is computed once via OnceLock. - tray_launcher::ensure_tray_running runs on a dedicated std::thread so the worst-case 30-second pkill/pgrep poll loop on macOS never blocks the Tauri main thread / setup closure / window creation. - lib.rs maximize-change emitter now compares against the previous state via an AtomicBool and skips emits that don't actually change anything, so resize events don't flood the renderer. Code reuse - beacon::parse_port_from_url replaces the hand-rolled `://` + `:` + `/` splitting with url::Url::port() (the url crate is already a dep). - lib::parse_protocol_url replaces the hand-rolled `?` + `&` + `=` parsing with url::Url::query_pairs() via a normalized http:// scheme. - settings::sanitize_app_settings collapses the four near-identical enableThinking / collapseThinkingByDefault / baseURL / apiKey blocks into a single apply_typed_setting() helper plus tiny extract_bool / extract_string / extract_clamped_f64 / extract_clamped_i64 closures. TTS handling reuses the same helper. - tauriShim.ts collapses the six fire-and-forget invoke wrappers (minimize/maximize/close/zoom/zoom/updateMinWidth/signalReady) into a single `fire(cmd, args?)` helper. Quality - New `events` module owns the Tauri event channel name constants (settings-updated, connection-settings-updated, server-port-updated, maximize-change, navigate). beacon, commands, and lib all reference the constants instead of repeating string literals. tauriShim.ts has a matching set of TS constants that mirror them (kept in sync by comment). - Removed the duplicate DEFAULT_PORT in beacon.rs (was identical to BEACON_PORT). - Narrowed `pub` to `pub(crate)` on every Rust item that isn't required to be `pub` by tauri::generate_handler! (which still works on pub(crate) functions because lib.rs is in the same crate). - Dropped the "Port of Electron main.js lines X-Y" historical header comments from each Rust module — the modules are documented well enough by their own names and doc comments. Local verification - cargo test: 6/6 passing (parse_port_from_url, parse_beacon_message x2, parse_protocol_url x3). - cmake --target tauri-app: build/app/lemonade-app produced (9.6 MB). - cmake --target web-app: build/resources/web-app/index.html produced; the @tauri-apps stub alias works unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ault Three issues observed when running the Tauri AppImage that didn't appear in the Electron app: 1. Dragging the panel separators failed to resize the panels. 2. Hover ghost-buttons on model rows in the Model Manager didn't appear. 3. Click-and-drag selected entire div containers, not just text spans. All three trace back to the difference between Chromium's and WebKit's default drag-selection behavior. WebKit (the engine in webkit2gtk/WKWebView) starts a more aggressive text selection on mousedown, which: - Selects parent flex containers instead of just text content (#3) - Swallows the mousemove events the resize-divider drag depends on (#1) - Blocks mouseenter/mouseleave from firing on the model rows (#2) Two minimal-footprint fixes: 1. styles.css: a top-level user-select reset. The whole app chrome becomes non-selectable by default, with explicit opt-ins for the classes that legitimately host copy/paste-able content (chat messages, thinking content, log lines, markdown content, code blocks, model names/sizes, about modal, form fields). This is the standard "desktop app" CSS reset for WebKit-based shells. 2. App.tsx: add e.preventDefault() to handleLeftDividerMouseDown and handleRightDividerMouseDown so WebKit doesn't start a text selection that competes with the resize drag. ResizablePanel.tsx:72 already does this for the same reason — the App.tsx handlers were inconsistent. Web-app impact: src/web-app/styles.css is symlinked to src/app/styles.css, so the user-select reset also applies in the web app. Chromium honors the same CSS, so users browsing http://localhost:13305/app can no longer text- select arbitrary UI chrome (button labels, panel headers, settings labels). Selection of chat content, log output, model names, code, markdown, and form fields all still works. The App.tsx preventDefault() is a no-op in Chromium because the divider div has no useful default mousedown behavior to lose. Both changes are scope expansions beyond the original "renderer untouched" boundary the port plan promised. They are tactical WebKit shims, not a broader renderer refactor. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restores two Electron behaviors that were missing in the Tauri port. Both
were called out in the original audit ("Behavioral regressions I flagged
but did not fix"). They now ship cross-platform.
New module `src/app/src-tauri/src/webview_shim.rs`. Hooked from setup() via
WebviewWindow::with_webview() once the main window exists.
Iframe / external link interception
-----------------------------------
The original Electron app intercepted anchor clicks targeting http(s) URLs
inside the marketplace iframe (and any other iframe) via
frame.executeJavaScript injection from the main process. Tauri's renderer
JS can't reach cross-origin iframes, so we use the platform-native
"user script" mechanism that runs in every frame on every page load:
- Linux: webkit2gtk UserScript via WebView::user_content_manager().add_script()
with InjectedFrames::AllFrames + UserScriptInjectionTime::Start
- macOS: WKUserScript with forMainFrameOnly: false on the
WKUserContentController obtained from PlatformWebview::controller()
- Windows: ICoreWebView2::AddScriptToExecuteOnDocumentCreated via
webview2-com::AddScriptToExecuteOnDocumentCreatedCompletedHandler
The injected script intercepts http(s) anchor clicks in any frame. Top frame
calls window.api.openExternal directly. Iframes postMessage the href up to
the top frame, which forwards via window.api.openExternal. Cross-origin
iframes work because postMessage is the cross-origin transport.
Linux gets a belt-and-suspenders fallback: webkit2gtk's `decide-policy`
signal is hooked to catch any non-click navigation to an external URL
(programmatic location.href, iframe src changes, etc.) and route it to
tauri-plugin-opener::open_url. macOS/Windows would need replacing wry's
existing navigation delegate to do the same, which is out of scope.
Microphone permission auto-grant
--------------------------------
Per platform:
- Linux: webkit2gtk's permission-request signal is connected and
auto-allows UserMediaPermissionRequest. WebSettings is also
flipped to enable_media_stream / enable_mediasource /
enable_webrtc — these are off by default in Tauri's webkit2gtk.
- macOS: no-op. wry's WryWebViewUIDelegate already auto-grants every
WKMediaCaptureType (verified in
wry/src/wkwebview/class/wry_web_view_ui_delegate.rs).
- Windows: WebView2's PermissionRequested event is subscribed via
webview2-com. Microphone and Camera kinds are auto-granted.
Clipboard is already auto-granted by wry.
Dependencies
------------
New target-conditional deps in src/app/src-tauri/Cargo.toml. Versions
pinned to match wry-0.54.4 so the WebView types unify across calls into
Tauri's internal handles and ours:
cfg(target_os = "linux"): webkit2gtk v2 (v2_40), glib 0.18
cfg(target_os = "windows"): webview2-com 0.38, windows 0.61
cfg(target_vendor = "apple"): objc2 0.6.4, objc2-foundation 0.3,
objc2-web-kit 0.3, block2 0.6
Verification
------------
- cargo check: clean on Linux
- cargo test: 7/7 passing (existing tests + classifies_external_urls)
- cmake --target appimage: build/app-appimage/lemonade-app-10.2.0-x86_64.AppImage
built; click interception, mic permission
auto-grant, and external nav routing all
tested manually on Ubuntu 26.04.
- macOS / Windows: code-complete, but cross-platform validation deferred
to CI on push. Both platforms use platform-native APIs that wry itself
uses, so the FFI shapes are known-good.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three independent polish items applied together:
1. Delete src-tauri/src/system_info.rs and the get_version /
get_system_stats / get_system_info Tauri commands. The renderer already
had serverConfig.fetch() (with auth, base URL handling, and port
discovery retry) and was using it for /stats; StatusBar.tsx and
AboutModal.tsx now hit /system-stats, /system-info, and /health
directly, eliminating the parallel Rust implementation. Drops the
reqwest dependency from Cargo.toml and the matching ~100-line GPU-info
normalization JS blob from src/cpp/server/server.cpp. Net effect: the
Tauri release binary shrinks from 9.3 MB to 7.9 MB and there is one
less drift risk between the Tauri and web-app /system-info shapes.
2. Replace the OS-level symlinks under src/web-app/ (src, assets,
styles.css → ../app/) with webpack module resolution. The webpack
config's `entry` and HtmlWebpackPlugin `template` now point at
../app/src/... directly, and BuildWebApp.cmake stages both src/app/
and src/web-app/ side-by-side under build/web-app-staging/{app,web-app}/
so the relative paths resolve at build time. Removes the Windows
checkout hazard (symlinks needed core.symlinks=true + developer mode);
the staging step still uses cp -rL so the legitimate
src/app/assets/favicon.ico → docs/assets/favicon.ico symlink (icon
dedupe with the docs site) gets dereferenced.
3. Document the architectural invariants the previous review missed:
per-client local settings (AppImage is a remote client),
Debian-native-packaging constraint on src/web-app/package.json, and
the on-demand desktop app vs always-on lemond split on Windows. Added
to AGENTS.md as invariants 11–13 and explained in the affected
READMEs.
Verified: cargo test (7/7 pass), web-app build, Tauri renderer build,
and full Tauri release build all green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Four developer-experience polish items + one bonus PATH fix caught
during verification:
1. setup.sh / setup.ps1 now offer to install Rust via rustup with a
y/N confirmation prompt (CI gets the auto-install path). On success
the installer's environment is sourced into the script's own process
so the cmake configure step at the end can use cargo. A final-banner
reminder tells the user how to source ~/.cargo/env in their existing
shells. Removes the "now go install Rust manually" extra step a new
developer used to hit between setup.sh and the cmake build.
2. setup.sh now installs Tauri Linux deps for pacman (Arch) and zypper
(openSUSE Tumbleweed) in addition to the existing apt/dnf branches.
Closes the Arch/SUSE regression where the script used to silently
skip these distros and the developer would hit a confusing
webkit2gtk-not-found error at build time. The Tauri docs' openSUSE
recommendation is stale (it points at the 3.x/libsoup2 family); we
use the modern 4.1 / libsoup3 names that match what Tauri v2 actually
needs.
3. docs/dev-getting-started.md now documents `cd src/app && npm run dev`
as the fast iteration path for UI work, alongside a heads-up about
the multi-minute first-build cost. Both notes are in the
"Building the Tauri Desktop App" section so a developer reading the
onboarding doc top-to-bottom encounters them at the right moment.
4. The cmake tauri-app target now prints a heads-up banner before
running npm/cargo, explaining what's about to happen and pointing at
`npm run dev` for faster iteration. Removes the "is this hung?"
anxiety on the first build.
5. (Bonus) The cmake tauri-app target now injects ${CARGO_EXECUTABLE}'s
directory into PATH for the npm subprocess via `cmake -E env`. This
was a real DX gap caught while verifying (4): a developer running
cmake from a shell that hadn't sourced ~/.cargo/env (or any tooling
that spawns cmake without inheriting the cargo path) would hit a
confusing `cargo metadata: No such file or directory` error from
inside `tauri build`. Now the cmake target works regardless of the
parent shell's PATH because find_program(CARGO_EXECUTABLE) already
located cargo at configure time.
Verified: full pipeline (web-app build, cargo test, Tauri renderer
build, Tauri release binary) green. Build also succeeds with the
parent shell PATH stripped to /usr/local/bin:/usr/bin:/bin to confirm
the cmake-injected PATH does the right thing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two regressions caught by CI run 24288510941 after the previous two commits: 1. macOS Tauri build (job 70921824454) failed compiling src/app/src-tauri/src/webview_shim.rs with E0599: no function `alloc` found for `WKUserScript`. The compiler hint pointed at the missing `MainThreadOnly` trait import. The same code compiled fine 8 hours ago on the same Cargo.lock — this is a Rust toolchain tightening of trait-in-scope dispatch rules that was rolled out between the two CI runs. Fix: import `objc2::top_level_traits:: MainThreadOnly` inside the `#[cfg(target_os = "macos")]` block, using the exact path rustc suggests in its E0599 help message so we don't depend on which paths objc2 chooses to re-export at the crate root. 2. Linux .deb build (job 70921824452) failed at install time with `file INSTALL cannot find "src/web-app/assets/logo.svg"` — CMakeLists.txt:996 had a stale install rule pointing at the web-app symlink we deleted in fd5d10b (replace web-app symlinks with webpack module resolution). The shared logo asset lives at src/app/assets/logo.svg now; updating the install rule accordingly. Both fixes are minimal and surgical. Local builds verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two follow-up fixes for the previous CI failures (run 24288815598): 1. macOS Tauri build: my previous fix used `objc2::top_level_traits:: MainThreadOnly` based on rustc's E0599 hint, but `top_level_traits` is a private module (`mod top_level_traits;` without `pub`), giving an E0603. Use the public re-export at the crate root, `objc2::MainThreadOnly`, which is the correct path in objc2 0.6.x (verified against docs.rs/objc2/0.6.4). The first failed run's rustc hint pointed at the private path; the second run's hint pointed at the correct public path. 2. Windows MSI build: the cmake `-E env "PATH=..."` wrapper I added in the previous DX commit broke MSBuild's custom-build cmd-file generator with `The system cannot find the batch label specified - VCEnd`. This is the same MSBuild quirk that commit 2b515ce already had to dance around — nesting `cmake -E env` then `cmake -E chdir` then `cmd /c npm.cmd run` produces a command shape MSBuild can't compile into a .cmd file. Gate the env wrapper on `NOT WIN32`. Windows still works because cargo is on the parent shell PATH (CI via dtolnay/rust-toolchain, local devs via rustup-init.exe's user PATH update), which was the assumption the original cmake target made before my DX commit. Local Linux build verified with stripped PATH (no ~/.cargo/bin) — the non-Windows env wrapper still does its job. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI run 24289078833 (linux_distro_builds 🐧) caught two failures my previous DX commit (2c5c7b6) introduced — the desktop-app convenience made the C++-only setup flow more brittle: 1. Debian (debian:trixie container): the minimal image doesn't ship curl. setup.sh's Rust install ran BEFORE the apt batch install that would have installed curl, hit `command_exists curl` -> exit 1, and aborted the whole script before the C++ build could run. The linux_distro_builds workflow only builds the C++ server, so it shouldn't have been touching Rust at all. 2. openSUSE Tumbleweed: my guessed package names (`webkit2gtk-4_1-devel`, `libsoup-3_0-devel`, `libjavascriptcoregtk-4_1-devel`) don't exist in any Tumbleweed repo. zypper reported "No provider of <name> found" and exited 104. Because I'd added them to the mandatory `missing_packages` batch, that single failure killed the whole zypper install. Architectural fix: Tauri Linux deps and the Rust toolchain are NOT needed for the C++ server build, so they shouldn't be part of the mandatory setup batch. Restructure both setup.sh and setup.ps1 to mirror the existing optional tray-deps pattern: - Detect missing Tauri deps + missing Rust into separate variables (NOT added to missing_packages). - After the mandatory C++ deps batch installs, prompt the user (y/N for local devs, skip in CI by default, opt in via LEMONADE_SETUP_TAURI=1) for the optional Tauri install. - A failure to install Tauri deps or Rust prints a warning but does NOT abort the script — the C++ build still proceeds. - Curl is now guaranteed available before the Rust install runs because the mandatory batch above will have installed it on distros where it wasn't pre-installed. - The openSUSE branch is intentionally empty for now: the modern 4.1/libsoup3 package names are not yet confirmed in Tumbleweed, and the Tauri-docs-recommended `webkit2gtk3-devel` is the older 3.x family that Tauri v2 cannot use. openSUSE users who want the desktop app get a hint pointing at the Tauri prerequisites doc. Also: pass MainThreadMarker to WKUserScript::alloc on macOS. CI run 24289078840 surfaced this as E0061 ("this function takes 1 argument but 0 arguments were supplied") after the previous fix unblocked the trait import. objc2 0.6.4's signature is: fn alloc(mtm: MainThreadMarker) -> Allocated<Self> Fix: import `objc2::MainThreadMarker`, obtain one via the safe constructor `MainThreadMarker::new()` (returns Some iff currently on the main thread), and pass it. install_platform_shim is invoked from Tauri's webview setup hook which always runs on the main thread on macOS, so the expect() documents an invariant rather than guarding against a realistic failure. MainThreadMarker is re-exported at the objc2 crate root in 0.6.x (verified against docs.rs/objc2/0.6.4). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… doc Three follow-ups from review of #1611: 1. The renderer's `discoverServerPort()` invoke handler used to spin up a second `UdpSocket::bind` on port 13305, which fails with EADDRINUSE because the background `run_beacon_listener` already owns that socket for the process lifetime. Every call logged a bind error and silently fell back to the cached value. Delete the dead path entirely and have the command just read `beacon::get_cached_port()` (the listener is the single source of truth and already emits `server-port-updated` on real changes — re-emitting from the command was spamming subscribers with no-op updates). 2. tauriShim.ts `on()` had an unmount-before-`listen()`-resolves race where the returned cleanup fired with `unlisten` still null, leaking the underlying subscription once the promise eventually resolved. Add a `cancelled` flag and run the unlisten immediately when the promise resolves into a cancelled subscription. 3. src/app/README.md still listed `system_info.rs` (deleted in this PR tree) and missing several new modules; the architecture diagram still said the renderer was shared with src/web-app/ "via symlink" (also removed in this PR). Bring both in sync with the actual layout. Local validation: cargo test --quiet # 7 passed cargo build --release # clean cmake --build --preset default --target web-app # clean (tauri-stub still works) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Six fixes from hands-on Linux AppImage and Windows testing:
1. **"Failed to save settings" (Linux + Windows).** The Rust struct's
`#[serde(rename_all = "camelCase")]` serialized `base_url` as
`baseUrl`, `enable_tts` as `enableTts`, etc., but the renderer's
TypeScript types use uppercase acronyms (`baseURL`, `enableTTS`,
`enableUserTTS`). On the post-save round-trip, `mergeWithDefaultSettings`
walks `Object.keys(rawTTS)` and crashes on `defaults.tts['enableTts']`
(undefined) — caught and reported as a generic "failed to save"
alert. Fix: pin explicit `#[serde(rename = "...")]` on the three
mismatched fields. Also replaced the stale `is_marketplace_visible:
bool` layout field with `left_panel_view: String` to match the
renderer's `leftPanelView` union. Added regression tests.
2. **Titlebar drag broken (Linux).** webkit2gtk does not honor Chromium's
`-webkit-app-region: drag` CSS. Added `data-tauri-drag-region` to the
title bar wrapper and `data-tauri-drag-region="false"` on interactive
children (menus, buttons).
3. **Window resize broken (Linux).** Frameless windows on webkit2gtk get
no edge resize handles from the OS. Added 8 invisible CSS regions on
each edge/corner of the window that call `startResizeDragging(direction)`
on mousedown. Added `core:window:allow-start-dragging` and
`core:window:allow-start-resize-dragging` capabilities.
4. **LLM chat streaming ("No content received" / only first token,
Linux).** webkit2gtk delivers fetch ReadableStream chunks with
different boundaries than Chromium. A `data: {...}` SSE payload split
across two reads lost its second half because the current code didn't
buffer partial lines. Added a `lineBuffer` that accumulates text
across reads and only processes complete newline-terminated lines.
5. **WebKit style drift (Linux).** Status-indicator circles were rendered
via a Unicode ● character whose glyph size/baseline differs in WebKit.
Replaced with a CSS circle (`width/height + border-radius: 50% +
background-color`). Image Generator `<select>` elements were missing
`appearance: none`, so webkit2gtk drew the native OS select chrome
over the dark-theme styling. Added `-webkit-appearance: none` and a
custom SVG dropdown arrow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves merge conflicts to keep both the light theme system (CSS variables, theme switcher, dark/light selectors) from main and the WebKit compatibility fixes (user-select, resize handles, CSS-circle status indicators, custom dropdown arrows) from tauri. Status indicator colors updated to use CSS variables for theme awareness. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
I just tested the current PR with my usual workflow. What I noticed is:
-
I had to use
-D BUILD_TAURI_APP=OFFto build lemonade-server otherwise I got the error:CMake Error: File /home/gso/aur/lemonade-server-git/src/lemonade-server/cmake/run_tauri_with_version.cmake.in does not exist. -
When I try to build the desktop app, it tries to also build the AppImage, then fails with:
Error failed to bundle project failed to run linuxdeploy
I used the following commands to build in Arch Linux, maybe they are wrong:
npm ci
cargo install tauri-cli
npm run build:renderer:prod
cargo tauri build
- It also managed to build the desktop app (7.9MB binary) so I can run it fine on my PC.
sofiageo
left a comment
There was a problem hiding this comment.
As Jeremy said I must have been unlucky when I tested the branch. It works for me now, but it takes 2 minutes and 40 seconds to build the desktop app for the stable rust cargo.
I can try to test the nightly rust toolchain to see if it improves compilation times but I don't think it's realistic to expect all AUR users to have the nightly rust. With that in mind, let's merge it and we figure out how to make the AUR builds faster later.
|
Test drove the app on all common use cases across modalities and the experience was identical to the electron app. |
Replaces the Electron 39 desktop app with a Tauri v2 host. The React renderer is unchanged.
Release artifact sizes
Three release artifacts contain a desktop app and shrink; everything else (server debs, rpms, embeddables, minimal MSI) is unchanged. Numbers from runs 24272200505 (main) and 24289704993 (this PR).
lemonade.msi(Windows full)Lemonade-Darwin.pkg(macOS signed)lemonade-app-x86_64.AppImagelemonade-server-minimal.msi(no desktop app, reference)Tauri uses the OS-native webview (WebView2 / WKWebView / webkit2gtk) instead of bundling Chromium. AppImage shrinks the least because it still has to bundle webkit2gtk to stay portable across Linux distros.
Dependency footprint
src/app/package-lock.jsonsrc/app/src-tauri/Cargo.lockThe npm drop is the entire
electron/electron-builderfamily. The Rust crates are build-time only and never enter the supply chain we ship to users.Implementation
The old 1,100-line
main.jsbecomes a Rust crate atsrc/app/src-tauri/with the same responsibilities: window controls, settings file at~/.cache/lemonade/app_settings.json, UDP beacon discovery,lemonade://deep links, and per-platform webview hooks (mic permission, external-link routing). The React renderer reaches it through a TypeScript shim (tauriShim.ts) that installswindow.apiagainst Tauri commands, so the same bundle still runs unchanged in the browser-served/apppage.lemondis untouched.First cold cargo build compiles ~80 crates with LTO; CI caches via
Swatinem/rust-cache@v2and renderer-only iteration runscd src/app && npm run devto skip cargo. The Tauri target is opt-in (LEMONADE_SETUP_TAURI=1) so distro CI jobs that don't need it aren't slowed down.