Skip to content
Merged
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
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sys.executable>`
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.
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
27 changes: 24 additions & 3 deletions backend/services/git_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import re
import shutil
import subprocess
import sys
from typing import Any
Expand Down Expand Up @@ -231,14 +232,34 @@ 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 <sys.executable>`` 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():
return {"installed": False, "message": "requirements.txt was not found."}

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,
Expand All @@ -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 {
Expand Down
27 changes: 22 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/backend/test_extracted_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading