Skip to content

Commit 4a99beb

Browse files
committed
feat: bundle Python backend into desktop app (PyInstaller sidecar) + close packaging gaps
Gap 1 — Backend bundling: - backend/vortex-backend.spec: PyInstaller spec -> single vortex-backend exe - backend/backend_entry.py: entry point importing app.main directly (not by string) so PyInstaller bundles the full import graph; --host/--port flags - Verified: bundled exe serves /api/v1/health + /settings standalone Gap 2 — Real backend lifecycle in main.rs: - BackendState(Mutex<Option<CommandChild>>) tracks spawned process - stop_backend now kills the child (was a stub) - start_backend prefers the PyInstaller sidecar, falls back to python-uvicorn for dev; refuses double-start - get_backend_status: real HTTP health check (reqwest added) - tauri.conf.json: externalBin binaries/vortex-backend Gap 3 — User-data dirs: - config.py: %APPDATA%/Vortex (Win), ~/Library/Application Support/Vortex (mac), ~/.local/share/Vortex (linux); VORTEX_HOME still overrides for dev - config/memory/logs/models subdirs; settings.json + vortex.log moved there Gap 4 — Apple Silicon + CI bundling: - build matrix adds macos-arm64 (aarch64-apple-darwin) - CI build job now runs PyInstaller and places the target-triple-named sidecar before tauri:build - gitignore: backend/dist, build, .build-venv, frontend binaries
1 parent b104cd5 commit 4a99beb

10 files changed

Lines changed: 339 additions & 38 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ jobs:
7575
- os: macos-latest
7676
platform: macos
7777
target: x86_64-apple-darwin
78+
- os: macos-latest
79+
platform: macos-arm64
80+
target: aarch64-apple-darwin
7881
- os: ubuntu-latest
7982
platform: linux
8083
target: x86_64-unknown-linux-gnu
@@ -110,6 +113,30 @@ jobs:
110113
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev \
111114
librsvg2-dev patchelf libgtk-3-dev
112115
116+
- name: Set up Python (backend bundling)
117+
uses: actions/setup-python@v5
118+
with:
119+
python-version: ${{ env.PYTHON_VERSION }}
120+
121+
- name: Bundle backend with PyInstaller
122+
working-directory: backend
123+
run: |
124+
pip install -r requirements.txt pyinstaller
125+
python -m PyInstaller vortex-backend.spec --noconfirm
126+
127+
- name: Place sidecar for Tauri (target-triple naming)
128+
shell: bash
129+
run: |
130+
mkdir -p frontend/src-tauri/binaries
131+
# Tauri requires: <name>-<target-triple><ext>
132+
SIDECAR="frontend/src-tauri/binaries/vortex-backend-${{ matrix.target }}"
133+
case "${{ matrix.platform }}" in
134+
windows*) cp backend/dist/vortex-backend.exe "$SIDECAR.exe" ;;
135+
macos*) cp backend/dist/vortex-backend "$SIDECAR" ;;
136+
linux*) cp backend/dist/vortex-backend "$SIDECAR" ;;
137+
esac
138+
ls -la frontend/src-tauri/binaries/
139+
113140
- name: Build Tauri app
114141
working-directory: frontend
115142
run: npm run tauri:build

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ frontend/build_tauri.log
1919
frontend/src-tauri/target/
2020
frontend/src-tauri/gen/
2121

22+
# PyInstaller backend bundle (built by CI, not committed)
23+
backend/dist/
24+
backend/build/
25+
backend/.build-venv/
26+
frontend/src-tauri/binaries/
27+
2228
# Editor / OS
2329
.vscode/
2430
.idea/

backend/app/vortex/config.py

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,57 @@
11
# Backend configuration
22
import os
3+
import sys
34
from pathlib import Path
45

5-
# Resolve paths relative to this file
6-
BASE_DIR = Path(__file__).resolve().parent.parent.parent # backend/
6+
# Resolve paths relative to this file (backend/)
7+
BASE_DIR = Path(__file__).resolve().parent.parent.parent
78

