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:
parent
151070cdb8
commit
19454e7a6c
9 changed files with 196 additions and 43 deletions
30
install.ps1
30
install.ps1
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
install.sh
25
install.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
116
studio/src-tauri/src/studio_root.rs
Normal file
116
studio/src-tauri/src/studio_root.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue