diff --git a/deploy/windows/README.md b/deploy/windows/README.md index 80a01cf7..113aa671 100644 --- a/deploy/windows/README.md +++ b/deploy/windows/README.md @@ -20,8 +20,8 @@ What it does: 4. Installs `uv` (https://astral.sh/uv) if absent. 5. Clones the OpenJarvis repository to `%LOCALAPPDATA%\OpenJarvis` (override with `$env:OPENJARVIS_HOME`). -6. Runs `uv sync --extra desktop` so the FastAPI server and speech backend are - importable. +6. Runs `uv sync --extra desktop --group desktop-native` so the FastAPI server, + speech backend, and native extension are importable. 7. Optionally prompts to register a scheduled task that auto-starts the server at logon. @@ -105,7 +105,7 @@ To pull the latest: ```powershell cd "$env:LOCALAPPDATA\OpenJarvis\src" git pull --ff-only -uv sync --extra desktop +uv sync --extra desktop --group desktop-native ``` Or re-run the installer with `-Force`: diff --git a/deploy/windows/install.ps1 b/deploy/windows/install.ps1 index 5af627cb..387d6660 100644 --- a/deploy/windows/install.ps1 +++ b/deploy/windows/install.ps1 @@ -16,8 +16,8 @@ 4. Install uv (https://astral.sh/uv) if absent. 5. Clone the OpenJarvis repository to $env:LOCALAPPDATA\OpenJarvis (override with $env:OPENJARVIS_HOME). - 6. Run `uv sync --extra desktop` so the FastAPI server and speech - backend are importable. + 6. Run `uv sync --extra desktop --group desktop-native` so the FastAPI + server, speech backend, and native extension are importable. 7. Optionally register the scheduled-task service (see deploy/windows/jarvis-service.ps1). @@ -279,13 +279,13 @@ if (Test-Path (Join-Path $srcDir '.git')) { } # --------------------------------------------------------------------------- -# 6. uv sync --extra desktop +# 6. uv sync --extra desktop --group desktop-native # --------------------------------------------------------------------------- -Write-Info "Running 'uv sync --extra desktop' in $srcDir (this can take a few minutes)..." +Write-Info "Running 'uv sync --extra desktop --group desktop-native' in $srcDir (this can take a few minutes)..." Push-Location $srcDir try { - & $uvExe sync --extra desktop + & $uvExe sync --extra desktop --group desktop-native if ($LASTEXITCODE -ne 0) { Write-Fail "uv sync failed with exit code $LASTEXITCODE. Check the output above." } diff --git a/docs/getting-started/windows-native.md b/docs/getting-started/windows-native.md index 585a6a7c..36ad1085 100644 --- a/docs/getting-started/windows-native.md +++ b/docs/getting-started/windows-native.md @@ -8,7 +8,7 @@ avoid a Linux VM; WSL2 remains the smoother experience for most users. ## What you get - A PowerShell installer that probes prerequisites, installs `uv`, - clones the repo, and runs `uv sync --extra desktop`. + clones the repo, and runs `uv sync --extra desktop --group desktop-native`. - An optional Windows scheduled-task service equivalent to the systemd unit and launchd plist. - Loopback default — the service binds `127.0.0.1` so no API key is @@ -38,7 +38,7 @@ The installer will: 4. Install `uv` if absent (via the official `astral.sh/uv` PowerShell installer). 5. Clone the repo to `%LOCALAPPDATA%\OpenJarvis\src`. -6. Run `uv sync --extra desktop`. +6. Run `uv sync --extra desktop --group desktop-native`. 7. Prompt to register the scheduled-task service (skip with `-SkipService`). diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 4e9d744f..527512c2 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -8,6 +8,8 @@ use tokio::sync::Mutex; const OLLAMA_PORT: u16 = 11434; const JARVIS_PORT: u16 = 8000; +const DESKTOP_UV_SYNC_COMMAND: &str = + "uv sync --extra desktop --extra inference-cloud --extra inference-google --group desktop-native"; /// Small, fast model used when startup needs a default Ollama tag. const STARTUP_MODEL: &str = "qwen3.5:4b"; @@ -740,10 +742,11 @@ fn format_uv_sync_failure( format!( "`uv sync` failed in {} (exit {}). Last output:\n\n{}\n\n\ Try opening a terminal in that directory and running \ - `uv sync --extra desktop` manually for the full output.{}", + `{}` manually for the full output.{}", root.display(), code, tail, + DESKTOP_UV_SYNC_COMMAND, rust_hint, ) } @@ -829,7 +832,7 @@ fn format_extension_import_failure(root: &std::path::Path, stderr: &str) -> Stri "`openjarvis_rust` is still not importable after building. Last output:\n\n{}\n\n\ Run these manually for the full build log:\n\n\ cd {}\n\ - uv sync --extra desktop\n\ + {}\n\ uv run python -c \"import openjarvis_rust\"", if tail.is_empty() { "(no stderr output)" @@ -837,6 +840,7 @@ fn format_extension_import_failure(root: &std::path::Path, stderr: &str) -> Stri &tail }, root.display(), + DESKTOP_UV_SYNC_COMMAND, ) } @@ -1350,6 +1354,9 @@ async fn boot_backend(backend: SharedBackend, status: SharedStatus) { "--extra", "desktop", "--extra", "inference-cloud", "--extra", "inference-google", + // openjarvis_rust lives in a uv dependency group (not the published + // `desktop` extra) so pip installs from PyPI don't require it (#584). + "--group", "desktop-native", ]) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::piped()) @@ -2866,7 +2873,7 @@ mod tests { format_uv_sync_spawn_error, matching_installed_model, model_names_match, normalize_host, parse_inference_config, parse_ollama_model_names, preferred_installed_model, should_persist_resolved_model, startup_installed_model, upsert_engine_host, - uv_sync_stderr_tail, InferenceConfig, SourceKind, + uv_sync_stderr_tail, InferenceConfig, SourceKind, DESKTOP_UV_SYNC_COMMAND, }; use std::path::Path; @@ -2910,7 +2917,7 @@ mod tests { assert!(msg.contains("exit 2")); assert!(msg.contains("/home/u/.openjarvis/src")); assert!(msg.contains("failed to resolve numpy==2.1.3")); - assert!(msg.contains("uv sync --extra desktop")); // actionable next step + assert!(msg.contains(DESKTOP_UV_SYNC_COMMAND)); // actionable next step } #[test] @@ -2962,7 +2969,7 @@ mod tests { "ModuleNotFoundError: No module named 'openjarvis_rust'", ); assert!(msg.contains("openjarvis_rust")); - assert!(msg.contains("uv sync --extra desktop")); + assert!(msg.contains(DESKTOP_UV_SYNC_COMMAND)); assert!(msg.contains("uv run python -c \"import openjarvis_rust\"")); assert!(msg.contains("ModuleNotFoundError")); } diff --git a/pyproject.toml b/pyproject.toml index 5b9f669c..6e253ed7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,6 @@ desktop = [ "pydantic>=2.0", "python-multipart>=0.0.9", "faster-whisper>=1.0", - "openjarvis-rust", ] openhands = ["openhands-sdk>=1.0; python_version >= '3.12'"] gpu-metrics = ["pynvml>=12.0"] @@ -239,3 +238,12 @@ select = ["E", "F", "I", "W"] dev = [ "maturin>=1.12.6", ] +# openjarvis_rust is the native PyO3 extension, built from the local Rust +# workspace. It lives in a uv dependency group (PEP 735) — not the published +# `desktop` extra — so `uv sync --group desktop-native` builds it from source +# for the desktop app, while `pip install openjarvis[desktop]` from PyPI does +# NOT try to resolve openjarvis-rust from PyPI, where it isn't published +# (dependency groups are excluded from wheel metadata). See #584 / #615. +desktop-native = [ + "openjarvis-rust", +] diff --git a/tests/deployment/test_packaging.py b/tests/deployment/test_packaging.py new file mode 100644 index 00000000..ef79aa70 --- /dev/null +++ b/tests/deployment/test_packaging.py @@ -0,0 +1,61 @@ +"""Guards for the openjarvis-rust packaging split (#584 / #615). + +``openjarvis_rust`` is the native PyO3 extension. It is NOT published to PyPI, +so it must not appear in the published ``desktop`` extra — listing it there +breaks ``pip install openjarvis[desktop]`` at install time. It lives in the uv +``desktop-native`` dependency group instead (excluded from wheel metadata), +which the desktop app installs from source via +``uv sync --group desktop-native``. +""" + +from __future__ import annotations + +from pathlib import Path + +import tomllib + +ROOT = Path(__file__).resolve().parent.parent.parent +PYPROJECT = ROOT / "pyproject.toml" +DESKTOP_LIB_RS = ROOT / "frontend" / "src-tauri" / "src" / "lib.rs" +WINDOWS_INSTALL_PS1 = ROOT / "deploy" / "windows" / "install.ps1" + + +def _pyproject() -> dict: + return tomllib.loads(PYPROJECT.read_text()) + + +def test_openjarvis_rust_not_in_published_desktop_extra() -> None: + desktop = _pyproject()["project"]["optional-dependencies"]["desktop"] + assert not any("openjarvis-rust" in dep for dep in desktop), ( + "openjarvis-rust must not be in the published `desktop` extra — it is " + "not on PyPI, so it breaks `pip install openjarvis[desktop]`." + ) + + +def test_openjarvis_rust_lives_in_uv_dependency_group() -> None: + group = _pyproject()["dependency-groups"]["desktop-native"] + assert any("openjarvis-rust" in dep for dep in group) + + +def test_openjarvis_rust_has_local_uv_path_source() -> None: + src = _pyproject()["tool"]["uv"]["sources"]["openjarvis-rust"] + assert src["path"] == "rust/crates/openjarvis-python" + + +def test_desktop_app_syncs_the_native_group() -> None: + # Otherwise the group's openjarvis_rust is never installed for the app. + assert '"desktop-native"' in DESKTOP_LIB_RS.read_text(), ( + "the desktop app must `uv sync --group desktop-native` so the native " + "extension is built at launch." + ) + + +def test_windows_installer_syncs_the_native_group() -> None: + # The Windows source installer does not run maturin separately. + assert ( + "& $uvExe sync --extra desktop --group desktop-native" + in WINDOWS_INSTALL_PS1.read_text() + ), ( + "the Windows installer must include `--group desktop-native` so " + "openjarvis_rust is built during source install." + ) diff --git a/uv.lock b/uv.lock index 9936fc77..abf163f3 100644 --- a/uv.lock +++ b/uv.lock @@ -4206,7 +4206,6 @@ dashboard = [ desktop = [ { name = "fastapi" }, { name = "faster-whisper" }, - { name = "openjarvis-rust" }, { name = "pydantic" }, { name = "python-multipart" }, { name = "uvicorn" }, @@ -4339,6 +4338,9 @@ tools-search = [ ] [package.dev-dependencies] +desktop-native = [ + { name = "openjarvis-rust" }, +] dev = [ { name = "maturin" }, ] @@ -4391,7 +4393,6 @@ requires-dist = [ { name = "openai", marker = "extra == 'inference-cloud'", specifier = ">=1.30" }, { name = "openai", marker = "extra == 'media'", specifier = ">=1.30" }, { name = "openhands-sdk", marker = "python_full_version >= '3.12' and extra == 'openhands'", specifier = ">=1.0" }, - { name = "openjarvis-rust", marker = "extra == 'desktop'", directory = "rust/crates/openjarvis-python" }, { name = "pdfplumber", marker = "extra == 'memory-pdf'", specifier = ">=0.10" }, { name = "pdfplumber", marker = "extra == 'pdf'", specifier = ">=0.10" }, { name = "playwright", marker = "extra == 'browser'", specifier = ">=1.40" }, @@ -4445,6 +4446,7 @@ requires-dist = [ provides-extras = ["browser", "channel-discord", "channel-gmail", "channel-line", "channel-mastodon", "channel-messenger", "channel-nostr", "channel-reddit", "channel-rocketchat", "channel-slack", "channel-telegram", "channel-twilio", "channel-twitch", "channel-twitter", "channel-viber", "channel-xmpp", "channel-zulip", "dashboard", "desktop", "dev", "docs", "energy-all", "energy-amd", "energy-apple", "eval-sheets", "eval-wandb", "framework-comparison", "gpu-metrics", "inference-cloud", "inference-gemma", "inference-google", "inference-litellm", "inference-mlx", "inference-vllm", "learning-dspy", "learning-gepa", "media", "memory-bm25", "memory-colbert", "memory-faiss", "memory-pdf", "mining-pearl-cpu", "mining-pearl-vllm", "openhands", "orchestrator-training", "pdf", "sandbox-docker", "sandbox-wasm", "scheduler", "security-signing", "server", "speech", "speech-deepgram", "tools-search"] [package.metadata.requires-dev] +desktop-native = [{ name = "openjarvis-rust", directory = "rust/crates/openjarvis-python" }] dev = [{ name = "maturin", specifier = ">=1.12.6" }] [[package]]