From 1e025ba859dea92611ab3d9ea380f9ecce0ed8dd Mon Sep 17 00:00:00 2001 From: Luis Lobo Borobia Date: Tue, 25 Aug 2026 17:18:13 -0500 Subject: [PATCH] Add initial Linux desktop packaging support --- README.md | 12 +- packaging/build_linux.sh | 84 +++++++++++++ packaging/install_linux_desktop_deps.sh | 28 +++++ packaging/setup_dev_env.sh | 6 +- reports/linux-local-smoke-2026-08-25.md | 158 ++++++++++++++++++++++++ surfaces/gui/README.md | 13 ++ surfaces/gui/src-tauri/src/lib.rs | 3 + 7 files changed, 299 insertions(+), 5 deletions(-) create mode 100755 packaging/build_linux.sh create mode 100755 packaging/install_linux_desktop_deps.sh create mode 100644 reports/linux-local-smoke-2026-08-25.md diff --git a/README.md b/README.md index 60172b41d0..c83f11da79 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ OpenWorker is local-first. Everything lives on your machine: the agent loop, you Prerequisites: Python 3.10+, Node 20+, and (for the desktop shell) the Rust toolchain via [rustup](https://rustup.rs/). +Linux desktop builds also need WebKit/Tauri native packages. On Ubuntu 24.04: + +```shell +bash packaging/install_linux_desktop_deps.sh +``` + ```shell git clone https://github.com/andrewyng/openworker cd openworker @@ -111,9 +117,9 @@ The standalone server creates a per-launch token at For direct API calls, send its value in the `X-OpenWorker-Token` header. The 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. +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. On Linux, run `packaging/install_linux_desktop_deps.sh` first. -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`. ## Repository layout @@ -122,7 +128,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), 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 0000000000..5cbaa1f80f --- /dev/null +++ b/packaging/build_linux.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Build the Linux desktop app from source. +# +# Steps: +# 1. PyInstaller-bundle the Python server into a standalone onedir sidecar. +# 2. Stage that sidecar under the Tauri resources directory. +# 3. Run `tauri build` for Linux bundles. +# +# Prerequisites: +# - Ubuntu/Debian native packages from `packaging/install_linux_desktop_deps.sh`. +# - Rust via rustup. +# - Node/npm and GUI dependencies (`npm ci` or `npm install` in `surfaces/gui`). +# - The repo venv created by `packaging/setup_dev_env.sh`. +# +# Default bundles are `deb,appimage`. Override with: +# packaging/build_linux.sh rpm +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +GUI="$ROOT/surfaces/gui" +VENV="$ROOT/.venv" +BUNDLES="${1:-deb,appimage}" + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Required command '$1' not found on PATH." >&2 + exit 1 + fi +} + +require_pkg_config() { + if ! pkg-config --exists "$1"; then + echo "Missing pkg-config package '$1'. Run: packaging/install_linux_desktop_deps.sh" >&2 + exit 1 + fi +} + +require_cmd rustc +require_cmd npm +require_cmd pkg-config +require_pkg_config libsoup-3.0 +require_pkg_config webkit2gtk-4.1 + +if [ ! -x "$VENV/bin/pyinstaller" ]; then + echo "PyInstaller not found at $VENV/bin/pyinstaller. Run: bash packaging/setup_dev_env.sh" >&2 + exit 1 +fi + +TRIPLE="$(rustc -vV | sed -n 's/host: //p')" + +# Build the bundled whisper.cpp CPU backend for distribution, not for the +# build host. Some VMs expose AVX2 without FMA, which breaks ggml's AVX2 path, +# and native CPU flags can also produce binaries that fail on older Linux hosts. +export GGML_NATIVE="${GGML_NATIVE:-OFF}" +export GGML_AVX="${GGML_AVX:-OFF}" +export GGML_AVX2="${GGML_AVX2:-OFF}" +export GGML_AVX_VNNI="${GGML_AVX_VNNI:-OFF}" +export GGML_AVX512="${GGML_AVX512:-OFF}" +export GGML_AVX512_VBMI="${GGML_AVX512_VBMI:-OFF}" +export GGML_AVX512_VNNI="${GGML_AVX512_VNNI:-OFF}" +export GGML_AVX512_BF16="${GGML_AVX512_BF16:-OFF}" +export GGML_BMI2="${GGML_BMI2:-OFF}" +export GGML_F16C="${GGML_F16C:-OFF}" +export GGML_FMA="${GGML_FMA:-OFF}" +export GGML_SSE42="${GGML_SSE42:-OFF}" + +echo "==> [1/3] PyInstaller: bundling openworker-server ($TRIPLE)" +"$VENV/bin/pyinstaller" --noconfirm --clean \ + --distpath "$HERE/dist" --workpath "$HERE/build" "$HERE/openworker-server.spec" + +echo "==> [2/3] staging sidecar resources" +mkdir -p "$GUI/src-tauri/binaries" +rm -rf "$GUI/src-tauri/binaries/sidecar" "$GUI/src-tauri/binaries/openworker-server-$TRIPLE" +cp -R "$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)" +( cd "$GUI" && npm run tauri build -- --bundles "$BUNDLES" ) + +BUNDLE="$GUI/src-tauri/target/release/bundle" +echo +echo "Done. Linux bundles under: $BUNDLE" +find "$BUNDLE" -type f \( -name '*.deb' -o -name '*.AppImage' -o -name '*.rpm' \) -print 2>/dev/null || true diff --git a/packaging/install_linux_desktop_deps.sh b/packaging/install_linux_desktop_deps.sh new file mode 100755 index 0000000000..598edfa9b2 --- /dev/null +++ b/packaging/install_linux_desktop_deps.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install Ubuntu packages required to build/run the Tauri desktop shell. +# +# Tested on Ubuntu 24.04. The browser/dev UI does not need these native packages, +# but `npm run tauri dev` and `packaging/build_linux.sh` do. +set -euo pipefail + +if ! command -v apt-get >/dev/null 2>&1; then + echo "This helper supports apt-based Ubuntu/Debian systems only." >&2 + exit 1 +fi + +sudo apt-get update +sudo apt-get install -y \ + build-essential \ + cmake \ + curl \ + file \ + libasound2-dev \ + libayatana-appindicator3-dev \ + libclang-dev \ + librsvg2-dev \ + libsoup-3.0-dev \ + libssl-dev \ + libwebkit2gtk-4.1-dev \ + libxdo-dev \ + pkg-config \ + wget diff --git a/packaging/setup_dev_env.sh b/packaging/setup_dev_env.sh index 9e2eeaad5b..05bbe91fc7 100644 --- a/packaging/setup_dev_env.sh +++ b/packaging/setup_dev_env.sh @@ -11,11 +11,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" VENV="$ROOT/.venv" python3 -m venv "$VENV" -# The coworker package (server, engine, connectors) + inbound-messaging extras. +# The coworker package (server, engine, connectors) + extras needed by the +# documented local test/build flows. Keep this aligned with CI: a fresh venv +# should not rely on optional packages already installed on the machine. # aisuite comes in as a regular dependency (git-pinned in pyproject.toml until # the next PyPI release). "$VENV/bin/pip" install --quiet --upgrade pip -"$VENV/bin/pip" install --quiet -e "$ROOT[messaging,dev]" +"$VENV/bin/pip" install --quiet -e "$ROOT[messaging,dev,bedrock]" pyinstaller typer tzdata "$VENV/bin/python" -c 'import aisuite, coworker' # fail loudly if the wiring broke echo "Ready: $VENV" diff --git a/reports/linux-local-smoke-2026-08-25.md b/reports/linux-local-smoke-2026-08-25.md new file mode 100644 index 0000000000..5f9872af47 --- /dev/null +++ b/reports/linux-local-smoke-2026-08-25.md @@ -0,0 +1,158 @@ +# Linux Local Smoke Test - 2026-08-25 + +Environment: +- OS: Ubuntu 24.04.4 LTS (Noble) +- Python: 3.11.9 +- Node: 22.22.0 +- npm: 10.9.4 +- Rust: rustc 1.95.0-nightly + +## Result + +OpenWorker works locally on Linux for the Python server and browser GUI from source. +The initial Linux desktop `.deb` build also works from a fresh Ubuntu 24.04 VM +after installing the package prerequisites added in this PR. + +Verified: +- `bash packaging/setup_dev_env.sh` completed successfully. +- `npm install` completed successfully in `surfaces/gui`. +- Server started with `.venv/bin/openworker-server --cwd /home/luis.lobo@epicio.com/dev/luislobo/openworker --port 8765`. +- Server endpoints responded with valid JSON using the sidecar token: + - `/v1/health` + - `/v1/settings` + - `/v1/sessions` + - `/v1/personas` +- Browser UI started with `npm run dev -- --host 127.0.0.1` and served `http://127.0.0.1:1420/`. +- GUI production build passed with `npm run build`. +- GUI unit tests passed: 134 tests. +- GUI E2E tests passed on Linux Chromium: 221 tests. +- Backend tests passed after installing Bedrock optional dependencies: 1,861 passed, 1 skipped. +- Barebones Ubuntu 24.04 VM smoke passed from a fresh `.venv` and fresh + `node_modules`: + - `bash packaging/setup_dev_env.sh` + - `.venv/bin/pytest tests/test_config.py tests/test_environment.py tests/test_bedrock_provider.py -q` + - `npm install` + - `npm test -- --run` + - `npm run build` + - `packaging/build_linux.sh deb` +- VM Linux bundle produced: + `surfaces/gui/src-tauri/target/release/bundle/deb/OpenWorker_0.2.1_amd64.deb` +- Local `.deb` install passed with `sudo apt-get install -y ./surfaces/gui/src-tauri/target/release/bundle/deb/OpenWorker_0.2.1_amd64.deb`. +- Installed app launch passed from `/tmp`: `/usr/bin/openworker-desktop` + spawned `/usr/lib/OpenWorker/sidecar/openworker-server` and the GUI loaded + `/v1/health`, `/v1/settings`, `/v1/sessions`, `/v1/personas`, and websocket + endpoints successfully. + +## Issues Found + +### 1. README test command fails after the documented bootstrap + +The README says backend tests can be run with `.venv/bin/pytest`. After running the documented bootstrap (`bash packaging/setup_dev_env.sh`), the full suite failed only in `tests/test_bedrock_provider.py` because `boto3`/`botocore` were not installed. + +Repro: + +```shell +bash packaging/setup_dev_env.sh +.venv/bin/pytest -q +``` + +Observed failure class: + +```text +ModuleNotFoundError: No module named 'boto3' +ModuleNotFoundError: No module named 'botocore' +``` + +Workaround verified: + +```shell +.venv/bin/pip install -e '.[bedrock]' +.venv/bin/pytest tests/test_bedrock_provider.py -q +.venv/bin/pytest -q +``` + +PR fix: +- Include the `bedrock` extra in `packaging/setup_dev_env.sh` for developer/test environments. + +### 2. Tauri desktop build fails without Linux system packages + +`npm run tauri -- build` failed on this Ubuntu 24.04 machine because native Tauri/WebKit dependencies are not installed. + +Repro: + +```shell +cd surfaces/gui +npm run tauri -- build +``` + +Observed failure: + +```text +The system library `libsoup-3.0` required by crate `soup3-sys` was not found. +The file `libsoup-3.0.pc` needs to be installed and the PKG_CONFIG_PATH environment variable must contain its parent directory. +``` + +Confirmed missing: + +```shell +pkg-config --modversion libsoup-3.0 +pkg-config --modversion webkit2gtk-4.1 +``` + +Both commands failed on this machine. + +Ubuntu 24.04 package candidates: + +```shell +build-essential +cmake +curl +file +libasound2-dev +libsoup-3.0-dev +libwebkit2gtk-4.1-dev +libayatana-appindicator3-dev +libclang-dev +librsvg2-dev +libssl-dev +libxdo-dev +pkg-config +wget +``` + +PR fixes: +- Add Linux desktop prerequisites to the README. +- Add a Linux packaging/build script alongside `build_dmg.sh` and `build_windows.ps1`. +- Consider adding Linux artifacts to `packaging/make_update_manifest.py` if official Linux desktop releases are intended. + +### 3. Linux desktop build needs portable `whisper.cpp` CPU flags + +The clean Ubuntu 24.04 VM exposed `avx2` but not `fma` in `/proc/cpuinfo`. +The bundled `whisper.cpp` build enabled an AVX2 path that calls FMA intrinsics, +which failed during `whisper-rs-sys` compilation: + +```text +error: inlining failed in call to 'always_inline' '_mm256_fmadd_ps': target specific option mismatch +``` + +PR fix: +- `packaging/build_linux.sh` defaults the `GGML_*` CPU feature flags to a + portable distribution build. Builders can still override those environment + variables for host-optimized local packages. + +### 4. Installed Linux `.deb` app must resolve bundled sidecar under `/usr/lib` + +The initial local install launched, but when started from the source checkout it +fell back to the repo `.venv` sidecar. Starting from `/tmp` confirmed that the +Linux `.deb` executable lives at `/usr/bin/openworker-desktop` while Tauri +resources are installed under `/usr/lib/OpenWorker/sidecar`. + +PR fix: +- Add the Linux `.deb` resource path to the sidecar resolver before the dev + `.venv` fallback. + +## Known Linux Feature Limits From Source Inspection + +- Voice Input is explicitly unsupported on Linux in `surfaces/gui/src-tauri/src/lib.rs`. +- Keep-awake is a no-op on Linux in `surfaces/gui/src-tauri/src/lib.rs`. +- Official downloads/update manifest currently cover macOS and Windows, not Linux. diff --git a/surfaces/gui/README.md b/surfaces/gui/README.md index 5645320b20..fa7a09e36a 100644 --- a/surfaces/gui/README.md +++ b/surfaces/gui/README.md @@ -12,6 +12,13 @@ A fresh checkout has no server to run — create the venv both flows below expec bash packaging/setup_dev_env.sh # → .venv (server + aisuite) ``` +On Linux, install the native WebKit/Tauri packages before running the desktop +shell: + +```bash +bash packaging/install_linux_desktop_deps.sh +``` + ## Run it (browser, two terminals) 1. **Start the server** (needs a model key, e.g. `OPENAI_API_KEY`, in the environment — @@ -44,6 +51,12 @@ npm install # first time npm run tauri dev # builds the shell, launches the window, starts the server ``` +Linux release-style bundles can be built from the repo root with: + +```bash +packaging/build_linux.sh +``` + ## Tests ```bash diff --git a/surfaces/gui/src-tauri/src/lib.rs b/surfaces/gui/src-tauri/src/lib.rs index d96c3c6f4c..fcdf428949 100644 --- a/surfaces/gui/src-tauri/src/lib.rs +++ b/surfaces/gui/src-tauri/src/lib.rs @@ -193,6 +193,9 @@ fn server_bin() -> PathBuf { if let Some(contents) = dir.parent() { candidates.push(contents.join("Resources").join("sidecar").join(exe_name)); } + if cfg!(target_os = "linux") { + candidates.push(PathBuf::from("/usr/lib/OpenWorker/sidecar").join(exe_name)); + } candidates.push(dir.join(exe_name)); // legacy onefile externalBin slot for c in candidates { if c.exists() {