8-
# Data directory for persistence
9-
DATA_DIR = os.getenv('VORTEX_HOME', str(BASE_DIR / 'vortex-data'))
10-
DATA_DIR = Path(DATA_DIR)
119

12-
# Ensure data directory exists
10+
def _default_data_dir() -> Path:
11+
"""Platform-appropriate user-data directory (never the install dir).
12+
13+
Windows: %APPDATA%/Vortex (e.g. C:/Users/<u>/AppData/Roaming/Vortex)
14+
macOS: ~/Library/Application Support/Vortex
15+
Linux: $XDG_DATA_HOME or ~/.local/share/Vortex
16+
"""
17+
if sys.platform == "win32":
18+
base = os.getenv("APPDATA") or str(Path.home() / "AppData" / "Roaming")
19+
return Path(base) / "Vortex"
20+
if sys.platform == "darwin":
21+
return Path.home() / "Library" / "Application Support" / "Vortex"
22+
# Linux / other POSIX
23+
base = os.getenv("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
24+
return Path(base) / "Vortex"
25+
26+
27+
def _resolve_data_dir() -> Path:
28+
"""VORTEX_HOME env wins; fall back to platform user-data dir.
29+
30+
Dev override: set VORTEX_HOME=backend/vortex-data to keep data in-repo
31+
during development (the smoke tests do exactly this).
32+
"""
33+
env = os.getenv("VORTEX_HOME")
34+
if env:
35+
return Path(env)
36+
return _default_data_dir()
37+
38+
39+
# Data directory for persistence (user-data dir by default)
40+
DATA_DIR = _resolve_data_dir()
1341
DATA_DIR.mkdir(parents=True, exist_ok=True)
1442

1543
# Database path
1644
SQLITE_URL = f"sqlite:///{DATA_DIR / 'vortex.db'}"
1745

46+
# Sub-directories for separation of concerns
47+
CONFIG_DIR = DATA_DIR / "config"
48+
MEMORY_DIR = DATA_DIR / "memory"
49+
LOG_DIR = DATA_DIR / "logs"
50+
MODELS_DIR = DATA_DIR / "models"
51+
52+
for _d in (CONFIG_DIR, MEMORY_DIR, LOG_DIR, MODELS_DIR):
53+
_d.mkdir(parents=True, exist_ok=True)
54+
1855
# Logging configuration
1956
LOGGING_CONFIG = {
2057
'version': 1,
@@ -30,7 +67,7 @@
3067
},
3168
'file': {
3269
'formatter': 'standard',
33-
'path': str(DATA_DIR / 'vortex.log'),
70+
'path': str(LOG_DIR / 'vortex.log'),
3471
'maxBytes': 1024 * 1024 * 5, # 5 MB
3572
'backupCount': 3,
3673
},
@@ -39,4 +76,4 @@
3976
'level': 'INFO',
4077
'handlers': ['default', 'file'],
4178
},
42-
}
79+
}

backend/app/vortex/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import os
1414
from pathlib import Path
1515

16-
from .config import DATA_DIR
16+
from .config import CONFIG_DIR
1717

18-
SETTINGS_FILE = Path(DATA_DIR) / "settings.json"
18+
SETTINGS_FILE = CONFIG_DIR / "settings.json"
1919

