From 119b4292d959797bc0b89ed642c493802b90350f Mon Sep 17 00:00:00 2001 From: Raymond Yeung <130120884+Raymondycp@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:09:49 +0800 Subject: [PATCH 1/2] Support uv for environment setup and dependency install Detect uv in install.sh and in the in-app updater: create the venv with 'uv venv --seed' and install requirements with 'uv pip install --python' when uv is on PATH, falling back to the existing python -m venv / pip flow. Document the uv workflow in AGENTS.md and CONTRIBUTING.md. --- AGENTS.md | 18 +++++++++++++++++ CONTRIBUTING.md | 5 +++++ backend/services/git_update.py | 27 ++++++++++++++++++++++--- install.sh | 27 ++++++++++++++++++++----- tests/backend/test_extracted_routes.py | 28 ++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5148c882..aa83a0c7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,3 +59,21 @@ Run checks appropriate to the change; use `docs/tests.md` to select focused unit | Full frontend suite | `npm test` | Use the **project venv** for backend tests (`.venv/bin/python` on Unix). System Python may lack runtime dependencies and produce misleading failures. + +## Environment: uv + +`uv` is a supported alternative to `pip` for the project venv; everything works +the same whether the venv was created with `python -m venv` or `uv venv`. + +- The backend install paths are uv-aware: `install.sh` uses `uv venv --seed` + + `uv pip install` when `uv` is on PATH (falling back to `python -m venv` + + `pip`), and the in-app updater runs `uv pip install --python ` + when available (`backend/services/git_update.py:dependency_install_command`), + otherwise the venv's own `pip`. +- `--seed` is required when creating a venv at all: `install.sh`, the in-app + updater, and shell workflows that still call `python -m pip` all assume pip + exists in the venv. Recreating with a plain (pip-less) `uv venv` breaks them. +- There is no `pyproject.toml`, so manage dependencies with + `uv pip install -r requirements.txt` — do not use `uv sync`. +- Startup scripts (`mac_linux_start.sh`, `mac_linux_silent_start.sh`) only + execute `.venv/bin/python`, so they are uv-agnostic. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8de8713b..2d9182c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,6 +31,11 @@ Then install the Python dependencies **into the venv**: .venv/bin/python -m pip install -r requirements.txt ``` +> **Prefer uv?** `uv venv --seed` (or `--seed --python 3.12`) and +> `uv pip install -r requirements.txt` are drop-in equivalents; the setup, +> `install.sh`, and the in-app updater all detect uv and use it automatically. +> `--seed` matters: the updater and `install.sh` assume pip exists in the venv. + > **Always use the venv Python.** System Python lacks `huggingface_hub` and > the other runtime dependencies, so tests fail with misleading import > errors deep inside unrelated modules. Every Python command below shows the diff --git a/backend/services/git_update.py b/backend/services/git_update.py index 52ca9bec..ec810d7f 100644 --- a/backend/services/git_update.py +++ b/backend/services/git_update.py @@ -2,6 +2,7 @@ import os import re +import shutil import subprocess import sys from typing import Any @@ -231,6 +232,26 @@ def find_latest_release_tag(base_dir, upstream_ref): return {"tag": "", "error": ""} +def dependency_install_command(requirements_path): + """Pick uv when available, else the venv's own pip. + + ``uv pip install --python `` installs into the interpreter + that is running the app (the project venv), while falling back to pip keeps + plain ``python -m venv`` setups working unchanged. + """ + if shutil.which("uv"): + return [ + "uv", + "pip", + "install", + "--python", + sys.executable, + "-r", + str(requirements_path), + ] + return [sys.executable, "-m", "pip", "install", "-r", str(requirements_path)] + + def install_python_dependencies(ctx: AppContext) -> dict[str, Any]: requirements_path = ctx.paths.root / "requirements.txt" if not requirements_path.exists(): @@ -238,7 +259,7 @@ def install_python_dependencies(ctx: AppContext) -> dict[str, Any]: try: res = subprocess.run( - [sys.executable, "-m", "pip", "install", "-r", str(requirements_path)], + dependency_install_command(requirements_path), cwd=str(ctx.paths.root), capture_output=True, text=True, @@ -248,14 +269,14 @@ def install_python_dependencies(ctx: AppContext) -> dict[str, Any]: ) except subprocess.TimeoutExpired: print( - f"[git_update] pip install timed out after {DEPENDENCY_INSTALL_TIMEOUT_SECONDS}s", + f"[git_update] dependency install timed out after {DEPENDENCY_INSTALL_TIMEOUT_SECONDS}s", file=sys.stderr, ) return {"installed": False, "error": "Dependency installation timed out."} output = (res.stdout or res.stderr or "").strip() if res.returncode != 0: print( - f"[git_update] pip install failed: {(res.stderr or res.stdout or '').strip()}", + f"[git_update] dependency install failed: {(res.stderr or res.stdout or '').strip()}", file=sys.stderr, ) return { diff --git a/install.sh b/install.sh index 59af6680..2ca505a4 100755 --- a/install.sh +++ b/install.sh @@ -23,9 +23,21 @@ if ! "$PY_CMD" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 9) exit 1 fi +if command -v uv >/dev/null 2>&1; then + UV_AVAILABLE=1 +else + UV_AVAILABLE=0 +fi + if [ ! -d ".venv" ]; then echo "Creating local virtual environment..." - "$PY_CMD" -m venv .venv + if [ "$UV_AVAILABLE" -eq 1 ]; then + # --seed keeps pip available for the in-app updater and for shell + # workflows that still call "python -m pip". + uv venv --seed --python "$PY_CMD" .venv + else + "$PY_CMD" -m venv .venv + fi fi VENV_PYTHON="$SCRIPT_DIR/.venv/bin/python" @@ -43,11 +55,16 @@ if [ "$(uname -s)" = "Linux" ] && ! "$VENV_PYTHON" -c 'import tkinter' >/dev/nul echo "Llama GUI will still install; add Tk and restart it to enable Browse/Change dialogs." fi -echo "Upgrading pip..." -"$VENV_PYTHON" -m pip install --upgrade pip +if [ "$UV_AVAILABLE" -eq 1 ]; then + echo "Installing Python dependencies from requirements.txt with uv..." + uv pip install --python "$VENV_PYTHON" -r requirements.txt +else + echo "Upgrading pip..." + "$VENV_PYTHON" -m pip install --upgrade pip -echo "Installing Python dependencies from requirements.txt..." -"$VENV_PYTHON" -m pip install -r requirements.txt + echo "Installing Python dependencies from requirements.txt..." + "$VENV_PYTHON" -m pip install -r requirements.txt +fi mkdir -p llama/custom/bin llama/custom/grammars mkdir -p llama/custom-02/bin llama/custom-02/grammars diff --git a/tests/backend/test_extracted_routes.py b/tests/backend/test_extracted_routes.py index 6bac493e..df92994a 100644 --- a/tests/backend/test_extracted_routes.py +++ b/tests/backend/test_extracted_routes.py @@ -5744,6 +5744,34 @@ def test_install_deps_subprocess_called(self): self.assertIn("pip", args) self.assertIn("install", args) + def test_install_deps_prefers_uv_when_available(self): + (self.ctx.paths.root / "requirements.txt").write_text("requests\n") + with ( + mock.patch.object(srv.shutil, "which", return_value="/usr/local/bin/uv"), + mock.patch.object(srv.subprocess, "run") as mock_run, + ): + mock_run.return_value = self.proc_result(stdout="Successfully installed") + result = srv.install_python_dependencies(self.ctx) + self.assertTrue(result["installed"]) + args = mock_run.call_args[0][0] + self.assertEqual(args[0], "uv") + self.assertIn("pip", args) + self.assertIn("--python", args) + + def test_install_deps_falls_back_to_pip_without_uv(self): + (self.ctx.paths.root / "requirements.txt").write_text("requests\n") + with ( + mock.patch.object(srv.shutil, "which", return_value=None), + mock.patch.object(srv.subprocess, "run") as mock_run, + ): + mock_run.return_value = self.proc_result(stdout="Successfully installed") + result = srv.install_python_dependencies(self.ctx) + self.assertTrue(result["installed"]) + args = mock_run.call_args[0][0] + self.assertEqual(args[0], srv.sys.executable) + self.assertIn("-m", args) + self.assertIn("pip", args) + def test_install_deps_subprocess_fails(self): (self.ctx.paths.root / "requirements.txt").write_text("bad_package\n") with mock.patch.object(srv.subprocess, "run") as mock_run: From fe47707f9174504ff9d84dec41a4224617c992c0 Mon Sep 17 00:00:00 2001 From: Raymond Yeung <130120884+Raymondycp@users.noreply.github.com> Date: Thu, 17 Sep 2026 03:18:05 +0800 Subject: [PATCH 2/2] Document uv support in the README Mention uv as an automatic alternative to pip/venv in the requirements and the manual install steps. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4b52ece..ea776d79 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Special thanks to ggml-org for [llama.cpp](https://github.com/ggml-org/llama.cpp ## Requirements -- Python 3.9+, `pip`, and virtual environment support (`python -m venv`) +- Python 3.9+, `pip`, and virtual environment support (or [uv](https://astral.sh/uv), which the installers and the in-app updater detect and use automatically) - Internet access for release downloads, optional app updates, and optional Chat web search - A supported OS/architecture for the prebuilt `llama.cpp` binaries you want @@ -90,6 +90,8 @@ Install dependencies: - macOS/Linux: `./install.sh` - Windows: `windows_install.bat` +If [uv](https://astral.sh/uv) is on PATH, setup uses it automatically — the venv is created with `uv venv --seed` and requirements are installed with `uv pip install`. The in-app updater prefers uv the same way. The plain `pip` flow remains fully supported. + If macOS/Linux reports `permission denied`, restore the executable bit: ```bash