studio: tighten sys.prefix inference + Tauri env handling + llama.cpp env

Cycle 3 reviewer.py findings (3 P1s converging):

* sys.prefix inference too broad: a developer venv named 'unsloth_studio'
  was being treated as a custom Studio root. Narrow with an installer-
  sentinel check (presence of share/studio.conf or bin/unsloth shim
  inside the parent dir) in both unsloth_cli/commands/studio.py and
  studio/backend/utils/paths/storage_roots.py.

* Tauri studio/src-tauri/src/process.rs::find_unsloth_binary() hardcoded
  ~/.unsloth/studio. Honor UNSLOTH_STUDIO_HOME / STUDIO_HOME (in that
  priority order) before falling back to legacy.

* unsloth-zoo's GGUF export binds LLAMA_CPP_DEFAULT_DIR at import time
  from UNSLOTH_LLAMA_CPP_PATH. For env-override installs, persist
  UNSLOTH_LLAMA_CPP_PATH alongside UNSLOTH_STUDIO_HOME in studio.conf
  (Unix), in the generated PowerShell launcher (Windows), and via
  os.environ.setdefault in the Python CLI when running on a custom
  root, so GGUF export uses the custom-root llama.cpp build instead
  of the legacy ~/.unsloth/llama.cpp.

Default behaviour unchanged: no env vars are written to studio.conf
in default mode, no LLAMA_CPP_PATH is set, and the dev-venv inference
falls through to legacy when no installer sentinels are present.
This commit is contained in:
Daniel Han 2026-04-26 09:41:34 +00:00
commit 42f3adb2f6
5 changed files with 51 additions and 8 deletions

View file

@ -423,7 +423,8 @@ function Install-UnslothStudio {
# empty string here so behavior matches today exactly.
$studioHomeExport = if ($StudioRedirectMode -eq 'env') {
$_sq = $StudioHome -replace "'", "''"
"`$env:UNSLOTH_STUDIO_HOME = '$_sq'`n"
$_llama = (Join-Path $StudioHome "llama.cpp") -replace "'", "''"
"`$env:UNSLOTH_STUDIO_HOME = '$_sq'`n`$env:UNSLOTH_LLAMA_CPP_PATH = '$_llama'`n"
} else { "" }
$launcherContent = @"

View file

@ -606,11 +606,15 @@ LAUNCHER_EOF
printf '%s\n' "UNSLOTH_EXE='$_css_quoted_exe'"
# Persist UNSLOTH_STUDIO_HOME for env-override installs so the launcher,
# CLI, and backend pick up the same root in fresh shells where the user
# did not re-export the env var. Default installs do NOT get this line so
# the legacy ~/.unsloth/studio resolution is fully preserved.
# did not re-export the env var. Also persist UNSLOTH_LLAMA_CPP_PATH so
# unsloth-zoo's import-time LLAMA_CPP_DEFAULT_DIR binding finds the
# custom-root build. Default installs do NOT get these lines so the
# legacy ~/.unsloth/studio + ~/.unsloth/llama.cpp resolution stands.
if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then
_css_quoted_home=$(printf '%s' "$STUDIO_HOME" | sed "s/'/'\\\\''/g")
_css_quoted_llama=$(printf '%s' "$STUDIO_HOME/llama.cpp" | sed "s/'/'\\\\''/g")
printf '%s\n' "export UNSLOTH_STUDIO_HOME='$_css_quoted_home'"
printf '%s\n' "export UNSLOTH_LLAMA_CPP_PATH='$_css_quoted_llama'"
fi
} > "$_css_data_dir/studio.conf"

View file

