studio: tauri studio_root helper + marker-file persistence + ~ expansion

Address cycle-5 reviewer findings:

- Add studio/src-tauri/src/studio_root.rs: shared resolver with
  UNSLOTH_STUDIO_HOME / STUDIO_HOME (priority order), tilde expansion
  (~, ~/..., ~\...), installer-written marker fallback, then
  ~/.unsloth/studio. 5 unit tests cover the expansion paths.

- Tauri lookups now go through the shared resolver:
  - process.rs::find_unsloth_binary
  - desktop_auth.rs::desktop_secret_path
  - main.rs::setup_logging (tauri.log under custom root)
  - commands.rs::open_logs_dir (opens custom root dir)
  - install.rs work_dir uses parent of resolved root (avoids creating
    a stray ~/.unsloth on a custom-root install)

- install.sh / install.ps1 (env-mode only): write
  ~/.unsloth/studio-home marker so the desktop app launched from
  Finder/Start Menu (no shell env inheritance) still resolves the
  custom root.

- install.sh / install.ps1 non-interactive completion: when
  StudioRedirectMode=env, print the absolute custom-root shim path
  since the persistent rc/registry PATH update is intentionally
  skipped in env-override mode.

- unsloth_cli/commands/studio.py: replace setdefault() with
  truthy-check so a blank UNSLOTH_STUDIO_HOME / UNSLOTH_LLAMA_CPP_PATH
  in the parent env doesn't suppress the inferred custom root.

40/40 cargo test --bins pass.
This commit is contained in:
Daniel Han 2026-04-26 17:03:28 +00:00
commit 19454e7a6c
9 changed files with 196 additions and 43 deletions

View file

