From 38a0aeb7c0a98cfe35b25f22fe46c195c6a71dd5 Mon Sep 17 00:00:00 2001 From: Nikish Date: Sat, 8 Aug 2026 01:44:59 +0530 Subject: [PATCH] feat(linux): add .deb + AppImage builds, Linux CI, and sidecar resource_dir fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add native Linux packaging to the release pipeline (currently macOS + Windows only). Verified end-to-end: builds a working OpenWorker_0.1.7_amd64.deb (79M) and OpenWorker_0.1.7_amd64.AppImage (157M); backend suite 1114 passed / 1 skipped, GUI unit 108 passed, cargo warning-clean. Supersedes #173 (same goal, approved but stalled). Two correctness gaps in #173 that this addresses: 1. Runtime sidecar resolution — #173 bundles but does NOT fix server_bin(), so on a .deb install the app cannot find its sidecar: macOS/Windows resolve it next to the binary, but a .deb puts the binary in /usr/bin and resources under /usr/lib/OpenWorker/ (Tauri's resource_dir()). Without the fix the build is green but the app hangs on "Starting coworker…". Fixed: server_bin(app) resolves via app.path().resource_dir() on Linux (#[cfg(target_os = "linux")]); macOS/Windows paths unchanged. Verified against the deb's contents (sidecar at usr/lib/OpenWorker/sidecar/). 2. Build deps — #173 omits libasound2-dev (cpal/ALSA) and libclang-dev (whisper-rs bindgen), so the STT sidecar crate fails to compile in CI, and uses the deprecated libappindicator3-dev. This uses the ayatana successor and includes the STT build deps. Changes: - packaging/build_linux.sh: PyInstaller sidecar -> stage at binaries/sidecar -> tauri build --bundles deb,appimage. Unsigned; BUNDLES= overrides targets. - packaging/install_linux_deps.sh: distro-aware installer (Debian/Fedora/Arch) for the Tauri webkit GUI + ocw-stt (cpal/ALSA, whisper.cpp, libclang) libs. - src-tauri/src/lib.rs: server_bin() takes AppHandle and resolves the sidecar via resource_dir() on Linux; silence an unused_mut on non-macOS (the window builder is mut only inside the macOS title-bar cfg block). - .github/workflows/release.yml: ubuntu-22.04 matrix leg (stable glibc for broad compat), Linux deps step, build step, deb/AppImage staging, and a regression guard that fails CI if the deb ships without the sidecar at usr/lib/OpenWorker/sidecar/ (prevents the #173 bug class recurring). - packaging/make_update_manifest.py: register OpenWorker-linux-amd64.deb as the linux-x86_64 updater target. - README: "Build a Linux package" section + packaging table entry. Notes: - Voice Input is marked unsupported on Linux for now (the STT crate compiles on Linux, but the shell's compatibility gate is conservative by design); wiring a Linux audio backend is a natural follow-up, kept out of scope here. - Built on ubuntu-22.04 (glibc 2.35) so the artifacts run on 22.04+ and most derivatives. aarch64 is a follow-up (#218 covers Linux arm64 test CI). - Unsigned: Linux has no universal code-signing scheme; auto-update artifacts emit only when TAURI_SIGNING_PRIVATE_KEY is set, same as the other targets. Refs: #173 (supersedes), #19, #78, #218. Demand: #232 #171 #117 #414 #80. --- .github/workflows/release.yml | 52 ++++++++++++++++- README.md | 30 +++++++++- packaging/build_linux.sh | 94 +++++++++++++++++++++++++++++++ packaging/install_linux_deps.sh | 79 ++++++++++++++++++++++++++ packaging/make_update_manifest.py | 2 + surfaces/gui/src-tauri/src/lib.rs | 25 ++++++-- 6 files changed, 275 insertions(+), 7 deletions(-) create mode 100755 packaging/build_linux.sh create mode 100755 packaging/install_linux_deps.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b3e96d2..c6e3bf0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,10 +52,27 @@ jobs: slug: macos-x64 - os: windows-latest slug: windows + # Linux: built on ubuntu-22.04 (glibc 2.35) so the .deb/AppImage run on 22.04+ and + # most derivatives. webkit2gtk-4.1 + the STT sidecar's ALSA/whisper.cpp needs are + # installed in the dedicated deps step below. + - os: ubuntu-22.04 + slug: linux runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 + - name: Install Linux build dependencies (Tauri + STT sidecar) + # Tauri 2 webkit GUI + the ocw-stt crate (cpal→ALSA, whisper.cpp→C++). RPM targets + # would need rpmbuild; we ship deb + AppImage by default (see build_linux.sh). + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential pkg-config file curl \ + libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev \ + librsvg2-dev patchelf libasound2-dev \ + libclang-dev + - uses: actions/setup-node@v4 with: node-version: 20 @@ -122,6 +139,15 @@ jobs: TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: ./packaging/build_windows.ps1 + - name: Build .deb + AppImage (Linux) + if: runner.os == 'Linux' + env: + # Updater signing key (minisign, same secret as the other targets). Absent on + # fork/scratch runs → build_linux.sh skips updater artifacts with a warning. + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + run: bash packaging/build_linux.sh + - name: Stage artifacts (versioned + stable names) run: | mkdir -p out @@ -135,7 +161,7 @@ jobs: # secret is configured). The .sig signs CONTENT, so the stable rename is safe. SIG=$(ls "$BUNDLE"/nsis/*.exe.sig 2>/dev/null | head -1 || true) [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-windows-setup.exe.sig - else + elif [ "$RUNNER_OS" = "macOS" ]; then cp "$BUNDLE"/dmg/*.dmg out/ cp "$BUNDLE"/dmg/*.dmg out/OpenWorker-${{ matrix.slug }}.dmg # macOS updater artifact: the signed .app tarball the installed app swaps in. @@ -143,6 +169,30 @@ jobs: cp "$BUNDLE"/macos/OpenWorker.app.tar.gz out/OpenWorker-${{ matrix.slug }}.app.tar.gz cp "$BUNDLE"/macos/OpenWorker.app.tar.gz.sig out/OpenWorker-${{ matrix.slug }}.app.tar.gz.sig fi + else + # Linux: deb + AppImage (AppImage is optional — build_linux.sh emits it when + # linuxdeploy fetches cleanly; the deb always ships). Stable names keep the + # website's releases/latest/download links working as the version advances. + cp "$BUNDLE"/deb/*.deb out/ + cp "$BUNDLE"/deb/*.deb out/OpenWorker-linux-amd64.deb + if ls "$BUNDLE"/appimage/*.AppImage >/dev/null 2>&1; then + cp "$BUNDLE"/appimage/*.AppImage out/ + cp "$BUNDLE"/appimage/*.AppImage out/OpenWorker-linux-amd64.AppImage + fi + # Updater signature for the .deb (present only when the updater key secret is set). + SIG=$(ls "$BUNDLE"/deb/*.deb.sig 2>/dev/null | head -1 || true) + [ -n "$SIG" ] && cp "$SIG" out/OpenWorker-linux-amd64.deb.sig + # Regression guard: the deb MUST ship the sidecar at usr/lib/OpenWorker/sidecar/ — + # that is the resource_dir() path server_bin() resolves on Linux (src-tauri/src/lib.rs). + # A green build without this entry is a non-functional app (the macOS/Windows "next to + # the exe" resolution does NOT hold on a deb, where the binary is /usr/bin and + # resources live under /usr/lib). Fail loudly rather than ship a broken bundle. + # Dump to a file then grep it: `dpkg-deb -c | grep` breaks the pipe under pipefail + # (grep exits on first match → dpkg-deb gets SIGPIPE → 141), which would false-fail CI. + DEB=$(ls "$BUNDLE"/deb/*.deb | head -1) + dpkg-deb -c "$DEB" > /tmp/openworker-deb-contents.txt + grep -q 'usr/lib/OpenWorker/sidecar/openworker-server' /tmp/openworker-deb-contents.txt \ + || { echo "::error::deb is missing the sidecar at usr/lib/OpenWorker/sidecar/ — the app would not start"; exit 1; } fi ls -la out diff --git a/README.md b/README.md index b32a299d..e6c143f8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ It runs on your machine and doesn't lock you into any model: bring your own API [**⬇ Windows 10/11 (x64)**](https://download.openworker.com/windows) builds are not yet code-signed, so SmartScreen will warn; signing is in progress +**Linux (x86_64)** — `.deb` + AppImage are buildable from source; see [Build a Linux package](#build-a-linux-package-deb--appimage) below. Signed release downloads are coming. + Open the app, add a model key (or point it at Ollama), and ask for something real. ## How it works @@ -91,7 +93,31 @@ desktop app uses an in-memory launch token instead and never writes it to disk. To run the full desktop app instead of the browser UI, replace step 3 with `npm run tauri dev` (from `surfaces/gui/`) - the Tauri shell launches the window and supervises the server itself. -Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gui` (GUI unit + hermetic end-to-end). Desktop bundles are built with `packaging/build_dmg.sh` / `packaging/build_windows.ps1`. +Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gui` (GUI unit + hermetic end-to-end). Desktop bundles are built with `packaging/build_dmg.sh` / `packaging/build_windows.ps1` / `packaging/build_linux.sh`. + +## Build a Linux package (.deb + AppImage) + +Produces a `.deb` (and an `.AppImage` when `linuxdeploy` fetches cleanly) for x86_64, built on `ubuntu-22.04` for broad glibc compatibility. + +```shell +# 1. One-time: install the Tauri + STT-sidecar system libs (Debian/Ubuntu/Fedora/Arch aware) +bash packaging/install_linux_deps.sh # add -y to skip the confirm prompt + +# 2. One-time: create the build venv the bundler expects at .venv +python3 -m venv .venv +.venv/bin/pip install -e '.[bedrock]' pyinstaller typer + +# 3. Build the frontend + Rust shell + PyInstaller sidecar, then bundle +cd surfaces/gui && npm ci && cd ../.. +./packaging/build_linux.sh +``` + +Output lands in `surfaces/gui/src-tauri/target/release/bundle/{deb,appimage}/`. Override the targets with `BUNDLES=` (e.g. `BUNDLES=deb` or `BUNDLES=rpm,appimage` — `rpm` needs `rpmbuild` installed). + +Notes: +- The build is **unsigned**; Linux has no universal code-signing scheme. Verify release artifacts via the GitHub Release's GPG/signature if you need integrity guarantees. +- **Voice Input** is not yet wired up on Linux — the STT crate compiles (cpal/whisper.cpp), but the shell marks dictation unsupported on Linux for now. Wiring a Linux audio backend is a natural follow-up. +- **Auto-update** emits signed updater artifacts only when `TAURI_SIGNING_PRIVATE_KEY` is set; without it the build omits them (the app still runs, just no self-update). ## Repository layout @@ -100,7 +126,7 @@ Tests: `.venv/bin/pytest` (server), `npm test` and `npm run e2e` in `surfaces/gu | `coworker/` | Python backend - agent engine, model providers, connectors, MCP client, memory, automations | | `surfaces/gui/` | Desktop app - React UI + Tauri shell that supervises the server | | `stt/` | Speech-to-text sidecar (Rust) for voice input | -| `packaging/` | Installer builds (macOS DMG, Windows), auto-update manifest, dev bootstrap | +| `packaging/` | Installer builds (macOS DMG, Windows, Linux deb/AppImage), auto-update manifest, dev bootstrap | | `docs/` | Design specs and decision logs | | `tests/` | Backend test suite | diff --git a/packaging/build_linux.sh b/packaging/build_linux.sh new file mode 100755 index 00000000..5a4cdacc --- /dev/null +++ b/packaging/build_linux.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Build the OpenWorker Linux desktop app: a .deb and an AppImage. +# +# 1. PyInstaller-bundle the server into a standalone onedir folder (no venv at runtime). +# 2. Stage it at binaries/sidecar/ for Tauri's `resources` slot. +# 3. `tauri build --bundles deb,appimage` → installable packages (resources copied in). +# +# The Linux counterpart to build_dmg.sh / build_windows.ps1. It is UNSIGNED — Linux has no +# universal code-signing scheme; distro repos / AppArmor / GPG-signing the release artifacts +# are downstream concerns. +# +# Prerequisites (mirrors the macOS/Windows scripts' headers): +# - Rust (rustup) + Node/npm, and the GUI deps installed (`npm ci` in surfaces/gui). +# - Tauri Linux system libraries (Debian/Ubuntu names): +# sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev \ +# librsvg2-dev patchelf build-essential pkg-config file libclang-dev +# (Fedora: webkit2gtk4.1-devel libayatana-appindicator-devel librsvg2-devel patchelf clang-devel; +# Arch: webkit2gtk-4.1 libayatana-appindicator librsvg patchelf clang) +# The STT sidecar (ocw-stt) uses cpal → ALSA (libasound2-dev) and whisper.cpp needs a C/C++ +# compiler (build-essential / gcc-c++) plus libclang-dev for bindgen at build time. +# - A Python venv at .venv (repo root) with this package installed editable, plus the +# build-only deps: +# python3 -m venv .venv +# .venv/bin/pip install -e '.[bedrock]' pyinstaller typer +# `typer` is needed only at BUILD time: PyInstaller walks the `mcp` package and `mcp.cli` +# calls sys.exit() at import if typer is absent, which aborts the freeze. +# (aisuite installs like any other dependency — git-pinned in pyproject.toml.) +# - AppImage: Tauri fetches its own `linuxdeploy`/`appimagetool` at build time, so a network +# connection is required for the appimage target. Running an AppImage needs `libfuse2`. +# +# Experimental (use-at-your-own-risk) connectors are EXCLUDED from this build by default — the +# spec strips coworker.connectors.experimental. Self-builders can opt in with: +# COWORKER_EXPERIMENTAL=1 ./build_linux.sh +# +# Override the bundles with BUNDLES= (e.g. BUNDLES=deb or BUNDLES=rpm,appimage). `rpm` requires +# `rpm`/`rpmbuild` to be installed; Tauri only emits .rpm when it is requested explicitly. +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +PLATFORM="$(cd "$HERE/.." && pwd)" +GUI="$PLATFORM/surfaces/gui" +APP="OpenWorker" +# Single source of truth for the version: tauri.conf.json (also stamps the bundle). +VERSION="$(node -p "require('$GUI/src-tauri/tauri.conf.json').version")" +TRIPLE="$(rustc -vV | sed -n 's/host: //p')" # e.g. x86_64-unknown-linux-gnu +ARCH="${TRIPLE%%-*}" +BUNDLES="${BUNDLES:-deb,appimage}" + +# A running openworker-server (e.g. a prior dev sidecar) can hold a write lock on the PyInstaller +# output and make the overwrite fail. Best-effort: stop any before bundling. +if pgrep -x openworker-server >/dev/null 2>&1; then + echo "==> stopping running openworker-server process(es) holding the output exe" + pkill -x openworker-server || true + sleep 1 +fi + +echo "==> [1/3] PyInstaller: bundling openworker-server ($TRIPLE)" +"$PLATFORM/.venv/bin/pyinstaller" --noconfirm --clean \ + --distpath "$HERE/dist" --workpath "$HERE/build" "$HERE/openworker-server.spec" + +echo "==> [2/3] staging sidecar resources" +# Onedir bundle (exe + _internal/) ships via Tauri `resources`, landing under the install +# resource dir (e.g. /usr/lib//sidecar/ on a deb). rm -rf first: a dev-convenience +# symlink here once clobbered another worktree's venv; also clears stale pre-onedir onefile bins. +mkdir -p "$GUI/src-tauri/binaries" +rm -rf "$GUI/src-tauri/binaries/sidecar" "$GUI/src-tauri/binaries/openworker-server-$TRIPLE" +cp -RL "$HERE/dist/openworker-server" "$GUI/src-tauri/binaries/sidecar" +chmod +x "$GUI/src-tauri/binaries/sidecar/openworker-server" + +echo "==> [3/3] tauri build (--bundles $BUNDLES)" +# Auto-update artifacts (.deb/.AppImage + minisign .sig): produced only when the updater +# signing key is available — from the env (CI secret TAURI_SIGNING_PRIVATE_KEY), or from +# `.ocw-updater.env` one directory above the repo (same convention as the macOS script). +# Keyless builds skip the overlay entirely so dev/fork builds keep working; a keyless RELEASE +# would strand every install without auto-update, hence the loud warning. +UPDATER_ENV="${OCW_UPDATER_ENV:-$PLATFORM/../.ocw-updater.env}" +if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ] && [ -f "$UPDATER_ENV" ]; then + # shellcheck disable=SC1090 + source "$UPDATER_ENV" +fi +UPDATER_OVERLAY=() +if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then + UPDATER_OVERLAY=(--config '{"bundle":{"createUpdaterArtifacts":true}}') +else + echo " WARNING: no updater signing key — building WITHOUT auto-update artifacts (not releasable)." +fi +# ${arr[@]+…} guard: plain "${arr[@]}" on an EMPTY array is "unbound variable" under set -u. +( cd "$GUI" && npm run tauri build -- --bundles "$BUNDLES" ${UPDATER_OVERLAY[@]+"${UPDATER_OVERLAY[@]}"} ) + +BUNDLE="$GUI/src-tauri/target/release/bundle" +echo "" +echo "Done. Bundles under: $BUNDLE" +# List whatever was actually produced (each target dir may or may not exist). +find "$BUNDLE" \( -name '*.deb' -o -name '*.rpm' -o -name '*.AppImage' \) -print 2>/dev/null || true diff --git a/packaging/install_linux_deps.sh b/packaging/install_linux_deps.sh new file mode 100755 index 00000000..4a5c5685 --- /dev/null +++ b/packaging/install_linux_deps.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +# Install the build-time system dependencies for OpenWorker on Linux. +# +# Two groups are needed to build the desktop app (packaging/build_linux.sh): +# 1. Tauri 2 shell + webkit GUI — libwebkit2gtk-4.1, appindicator, librsvg, patchelf, … +# 2. The STT sidecar crate (ocw-stt) — cpal needs ALSA, whisper.cpp needs a C/C++ compiler. +# +# Detects the distro from /etc/os-release and uses the right package names. Run once, then +# create the Python venv per the header of build_linux.sh. Requires sudo for the install; +# pass -y to auto-confirm the apt/distro prompt. +set -euo pipefail + +ASSUME_YES=0 +for a in "$@"; do + case "$a" in -y|--yes) ASSUME_YES=1 ;; *) echo "unknown arg: $a" >&2; exit 2 ;; esac +done + +if [ ! -r /etc/os-release ]; then + echo "Cannot detect distro: /etc/os-release missing. Install the deps listed in build_linux.sh by hand." >&2 + exit 1 +fi +# shellcheck disable=SC1091 +. /etc/os-release +DISTRO="${ID:-}" +# Family detection — handle derivatives (e.g. Linux Mint → id=linuxmint, ID_LIKE=debian). +FAMILY="" +case "$DISTRO" in + debian|ubuntu|linuxmint|pop) FAMILY=debian ;; + fedora|rhel|rocky|almalinux|centos) FAMILY=fedora ;; + arch|manjaro| EndeavourOS) FAMILY=arch ;; +esac +if [ -z "$FAMILY" ]; then + for like in ${ID_LIKE:-}; do + case "$like" in debian|ubuntu) FAMILY=debian ;; fedora|rhel) FAMILY=fedora ;; arch) FAMILY=arch ;; esac + [ -n "$FAMILY" ] && break + done +fi +[ -n "$FAMILY" ] || { echo "Unsupported distro: '$DISTRO'. See build_linux.sh for the manual package list." >&2; exit 1; } + +CONFIRM="" +[ "$ASSUME_YES" = 1 ] && case "$FAMILY" in debian) CONFIRM=-y ;; fedora) CONFIRM="-y" ;; arch) CONFIRM="--noconfirm" ;; esac + +echo "==> detected: $DISTRO ($FAMILY)" +case "$FAMILY" in + debian) + PACKAGES=( + build-essential pkg-config file curl + libwebkit2gtk-4.1-dev libssl-dev libayatana-appindicator3-dev + librsvg2-dev patchelf libasound2-dev + # Build-time only: whisper-rs-sys (the STT sidecar) runs bindgen, which needs + # libclang to parse whisper.cpp's headers. Not a runtime dependency. + libclang-dev + ) + echo "==> apt-get install: ${PACKAGES[*]}" + sudo apt-get update + sudo apt-get install -y "${PACKAGES[@]}" + ;; + fedora) + PACKAGES=( + gcc-c++ pkgconfig file curl patchelf clang-devel + webkit2gtk4.1-devel openssl-devel libayatana-appindicator-devel + librsvg2-devel alsa-lib-devel + ) + echo "==> dnf install: ${PACKAGES[*]}" + sudo dnf install -y "${PACKAGES[@]}" + ;; + arch) + PACKAGES=(base-devel pkgconf file curl patchelf webkit2gtk-4.1 libayatana-appindicator librsvg alsa-lib clang) + echo "==> pacman -S: ${PACKAGES[*]}" + sudo pacman -S --noconfirm "${PACKAGES[@]}" + ;; +esac + +echo "" +echo "Done. Next: create the build venv and build —" +echo " python3 -m venv .venv" +echo " .venv/bin/pip install -e '.[bedrock]' pyinstaller typer" +echo " cd surfaces/gui && npm ci && cd ../.." +echo " ./packaging/build_linux.sh" diff --git a/packaging/make_update_manifest.py b/packaging/make_update_manifest.py index 84c4f295..38d9733b 100644 --- a/packaging/make_update_manifest.py +++ b/packaging/make_update_manifest.py @@ -12,6 +12,7 @@ OpenWorker-macos-arm64.app.tar.gz(.sig) -> platforms["darwin-aarch64"] OpenWorker-macos-x64.app.tar.gz(.sig) -> platforms["darwin-x86_64"] OpenWorker-windows-setup.exe(.sig) -> platforms["windows-x86_64"] + OpenWorker-linux-amd64.deb(.sig) -> platforms["linux-x86_64"] URLs point at the TAG-pinned GitHub download path (releases/download//), never at `latest/` — a manifest must reference exactly the artifacts it shipped with, @@ -37,6 +38,7 @@ "OpenWorker-macos-arm64.app.tar.gz": "darwin-aarch64", "OpenWorker-macos-x64.app.tar.gz": "darwin-x86_64", "OpenWorker-windows-setup.exe": "windows-x86_64", + "OpenWorker-linux-amd64.deb": "linux-x86_64", } diff --git a/surfaces/gui/src-tauri/src/lib.rs b/surfaces/gui/src-tauri/src/lib.rs index 460021b4..724d90a8 100644 --- a/surfaces/gui/src-tauri/src/lib.rs +++ b/surfaces/gui/src-tauri/src/lib.rs @@ -50,13 +50,15 @@ fn launch_token() -> String { /// Path to the server entrypoint. Resolution order: /// 1. `COWORKER_SERVER_BIN` env override. /// 2. The bundled onedir sidecar shipped via Tauri `resources` (production): the -/// `sidecar/` folder lands in Contents/Resources on macOS and in the install dir -/// (next to the app exe) on Windows. +/// `sidecar/` folder lands in Contents/Resources on macOS, in the install dir +/// (next to the app exe) on Windows, and under Tauri's `resource_dir()` +/// (e.g. `/usr/lib//`) on Linux — NOT next to the binary like the +/// other two, so Linux resolves via the runtime path API instead of `current_exe`. /// 3. Legacy onefile slot: `openworker-server[.exe]` next to the app binary (pre-onedir /// builds used Tauri externalBin). /// 4. Dev fallback: the repo venv, relative to this crate (`src-tauri` → `platform/.venv`; /// `bin/` on POSIX, `Scripts\` on Windows). -fn server_bin() -> PathBuf { +fn server_bin(app: &tauri::AppHandle) -> PathBuf { if let Ok(p) = std::env::var("COWORKER_SERVER_BIN") { return PathBuf::from(p); } @@ -65,6 +67,18 @@ fn server_bin() -> PathBuf { } else { "openworker-server" }; + // Linux: .deb/.rpm/AppImage install resources under `resource_dir()`, not next to the + // exe (`current_exe` is /usr/bin/ on a deb, while the sidecar is in /usr/lib//). + // Resolving through Tauri's own path API stays correct across every Linux bundle format. + #[cfg(target_os = "linux")] + { + if let Ok(rd) = app.path().resource_dir() { + let cand = rd.join("sidecar").join(exe_name); + if cand.exists() { + return cand; + } + } + } if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { // macOS: Contents/MacOS/ → Contents/Resources/sidecar/; Windows: resources @@ -626,7 +640,7 @@ pub fn run() { ]) .setup(move |app| { // 1. Start the Python server sidecar on the chosen port (inherits our env). - let mut server_cmd = Command::new(server_bin()); + let mut server_cmd = Command::new(server_bin(app.handle())); server_cmd .args(["--host", "127.0.0.1", "--port", &port.to_string()]) // The sidecar self-exits if we die abruptly (dev-watcher restart, crash) — @@ -688,6 +702,9 @@ pub fn run() { // 2. Build the window, injecting the sidecar endpoints before the SPA loads. // Overlay title bar (macOS): traffic lights float over the edge-to-edge UI. + // `mut` is only used in the macOS title-bar block below; silence the unused-mut + // warning on Windows/Linux without changing macOS behavior. + #[cfg_attr(not(target_os = "macos"), allow(unused_mut))] let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into())) .title("OpenWorker")