#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 set -euo pipefail # PyPI/Studio release publishing must use `./build.sh publish` (or an # equivalent stamp -> build -> verify-dist -> upload flow) so packaged Studio # artifacts include the display-only Studio release version. # 1. Build frontend (Vite outputs to dist/) cd studio/frontend # Clean stale dist to force a full rebuild rm -rf dist # Tailwind v4's oxide scanner respects .gitignore in parent directories. # Python venvs create a .gitignore with "*" (ignore everything), which # prevents Tailwind from scanning .tsx source files for class names. # Temporarily hide any such .gitignore during the build, then restore it. _HIDDEN_GITIGNORES=() _dir="$(pwd)" while [ "$_dir" != "/" ]; do _dir="$(dirname "$_dir")" if [ -f "$_dir/.gitignore" ] && grep -qx '\*' "$_dir/.gitignore" 2>/dev/null; then mv "$_dir/.gitignore" "$_dir/.gitignore._twbuild" _HIDDEN_GITIGNORES+=("$_dir/.gitignore") fi done _restore_gitignores() { for _gi in "${_HIDDEN_GITIGNORES[@]+"${_HIDDEN_GITIGNORES[@]}"}"; do mv "${_gi}._twbuild" "$_gi" 2>/dev/null || true done } 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 "${_NPM_REGISTRY_ARGS[@]+"${_NPM_REGISTRY_ARGS[@]}"}"; then _install_ok=true else echo "⚠ bun install failed, falling back to npm" rm -rf node_modules fi fi if [ "$_install_ok" != "true" ]; 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 npm run build # outputs to studio/frontend/dist/ _restore_gitignores trap - EXIT # Validate CSS output -- catch truncated Tailwind builds before packaging MAX_CSS_SIZE=$(find dist/assets -name '*.css' -exec wc -c {} + 2>/dev/null | sort -n | tail -1 | awk '{print $1}') if [ -z "$MAX_CSS_SIZE" ]; then echo "❌ ERROR: No CSS files were emitted into dist/assets." echo " The frontend build may have failed silently." exit 1 fi if [ "$MAX_CSS_SIZE" -lt 100000 ]; then echo "❌ ERROR: Largest CSS file is only $((MAX_CSS_SIZE / 1024))KB (expected >100KB)." echo " Tailwind may not have scanned all source files." echo " Check for .gitignore files blocking the Tailwind oxide scanner." exit 1 fi echo "✅ Frontend CSS validated (${MAX_CSS_SIZE} bytes)" cd ../.. # 2. Clean old artifacts rm -rf build dist *.egg-info # 3. Stamp display-only Studio release metadata for packaged builds. _STUDIO_BUILD_INFO="studio/backend/utils/_studio_release_build.py" _STUDIO_BUILD_INFO_BACKUP="$(mktemp)" cp "$_STUDIO_BUILD_INFO" "$_STUDIO_BUILD_INFO_BACKUP" _restore_studio_build_info() { cp "$_STUDIO_BUILD_INFO_BACKUP" "$_STUDIO_BUILD_INFO" 2>/dev/null || true rm -f "$_STUDIO_BUILD_INFO_BACKUP" } trap _restore_studio_build_info EXIT if [ "${1:-}" = "publish" ]; then STUDIO_STAMPED_VERSION="$(python scripts/stamp_studio_release.py --require-release)" else STUDIO_STAMPED_VERSION="$(python scripts/stamp_studio_release.py)" fi # 4. Build wheel/sdist python -m build if [ "${1:-}" = "publish" ]; then python scripts/stamp_studio_release.py --verify-dist dist --expected "$STUDIO_STAMPED_VERSION" fi _restore_studio_build_info trap - EXIT # 5. Optionally publish if [ "${1:-}" = "publish" ]; then python -m twine upload dist/* fi