# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 from __future__ import annotations import os from pathlib import Path import tempfile def studio_root() -> Path: return Path.home() / ".unsloth" / "studio" def cache_root() -> Path: """Central cache directory for all studio downloads (models, datasets, etc.).""" return Path.home() / ".unsloth" / "studio" / "cache" def assets_root() -> Path: return studio_root() / "assets" def datasets_root() -> Path: return assets_root() / "datasets" def dataset_uploads_root() -> Path: return datasets_root() / "uploads" def recipe_datasets_root() -> Path: return datasets_root() / "recipes" def outputs_root() -> Path: return studio_root() / "outputs" def exports_root() -> Path: return studio_root() / "exports" def auth_root() -> Path: return studio_root() / "auth" def auth_db_path() -> Path: return auth_root() / "auth.db" def tmp_root() -> Path: return Path(tempfile.gettempdir()) / "unsloth-studio" def seed_uploads_root() -> Path: return tmp_root() / "seed-uploads" def unstructured_seed_cache_root() -> Path: return tmp_root() / "unstructured-seed-cache" def oxc_validator_tmp_root() -> Path: return tmp_root() / "oxc-validator" def tensorboard_root() -> Path: return studio_root() / "runs" def ensure_dir(path: Path) -> Path: path.mkdir(parents = True, exist_ok = True) return path def _setup_cache_env() -> None: """Set cache environment variables for HuggingFace, uv, and vLLM. Only sets variables that are not already set by the user, so explicit overrides (e.g. HF_HOME=/data/hf) are respected. Works on Linux, macOS, and Windows. """ root = cache_root() hf_dir = root / "huggingface" defaults = { "HF_HOME": str(hf_dir), "HF_HUB_CACHE": str(hf_dir / "hub"), "HF_XET_CACHE": str(hf_dir / "xet"), "UV_CACHE_DIR": str(root / "uv"), "VLLM_CACHE_ROOT": str(root / "vllm"), } for key, value in defaults.items(): if key not in os.environ: os.environ[key] = value Path(value).mkdir(parents = True, exist_ok = True) def ensure_studio_directories() -> None: """Create all standard studio directories on startup.""" for dir_fn in ( studio_root, assets_root, datasets_root, dataset_uploads_root, recipe_datasets_root, outputs_root, exports_root, auth_root, tensorboard_root, ): ensure_dir(dir_fn()) _setup_cache_env() def _clean_relative_path( path_value: str, *, strip_prefixes: tuple[str, ...] = () ) -> Path: path = Path(path_value).expanduser() parts = [part for part in path.parts if part not in ("", ".")] while parts and parts[0] in strip_prefixes: parts = parts[1:] return Path(*parts) if parts else Path() def resolve_under_root( path_value: str | None, *, root: Path, strip_prefixes: tuple[str, ...] = (), ) -> Path: if not path_value or not str(path_value).strip(): return root path = Path(str(path_value).strip()).expanduser() if path.is_absolute(): return path cleaned = _clean_relative_path(str(path), strip_prefixes = strip_prefixes) return root / cleaned def resolve_output_dir(path_value: str | None = None) -> Path: return resolve_under_root( path_value, root = outputs_root(), strip_prefixes = ("outputs",), ) def resolve_export_dir(path_value: str | None = None) -> Path: return resolve_under_root( path_value, root = exports_root(), strip_prefixes = ("exports",), ) def resolve_tensorboard_dir(path_value: str | None = None) -> Path: return resolve_under_root( path_value, root = tensorboard_root(), strip_prefixes = ("runs", "tensorboard"), ) def resolve_dataset_path(path_value: str) -> Path: path = Path(path_value).expanduser() if path.is_absolute(): return path parts = [part for part in Path(path_value).parts if part not in ("", ".")] if parts[:2] == ["assets", "datasets"]: parts = parts[2:] if parts and parts[0] == "uploads": cleaned = Path(*parts[1:]) if len(parts) > 1 else Path() return dataset_uploads_root() / cleaned if parts and parts[0] == "recipes": cleaned = Path(*parts[1:]) if len(parts) > 1 else Path() return recipe_datasets_root() / cleaned cleaned = Path(*parts) if parts else Path() candidates = [ dataset_uploads_root() / cleaned, recipe_datasets_root() / cleaned, datasets_root() / cleaned, dataset_uploads_root() / cleaned.name, recipe_datasets_root() / cleaned.name, ] for candidate in candidates: if candidate.exists(): return candidate return candidates[0]