Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
22eb2a8
Add --with-llama-cpp-dir flag to install.ps1 and install.sh
LeoBorcherding Jun 17, 2026
35542c6
test: add static wiring test for --with-llama-cpp-dir flag
LeoBorcherding Jun 19, 2026
fe4b43d
Address review feedback on --with-llama-cpp-dir flag
LeoBorcherding Jun 23, 2026
b2587dc
Merge remote-tracking branch 'upstream/main' into with-llamacpp-dir-flag
LeoBorcherding Jun 23, 2026
0d16c6a
Merge branch 'main' into with-llamacpp-dir-flag
LeoBorcherding Jun 26, 2026
56b1e3c
Harden --with-llama-cpp-dir against Codex/Gemini review findings
LeoBorcherding Jun 29, 2026
d5e9f51
Validate/reuse local llama.cpp tree and guard the in-use case
LeoBorcherding Jun 29, 2026
43f2082
Merge branch 'main' into with-llamacpp-dir-flag
Imagineer99 Jun 30, 2026
d254f7e
Accept all backend llama-server layouts in --with-llama-cpp-dir valid…
LeoBorcherding Jun 30, 2026
5252163
Treat --with-llama-cpp-dir local links as externally managed
LeoBorcherding Jun 30, 2026
1206943
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 30, 2026
be8a3c3
Add behavioral shell test for --with-llama-cpp-dir linking
LeoBorcherding Jun 30, 2026
48b37bb
Install psutil in backend CI so orphan-cleanup tests run
LeoBorcherding Jun 30, 2026
829b3b1
Merge branch 'main' into with-llamacpp-dir-flag
Imagineer99 Jul 2, 2026
9a365dc
Merge branch 'main' into with-llamacpp-dir-flag
Imagineer99 Jul 2, 2026
3877bea
Merge branch 'main' into with-llamacpp-dir-flag
Imagineer99 Jul 2, 2026
8d398a2
Merge branch 'main' into with-llamacpp-dir-flag
Imagineer99 Jul 2, 2026
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
4 changes: 3 additions & 1 deletion .github/workflows/studio-backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,9 @@ jobs:
tests/sh/test_resolve_cuda_archs.sh \
tests/sh/test_tauri_install_exit_order.sh \
tests/sh/test_torch_constraint.sh \
tests/sh/test_torch_flavor.sh; do
tests/sh/test_torch_flavor.sh \
tests/sh/test_with_llama_cpp_dir_flag.sh \
tests/sh/test_with_llama_cpp_dir_link_behavior.sh; do
echo "::group::$s"
bash "$s"
echo "::endgroup::"
Expand Down
17 changes: 17 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function Install-UnslothStudio {
$TauriMode = $false
$SkipTorch = $false
$ShortcutsOnly = $false
$WithLlamaCppDir = ""
$argList = $args
for ($i = 0; $i -lt $argList.Count; $i++) {
switch ($argList[$i]) {
Expand All @@ -116,6 +117,14 @@ function Install-UnslothStudio {
}
$PackageName = $argList[$i]
}
"--with-llama-cpp-dir" {
$i++
if ($i -ge $argList.Count) {
Write-Host "[ERROR] --with-llama-cpp-dir requires a path argument." -ForegroundColor Red
return (Exit-InstallFailure "--with-llama-cpp-dir requires a path argument.")
}
$WithLlamaCppDir = $argList[$i]
}
}
}

