Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 51 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -135,14 +161,38 @@ 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.
if [ -f "$BUNDLE"/macos/OpenWorker.app.tar.gz ]; then
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

Expand Down
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<sub>builds are not yet code-signed, so SmartScreen will warn; signing is in progress</sub>

**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
Expand Down Expand Up @@ -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

Expand All @@ -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 |

Expand Down
94 changes: 94 additions & 0 deletions packaging/build_linux.sh
Original file line number Diff line number Diff line change
@@ -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/<productName>/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
79 changes: 79 additions & 0 deletions packaging/install_linux_deps.sh
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions packaging/make_update_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<tag>/<asset>),
never at `latest/` — a manifest must reference exactly the artifacts it shipped with,
Expand All @@ -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",
}


Expand Down
Loading