@ -433,6 +433,21 @@ function Install-UnslothStudio {
}
$_sq = $StudioHome -replace "'", "''"
$_llama = $_llamaPath -replace "'", "''"
# Marker file so the Tauri desktop app (launched from
# Start Menu / Desktop, where the launching shell env vars
# aren't inherited) can still resolve the custom root.
try {
$_markerDir = Join-Path $env:USERPROFILE ".unsloth"
if (-not (Test-Path $_markerDir)) {
New-Item -ItemType Directory -Path $_markerDir -Force | Out-Null
}
Set-Content -LiteralPath (Join-Path $_markerDir "studio-home") `
-Value $StudioHome -NoNewline -ErrorAction Stop
} catch {
# Non-fatal: env var still works for shells that inherit it.
}
"`$env:UNSLOTH_STUDIO_HOME = '$_sq'`n`$env:UNSLOTH_LLAMA_CPP_PATH = '$_llama'`n"
} else { "" }
@ -1280,8 +1295,19 @@ shell.Run cmd, 0, False
& $UnslothExe studio -H 0.0.0.0 -p 8888
} else {
step "launch" "manual commands:"
substep "& `"$VenvDir\Scripts\Activate.ps1`""
substep "unsloth studio -H 0.0.0.0 -p 8888"
if ($StudioRedirectMode -eq 'env') {
# Env-override mode skips persistent registry PATH update, so
# `unsloth` may not resolve in a fresh shell. Print the
# absolute shim path so callers can launch directly.
$_shim = Join-Path $StudioHome "bin\unsloth.exe"
substep "& `"$_shim`" studio -H 0.0.0.0 -p 8888"
substep "or activate env first:"
substep "& `"$VenvDir\Scripts\Activate.ps1`""
substep "unsloth studio -H 0.0.0.0 -p 8888"
} else {
substep "& `"$VenvDir\Scripts\Activate.ps1`""
substep "unsloth studio -H 0.0.0.0 -p 8888"
}
Write-Host ""
}
}

View file

@ -625,6 +625,12 @@ LAUNCHER_EOF
_css_quoted_llama=$(printf '%s' "$_css_llama_path" | sed "s/'/'\\\\''/g")
printf '%s\n' "export UNSLOTH_STUDIO_HOME='$_css_quoted_home'"
printf '%s\n' "export UNSLOTH_LLAMA_CPP_PATH='$_css_quoted_llama'"
# Marker file so the Tauri desktop app (launched from
# Finder/Start Menu/Desktop, where the launching shell's env
# vars are not inherited) can still resolve the custom root.
mkdir -p "$HOME/.unsloth" 2>/dev/null || true
printf '%s\n' "$STUDIO_HOME" > "$HOME/.unsloth/studio-home" 2>/dev/null || true
fi
} > "$_css_data_dir/studio.conf"
@ -1822,9 +1828,20 @@ if [ -t 1 ]; then
exit "$_LAUNCH_EXIT"
else
step "launch" "manual commands:"
substep "unsloth studio -H 0.0.0.0 -p 8888"
substep "or activate env first:"
substep "source ${VENV_DIR}/bin/activate"
substep "unsloth studio -H 0.0.0.0 -p 8888"
if [ "$_STUDIO_HOME_REDIRECT" = "env" ]; then
# In env-override mode we deliberately skip the persistent shell
# rc PATH append, so a fresh shell will not have `unsloth` on PATH
# unless the caller re-exports UNSLOTH_STUDIO_HOME. Print the
# absolute shim path and the activate-then-run alternative.
substep "${_LOCAL_BIN}/unsloth studio -H 0.0.0.0 -p 8888"
substep "or activate env first:"
substep "source ${VENV_DIR}/bin/activate"
substep "unsloth studio -H 0.0.0.0 -p 8888"
else
substep "unsloth studio -H 0.0.0.0 -p 8888"
substep "or activate env first:"
substep "source ${VENV_DIR}/bin/activate"
substep "unsloth studio -H 0.0.0.0 -p 8888"
fi
echo ""
fi

View file

@ -184,8 +184,8 @@ pub fn get_server_logs(state: tauri::State<'_, BackendState>) -> Vec<String> {
/// Open the Unsloth Studio directory in the system file manager.
#[tauri::command]
pub fn open_logs_dir() -> Result<(), String> {
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
let dir = home.join(".unsloth").join("studio");
let dir = crate::studio_root::resolve_studio_root()
.ok_or("Could not determine Studio install root")?;
if !dir.exists() {
return Err(format!("Directory does not exist: {}", dir.display()));

View file

@ -69,19 +69,11 @@ fn home_dir() -> Result<PathBuf, String> {
dirs::home_dir().ok_or_else(|| "Could not determine home directory".to_string())
}
fn studio_root_from_env() -> Option<PathBuf> {
for var in ["UNSLOTH_STUDIO_HOME", "STUDIO_HOME"] {
if let Some(value) = std::env::var_os(var) {
if !value.is_empty() {
return Some(PathBuf::from(value));
}
}
}
None
}
fn desktop_secret_path() -> Result<PathBuf, String> {
if let Some(studio) = studio_root_from_env() {
// Use the shared studio_root resolver so env vars (with ~ expansion)
// and the installer-written marker file are honored before falling
// back to ~/.unsloth/studio.
if let Some(studio) = crate::studio_root::resolve_studio_root() {
return Ok(auth_secret_path_in_studio(&studio, ".desktop_secret"));
}
Ok(auth_secret_path(&home_dir()?, ".desktop_secret"))

View file

@ -146,9 +146,13 @@ fn spawn_script(
install.intentional_stop = false;
install.needed_packages.clear();
// Scripts create ~/.unsloth/studio/ themselves, but need a writable cwd.
let home = dirs::home_dir().ok_or("Could not determine home directory")?;
let work_dir = home.join(".unsloth");
// Scripts create the Studio root themselves, but need a writable cwd.
// Resolve via the shared helper so a custom-root install does not
// create a stray ~/.unsloth on the user's machine.
let work_dir = crate::studio_root::resolve_studio_root()
.and_then(|studio| studio.parent().map(|p| p.to_path_buf()))
.or_else(|| dirs::home_dir().map(|h| h.join(".unsloth")))
.ok_or("Could not determine Studio install root")?;
if !work_dir.exists() {
std::fs::create_dir_all(&work_dir)
.map_err(|e| format!("Failed to create {}: {}", work_dir.display(), e))?;

View file

@ -5,6 +5,7 @@ mod desktop_auth;
mod install;
mod preflight;
mod process;
mod studio_root;
mod update;
mod windows_job;
@ -29,9 +30,10 @@ fn setup_logging() {
simplelog::ColorChoice::Auto,
));
// Try to set up file logging to ~/.unsloth/studio/tauri.log
if let Some(home) = dirs::home_dir() {
let log_dir = home.join(".unsloth").join("studio");
// File logging to <studio_root>/tauri.log. Honors UNSLOTH_STUDIO_HOME /
// STUDIO_HOME (with ~ expansion) and the installer-written marker file
// before falling back to ~/.unsloth/studio.
if let Some(log_dir) = studio_root::resolve_studio_root() {
if fs::create_dir_all(&log_dir).is_ok() {
let log_path = log_dir.join("tauri.log");
let rotated_path = log_dir.join("tauri.log.1");

View file

@ -111,23 +111,15 @@ 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);
}
}
// Resolve via the shared studio_root helper so env vars (with ~
// expansion) and the installer-written marker file are honored. Falls
// back to ~/.unsloth/studio when nothing else applies.
if let Some(studio) = crate::studio_root::resolve_studio_root() {
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)
None
}
#[cfg(test)]

View file

@ -0,0 +1,116 @@
//! Shared resolver for the Studio install root inside the Tauri desktop app.
//!
//! Priority (highest first):
//! 1. `UNSLOTH_STUDIO_HOME` / `STUDIO_HOME` env vars (current process).
//! 2. `~/.unsloth/studio-home` marker file written by the installer in
//! env-override mode, so the desktop app launched from
//! Finder/Start Menu/Desktop (where the launching shell's env vars are
//! not inherited) still resolves the custom root.
//! 3. Legacy default `~/.unsloth/studio`.
//!
//! Mirrors the shell / PowerShell / Python resolvers in this PR so a
//! `UNSLOTH_STUDIO_HOME=~/studio` value is interpreted the same in every
//! component.
use std::ffi::OsString;
use std::path::PathBuf;
/// Expand a leading `~`, `~/...`, or `~\...` against `dirs::home_dir()`.
/// Empty values return `None`. Other absolute or relative paths pass
/// through unchanged.
pub fn expand_studio_home_value(value: OsString) -> Option<PathBuf> {
if value.is_empty() {
return None;
}
if let Some(text) = value.to_str() {
if text == "~" {
return dirs::home_dir();
}
if let Some(rest) = text
.strip_prefix("~/")
.or_else(|| text.strip_prefix("~\\"))
{
return dirs::home_dir().map(|home| home.join(rest));
}
}
Some(PathBuf::from(value))
}
fn studio_root_from_env() -> Option<PathBuf> {
for var in ["UNSLOTH_STUDIO_HOME", "STUDIO_HOME"] {
if let Some(value) = std::env::var_os(var) {
if let Some(path) = expand_studio_home_value(value) {
return Some(path);
}
}
}
None
}
/// Marker file the installer writes (in env-override mode) so a fresh
/// desktop launch with no shell env vars can still find the custom root.
fn studio_root_from_marker() -> Option<PathBuf> {
let home = dirs::home_dir()?;
let marker = home.join(".unsloth").join("studio-home");
let raw = std::fs::read_to_string(&marker).ok()?;
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
expand_studio_home_value(OsString::from(trimmed))
}
/// Resolve the Studio install root using the priority chain. Returns
/// `None` only when the home directory cannot be determined and no env
/// override is set (extremely rare on supported platforms).
pub fn resolve_studio_root() -> Option<PathBuf> {
if let Some(p) = studio_root_from_env() {
return Some(p);
}
if let Some(p) = studio_root_from_marker() {
return Some(p);
}
let home = dirs::home_dir()?;
Some(home.join(".unsloth").join("studio"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn expand_handles_literal_paths() {
let out = expand_studio_home_value(OsString::from("/srv/foo")).unwrap();
assert_eq!(out, Path::new("/srv/foo"));
}
#[test]
fn expand_returns_none_for_empty() {
assert!(expand_studio_home_value(OsString::new()).is_none());
}
#[test]
fn expand_handles_tilde_only() {
let out = expand_studio_home_value(OsString::from("~"));
if let Some(home) = dirs::home_dir() {
assert_eq!(out.unwrap(), home);
}
}
#[test]
fn expand_handles_tilde_slash_prefix() {
let out = expand_studio_home_value(OsString::from("~/studio")).unwrap();
if let Some(home) = dirs::home_dir() {
assert_eq!(out, home.join("studio"));
}
}
#[test]
fn expand_handles_tilde_backslash_prefix() {
let out = expand_studio_home_value(OsString::from("~\\studio")).unwrap();
if let Some(home) = dirs::home_dir() {
assert_eq!(out, home.join("studio"));
}
}
}

View file

@ -66,7 +66,10 @@ STUDIO_HOME, _STUDIO_HOME_IS_CUSTOM = _resolve_studio_home()
# 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))
# Use truthy-check rather than setdefault so a blank env var (e.g.
# UNSLOTH_STUDIO_HOME=) doesn't suppress the inferred custom root.
if not os.environ.get("UNSLOTH_STUDIO_HOME"):
os.environ["UNSLOTH_STUDIO_HOME"] = str(STUDIO_HOME)
# Mirror setup.sh / setup.ps1's legacy-equality check: when an env
# override happens to equal the legacy default, llama.cpp still lives
# at ~/.unsloth/llama.cpp (one shared build across legacy installs).
@ -75,7 +78,8 @@ if _STUDIO_HOME_IS_CUSTOM:
_llama_dir = Path.home() / ".unsloth" / "llama.cpp"
else:
_llama_dir = STUDIO_HOME / "llama.cpp"
os.environ.setdefault("UNSLOTH_LLAMA_CPP_PATH", str(_llama_dir))
if not os.environ.get("UNSLOTH_LLAMA_CPP_PATH"):
os.environ["UNSLOTH_LLAMA_CPP_PATH"] = str(_llama_dir)
BOOTSTRAP_PASSWORD_FILE = ".bootstrap_password"
DESKTOP_SECRET_FILE = ".desktop_secret"
DEFAULT_ADMIN_USERNAME = "unsloth"