2020
DEFAULTS = {
2121
"llm_base_url": "http://localhost:8645/v1",

backend/backend_entry.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env python3
2+
"""PyInstaller entry point — starts the Vortex backend with CLI passthrough.
3+
4+
Usage:
5+
vortex-backend.exe [--host 0.0.0.0] [--port 8000]
6+
"""
7+
8+
import argparse
9+
import os
10+
import sys
11+
12+
# Make bundled `app` package importable regardless of CWD
13+
_HERE = os.path.dirname(os.path.abspath(__file__))
14+
if _HERE not in sys.path:
15+
sys.path.insert(0, _HERE)
16+
17+
18+
def main() -> int:
19+
parser = argparse.ArgumentParser(description="Vortex Agent backend server")
20+
parser.add_argument("--host", default=os.getenv("VORTEX_HOST", "0.0.0.0"))
21+
parser.add_argument("--port", type=int, default=int(os.getenv("VORTEX_PORT", "8000")))
22+
parser.add_argument("--no-reload", action="store_true", help="disable auto-reload (release)")
23+
args = parser.parse_args()
24+
25+
# Import the ASGI app DIRECTLY (not via string) so PyInstaller's static
26+
# analysis follows the full import graph and bundles every module.
27+
from app.main import app # noqa: E402
28+
29+
import uvicorn
30+
31+
uvicorn.run(
32+
app, # app object, not "app.main:app" string
33+
host=args.host,
34+
port=args.port,
35+
reload=False, # never reload in a frozen bundle
36+
log_level="info",
37+
)
38+
return 0
39+
40+
41+
if __name__ == "__main__":
42+
sys.exit(main())

backend/vortex-backend.spec

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
"""PyInstaller spec — bundle the Vortex FastAPI backend into a single exe.
3+
4+
Build (Windows):
5+
cd backend
6+
pip install pyinstaller
7+
pyinstaller vortex-backend.spec
8+
9+
Output: dist/vortex-backend.exe
10+
(Tauri sidecar expects it at frontend/src-tauri/binaries/vortex-backend.exe)
11+
12+
The exe serves the API on 0.0.0.0:8000 with --host/--port passthrough:
13+
vortex-backend.exe [--host 0.0.0.0] [--port 8000]
14+
"""
15+
16+
import os
17+
from pathlib import Path
18+
19+
ROOT = Path(SPECPATH) # backend/ (where the spec lives)
20+
21+
a = Analysis(
22+
["backend_entry.py"],
23+
pathex=[str(ROOT)],
24+
binaries=[],
25+
datas=[
26+
# package data if any (e.g. governance policy JSON) would go here
27+
],
28+
hiddenimports=[
29+
"uvicorn.logging",
30+
"uvicorn.loops",
31+
"uvicorn.lifespan",
32+
"uvicorn.protocols",
33+
"uvicorn.protocols.http",
34+
"uvicorn.protocols.http.auto",
35+
"uvicorn.protocols.websockets",
36+
"uvicorn.protocols.websockets.auto",
37+
"uvicorn.lifespan.on",
38+
"uvicorn.lifespan.off",
39+
"sqlalchemy.dialects.sqlite",
40+
"app.api.v1.routers",
41+
"app.api.v1.settings",
42+
"app.core.llm_client",
43+
"app.core.memory_system",
44+
"app.core.orchestration",
45+
"app.core.tools",
46+
"app.council.council",
47+
"app.governance.governance",
48+
"app.sovereign.sovereign",
49+
"app.knowledge.graph",
50+
"app.evolution.evolution_engine",
51+
"app.observability.trace",
52+
"app.tools.tool_registry",
53+
"app.vortex.config",
54+
"app.vortex.settings",
55+
],
56+
hookspath=[],
57+
hooksconfig={},
58+
runtime_hooks=[],
59+
excludes=[],
60+
noarchive=False,
61+
)
62+
63+
pyz = PYZ(a.pure)
64+
65+
exe = EXE(
66+
pyz,
67+
a.scripts,
68+
a.binaries,
69+
a.datas,
70+
[],
71+
name="vortex-backend",
72+
debug=False,
73+
bootloader_ignore_signals=False,
74+
strip=False,
75+
upx=True,
76+
upx_exclude=[],
77+
runtime_tmpdir=None,
78+
console=True, # console window so logs are visible in dev; False for release
79+
disable_windowed_traceback=False,
80+
argv_emulation=False,
81+
target_arch=None,
82+
codesign_identity=None,
83+
entitlements_file=None,
84+
)

frontend/src-tauri/Cargo.lock

Lines changed: 55 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src-tauri/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ tokio = { version = "1", features = ["full"] }
3131
serde = { version = "1", features = ["derive"] }
3232
serde_json = "1"
3333

34+
# HTTP client (backend health check)
35+
reqwest = { version = "0.12", features = ["blocking"], default-features = false }
36+
3437
# Logging
3538
tracing = "0.1"
3639
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

0 commit comments

Comments
 (0)