@ -11,17 +11,25 @@ import tempfile
def _infer_studio_home_from_venv() -> Path | None:
"""If running from the unsloth_studio venv, the parent dir is STUDIO_HOME.
"""If running from an installer-managed unsloth_studio venv, return the
parent dir as STUDIO_HOME.
Fallback for fresh shells after a custom install where the installer
wrote to a workspace path but the user did not re-export the env var.
Narrowed via installer-sentinel check (share/studio.conf or bin shim)
so a developer venv that happens to be named ``unsloth_studio`` is not
misidentified as a custom Studio root.
"""
try:
prefix = Path(sys.prefix).resolve()
except (OSError, ValueError):
return None
if prefix.name == "unsloth_studio":
return prefix.parent
if prefix.name != "unsloth_studio":
return None
candidate = prefix.parent
shim_name = "unsloth.exe" if os.name == "nt" else "unsloth"
if (candidate / "share" / "studio.conf").is_file() or (candidate / "bin" / shim_name).exists():
return candidate
return None

View file

@ -111,9 +111,22 @@ fn find_unsloth_binary_in_studio_dir(studio: &std::path::Path) -> Option<std::pa
}
pub fn find_unsloth_binary() -> Option<std::path::PathBuf> {
// Honor UNSLOTH_STUDIO_HOME / STUDIO_HOME (in that priority order) so
// Tauri custom-root installs can locate the venv. Falls back to the
// legacy ~/.unsloth/studio when neither is set.
for var in ["UNSLOTH_STUDIO_HOME", "STUDIO_HOME"] {
if let Some(value) = std::env::var_os(var) {
if !value.is_empty() {
let studio = std::path::PathBuf::from(value);
if let Some(bin) = find_unsloth_binary_in_studio_dir(&studio) {
return Some(bin);
}
}
}
}
let home = dirs::home_dir()?;
let studio = home.join(".unsloth").join("studio");
find_unsloth_binary_in_studio_dir(&studio)
}

View file

@ -28,6 +28,18 @@ studio_app = typer.Typer(help = "Unsloth Studio commands.")
# Returns (path, is_custom): is_custom=True only when the resolved root is a
# real override; we use this to decide whether to re-export the env var to
# child processes.
def _looks_like_installer_managed_studio_home(candidate: Path) -> bool:
"""Heuristic: only treat a directory as an installer-managed Studio root
if it carries installer-written sentinels (studio.conf or the bin shim).
Avoids over-matching on a dev venv that happens to be named unsloth_studio.
"""
shim_name = "unsloth.exe" if platform.system() == "Windows" else "unsloth"
return (
(candidate / "share" / "studio.conf").is_file()
or (candidate / "bin" / shim_name).exists()
)
def _resolve_studio_home() -> tuple[Path, bool]:
override = os.environ.get("UNSLOTH_STUDIO_HOME") or os.environ.get("STUDIO_HOME")
if override:
@ -37,7 +49,8 @@ def _resolve_studio_home() -> tuple[Path, bool]:
if prefix.name == "unsloth_studio":
inferred = prefix.parent
legacy = (Path.home() / ".unsloth" / "studio").resolve()
return inferred, inferred != legacy
if inferred != legacy and _looks_like_installer_managed_studio_home(inferred):
return inferred, True
except (OSError, ValueError):
pass
return Path.home() / ".unsloth" / "studio", False
@ -48,8 +61,12 @@ STUDIO_HOME, _STUDIO_HOME_IS_CUSTOM = _resolve_studio_home()
# backend run.py) inherit it. Default installs MUST NOT re-export the env
# var, because setup.sh / setup.ps1 treat its presence as "env-override
# mode" and would relocate llama.cpp / DATA_DIR away from legacy paths.
# UNSLOTH_LLAMA_CPP_PATH is also setdefault for custom roots so
# unsloth-zoo's import-time LLAMA_CPP_DEFAULT_DIR binding picks up the
# correct build dir for GGUF export.
if _STUDIO_HOME_IS_CUSTOM:
os.environ.setdefault("UNSLOTH_STUDIO_HOME", str(STUDIO_HOME))
os.environ.setdefault("UNSLOTH_LLAMA_CPP_PATH", str(STUDIO_HOME / "llama.cpp"))
BOOTSTRAP_PASSWORD_FILE = ".bootstrap_password"
DESKTOP_SECRET_FILE = ".desktop_secret"
DEFAULT_ADMIN_USERNAME = "unsloth"