mirror of
https://github.com/unslothai/unsloth.git
synced 2026-05-02 13:20:28 +00:00
studio: setup log styling (#4494)
* refactor(studio): unify setup terminal output style and add verbose setup mode * studio(windows): align setup.ps1 banner/steps with setup.sh (ANSI, verbose) * studio(setup): revert nvcc path reordering to match main * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio(setup): restore fail-fast llama.cpp setup flow * studio(banner): use IPv6 loopback URL when binding :: or ::1 * Fix IPv6 URL bracketing, try_quiet stderr, _step label clamp - Bracket IPv6 display_host in external_url to produce clickable URLs - Redirect try_quiet failure log to stderr instead of stdout - Clamp _step label to column width to prevent negative padding * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add sandbox integration tests for PR #4494 UX fixes Simulation harness (tests/simulate_pr4494.py) creates an isolated uv venv, copies the real source files into it, and runs subprocess tests for all three fixes with visual before/after demos and edge cases. Standalone bash test (tests/test_try_quiet.sh) validates try_quiet stderr redirect across 8 scenarios including broken-version contrast. 39 integration tests total (14 IPv6 + 15 try_quiet + 10 _step), all existing 75 unit tests still pass. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Truncate step() labels in setup.sh to match PS1 and Python The %-15s printf format pads short labels but does not truncate long ones. Change to %-15.15s so labels wider than 15 chars are clipped, matching the PowerShell .Substring(0,15) and Python label[:15] logic. * Remove sandbox integration tests from PR These test files are not part of the styling fix and should not ship with this PR. * Show error output on failure instead of suppressing it - install_python_stack.py: restore _red for patch_package_file warnings (was downgraded to _dim) - setup.ps1: capture winget output and show on failure for CUDA, Node, Python, and OpenSSL installs (was piped to Out-Null) - setup.ps1: always show git pull failure warning, not just in verbose mode * Show winget error output for Git and CMake installs on failure Same capture-and-print-on-failure pattern already used for Node, Python, CUDA, and OpenSSL winget installs. * fix: preserve stderr for _run_quiet error messages in setup.sh The step() helper writes to stdout, but _run_quiet's error header was originally sent to stderr (>&2). Without the redirect, callers that separate stdout/stderr would miss the failure headline while still seeing the log body on stderr. Add >&2 to both step calls inside _run_quiet to match main's behavior. * feat: add --verbose flag to setup and update commands Wire UNSLOTH_VERBOSE=1 through _run_setup_script() so that 'unsloth studio update --verbose' (and the deprecated 'setup') passes the flag to setup.sh / setup.ps1 / install_python_stack.py. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
parent
3a5e3bbd6d
commit
0233fe7f9c
6 changed files with 489 additions and 283 deletions
|
|
@ -45,6 +45,7 @@ NO_TORCH = _infer_no_torch()
|
|||
# -- Verbosity control ----------------------------------------------------------
|
||||
# By default the installer shows a minimal progress bar (one line, in-place).
|
||||
# Set UNSLOTH_VERBOSE=1 in the environment to restore full per-step output:
|
||||
# CLI: unsloth studio setup --verbose
|
||||
# Linux/Mac: UNSLOTH_VERBOSE=1 ./studio/setup.sh
|
||||
# Windows: $env:UNSLOTH_VERBOSE="1" ; .\studio\setup.ps1
|
||||
VERBOSE: bool = os.environ.get("UNSLOTH_VERBOSE", "0") == "1"
|
||||
|
|
@ -96,15 +97,18 @@ def _safe_print(*args: object, **kwargs: object) -> None:
|
|||
)
|
||||
|
||||
|
||||
# -- Color support ------------------------------------------------------
|
||||
# ── Color support ──────────────────────────────────────────────────────
|
||||
# Same logic as startup_banner: NO_COLOR disables, FORCE_COLOR or TTY enables.
|
||||
|
||||
|
||||
def _enable_colors() -> bool:
|
||||
"""Try to enable ANSI color support. Returns True if available."""
|
||||
if not hasattr(sys.stdout, "fileno"):
|
||||
def _stdout_supports_color() -> bool:
|
||||
"""True if we should emit ANSI colors (matches startup_banner)."""
|
||||
if os.environ.get("NO_COLOR", "").strip():
|
||||
return False
|
||||
if os.environ.get("FORCE_COLOR", "").strip():
|
||||
return True
|
||||
try:
|
||||
if not os.isatty(sys.stdout.fileno()):
|
||||
if not sys.stdout.isatty():
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
|
@ -113,24 +117,26 @@ def _enable_colors() -> bool:
|
|||
import ctypes
|
||||
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
# Enable ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x0004) on stdout
|
||||
handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
|
||||
handle = kernel32.GetStdHandle(-11)
|
||||
mode = ctypes.c_ulong()
|
||||
kernel32.GetConsoleMode(handle, ctypes.byref(mode))
|
||||
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
return True # Unix terminals support ANSI by default
|
||||
return True
|
||||
|
||||
|
||||
# Colors disabled -- Colab and most CI runners render ANSI fine, but plain output
|
||||
# is cleaner in the notebook cell. Re-enable by setting _HAS_COLOR = _enable_colors()
|
||||
_HAS_COLOR = False
|
||||
_HAS_COLOR = _stdout_supports_color()
|
||||
|
||||
|
||||
# Column layout — matches setup.sh step() helper:
|
||||
# 2-space indent, 15-char label (dim), then value.
|
||||
_LABEL = "deps"
|
||||
_COL = 15
|
||||
|
||||
|
||||
def _green(msg: str) -> str:
|
||||
return f"\033[92m{msg}\033[0m" if _HAS_COLOR else msg
|
||||
return f"\033[38;5;108m{msg}\033[0m" if _HAS_COLOR else msg
|
||||
|
||||
|
||||
def _cyan(msg: str) -> str:
|
||||
|
|
@ -141,21 +147,39 @@ def _red(msg: str) -> str:
|
|||
return f"\033[91m{msg}\033[0m" if _HAS_COLOR else msg
|
||||
|
||||
|
||||
def _progress(label: str) -> None:
|
||||
"""Print an in-place progress bar for the current install step.
|
||||
def _dim(msg: str) -> str:
|
||||
return f"\033[38;5;245m{msg}\033[0m" if _HAS_COLOR else msg
|
||||
|
||||
Uses only stdlib (sys.stdout) -- no extra packages required.
|
||||
In VERBOSE mode this is a no-op; per-step labels are printed by run() instead.
|
||||
"""
|
||||
|
||||
def _title(msg: str) -> str:
|
||||
return f"\033[38;5;150m{msg}\033[0m" if _HAS_COLOR else msg
|
||||
|
||||
|
||||
_RULE = "\u2500" * 52
|
||||
|
||||
|
||||
def _step(label: str, value: str, color_fn = None) -> None:
|
||||
"""Print a single step line in the column format."""
|
||||
if color_fn is None:
|
||||
color_fn = _green
|
||||
padded = label[:_COL]
|
||||
print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}")
|
||||
|
||||
|
||||
def _progress(label: str) -> None:
|
||||
"""Print an in-place progress bar aligned to the step column layout."""
|
||||
global _STEP
|
||||
_STEP += 1
|
||||
if VERBOSE:
|
||||
return # verbose mode: run() already printed the label
|
||||
return
|
||||
width = 20
|
||||
filled = int(width * _STEP / _TOTAL)
|
||||
bar = "=" * filled + "-" * (width - filled)
|
||||
end = "\n" if _STEP >= _TOTAL else "" # newline only on the final step
|
||||
sys.stdout.write(f"\r[{bar}] {_STEP:2}/{_TOTAL} {label:<40}{end}")
|
||||
pad = " " * (_COL - len(_LABEL))
|
||||
end = "\n" if _STEP >= _TOTAL else ""
|
||||
sys.stdout.write(
|
||||
f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
|
|
@ -164,14 +188,14 @@ def run(
|
|||
) -> subprocess.CompletedProcess[bytes]:
|
||||
"""Run a command; on failure print output and exit."""
|
||||
if VERBOSE:
|
||||
print(f" {label}...")
|
||||
_step(_LABEL, f"{label}...", _dim)
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
stdout = subprocess.PIPE if quiet else None,
|
||||
stderr = subprocess.STDOUT if quiet else None,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_safe_print(_red(f"❌ {label} failed (exit code {result.returncode}):"))
|
||||
_step("error", f"{label} failed (exit code {result.returncode})", _red)
|
||||
if result.stdout:
|
||||
print(result.stdout.decode(errors = "replace"))
|
||||
sys.exit(result.returncode)
|
||||
|
|
@ -353,9 +377,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
|
|||
text = True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_safe_print(
|
||||
_red(f" ⚠️ Could not find package {package_name}, skipping patch")
|
||||
)
|
||||
_step(_LABEL, f"package {package_name} not found, skipping patch", _red)
|
||||
return
|
||||
|
||||
location = None
|
||||
|
|
@ -365,11 +387,11 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None:
|
|||
break
|
||||
|
||||
if not location:
|
||||
_safe_print(_red(f" ⚠️ Could not determine location of {package_name}"))
|
||||
_step(_LABEL, f"could not locate {package_name}", _red)
|
||||
return
|
||||
|
||||
dest = Path(location) / relative_path
|
||||
print(_cyan(f" Patching {dest.name} in {package_name}..."))
|
||||
_step(_LABEL, f"patching {dest.name} in {package_name}...", _dim)
|
||||
download_file(url, dest)
|
||||
|
||||
|
||||
|
|
@ -633,7 +655,7 @@ def install_python_stack() -> int:
|
|||
stderr = subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
_safe_print(_green("✅ Python dependencies installed"))
|
||||
_step(_LABEL, "installed")
|
||||
return 0
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue