Studio: add UNSLOTH_SKIP_AUTOSTART installer flag (#7093)
* Studio: add installer autostart opt-out * CI: run installer autostart tests cross-platform * Tests: combine Studio installer skip flags
This commit is contained in:
parent
2a22da9fd7
commit
ca979e9643
6 changed files with 169 additions and 10 deletions
14
.github/workflows/cross-platform-parity-ci.yml
vendored
14
.github/workflows/cross-platform-parity-ci.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
# Runs tests/python/test_cross_platform_parity.py on Windows and macOS.
|
||||
# Runs installer parity and autostart opt-out tests on Windows and macOS.
|
||||
#
|
||||
# Why: that test is the guard that install.sh and install.ps1 stay in
|
||||
# sync, but today it only runs on ubuntu-latest (auto-discovered by
|
||||
|
|
@ -21,6 +21,7 @@ on:
|
|||
paths:
|
||||
- 'install.sh'
|
||||
- 'install.ps1'
|
||||
- 'tests/test_installer_skip_autostart.py'
|
||||
- 'tests/python/test_cross_platform_parity.py'
|
||||
- '.github/workflows/cross-platform-parity-ci.yml'
|
||||
push:
|
||||
|
|
@ -28,6 +29,7 @@ on:
|
|||
paths:
|
||||
- 'install.sh'
|
||||
- 'install.ps1'
|
||||
- 'tests/test_installer_skip_autostart.py'
|
||||
- 'tests/python/test_cross_platform_parity.py'
|
||||
- '.github/workflows/cross-platform-parity-ci.yml'
|
||||
workflow_dispatch:
|
||||
|
|
@ -57,5 +59,11 @@ jobs:
|
|||
python-version: '3.12'
|
||||
cache: 'pip'
|
||||
- run: python -m pip install -U pip pytest
|
||||
- name: Cross-platform parity test
|
||||
run: python -m pytest tests/python/test_cross_platform_parity.py -q
|
||||
- name: Cross-platform parity tests
|
||||
env:
|
||||
UNSLOTH_NO_TORCH: '1'
|
||||
run: >-
|
||||
python -m pytest
|
||||
tests/python/test_cross_platform_parity.py
|
||||
tests/test_installer_skip_autostart.py
|
||||
-q
|
||||
|
|
|
|||
|
|
@ -230,6 +230,14 @@ curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_NO_TORCH=1 sh
|
|||
$env:UNSLOTH_NO_TORCH=1; irm https://unsloth.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
Skip the post-install prompt that starts Studio (useful for automated installs):
|
||||
```bash
|
||||
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_SKIP_AUTOSTART=1 sh
|
||||
```
|
||||
```powershell
|
||||
$env:UNSLOTH_SKIP_AUTOSTART=1; irm https://unsloth.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
Pin the Python version:
|
||||
```bash
|
||||
curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
# irm | iex cannot forward arguments, so web installs take options as env vars set
|
||||
# before the pipe (flags still work via .\install.ps1):
|
||||
# $env:UNSLOTH_NO_TORCH=1; irm https://unsloth.ai/install.ps1 | iex # skip PyTorch (GGUF-only)
|
||||
# $env:UNSLOTH_SKIP_AUTOSTART=1; irm https://unsloth.ai/install.ps1 | iex # do not prompt to launch
|
||||
# $env:UNSLOTH_PYTHON='3.12'; irm https://unsloth.ai/install.ps1 | iex # pin Python version
|
||||
# $env:UNSLOTH_STUDIO_HOME='C:\path'; irm https://unsloth.ai/install.ps1 | iex
|
||||
# .\install.ps1 --no-torch # equivalent flag
|
||||
|
|
@ -98,6 +99,7 @@ function Install-UnslothStudio {
|
|||
$RepoRoot = ""
|
||||
$TauriMode = $false
|
||||
$SkipTorch = $false
|
||||
$SkipAutostart = $false
|
||||
$ShortcutsOnly = $false
|
||||
$WithLlamaCppDir = ""
|
||||
$argList = $args
|
||||
|
|
@ -130,6 +132,7 @@ function Install-UnslothStudio {
|
|||
|
||||
# Env-var equivalent for web installs; an explicit flag still wins.
|
||||
if ($env:UNSLOTH_NO_TORCH -in @('1', 'true', 'yes', 'on')) { $SkipTorch = $true }
|
||||
if ($env:UNSLOTH_SKIP_AUTOSTART -in @('1', 'true', 'yes', 'on')) { $SkipAutostart = $true }
|
||||
|
||||
# Propagate to child processes so they also respect verbose mode.
|
||||
# Process-scoped -- does not persist.
|
||||
|
|
@ -2612,9 +2615,10 @@ exit 0
|
|||
# Diagnostic only; never block install on a probe failure.
|
||||
}
|
||||
|
||||
# In interactive terminals, ask the user before starting Studio.
|
||||
# In interactive terminals, ask the user before starting Studio unless the
|
||||
# caller explicitly disabled the post-install prompt.
|
||||
# In non-interactive environments (CI, Docker) just print instructions.
|
||||
$IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
|
||||
$IsInteractive = (-not $SkipAutostart) -and [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)
|
||||
if ($IsInteractive) {
|
||||
Write-Host ""
|
||||
$reply = Read-Host " Start Unsloth Studio now? [Y/n]"
|
||||
|
|
|
|||
13
install.sh
13
install.sh
|
|
@ -8,8 +8,9 @@
|
|||
#
|
||||
# Piped installs take options as env vars after the pipe (a bare `| sh --no-torch`
|
||||
# makes sh reject --no-torch as its own option). Flags still work via ./install.sh:
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_NO_TORCH=1 sh # skip PyTorch (GGUF-only)
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh # pin Python version
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_NO_TORCH=1 sh # skip PyTorch (GGUF-only)
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_SKIP_AUTOSTART=1 sh # do not prompt to launch
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_PYTHON=3.12 sh # pin Python version
|
||||
# curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh
|
||||
# Equivalent flags: ./install.sh --no-torch --python 3.12 (or pipe them: sh -s -- --no-torch)
|
||||
#
|
||||
|
|
@ -49,6 +50,7 @@ PACKAGE_NAME="unsloth"
|
|||
TAURI_MODE=false
|
||||
_USER_PYTHON=""
|
||||
_NO_TORCH_FLAG=false
|
||||
_SKIP_AUTOSTART=false
|
||||
_VERBOSE=false
|
||||
_SHORTCUTS_ONLY=false
|
||||
_next_is_package=false
|
||||
|
|
@ -88,6 +90,7 @@ done
|
|||
|
||||
# Env-var equivalents for piped installs; an explicit flag still wins.
|
||||
case "${UNSLOTH_NO_TORCH:-}" in 1|true|TRUE|yes|YES|on|ON) _NO_TORCH_FLAG=true ;; esac
|
||||
case "${UNSLOTH_SKIP_AUTOSTART:-}" in 1|true|TRUE|yes|YES|on|ON) _SKIP_AUTOSTART=true ;; esac
|
||||
[ -z "$_USER_PYTHON" ] && [ -n "${UNSLOTH_PYTHON:-}" ] && _USER_PYTHON="$UNSLOTH_PYTHON"
|
||||
|
||||
if [ "$_VERBOSE" = true ]; then
|
||||
|
|
@ -1631,6 +1634,7 @@ _maybe_reroute_strixhalo_to_2404() {
|
|||
# Forward explicit ROCm-bootstrap consent (e.g. Tauri) so the child auto-enables the
|
||||
# GPU instead of falling back to the desktop-app prompt path.
|
||||
[ "${UNSLOTH_ROCM_WSL_AUTO:-0}" = "1" ] && _rr_exports="$_rr_exports; export UNSLOTH_ROCM_WSL_AUTO=1"
|
||||
[ "$_SKIP_AUTOSTART" = true ] && _rr_exports="$_rr_exports; export UNSLOTH_SKIP_AUTOSTART=1"
|
||||
_rr_args=""
|
||||
[ "$PACKAGE_NAME" != "unsloth" ] && _rr_args="$_rr_args --package $(_rr_q "$PACKAGE_NAME")"
|
||||
[ -n "$_USER_PYTHON" ] && _rr_args="$_rr_args --python $(_rr_q "$_USER_PYTHON")"
|
||||
|
|
@ -3223,9 +3227,10 @@ printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!"
|
|||
printf " ${C_DIM}%s${C_RST}\n" "$RULE"
|
||||
echo ""
|
||||
|
||||
# In interactive terminals, ask the user before starting Studio.
|
||||
# In interactive terminals, ask the user before starting Studio unless the
|
||||
# caller explicitly disabled the post-install prompt.
|
||||
# In non-interactive environments (Docker, CI, cloud-init) just print instructions.
|
||||
if [ -t 1 ]; then
|
||||
if [ "$_SKIP_AUTOSTART" != true ] && [ -t 1 ]; then
|
||||
echo ""
|
||||
printf " Start Unsloth Studio now? [Y/n] "
|
||||
# No readable answer (closed/EOF tty) defaults to no; Enter is still yes.
|
||||
|
|
|
|||
|
|
@ -309,7 +309,15 @@ if [ "$_rc" = "2" ]; then echo " PASS: tauri child exit 2 -> reroute propagates
|
|||
assert_absent "tauri exit 2 -> not a CPU fallback" "$_out" "__NOROUTE__"
|
||||
rm -rf "$_d"
|
||||
|
||||
# 27) Non-tauri mode: a child exit 2 is just a failure -> CPU fallback, not propagated.
|
||||
# 27) The post-install autostart opt-out must reach the target distro, where the
|
||||
# final launch prompt is evaluated.
|
||||
_d=$(make_fixture 1 strix 0 26.04 1)
|
||||
_out=$(run_func "$_d" _SKIP_AUTOSTART=true UNSLOTH_SKIP_AUTOSTART= \
|
||||
UNSLOTH_WSL_REROUTE_CMD='echo skip=[$UNSLOTH_SKIP_AUTOSTART]')
|
||||
assert_contains "UNSLOTH_SKIP_AUTOSTART forwarded to reroute" "$_out" "skip=[1]"
|
||||
rm -rf "$_d"
|
||||
|
||||
# 28) Non-tauri mode: a child exit 2 is just a failure -> CPU fallback, not propagated.
|
||||
_d=$(make_fixture 1 strix 0 26.04 1)
|
||||
_rc=0
|
||||
_out=$(run_func "$_d" UNSLOTH_WSL_REROUTE_CMD='exit 2') || _rc=$?
|
||||
|
|
|
|||
126
tests/test_installer_skip_autostart.py
Normal file
126
tests/test_installer_skip_autostart.py
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
"""Regression tests for the installers' post-install autostart opt-out."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
INSTALL_SH = REPO_ROOT / "install.sh"
|
||||
INSTALL_PS1 = REPO_ROOT / "install.ps1"
|
||||
README = REPO_ROOT / "README.md"
|
||||
|
||||
TRUTHY_VALUES = ("1", "true", "TRUE", "yes", "YES", "on", "ON")
|
||||
FALSEY_VALUES = ("", "0", "false", "no", "off", "anything-else")
|
||||
|
||||
|
||||
def _extract(pattern: str, source: str) -> str:
|
||||
match = re.search(pattern, source, flags = re.DOTALL)
|
||||
assert match is not None, f"installer block not found: {pattern}"
|
||||
return match.group(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[(value, "true") for value in TRUTHY_VALUES] + [(value, "false") for value in FALSEY_VALUES],
|
||||
)
|
||||
@pytest.mark.skipif(shutil.which("sh") is None, reason = "POSIX shell is unavailable")
|
||||
def test_posix_skip_autostart_value_parsing_with_no_torch(value: str, expected: str):
|
||||
source = INSTALL_SH.read_text(encoding = "utf-8")
|
||||
no_torch_parser = _extract(
|
||||
r'case "\$\{UNSLOTH_NO_TORCH:-\}" in.*?esac',
|
||||
source,
|
||||
)
|
||||
autostart_parser = _extract(
|
||||
r'case "\$\{UNSLOTH_SKIP_AUTOSTART:-\}" in.*?esac',
|
||||
source,
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["UNSLOTH_NO_TORCH"] = "1"
|
||||
env["UNSLOTH_SKIP_AUTOSTART"] = value
|
||||
result = subprocess.run(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
(
|
||||
f"_NO_TORCH_FLAG=false\n_SKIP_AUTOSTART=false\n{no_torch_parser}\n"
|
||||
f'{autostart_parser}\nprintf "%s %s" "$_NO_TORCH_FLAG" "$_SKIP_AUTOSTART"'
|
||||
),
|
||||
],
|
||||
check = True,
|
||||
capture_output = True,
|
||||
text = True,
|
||||
env = env,
|
||||
)
|
||||
assert result.stdout == f"true {expected}"
|
||||
|
||||
|
||||
def test_posix_skip_autostart_bypasses_only_the_interactive_prompt():
|
||||
source = INSTALL_SH.read_text(encoding = "utf-8")
|
||||
gate = 'if [ "$_SKIP_AUTOSTART" != true ] && [ -t 1 ]; then'
|
||||
assert gate in source
|
||||
assert source.index(gate) < source.index("Start Unsloth Studio now? [Y/n]")
|
||||
assert source.count("Start Unsloth Studio now? [Y/n]") == 1
|
||||
assert source.index("Start Unsloth Studio now? [Y/n]") < source.index(
|
||||
'step "launch" "manual commands:"'
|
||||
)
|
||||
assert "export UNSLOTH_SKIP_AUTOSTART=1" in source
|
||||
|
||||
|
||||
@pytest.mark.skipif(shutil.which("pwsh") is None, reason = "PowerShell is unavailable")
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[(value, "True") for value in TRUTHY_VALUES] + [(value, "False") for value in FALSEY_VALUES],
|
||||
)
|
||||
def test_windows_skip_autostart_value_parsing_with_no_torch(value: str, expected: str):
|
||||
source = INSTALL_PS1.read_text(encoding = "utf-8")
|
||||
parser = _extract(
|
||||
r"\$SkipTorch = \$false\s+\$SkipAutostart = \$false\s+.*?"
|
||||
r"if \(\$env:UNSLOTH_NO_TORCH -in @\('1', 'true', 'yes', 'on'\)\) "
|
||||
r"\{ \$SkipTorch = \$true \}\s+"
|
||||
r"if \(\$env:UNSLOTH_SKIP_AUTOSTART -in @\('1', 'true', 'yes', 'on'\)\) "
|
||||
r"\{ \$SkipAutostart = \$true \}",
|
||||
source,
|
||||
)
|
||||
env = os.environ.copy()
|
||||
env["UNSLOTH_NO_TORCH"] = "1"
|
||||
env["UNSLOTH_SKIP_AUTOSTART"] = value
|
||||
result = subprocess.run(
|
||||
[
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
f'{parser}; "$SkipTorch $SkipAutostart"',
|
||||
],
|
||||
check = True,
|
||||
capture_output = True,
|
||||
text = True,
|
||||
env = env,
|
||||
)
|
||||
assert result.stdout.strip() == f"True {expected}"
|
||||
|
||||
|
||||
def test_windows_skip_autostart_bypasses_only_the_interactive_prompt():
|
||||
source = INSTALL_PS1.read_text(encoding = "utf-8")
|
||||
gate = (
|
||||
"$IsInteractive = (-not $SkipAutostart) -and "
|
||||
"[Environment]::UserInteractive -and (-not [Console]::IsInputRedirected)"
|
||||
)
|
||||
assert gate in source
|
||||
assert source.index(gate) < source.index("Start Unsloth Studio now? [Y/n]")
|
||||
assert source.count("Start Unsloth Studio now? [Y/n]") == 1
|
||||
assert source.index("Start Unsloth Studio now? [Y/n]") < source.index(
|
||||
'step "launch" "manual commands:"'
|
||||
)
|
||||
|
||||
|
||||
def test_skip_autostart_is_documented_for_all_installers():
|
||||
readme = README.read_text(encoding = "utf-8")
|
||||
assert "UNSLOTH_SKIP_AUTOSTART=1 sh" in readme
|
||||
assert "$env:UNSLOTH_SKIP_AUTOSTART=1" in readme
|
||||
Loading…
Add table
Add a link
Reference in a new issue