Expand Down Expand Up @@ -2430,6 +2439,13 @@ exit 0
}
$studioArgs = @('studio', 'setup')
if ($script:UnslothVerbose) { $studioArgs += '--verbose' }
if ($WithLlamaCppDir) {
if (-not (Test-Path -LiteralPath $WithLlamaCppDir -PathType Container)) {
Write-Host "[ERROR] --with-llama-cpp-dir path does not exist: $WithLlamaCppDir" -ForegroundColor Red
return (Exit-InstallFailure "--with-llama-cpp-dir path does not exist.")
Comment on lines +2443 to +2445

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore the environment on invalid local llama.cpp paths

When --with-llama-cpp-dir is mistyped in a PowerShell session that already has UNSLOTH_STUDIO_HOME or the installer state env vars set, this validation returns after the code above has removed/overwritten those env vars but before the try/finally that restores them starts. That leaves the caller's session pointed back at the default Studio home (or with stale STUDIO_LOCAL_INSTALL/skip flags), so a subsequent setup in the same shell can install into the wrong root; move this validation before mutating env vars or include it inside the restore finally.

Useful? React with 👍 / 👎.

}
$env:UNSLOTH_LOCAL_LLAMA_CPP_DIR = (Resolve-Path -LiteralPath $WithLlamaCppDir).Path
}
$env:UNSLOTH_INSTALL_ROLLBACK_MANAGED = "1"
# Hand the venv interpreter to setup.ps1 so it reuses the Python we already
# resolved and built the venv with, instead of re-probing the system (which
Expand All @@ -2445,6 +2461,7 @@ exit 0
} else {
Remove-Item Env:UNSLOTH_STUDIO_HOME -ErrorAction SilentlyContinue
}
Remove-Item Env:UNSLOTH_LOCAL_LLAMA_CPP_DIR -ErrorAction SilentlyContinue
Remove-Item Env:UNSLOTH_INSTALL_ROLLBACK_MANAGED -ErrorAction SilentlyContinue
Remove-Item Env:UNSLOTH_SETUP_PYTHON -ErrorAction SilentlyContinue
}
Expand Down
24 changes: 24 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ _VERBOSE=false
_SHORTCUTS_ONLY=false
_next_is_package=false
_next_is_python=false
_next_is_llama_cpp_dir=false
# Seed from the environment so a caller who exports UNSLOTH_LOCAL_LLAMA_CPP_DIR
# (the documented piped-install style) is honored; the --with-llama-cpp-dir
# flag below overrides it when given.
_WITH_LLAMA_CPP_DIR="${UNSLOTH_LOCAL_LLAMA_CPP_DIR:-}"
for arg in "$@"; do
if [ "$_next_is_package" = true ]; then
PACKAGE_NAME="$arg"
Expand All @@ -64,6 +69,11 @@ for arg in "$@"; do
_next_is_python=false
continue
fi
if [ "$_next_is_llama_cpp_dir" = true ]; then
_WITH_LLAMA_CPP_DIR="$arg"
_next_is_llama_cpp_dir=false
continue
fi
case "$arg" in
--local) STUDIO_LOCAL_INSTALL=true ;;
--package) _next_is_package=true ;;
Expand All @@ -72,6 +82,7 @@ for arg in "$@"; do
--no-torch) _NO_TORCH_FLAG=true ;;
--verbose|-v) _VERBOSE=true ;;
--shortcuts-only) _SHORTCUTS_ONLY=true ;;
--with-llama-cpp-dir) _next_is_llama_cpp_dir=true ;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject a missing llama.cpp path in the shell parser

If --with-llama-cpp-dir is the final argument, _next_is_llama_cpp_dir remains true after the loop and no error is raised, so _WITH_LLAMA_CPP_DIR stays empty and the installer silently falls back to the normal prebuilt/source install path. This makes a mistyped invocation behave as if the new flag was not supplied; add a post-loop check for the pending argument state, as the PowerShell installer does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Forward the local llama.cpp flag during WSL reroutes

This parser records --with-llama-cpp-dir, but the Strix Halo WSL reroute path later reconstructs _rr_args for the Ubuntu-24.04 child install without appending this option or exporting UNSLOTH_LOCAL_LLAMA_CPP_DIR. On those rerouted installs, a user-provided local llama.cpp directory is silently ignored and the child falls back to downloading/building instead of reusing the requested cache.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If --with-llama-cpp-dir is passed as the last argument to install.sh, the loop terminates with _next_is_llama_cpp_dir set to true but without assigning any value to _WITH_LLAMA_CPP_DIR. Unlike --package and --python, there is no post-loop validation to catch this, which can lead to silent failures or unexpected behavior.

Consider adding a validation check after the loop (around line 268) to ensure that _next_is_llama_cpp_dir is not left as true.

esac
done

Expand Down Expand Up @@ -255,6 +266,10 @@ if [ "$_next_is_python" = true ]; then
echo "❌ ERROR: --python requires a version argument (e.g. --python 3.12)." >&2
exit 1
fi
if [ "$_next_is_llama_cpp_dir" = true ]; then
echo "❌ ERROR: --with-llama-cpp-dir requires a path argument." >&2
exit 1
fi

# Validate --package to prevent injection into shell/Python commands.
# Must start with a letter/digit (rejects leading dashes that uv would parse as flags).
Expand Down Expand Up @@ -3023,6 +3038,13 @@ _run_setup_with_studio_home() {
"$@"
fi
}
if [ -n "$_WITH_LLAMA_CPP_DIR" ]; then
if [ ! -d "$_WITH_LLAMA_CPP_DIR" ]; then
echo "[ERROR] --with-llama-cpp-dir path does not exist: $_WITH_LLAMA_CPP_DIR" >&2
exit 1
fi
_WITH_LLAMA_CPP_DIR="$(CDPATH= cd -P -- "$_WITH_LLAMA_CPP_DIR" && pwd -P)"
fi
if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
_run_setup_with_studio_home env \
SKIP_STUDIO_BASE="$_SKIP_BASE" \
Expand All @@ -3031,6 +3053,7 @@ if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
STUDIO_LOCAL_INSTALL=1 \
STUDIO_LOCAL_REPO="$_REPO_ROOT" \
UNSLOTH_NO_TORCH="$SKIP_TORCH" \
UNSLOTH_LOCAL_LLAMA_CPP_DIR="$_WITH_LLAMA_CPP_DIR" \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the env-provided local llama.cpp path

When the POSIX installer is used in the documented piped-install style with UNSLOTH_LOCAL_LLAMA_CPP_DIR=/path sh (or the variable is otherwise exported before invoking install.sh), _WITH_LLAMA_CPP_DIR remains empty unless the CLI flag is used, and this assignment shadows the caller's value with an empty string before setup.sh runs. That makes the env-var path that setup.sh now supports unreachable through install.sh, so web installs silently download/build llama.cpp instead of reusing the requested local directory.

Useful? React with 👍 / 👎.

bash "$SETUP_SH" </dev/null || _SETUP_EXIT=$?
else
# Explicitly reset STUDIO_LOCAL_INSTALL / STUDIO_LOCAL_REPO so a stale
Expand All @@ -3045,6 +3068,7 @@ else
STUDIO_LOCAL_INSTALL=0 \
STUDIO_LOCAL_REPO= \
UNSLOTH_NO_TORCH="$SKIP_TORCH" \
UNSLOTH_LOCAL_LLAMA_CPP_DIR="$_WITH_LLAMA_CPP_DIR" \
bash "$SETUP_SH" </dev/null || _SETUP_EXIT=$?
fi

Expand Down
26 changes: 26 additions & 0 deletions studio/backend/core/inference/llama_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,25 @@ def _backfill_usage_from_timings(usage, timings):
return out


def _is_external_link(path: Path) -> bool:
"""True when ``path`` is a --with-llama-cpp-dir local link: a POSIX symlink
or a Windows directory junction / reparse point. Such a link resolves into
the user's own llama.cpp checkout, which Studio does not own."""
try:
if os.path.islink(path):
return True
except OSError:
return False
if os.name == "nt":
try:
import stat
attrs = os.lstat(path).st_file_attributes # type: ignore[attr-defined]
return bool(attrs & stat.FILE_ATTRIBUTE_REPARSE_POINT)
except (OSError, AttributeError):
return False
return False


class LlamaCppBackend:
"""Manages a llama-server subprocess for GGUF model inference.

Expand Down Expand Up @@ -7140,6 +7159,13 @@ def _kill_orphaned_servers() -> int:
resolved_roots: list[Path] = []
for root in install_roots:
try:
# A --with-llama-cpp-dir local link (symlink/junction)
# resolves into the user's own checkout. Adding it would let
# us treat the user's externally-launched llama-server as our
# orphan and kill it, so leave such roots out of the
# allowlist (we forgo orphan-reaping for local-link installs).
if _is_external_link(root):
continue
Comment on lines +7193 to +7194

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude linked targets from orphan roots

When --with-llama-cpp-dir points at a checkout that is also one of the other known roots, e.g. a local/dev install reusing <repo>/llama.cpp, this guard only skips the canonical symlink/junction root. The same target is still added earlier as project_root / "llama.cpp", then resolved into resolved_roots, so an externally launched llama-server from the requested local checkout is still classified as Studio-owned and killed. Fresh evidence beyond the earlier local-link concern is that the new guard filters each root before resolving it but does not remove duplicate resolved targets already present in install_roots.

Useful? React with 👍 / 👎.

resolved_roots.append(root.resolve())
except OSError:
pass
Expand Down
135 changes: 135 additions & 0 deletions studio/backend/tests/test_local_llama_cpp_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Behavioral tests for the --with-llama-cpp-dir 'unmanaged local link' contract.

When the canonical llama.cpp dir is a symlink (POSIX) / junction (Windows) to a
user's own checkout, Studio must treat it as externally managed:
- the in-app updater must not offer or apply a prebuilt over the link
- orphan cleanup must not kill a llama-server the user launched from that tree

These exercise real link behavior rather than grepping the scripts.
"""

import os
import subprocess
from pathlib import Path

import pytest

from utils import llama_cpp_update as u
from core.inference.llama_cpp import LlamaCppBackend


def _make_link(link: Path, target: Path) -> None:
"""Create a directory junction (Windows) / symlink (POSIX); neither needs
elevation."""
target.mkdir(parents = True, exist_ok = True)
if os.name == "nt":
subprocess.run(
["cmd", "/c", "mklink", "/J", str(link), str(target)],
check = True,
capture_output = True,
text = True,
)
else:
link.symlink_to(target, target_is_directory = True)


def _server_subpath() -> Path:
return Path(
"build/bin/Release/llama-server.exe" if os.name == "nt" else "build/bin/llama-server"
)


class _FakeProc:
def __init__(self, pid: int, exe: str) -> None:
self.info = {"pid": pid, "name": "llama-server", "exe": exe}
self.killed = False

def kill(self) -> None:
self.killed = True


def test_is_external_link_detects_link_vs_plain_dir(tmp_path: Path) -> None:
plain = tmp_path / "plain"
plain.mkdir()
assert u._is_external_link(plain) is False

link = tmp_path / "link"
_make_link(link, tmp_path / "tgt")
assert u._is_external_link(link) is True


def test_active_install_is_local_link(tmp_path: Path) -> None:
link = tmp_path / "llama.cpp"
_make_link(link, tmp_path / "tgt")
binary = str(link / _server_subpath())
assert u._active_install_is_local_link(binary) is True

# A plain (non-link) llama.cpp dir is Studio-managed, not a local link.
plain = tmp_path / "plain" / "llama.cpp"
plain.mkdir(parents = True)
assert u._active_install_is_local_link(str(plain / _server_subpath())) is False


def test_get_update_status_reports_local_link(tmp_path: Path, monkeypatch) -> None:
link = tmp_path / "llama.cpp"
_make_link(link, tmp_path / "tgt")
monkeypatch.setattr(u, "_find_binary", lambda: str(link / _server_subpath()))
st = u.get_update_status()
assert st["supported"] is False
assert st["update_available"] is False
assert st["local_link"] is True


def test_start_update_refuses_local_link(tmp_path: Path, monkeypatch) -> None:
link = tmp_path / "llama.cpp"
_make_link(link, tmp_path / "tgt")
monkeypatch.setattr(u, "_find_binary", lambda: str(link / _server_subpath()))
res = u.start_update()
assert res["started"] is False
assert res["reason"] == "local_link"


def _run_orphan_scan(monkeypatch, studio_root: Path, fake: _FakeProc) -> int:
import psutil
Comment thread
Imagineer99 marked this conversation as resolved.
Outdated

monkeypatch.setattr(
LlamaCppBackend,
"_resolved_studio_root_and_is_legacy",
staticmethod(lambda: (studio_root.resolve(), False)),
)
monkeypatch.setattr(LlamaCppBackend, "_reap_recorded_pid", staticmethod(lambda: 0))
monkeypatch.setattr(psutil, "process_iter", lambda attrs = None: iter([fake]))
return LlamaCppBackend._kill_orphaned_servers()


def test_orphan_cleanup_spares_local_link_tree(tmp_path: Path, monkeypatch) -> None:
studio_root = tmp_path / "studio-home"
studio_root.mkdir()
external = tmp_path / "external"
(external / _server_subpath().parent).mkdir(parents = True)
(external / _server_subpath()).write_text("x")
_make_link(studio_root / "llama.cpp", external)

exe_under_link = str((external / _server_subpath()).resolve())
fake = _FakeProc(os.getpid() + 777, exe_under_link)
killed = _run_orphan_scan(monkeypatch, studio_root, fake)
assert killed == 0
assert fake.killed is False


def test_orphan_cleanup_kills_under_real_root(tmp_path: Path, monkeypatch) -> None:
# Control: a real (non-link) managed root still gets its orphan reaped, so
# the spare-the-link test above is meaningful (not a no-op).
studio_root = tmp_path / "studio-home"
bin_dir = studio_root / "llama.cpp" / _server_subpath().parent
bin_dir.mkdir(parents = True)
exe = studio_root / "llama.cpp" / _server_subpath()
exe.write_text("x")

fake = _FakeProc(os.getpid() + 888, str(exe.resolve()))
killed = _run_orphan_scan(monkeypatch, studio_root, fake)
assert killed == 1
assert fake.killed is True
75 changes: 75 additions & 0 deletions studio/backend/utils/llama_cpp_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,12 +324,74 @@ def _source_build_status(binary: str, *, force_refresh: bool) -> Optional[dict]:
}


def _is_external_link(path: Optional[Path]) -> bool:
"""True when ``path`` is a --with-llama-cpp-dir local link: a POSIX symlink
or a Windows directory junction / reparse point. Such a link resolves into
the user's own llama.cpp checkout, so Studio must never auto-update it."""
if path is None:
return False
try:
if os.path.islink(path):
return True
except OSError:
return False
if os.name == "nt":
try:
import stat
attrs = os.lstat(path).st_file_attributes # type: ignore[attr-defined]
return bool(attrs & stat.FILE_ATTRIBUTE_REPARSE_POINT)
except (OSError, AttributeError):
return False
return False


def _active_install_is_local_link(binary: Optional[str]) -> bool:
"""True when the active llama-server resolves through a --with-llama-cpp-dir
local link at the canonical llama.cpp directory. An update would write
through that link into the user's own checkout (or fail), so the install is
treated as externally managed: no update is offered or applied. Checks only
up to and including the ``llama.cpp`` dir so a symlinked HOME / studio root
above it can't trip a false positive."""
if not binary:
return False
for parent in Path(binary).parents:
if _is_external_link(parent):
return True
if parent.name == "llama.cpp":
Comment on lines +358 to +360

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark canonical local reuse as unmanaged

When the user points --with-llama-cpp-dir at the canonical llama.cpp directory itself, setup now treats it as reused but creates no symlink/junction marker. This updater guard only recognizes actual external links, so that plain canonical source tree later falls through the markerless source-build update path and the in-app Update action can replace the local build the installer explicitly skipped; persist an unmanaged marker for this no-op reuse path or teach the updater to detect it.

Useful? React with 👍 / 👎.

break
return False


def _local_link_status() -> dict:
"""Status payload for a local-link install: unmanaged, no update offered."""
with _job_lock:
job = dict(_job)
return {
"supported": False,
"update_available": False,
"stale": False,
"installed_tag": None,
"latest_tag": None,
"published_repo": None,
"installed_at_utc": None,
"age_days": None,
"source_build": False,
"local_link": True,
"update_size_bytes": None,
"job": job,
}


def get_update_status(*, force_refresh: bool = False) -> dict:
"""Report whether a newer prebuilt exists plus the current job state.

force_refresh bypasses the 24h release cache for an explicit "check now".
"""
binary = _find_binary()
# A --with-llama-cpp-dir local link is the user's own tree; never offer to
# replace it. Bail before any network/freshness work.
if _active_install_is_local_link(binary):
return _local_link_status()
marker = read_install_marker(binary)

with _job_lock:
Expand Down Expand Up @@ -537,6 +599,19 @@ def start_update() -> dict:
"""Kick off a background update. Idempotent: a second call while one is
running returns the in-flight job rather than starting another."""
binary = _find_binary()
# Refuse to update a --with-llama-cpp-dir local link: installing a prebuilt
# here would write through the link into the user's own checkout (or fail)
# and silently drop the link the flag created.
if _active_install_is_local_link(binary):
return {
"started": False,
"reason": "local_link",
"message": (
"llama.cpp is a local directory linked with --with-llama-cpp-dir; "
"Studio won't replace it. Update your own llama.cpp checkout instead."
),
"job": get_update_status()["job"],
}
marker = read_install_marker(binary)
script = _installer_script()
if script is None:
Expand Down
Loading
Loading