From 54f25bf17e995c290a8a8c9e3d1472eddb2bd5bb Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 25 Jun 2026 04:01:43 -0700 Subject: [PATCH] Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491) (#6663) * Studio: UNSLOTH_NPM_REGISTRY opt-in for corporate npm mirrors (#6491) studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain lock. A project-level pin takes precedence over a user's ~/.npmrc, so behind a corporate firewall that blocks npmjs.org the frontend bun/npm install hit npmjs.org directly and failed with 403. Add an opt-in UNSLOTH_NPM_REGISTRY env var (off by default). When set it is threaded as --registry into every registry-touching install in setup.sh, setup.ps1 and build.sh (bun bootstrap, bun install + retry, npm fallback, OXC validator runtime). --registry is the highest-precedence override for both bun and npm and leaves min-release-age and save-exact in force, so the default lock is unchanged for everyone else. On an install failure that looks like a blocked registry, print guidance pointing at UNSLOTH_NPM_REGISTRY and auto-suggest the mirror already set in the user's npm config. Registries are never switched automatically. Also correct the .npmrc comment: the pin does not block an ambient NPM_CONFIG_REGISTRY env var (npm and bun honor that at higher precedence); it only guards against a lower-precedence stale ~/.npmrc. * Studio: make the registry hint reachable under set -e; clean temp log (#6491) run_quiet_no_exit returns non-zero on failure, which under `set -euo pipefail` exits the script at the call site before the exit code is captured, so the new UNSLOTH_NPM_REGISTRY hint never printed on the npm fallback and OXC validator paths. Guard both with `|| _rc=$?` (the same idiom every other run_quiet_no_exit caller already uses) so the failure branch runs, and remove the _FRONTEND_INSTALL_LOG temp file on the early-exit path. * Studio: detect the user's mirror outside the pinned frontend dir (#6491) _suggest_npm_registry / Show-NpmRegistryHint run while the cwd is still studio/frontend, whose .npmrc pins registry=https://registry.npmjs.org/. So `npm config get registry` returned that pin instead of the user's ~/.npmrc mirror, and the "Detected a registry" branch was skipped for the main corporate case (mirror set in ~/.npmrc). Run the lookup from a directory with no project .npmrc (/ in bash, the temp dir in PowerShell) so the user/global mirror is surfaced. The NPM_CONFIG_REGISTRY env check is unchanged and still takes precedence. --- README.md | 9 +++++ build.sh | 15 ++++++- studio/frontend/.npmrc | 15 +++++-- studio/setup.ps1 | 59 +++++++++++++++++++++++++--- studio/setup.sh | 88 +++++++++++++++++++++++++++++++++++++++--- 5 files changed, 170 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fcb0ee8dc..9162d29b1 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,15 @@ curl -fsSL https://unsloth.ai/install.sh | UNSLOTH_STUDIO_HOME=/abs/path sh $env:UNSLOTH_STUDIO_HOME='C:\path'; irm https://unsloth.ai/install.ps1 | iex ``` +Point the frontend build at a corporate npm mirror/proxy with `UNSLOTH_NPM_REGISTRY` (for the developer install behind a firewall that blocks `registry.npmjs.org`): +```bash +UNSLOTH_NPM_REGISTRY=https://artifactory.example.com/api/npm/npm/ ./install.sh --local +``` +```powershell +$env:UNSLOTH_NPM_REGISTRY='https://artifactory.example.com/api/npm/npm/'; .\install.ps1 --local +``` +It is threaded as `--registry` into the Studio frontend `npm`/`bun` installs; the supply-chain locks (7-day `min-release-age`, exact version pins) stay in force. + Cap Studio's native CPU thread pools on high-core hosts: `UNSLOTH_CPU_THREADS=8 unsloth studio -p 8888`. #### Uninstall diff --git a/build.sh b/build.sh index 286664b5e..dc272f0de 100644 --- a/build.sh +++ b/build.sh @@ -35,10 +35,19 @@ _restore_gitignores() { } trap _restore_gitignores EXIT +# Corporate-mirror / proxy escape hatch (#6491). When UNSLOTH_NPM_REGISTRY is set we +# thread it as `--registry ` into the installs (overrides frontend/.npmrc's pinned +# registry for both bun and npm; min-release-age / save-exact stay in force). Empty +# array (the default) expands to nothing under `set -u`. +_NPM_REGISTRY_ARGS=() +if [ -n "${UNSLOTH_NPM_REGISTRY:-}" ]; then + _NPM_REGISTRY_ARGS=(--registry "$UNSLOTH_NPM_REGISTRY") +fi + # Use bun for install if available (faster), fall back to npm. _install_ok=false if command -v bun &>/dev/null; then - if bun install; then + if bun install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}"; then _install_ok=true else echo "⚠ bun install failed, falling back to npm" @@ -46,8 +55,10 @@ if command -v bun &>/dev/null; then fi fi if [ "$_install_ok" != "true" ]; then - if ! npm install; then + if ! npm install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}"; then echo "❌ ERROR: package install failed" >&2 + echo " If you are behind a corporate firewall/proxy, set UNSLOTH_NPM_REGISTRY to your mirror and retry, e.g.:" >&2 + echo " UNSLOTH_NPM_REGISTRY=https://your-mirror.example/api/npm/ ./build.sh" >&2 exit 1 fi fi diff --git a/studio/frontend/.npmrc b/studio/frontend/.npmrc index f2d15a4f1..19783b5ff 100644 --- a/studio/frontend/.npmrc +++ b/studio/frontend/.npmrc @@ -14,9 +14,18 @@ min-release-age=7 # `npm install @ --save-exact` pass) but it stops new # carets from creeping into the manifest as patch-version footguns. save-exact=true -# Lock the registry. A user-set PIP_INDEX_URL-style override (here: -# NPM_CONFIG_REGISTRY env var or a stale ~/.npmrc) shouldn't redirect -# our installs to an attacker registry. +# Pin the default registry so a stale or hostile *lower-precedence* ~/.npmrc +# can't silently redirect our installs to an attacker registry. Note this does +# NOT block an ambient NPM_CONFIG_REGISTRY env var: npm and bun honor that at a +# higher precedence than this project file. That is exactly why Unsloth does not +# read NPM_CONFIG_REGISTRY and instead exposes one deliberate, explicit opt-in. +# +# Corporate mirror / proxy (issue #6491): if your firewall blocks +# registry.npmjs.org, set UNSLOTH_NPM_REGISTRY= when running +# ./install.sh (or setup.sh / setup.ps1). The installer threads it as +# `--registry `, which overrides this line for both npm and bun while +# leaving the min-release-age and save-exact locks above in force. Do not edit +# this line for that -- the env var keeps the default pinned for everyone else. registry=https://registry.npmjs.org/ audit-level=high fund=false diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 6b16702d3..38398eb5f 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -37,6 +37,18 @@ $DefaultLlamaSource = "https://github.com/ggml-org/llama.cpp" $DefaultLlamaTag = "latest" $DefaultLlamaForceCompileRef = "master" +# Corporate-mirror / proxy escape hatch for the frontend npm/bun install (#6491). +# studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain +# lock, which overrides a corporate user's ~/.npmrc proxy and causes 403s behind a +# firewall. UNSLOTH_NPM_REGISTRY is a deliberate opt-in: when set we splat it as +# `--registry ` into every npm/bun install. `--registry` is the highest-precedence +# override for BOTH tools and leaves min-release-age / save-exact in force. Empty array +# (the default) splats to nothing, so normal installs are unchanged. +$NpmRegistryArgs = @() +if ($env:UNSLOTH_NPM_REGISTRY) { + $NpmRegistryArgs = @('--registry', $env:UNSLOTH_NPM_REGISTRY) +} + # Verbose can be enabled either by CLI flag or by UNSLOTH_VERBOSE=1. $script:UnslothVerbose = ($env:UNSLOTH_VERBOSE -eq '1') foreach ($a in $args) { @@ -877,6 +889,41 @@ function substep { Write-StudioStdoutMirror (" {0,-15}{1}" -f "", $Message) } +function Show-NpmRegistryHint { + # Print actionable guidance when a frontend/OXC npm/bun install fails and the + # registry lock is the likely cause (corporate firewall/proxy). No-op once the + # user has opted in via UNSLOTH_NPM_REGISTRY. We never switch registries + # automatically -- we only guide. + if ($env:UNSLOTH_NPM_REGISTRY) { return } + $mirror = $env:NPM_CONFIG_REGISTRY + if (-not $mirror) { + # Read npm config from a dir with no project .npmrc so the frontend's pinned + # registry= does not mask the user's ~/.npmrc / global mirror. + $pushed = $false + try { + Push-Location ([System.IO.Path]::GetTempPath()) -ErrorAction Stop + $pushed = $true + $mirror = (& npm config get registry 2>$null | Out-String).Trim() + } catch { $mirror = "" } finally { if ($pushed) { Pop-Location } } + } + if ($mirror -in @("", "undefined", "null", "https://registry.npmjs.org", "https://registry.npmjs.org/")) { + $mirror = "" + } + Write-Host "" + step "frontend" "registry.npmjs.org looks blocked (corporate firewall/proxy?)" "Yellow" + if ($mirror) { + substep "Studio pins the public npm registry; your mirror is being ignored." + substep "Detected a registry in your npm config:" + substep " $mirror" + substep "Re-run pointing Studio at it:" + substep " `$env:UNSLOTH_NPM_REGISTRY='$mirror'; .\install.ps1 --local" + } else { + substep "If you use a private mirror/proxy, point Studio at it and re-run:" + substep " `$env:UNSLOTH_NPM_REGISTRY='https://your-mirror.example/api/npm/'; .\install.ps1 --local" + } + substep "(min-release-age and save-exact stay enforced.)" +} + # ───────────────────────────────────────────── # Banner # ───────────────────────────────────────────── @@ -2113,7 +2160,7 @@ if ($NeedNodeForSetup) { substep "installing bun (faster frontend package installs)..." $prevEAP_bun = $ErrorActionPreference $ErrorActionPreference = "Continue" - Invoke-SetupCommand { npm install -g bun --allow-scripts=bun } | Out-Null + Invoke-SetupCommand { npm install -g bun --allow-scripts=bun @NpmRegistryArgs } | Out-Null $ErrorActionPreference = $prevEAP_bun Refresh-Environment # Refresh-Environment rebuilds PATH (Machine;User;current), demoting the @@ -2173,7 +2220,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { # the cache + retry once before falling back to npm. if ($UseBun) { Write-Host " Using bun for package install (faster)" -ForegroundColor DarkGray - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install @NpmRegistryArgs } # On Windows, .bin/ entries vary by package manager: # npm → tsc, tsc.cmd, tsc.ps1 # bun → tsc.exe, tsc.bunx @@ -2187,7 +2234,7 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { Remove-Item "node_modules" -Recurse -Force -ErrorAction SilentlyContinue } Invoke-SetupCommand { bun pm cache rm } | Out-Null - $bunExit = Invoke-SetupCommand { bun install } + $bunExit = Invoke-SetupCommand { bun install @NpmRegistryArgs } $hasTsc = (Test-Path "node_modules\.bin\tsc") -or (Test-Path "node_modules\.bin\tsc.cmd") -or (Test-Path "node_modules\.bin\tsc.exe") -or (Test-Path "node_modules\.bin\tsc.bunx") $hasVite = (Test-Path "node_modules\.bin\vite") -or (Test-Path "node_modules\.bin\vite.cmd") -or (Test-Path "node_modules\.bin\vite.exe") -or (Test-Path "node_modules\.bin\vite.bunx") if ($bunExit -ne 0 -or -not $hasTsc -or -not $hasVite) { @@ -2206,13 +2253,14 @@ if ($NeedFrontendBuild -and -not $IsPipInstall) { } } if (-not $UseBun) { - $npmExit = Invoke-SetupCommand { npm install } + $npmExit = Invoke-SetupCommand { npm install @NpmRegistryArgs } if ($npmExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_npm foreach ($gi in $HiddenGitignores) { Rename-Item -Path "$gi._twbuild" -NewName (Split-Path $gi -Leaf) -Force -ErrorAction SilentlyContinue } Write-Host "[ERROR] npm install failed (exit code $npmExit)" -ForegroundColor Red Write-Host " Try running 'npm install' manually in frontend/ to see errors" -ForegroundColor Yellow + Show-NpmRegistryHint exit 1 } } @@ -2249,11 +2297,12 @@ if ((Test-Path $OxcValidatorDir) -and $NodeSource -ne "skip" -and (Get-Command n $prevEAP_oxc = $ErrorActionPreference $ErrorActionPreference = "Continue" Push-Location $OxcValidatorDir - $oxcInstallExit = Invoke-SetupCommand { npm install } + $oxcInstallExit = Invoke-SetupCommand { npm install @NpmRegistryArgs } if ($oxcInstallExit -ne 0) { Pop-Location $ErrorActionPreference = $prevEAP_oxc Write-Host "[ERROR] OXC validator npm install failed (exit code $oxcInstallExit)" -ForegroundColor Red + Show-NpmRegistryHint exit 1 } Pop-Location diff --git a/studio/setup.sh b/studio/setup.sh index 2e1be1c9c..43ec9fe7b 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -74,6 +74,62 @@ verbose_substep() { return 0 } +# ── Corporate-mirror / proxy escape hatch for the frontend npm/bun install (#6491) ── +# studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain +# lock. A project-level pin overrides a corporate user's ~/.npmrc proxy, so the install +# hits npmjs.org directly and a firewall returns 403. UNSLOTH_NPM_REGISTRY is a +# deliberate opt-in: when set we thread it as `--registry ` into every npm/bun +# install. `--registry` is the highest-precedence override for BOTH tools and leaves +# min-release-age / save-exact in force. Empty array (the default) expands to nothing +# under `set -u`, so normal installs are unchanged. +_NPM_REGISTRY_ARGS=() +if [ -n "${UNSLOTH_NPM_REGISTRY:-}" ]; then + _NPM_REGISTRY_ARGS=(--registry "$UNSLOTH_NPM_REGISTRY") +fi +# Failure-path capture log consumed by _suggest_npm_registry. Set to a temp file +# around the npm/bun installs; "" elsewhere so unrelated run_quiet calls don't capture. +_CAPTURE_LOG="" + +# Print actionable guidance when a frontend/OXC npm/bun install fails and the registry +# lock is the likely cause (corporate firewall/proxy). No-op once the user has opted in +# via UNSLOTH_NPM_REGISTRY. We never switch registries automatically -- we only guide. +# $1 = path to a captured install log (may be empty/missing). +_suggest_npm_registry() { + [ -n "${UNSLOTH_NPM_REGISTRY:-}" ] && return 0 + local _log="${1:-}" + # If we captured output and it does NOT look like a registry/network problem, stay + # quiet -- the raw error already shown is more useful than a misleading hint. + if [ -n "$_log" ] && [ -s "$_log" ] \ + && ! grep -Eqi '40[13]|ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|ConnectionRefused|failed to resolve|registry\.npmjs\.org|getaddrinfo|tunneling socket|network|proxy|self.?signed|unable to (get|verify)' "$_log"; then + return 0 + fi + # Best-effort: surface a mirror the user already configured (env or ~/.npmrc). + # Read npm config from / (a dir with no project .npmrc) so the frontend's pinned + # registry= does not mask the user's ~/.npmrc / global mirror -- the caller is + # still inside studio/frontend when this runs. + local _mirror="${NPM_CONFIG_REGISTRY:-${npm_config_registry:-}}" + if [ -z "$_mirror" ] && command -v npm >/dev/null 2>&1; then + _mirror="$( (cd / 2>/dev/null && npm config get registry) 2>/dev/null || true )" + fi + case "$_mirror" in + ""|undefined|null|https://registry.npmjs.org|https://registry.npmjs.org/) _mirror="" ;; + esac + printf '\n' >&2 + step "frontend" "registry.npmjs.org looks blocked (corporate firewall/proxy?)" "$C_WARN" >&2 + if [ -n "$_mirror" ]; then + substep "Studio pins the public npm registry; your mirror is being ignored." >&2 + substep "Detected a registry in your npm config:" >&2 + substep " $_mirror" >&2 + substep "Re-run pointing Studio at it:" >&2 + substep " UNSLOTH_NPM_REGISTRY=$_mirror ./install.sh --local" >&2 + else + substep "If you use a private mirror/proxy, point Studio at it and re-run:" >&2 + substep " UNSLOTH_NPM_REGISTRY=https://your-mirror.example/api/npm/ ./install.sh --local" >&2 + fi + substep "(min-release-age and save-exact stay enforced.)" >&2 + return 0 +} + run_maybe_quiet() { if _is_verbose; then "$@" @@ -113,6 +169,7 @@ _run_quiet() { local exit_code=$? step "error" "$label failed (exit code $exit_code)" "$C_ERR" >&2 cat "$tmplog" >&2 + if [ -n "${_CAPTURE_LOG:-}" ]; then cat "$tmplog" >> "$_CAPTURE_LOG" 2>/dev/null || true; fi rm -f "$tmplog" if [ "$on_fail" = "exit" ]; then @@ -647,7 +704,7 @@ elif [ "$NODE_SOURCE" = bundled ]; then substep "installing bun..." # --allow-scripts=bun: npm >=11.16 gates install scripts and bun's # postinstall fetches its binary; without it the install is a broken stub. - if run_maybe_quiet npm install -g bun --allow-scripts=bun && command -v bun &>/dev/null; then + if run_maybe_quiet npm install -g bun --allow-scripts=bun "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" && command -v bun &>/dev/null; then substep "bun installed ($(bun --version))" else substep "bun install skipped (npm will be used instead)" @@ -690,7 +747,7 @@ trap _restore_gitignores EXIT _try_bun_install() { local _log _exit_code=0 _log=$(mktemp) - bun install >"$_log" 2>&1 || _exit_code=$? + bun install "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" >"$_log" 2>&1 || _exit_code=$? # bun may create .exe shims on Windows (Git Bash / MSYS2) instead of plain scripts if [ "$_exit_code" -eq 0 ] \ @@ -707,11 +764,15 @@ _try_bun_install() { echo " bun install exited 0 but critical binaries are missing:" fi sed 's/^/ | /' "$_log" >&2 + if [ -n "${_CAPTURE_LOG:-}" ]; then cat "$_log" >> "$_CAPTURE_LOG" 2>/dev/null || true; fi rm -f "$_log" rm -rf node_modules return 1 } +# Capture install output (bun + npm fallback) so we can detect a registry block. +_FRONTEND_INSTALL_LOG=$(mktemp) +_CAPTURE_LOG="$_FRONTEND_INSTALL_LOG" _bun_install_ok=false if command -v bun &>/dev/null; then substep "using bun for package install (faster)" @@ -728,12 +789,19 @@ if command -v bun &>/dev/null; then fi fi if [ "$_bun_install_ok" = false ]; then - run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error - _npm_install_rc=$? + # `|| _npm_install_rc=$?` keeps this off `set -e`'s exit path (run_quiet_no_exit + # returns non-zero on failure) so the hint branch is reachable; it also captures + # the exact exit code. Mirrors the `|| BUILD_OK=false` idiom used below. + _npm_install_rc=0 + run_quiet_no_exit "npm install" npm install --no-fund --no-audit --loglevel=error "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" || _npm_install_rc=$? if [ "$_npm_install_rc" -ne 0 ]; then + _suggest_npm_registry "$_FRONTEND_INSTALL_LOG" + rm -f "$_FRONTEND_INSTALL_LOG" exit "$_npm_install_rc" fi fi +_CAPTURE_LOG="" +rm -f "$_FRONTEND_INSTALL_LOG" run_quiet "npm run build" npm run build _restore_gitignores @@ -759,11 +827,19 @@ fi # end frontend build check # Node, so do not run npm install against an unsuitable/absent system Node. if [ -d "$_OXC_DIR" ] && [ "${NODE_SOURCE:-}" != skip ] && command -v npm &>/dev/null; then cd "$_OXC_DIR" - run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error - _oxc_install_rc=$? + _OXC_INSTALL_LOG=$(mktemp) + _CAPTURE_LOG="$_OXC_INSTALL_LOG" + # `|| _oxc_install_rc=$?` keeps this off `set -e`'s exit path so the hint branch + # below is reachable; it also captures the exact exit code. + _oxc_install_rc=0 + run_quiet_no_exit "npm install (oxc validator runtime)" npm install --no-fund --no-audit --loglevel=error "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}" || _oxc_install_rc=$? + _CAPTURE_LOG="" if [ "$_oxc_install_rc" -ne 0 ]; then + _suggest_npm_registry "$_OXC_INSTALL_LOG" + rm -f "$_OXC_INSTALL_LOG" exit "$_oxc_install_rc" fi + rm -f "$_OXC_INSTALL_LOG" cd "$SCRIPT_DIR" elif [ -d "$_OXC_DIR" ] && [ "${NODE_SOURCE:-}" != skip ]; then # No npm on PATH: skip rather than abort; the backend Node resolver degrades