mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-11-09 10:39:41 -06:00
1187 lines
47 KiB
Bash
Executable File
1187 lines
47 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================================
|
|
# TinyTorch Installer
|
|
# ============================================================================
|
|
#
|
|
# USAGE
|
|
# -----
|
|
# # Standard installation (the latest release, tag tinytorch-v<version>):
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash
|
|
#
|
|
# # Install latest development version from dev branch:
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dev
|
|
#
|
|
# # Install from a specific branch (e.g. main, the untagged tip):
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --branch <branch>
|
|
#
|
|
# # Install an exact tag or commit (full 40-character SHA):
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --ref <tag-or-sha>
|
|
#
|
|
# WHAT THIS SCRIPT DOES
|
|
# ---------------------
|
|
# 1. Checks prerequisites (git, Python 3.10+, venv module)
|
|
# 2. Asks where to install (default: ./tinytorch)
|
|
# 3. Shows installation plan and asks for confirmation
|
|
# 4. Downloads TinyTorch via git sparse checkout (minimal download)
|
|
# 5. Creates an isolated Python virtual environment (.venv/)
|
|
# 6. Installs all dependencies and the tito CLI
|
|
#
|
|
# AFTER INSTALLATION
|
|
# ------------------
|
|
# cd tinytorch
|
|
# source .venv/bin/activate
|
|
# tito setup # First-time profile setup
|
|
#
|
|
# WHAT GETS CREATED
|
|
# -----------------
|
|
# tinytorch/ # Created in your current directory
|
|
# ├── .venv/ # Python virtual environment
|
|
# ├── src/ # Module source notebooks (20 modules)
|
|
# ├── tinytorch/ # Package - your implementations go here
|
|
# ├── tito/ # CLI tool source
|
|
# ├── milestones/ # Historical ML recreations
|
|
# ├── tests/ # Test suites for your code
|
|
# ├── datasets/ # Sample datasets (tinydigits, tinytalks)
|
|
# ├── modules/ # Working directory (populated by tito)
|
|
# ├── bin/ # CLI entry point
|
|
# ├── requirements.txt # Python dependencies
|
|
# ├── pyproject.toml # Package configuration
|
|
# ├── settings.ini # nbdev configuration
|
|
# └── README.md, LICENSE # Documentation
|
|
#
|
|
# REQUIREMENTS
|
|
# ------------
|
|
# - git (any recent version)
|
|
# - Python 3.10 or higher
|
|
# - Python venv module (usually included; on Debian/Ubuntu: apt install python3-venv)
|
|
# - Internet connection to GitHub
|
|
#
|
|
# DOCUMENTATION
|
|
# -------------
|
|
# https://tinytorch.ai
|
|
# https://mlsysbook.ai/tinytorch/
|
|
#
|
|
# SOURCE
|
|
# ------
|
|
# https://github.com/harvard-edge/cs249r_book (tinytorch/ subdirectory)
|
|
#
|
|
# ============================================================================
|
|
|
|
set -e # Exit on any error
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
# These can be configured via CLI flags or environment variables:
|
|
# CLI flags (pass via bash -s -- when curling):
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dev
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --branch <branch>
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --ref <tag-or-sha>
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dir <path>
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --non-interactive
|
|
# Environment variables:
|
|
# curl -sSL mlsysbook.ai/tinytorch/install.sh | TINYTORCH_BRANCH=dev bash
|
|
# TINYTORCH_DEV=1 ./install.sh
|
|
# TINYTORCH_VERSION=0.1.5 ./install.sh # install release tag tinytorch-v0.1.5
|
|
# TINYTORCH_REF=<tag-or-sha> ./install.sh # exact tag or full commit SHA
|
|
# TINYTORCH_NON_INTERACTIVE=1 ./install.sh # Skip all prompts (for CI)
|
|
REPO_SHORT="${TINYTORCH_REPO:-harvard-edge/cs249r_book}"
|
|
REPO_URL="${TINYTORCH_REPO_URL:-https://github.com/${REPO_SHORT}.git}"
|
|
TAGS_API="https://api.github.com/repos/${REPO_SHORT}/tags"
|
|
TAG_PREFIX="tinytorch-v"
|
|
# What to install. Default: the release tag tinytorch-v$TINYTORCH_VERSION, so a
|
|
# student gets exactly what was tested and tagged, not whatever main's tip is.
|
|
# Overrides (highest first): --branch/--dev/TINYTORCH_BRANCH/TINYTORCH_DEV,
|
|
# then --ref/TINYTORCH_REF. A full 40-hex SHA is fetched as a commit.
|
|
BRANCH="${TINYTORCH_BRANCH:-}"
|
|
if [ -n "${TINYTORCH_DEV:-}" ] && [ "$TINYTORCH_DEV" != "0" ]; then
|
|
BRANCH="dev"
|
|
fi
|
|
REF_OVERRIDE="${TINYTORCH_REF:-}"
|
|
# Resolved by resolve_install_ref (after flags are parsed):
|
|
INSTALL_REF="" # what git fetches
|
|
REF_KIND="" # release | branch | ref | commit
|
|
REF_LABEL="" # shown to the user; empty for the default release
|
|
INSTALL_DIR="${TINYTORCH_INSTALL_DIR:-tinytorch}"
|
|
SPARSE_PATH="tinytorch"
|
|
# Non-interactive mode: skip prompts, use defaults (for CI/testing)
|
|
NON_INTERACTIVE="${TINYTORCH_NON_INTERACTIVE:-}"
|
|
# Version is fetched from GitHub tags or branch pyproject.toml
|
|
# Can be overridden for testing: TINYTORCH_VERSION=0.1.5 ./install.sh
|
|
TINYTORCH_DEFAULT_VERSION="0.2.2" # TINYTORCH_INSTALL_VERSION
|
|
TINYTORCH_VERSION="${TINYTORCH_VERSION:-$TINYTORCH_DEFAULT_VERSION}"
|
|
|
|
# ============================================================================
|
|
# Timeouts
|
|
# ============================================================================
|
|
# Every external command below (Python detection, network calls, downloads,
|
|
# package installs, interactive prompts) is capped by one of these. None of
|
|
# them should ever be reached in a healthy environment; they exist purely so
|
|
# that a broken environment produces a clear, timed-out error message instead
|
|
# of an installer that just sits there forever with no output and no way to
|
|
# tell what's wrong. See https://github.com/harvard-edge/cs249r_book/issues/1960
|
|
PYTHON_CHECK_TIMEOUT=5 # Detecting/validating a python command
|
|
NETWORK_CHECK_TIMEOUT=15 # Checking GitHub is reachable
|
|
CLONE_TIMEOUT=120 # Downloading TinyTorch (shallow sparse clone)
|
|
VENV_CREATE_TIMEOUT=60 # Creating the virtual environment
|
|
PIP_INSTALL_TIMEOUT=600 # Installing Python packages (10 min ceiling)
|
|
TTY_READ_TIMEOUT=30 # Waiting on an interactive prompt
|
|
|
|
# ============================================================================
|
|
# ANSI Color Codes (for terminal output)
|
|
# ============================================================================
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m' # No Color / Reset
|
|
|
|
# ============================================================================
|
|
# Cleanup Handler
|
|
# Removes temporary files if script exits unexpectedly
|
|
# ============================================================================
|
|
TEMP_DIR=""
|
|
cleanup() {
|
|
if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then
|
|
rm -rf "$TEMP_DIR"
|
|
fi
|
|
}
|
|
trap cleanup EXIT
|
|
trap 'echo ""; echo -e "${RED}Installation cancelled.${NC}"; exit 1' INT TERM
|
|
|
|
# ============================================================================
|
|
# Output Helpers
|
|
# ============================================================================
|
|
print_success() { echo -e "${GREEN}✓${NC} $1"; }
|
|
print_error() { echo -e "${RED}✗${NC} $1"; }
|
|
print_warning() { echo -e "${YELLOW}!${NC} $1"; }
|
|
print_info() { echo -e "${BLUE}→${NC} $1"; }
|
|
|
|
# Spinner animation for long-running background tasks
|
|
spin() {
|
|
local pid=$1
|
|
local msg=$2
|
|
local spinchars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
|
|
local i=0
|
|
while kill -0 "$pid" 2>/dev/null; do
|
|
printf "\r ${DIM}%s${NC} %s" "${spinchars:i++%10:1}" "$msg"
|
|
sleep 0.1
|
|
done
|
|
printf "\r %-50s\r" ""
|
|
}
|
|
|
|
print_banner() {
|
|
echo ""
|
|
if [ -n "$REF_LABEL" ]; then
|
|
echo -e " ${BOLD}Tiny${NC}${YELLOW}🔥Torch${NC} ${DIM}v${TINYTORCH_VERSION}${NC} ${YELLOW}[${REF_LABEL}]${NC}"
|
|
else
|
|
echo -e " ${BOLD}Tiny${NC}${YELLOW}🔥Torch${NC} ${DIM}v${TINYTORCH_VERSION}${NC}"
|
|
fi
|
|
echo -e " ${DIM}Don't import it. Build it.${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Utility Functions
|
|
# ============================================================================
|
|
command_exists() {
|
|
command -v "$1" >/dev/null 2>&1
|
|
}
|
|
|
|
# Run a command with a hard time limit, so a hung or misbehaving external
|
|
# program (a broken Python launcher, a stalled network call, a package
|
|
# manager waiting on a credential prompt nobody can answer) fails loudly
|
|
# after a bounded wait instead of freezing this installer forever with no
|
|
# output. That silent-forever failure is exactly what was reported in
|
|
# https://github.com/harvard-edge/cs249r_book/issues/1960: the script
|
|
# printed "Git OK" and then simply never continued, with no error at all.
|
|
#
|
|
# Usage: run_with_timeout <seconds> <command> [args...]
|
|
run_with_timeout() {
|
|
local seconds="$1"
|
|
shift
|
|
if command_exists timeout; then
|
|
timeout "$seconds" "$@"
|
|
elif command_exists gtimeout; then
|
|
# macOS without GNU coreutils names it gtimeout to avoid clobbering
|
|
# the BSD tools of the same short names.
|
|
gtimeout "$seconds" "$@"
|
|
else
|
|
# No timeout utility available at all. Running unguarded is better
|
|
# than refusing to run, but a hang here can no longer be caught.
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
# Decide what to install. Sets INSTALL_REF, REF_KIND, REF_LABEL.
|
|
resolve_install_ref() {
|
|
local ref kind
|
|
if [ -n "$BRANCH" ]; then
|
|
ref="$BRANCH"; kind="branch"
|
|
elif [ -n "$REF_OVERRIDE" ]; then
|
|
ref="$REF_OVERRIDE"; kind="ref"
|
|
else
|
|
ref="${TAG_PREFIX}${TINYTORCH_VERSION}"; kind="release"
|
|
fi
|
|
# A full commit SHA cannot be cloned with --branch; fetch it directly.
|
|
if echo "$ref" | grep -Eq '^[0-9a-f]{40}$'; then
|
|
kind="commit"
|
|
fi
|
|
INSTALL_REF="$ref"
|
|
REF_KIND="$kind"
|
|
case "$kind" in
|
|
release) REF_LABEL="" ;;
|
|
branch) REF_LABEL="branch: $ref" ;;
|
|
commit) REF_LABEL="commit: ${ref:0:12}" ;;
|
|
*) REF_LABEL="ref: $ref" ;;
|
|
esac
|
|
}
|
|
|
|
# Download tinytorch/ at INSTALL_REF into <dest> (shallow, sparse, blobless).
|
|
# Usage: clone_install_ref <dest> (run under run_with_timeout by the caller)
|
|
clone_install_ref() {
|
|
local dest="$1"
|
|
if [ "$REF_KIND" = "commit" ]; then
|
|
# GitHub serves any reachable commit by SHA (incl. PR merge commits).
|
|
git init -q "$dest" &&
|
|
git -C "$dest" remote add origin "$REPO_URL" &&
|
|
git -C "$dest" config core.sparseCheckout true &&
|
|
git -C "$dest" config core.sparseCheckoutCone true &&
|
|
printf '/*\n!/*/\n/%s/\n' "$SPARSE_PATH" > "$dest/.git/info/sparse-checkout" &&
|
|
git -C "$dest" fetch -q --depth 1 --filter=blob:none origin "$INSTALL_REF" &&
|
|
git -C "$dest" -c advice.detachedHead=false checkout -q FETCH_HEAD
|
|
else
|
|
# --branch accepts both branch and tag names.
|
|
git clone --depth 1 --filter=blob:none --sparse --branch "$INSTALL_REF" \
|
|
"$REPO_URL" "$dest"
|
|
fi
|
|
}
|
|
|
|
# Fetch latest version from GitHub tags API or branch pyproject.toml
|
|
# Sets TINYTORCH_VERSION global variable
|
|
fetch_latest_version() {
|
|
# Skip if already set via environment variable or CLI flag
|
|
if [ -n "$TINYTORCH_VERSION" ]; then
|
|
return 0
|
|
fi
|
|
|
|
# If targeting a non-main branch (e.g. dev), fetch version directly from that branch's pyproject.toml
|
|
if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ]; then
|
|
if command_exists curl; then
|
|
local pyproject_url="https://raw.githubusercontent.com/${REPO_SHORT}/${BRANCH}/tinytorch/pyproject.toml"
|
|
local pyproject
|
|
pyproject=$(curl -fsSL --max-time 10 "$pyproject_url" 2>/dev/null) || true
|
|
if [ -n "$pyproject" ]; then
|
|
local version
|
|
version=$(echo "$pyproject" | grep -E "^version" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
|
|
if [ -n "$version" ]; then
|
|
TINYTORCH_VERSION="$version"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
TINYTORCH_VERSION="dev"
|
|
return 0
|
|
fi
|
|
|
|
# For main branch, try tags API first (single source of truth for releases)
|
|
if command_exists curl; then
|
|
local response
|
|
response=$(curl -fsSL --max-time 10 "$TAGS_API" 2>/dev/null) || true
|
|
if [ -n "$response" ]; then
|
|
# Parse JSON to find first tinytorch-v* tag
|
|
# Uses grep/sed for portability (no jq dependency)
|
|
local tag_name
|
|
tag_name=$(echo "$response" | grep -o "\"name\": *\"${TAG_PREFIX}[^\"]*\"" | head -1 | sed 's/.*"name": *"\([^"]*\)".*/\1/')
|
|
if [ -n "$tag_name" ]; then
|
|
TINYTORCH_VERSION="${tag_name#$TAG_PREFIX}"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Fallback: fetch pyproject.toml directly from raw.githubusercontent.com
|
|
if command_exists curl; then
|
|
local pyproject_url="https://raw.githubusercontent.com/${REPO_SHORT}/${BRANCH:-main}/tinytorch/pyproject.toml"
|
|
local pyproject
|
|
pyproject=$(curl -fsSL --max-time 10 "$pyproject_url" 2>/dev/null) || true
|
|
if [ -n "$pyproject" ]; then
|
|
local version
|
|
version=$(echo "$pyproject" | grep -E "^version" | head -1 | sed 's/.*= *"\([^"]*\)".*/\1/')
|
|
if [ -n "$version" ]; then
|
|
TINYTORCH_VERSION="$version"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Final fallback: unknown version (will still work, just won't show version)
|
|
TINYTORCH_VERSION="latest"
|
|
}
|
|
|
|
# Check if Python version is 3.10+
|
|
#
|
|
# Every candidate is run through run_with_timeout: on Windows, "python" can
|
|
# resolve to a Store "App Execution Alias" placeholder
|
|
# (%LOCALAPPDATA%\Microsoft\WindowsApps\python.exe) that Windows registers
|
|
# by default even when no real Python is installed. Running that
|
|
# placeholder can silently try to pop open the Microsoft Store and never
|
|
# return, which otherwise hangs this whole installer with zero output
|
|
# (issue #1960) instead of just failing over to the next candidate.
|
|
check_python_version() {
|
|
local python_cmd="$1"
|
|
local output version major minor
|
|
# One combined call instead of three separate ones, so a hung/broken
|
|
# launcher only costs one timeout wait, not three.
|
|
output=$(run_with_timeout "$PYTHON_CHECK_TIMEOUT" "$python_cmd" -c \
|
|
"import sys; print(sys.version_info.major, sys.version_info.minor, f'{sys.version_info.major}.{sys.version_info.minor}')" \
|
|
2>/dev/null)
|
|
read -r major minor version <<< "$output"
|
|
|
|
# Check for Python 3.10+ (Required for TinyTorch)
|
|
if [ "$major" -eq 3 ] && [ "$minor" -ge 10 ]; then
|
|
echo "$version"
|
|
return 0
|
|
elif [ "$major" -gt 3 ]; then
|
|
echo "$version"
|
|
return 0
|
|
else
|
|
echo "$version"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Look for a working Python install in the places Windows installers
|
|
# commonly put one, for when nothing usable is on PATH at all. This is
|
|
# deliberately a last resort after get_python_cmd's normal PATH-based
|
|
# search fails: it exists so a user whose real Python just isn't on PATH
|
|
# (a very common state after installing via Anaconda, the Microsoft
|
|
# Store, or python.org without checking "Add to PATH") gets TinyTorch
|
|
# working immediately, rather than being sent off to fix PATH themselves
|
|
# first. Every candidate still goes through check_python_version, so
|
|
# this can't pick a broken or too-old Python, and it can't hang: each
|
|
# attempt is bounded by the same PYTHON_CHECK_TIMEOUT as everything else.
|
|
search_for_python_windows() {
|
|
local candidates=()
|
|
|
|
# The Python Launcher (py.exe) keeps its own registry of installs
|
|
# independent of PATH, so it can find a working Python even when the
|
|
# bare "python" command resolves to Windows' Store placeholder.
|
|
if command_exists py; then
|
|
candidates+=("py")
|
|
fi
|
|
|
|
# Common install locations, newest-looking first. Using glob patterns
|
|
# directly (with nullglob) rather than `find`, since these are all
|
|
# shallow, well-known paths and this needs to stay fast.
|
|
local search_globs=(
|
|
"$LOCALAPPDATA/Programs/Python/Python3*/python.exe"
|
|
"$PROGRAMFILES/Python3*/python.exe"
|
|
"/c/Program Files (x86)/Python3*/python.exe"
|
|
"$USERPROFILE/anaconda3/python.exe"
|
|
"$USERPROFILE/miniconda3/python.exe"
|
|
"$USERPROFILE/AppData/Local/anaconda3/python.exe"
|
|
"/c/ProgramData/Anaconda3/python.exe"
|
|
"/c/ProgramData/miniconda3/python.exe"
|
|
"$USERPROFILE/.pyenv/pyenv-win/versions/*/python.exe"
|
|
"$LOCALAPPDATA/uv/python/*/python.exe"
|
|
)
|
|
|
|
local pattern path
|
|
shopt -s nullglob
|
|
for pattern in "${search_globs[@]}"; do
|
|
for path in $pattern; do
|
|
candidates+=("$path")
|
|
done
|
|
done
|
|
shopt -u nullglob
|
|
|
|
local candidate
|
|
for candidate in "${candidates[@]}"; do
|
|
if check_python_version "$candidate" >/dev/null 2>&1; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Find the best Python command (prioritize newer versions)
|
|
get_python_cmd() {
|
|
local platform
|
|
platform=$(get_platform)
|
|
|
|
# Check specific versions first, prioritizing newer versions
|
|
# On Windows, prefer 'python' to avoid Microsoft Store alias that
|
|
# resolves 'python3' to a stub and creates Unix-style venv paths.
|
|
# Contributed by @adil-mubashir-ch (PR #1169)
|
|
#
|
|
# Windows Python installs are not all the same shape. python.org's
|
|
# installer and Anaconda create a real python.exe. But Python installed
|
|
# through a version manager (`uv python install`, pyenv-win, etc.) is
|
|
# commonly exposed only as a python.cmd or python.bat shim. Git Bash
|
|
# (MSYS) does not apply Windows' own PATHEXT extension search to a
|
|
# bare, extension-less name the way cmd.exe/PowerShell do, so
|
|
# `command -v python` can fail to find a perfectly good Python that
|
|
# only exists as python.cmd -- reporting "Python not found" even
|
|
# though it is installed and working. Try every extension Windows
|
|
# itself would search, in the same order, and use whichever actually
|
|
# runs.
|
|
if [ "$platform" = "windows" ]; then
|
|
local suffix candidate
|
|
for suffix in "" .exe .cmd .bat; do
|
|
candidate="python${suffix}"
|
|
if command_exists "$candidate" && check_python_version "$candidate" >/dev/null 2>&1; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
# Nothing usable on PATH. Rather than stop here and make the user
|
|
# go fix PATH or Windows settings themselves, look in the handful
|
|
# of places Windows Python installers actually put python.exe and
|
|
# use the first working one we find. This is echoed as an
|
|
# absolute path (unlike the bare command names above), which is
|
|
# how the caller tells apart "found on PATH" from "found by
|
|
# searching" to explain itself to the user.
|
|
candidate=$(search_for_python_windows)
|
|
if [ -n "$candidate" ]; then
|
|
echo "$candidate"
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
return 0
|
|
fi
|
|
|
|
local candidates=("python3.14" "python3.13" "python3.12" "python3.11" "python3.10" "python3" "python")
|
|
|
|
for cmd in "${candidates[@]}"; do
|
|
if command_exists "$cmd"; then
|
|
# Verify this specific candidate actually meets the version requirement
|
|
if check_python_version "$cmd" >/dev/null 2>&1; then
|
|
echo "$cmd"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
# Find the system platform (linux, macos, windows)
|
|
# Contributed by @rnjema (PR #1105)
|
|
get_platform() {
|
|
local uname_out
|
|
uname_out=$(uname -s)
|
|
case "${uname_out}" in
|
|
Linux*) echo "linux";;
|
|
Darwin*) echo "macos";;
|
|
CYGWIN*|MINGW*|MSYS*) echo "windows";;
|
|
*) echo "unknown";;
|
|
esac
|
|
}
|
|
|
|
# ============================================================================
|
|
# Pre-flight Checks
|
|
# These run before any installation to catch problems early
|
|
# ============================================================================
|
|
|
|
check_write_permission() {
|
|
if ! touch ".tinytorch_write_test" 2>/dev/null; then
|
|
print_error "Cannot write to this directory"
|
|
echo " Check your permissions or cd to a writable directory."
|
|
exit 1
|
|
fi
|
|
rm -f ".tinytorch_write_test"
|
|
}
|
|
|
|
check_not_in_venv() {
|
|
if [ -n "$VIRTUAL_ENV" ]; then
|
|
print_warning "You're inside a virtual environment: $VIRTUAL_ENV"
|
|
echo " Consider deactivating first: deactivate"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
check_internet() {
|
|
# git ls-remote has no built-in timeout: a firewall or proxy that
|
|
# silently drops packets (rather than rejecting the connection) can
|
|
# leave this hanging for minutes with no feedback. Bound it so a dead
|
|
# network produces a clear message quickly instead of an installer
|
|
# that just appears to be stuck.
|
|
if ! run_with_timeout "$NETWORK_CHECK_TIMEOUT" git ls-remote --exit-code "$REPO_URL" >/dev/null 2>&1; then
|
|
print_error "Cannot reach GitHub"
|
|
echo " This usually means one of:"
|
|
echo " - Your internet connection is down or very slow"
|
|
echo " - A firewall, VPN, or antivirus is blocking git/GitHub"
|
|
echo " - You're on a work/school network that blocks GitHub directly"
|
|
echo " Try opening https://github.com in a browser first to confirm you can reach it,"
|
|
echo " then run this installer again."
|
|
exit 1
|
|
fi
|
|
print_success "GitHub reachable"
|
|
}
|
|
|
|
# Check if git version > 2.25.1
|
|
check_git_version() {
|
|
local major minor rev
|
|
major="$(echo "$1" | cut -d '.' -f 1)" 2>/dev/null
|
|
minor="$(echo "$1" | cut -d '.' -f 2)" 2>/dev/null
|
|
rev="$(echo "$1" | cut -d '.' -f 3)" 2>/dev/null
|
|
|
|
# Check for git >= 2.25.2 (sparse url bug fixed)
|
|
if [ "$major" -eq 2 ] && [ "$minor" -ge 26 ]; then
|
|
return 0
|
|
elif [ "$major" -gt 3 ]; then
|
|
return 0
|
|
elif [ "$major" -eq 2 ] && [ "$minor" -eq 25 ] && [ "$rev" -ge 2 ] ; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
check_prerequisites() {
|
|
local errors=0
|
|
|
|
# Check for git
|
|
if command_exists git; then
|
|
GIT_VERSION="$(git --version | cut -d ' ' -f3)"
|
|
if check_git_version "$GIT_VERSION"; then
|
|
print_success "Git $GIT_VERSION"
|
|
else
|
|
print_error "Git version $GIT_VERSION is too old (2.25.2 or newer is required)"
|
|
echo " Download the latest version: https://git-scm.com/downloads"
|
|
echo " (On Ubuntu 20.04, upgrading to 22.04+ also fixes this.)"
|
|
errors=$((errors + 1))
|
|
fi
|
|
else
|
|
print_error "Git not found"
|
|
echo " TinyTorch uses git to download itself. Install it from:"
|
|
echo " https://git-scm.com/downloads"
|
|
echo " then restart your terminal and try again."
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
# Check for Python 3.10+
|
|
PYTHON_CMD=$(get_python_cmd)
|
|
PLATFORM=$(get_platform)
|
|
if [ -n "$PYTHON_CMD" ]; then
|
|
# We know it's good because get_python_cmd validates it, but we run check again to get the version string
|
|
PY_VERSION=$(check_python_version "$PYTHON_CMD")
|
|
print_success "Python $PY_VERSION ($PYTHON_CMD)"
|
|
# An absolute path (as opposed to a bare command name like "python"
|
|
# or "python.cmd") means this came from search_for_python_windows,
|
|
# not from PATH -- tell the user, since silently using a Python
|
|
# they don't know about is more confusing than helpful.
|
|
case "$PYTHON_CMD" in
|
|
*/*)
|
|
print_info "This Python isn't on your PATH -- using it just for this install"
|
|
echo " To use it directly later too, add it to your PATH: $PYTHON_CMD"
|
|
;;
|
|
esac
|
|
else
|
|
# Diagnostic: Check if they have ANY python, just too old
|
|
if command_exists python3; then
|
|
CURRENT_VER=$(run_with_timeout "$PYTHON_CHECK_TIMEOUT" python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null)
|
|
print_error "Found Python $CURRENT_VER, but 3.10+ is required"
|
|
echo " Please install a newer Python: https://python.org/downloads"
|
|
else
|
|
print_error "Python 3.10+ not found"
|
|
echo " Install it from: https://python.org/downloads"
|
|
echo " (macOS: 'brew install python'. Debian/Ubuntu: 'apt install python3.12'.)"
|
|
fi
|
|
if [ "$(get_platform)" = "windows" ]; then
|
|
echo ""
|
|
echo " This installer already checked the common install locations"
|
|
echo " (python.org, Anaconda/Miniconda, the Python Launcher, pyenv-win,"
|
|
echo " uv) and none of them had a working Python 3.10+, so it really"
|
|
echo " does look like Python isn't installed, or is somewhere unusual."
|
|
echo ""
|
|
echo " If you're sure Python IS installed somewhere, Windows may be"
|
|
echo " silently redirecting the 'python' command to its own placeholder"
|
|
echo " instead of your real install. To fix that:"
|
|
echo " Settings > Apps > Advanced app settings > App execution aliases"
|
|
echo " then turn OFF the 'python.exe' and 'python3.exe' entries."
|
|
echo " After that, close and reopen your terminal and run this again."
|
|
fi
|
|
errors=$((errors + 1))
|
|
fi
|
|
|
|
# Check for venv module
|
|
if [ -n "$PYTHON_CMD" ]; then
|
|
if run_with_timeout "$PYTHON_CHECK_TIMEOUT" "$PYTHON_CMD" -c "import venv" 2>/dev/null; then
|
|
print_success "Python venv module"
|
|
else
|
|
print_error "Python's built-in 'venv' module is missing or unresponsive"
|
|
echo " Debian/Ubuntu: sudo apt install python3-venv"
|
|
echo " Other platforms: reinstall Python from https://python.org/downloads"
|
|
echo " (make sure not to select a 'minimal'/custom install that skips it)"
|
|
errors=$((errors + 1))
|
|
fi
|
|
fi
|
|
|
|
# Show Windows-specific guidance (contributed by @rnjema)
|
|
if [ "$PLATFORM" = "windows" ]; then
|
|
print_info "Windows detected - using Git Bash/WSL compatible mode"
|
|
fi
|
|
|
|
if [ $errors -gt 0 ]; then
|
|
echo ""
|
|
print_error "Missing prerequisites. Please fix the issues above."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_existing_directory() {
|
|
if [ -d "$INSTALL_DIR" ]; then
|
|
if [ -f "$INSTALL_DIR/pyproject.toml" ] || [ -d "$INSTALL_DIR/.tito" ]; then
|
|
print_error "Directory '$INSTALL_DIR' already exists and contains a TinyTorch workspace."
|
|
echo ""
|
|
echo -e " ${BOLD}To update without losing your exercise notebooks or progress:${NC}"
|
|
echo -e " ${CYAN}cd $INSTALL_DIR${NC}"
|
|
if [ -f "$INSTALL_DIR/.venv/bin/activate" ]; then
|
|
echo -e " ${CYAN}source .venv/bin/activate${NC}"
|
|
elif [ -f "$INSTALL_DIR/.venv/Scripts/activate" ]; then
|
|
echo -e " ${CYAN}source .venv/Scripts/activate${NC}"
|
|
fi
|
|
echo -e " ${CYAN}tito update${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}To install fresh in a separate location:${NC}"
|
|
echo -e " ${CYAN}curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dir ./${INSTALL_DIR}-fresh${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}(Do not run 'rm -rf $INSTALL_DIR' if you have completed exercises in modules/!)${NC}"
|
|
else
|
|
print_error "Directory '$INSTALL_DIR' already exists."
|
|
echo " Please choose a different location with --dir <path> or remove the empty directory."
|
|
fi
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================================
|
|
# Installation Steps
|
|
# ============================================================================
|
|
|
|
prompt_install_directory() {
|
|
# Non-interactive mode: use INSTALL_DIR as-is (from env var or default)
|
|
if [ -n "$NON_INTERACTIVE" ]; then
|
|
return
|
|
fi
|
|
|
|
# No TTY available: use defaults silently
|
|
if ! [ -t 0 ] && ! [ -e /dev/tty ]; then
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "Where would you like to install Tiny${YELLOW}🔥Torch${NC}?"
|
|
echo -e " ${DIM}Press Enter for default: ${BOLD}$PWD/tinytorch${NC}"
|
|
echo ""
|
|
printf "Install directory [tinytorch]: "
|
|
|
|
# /dev/tty can exist as a path without actually delivering input (some
|
|
# IDE-embedded terminals, containers, restricted shells). Without a
|
|
# timeout, `read` would wait for that input forever with no way for the
|
|
# user to tell the difference between "still thinking" and "stuck".
|
|
local user_dir
|
|
if ! read -r -t "$TTY_READ_TIMEOUT" user_dir </dev/tty; then
|
|
echo ""
|
|
print_warning "No response after ${TTY_READ_TIMEOUT}s, using the default location."
|
|
return
|
|
fi
|
|
|
|
if [ -n "$user_dir" ]; then
|
|
INSTALL_DIR="$user_dir"
|
|
fi
|
|
}
|
|
|
|
show_plan_and_confirm() {
|
|
echo ""
|
|
echo -e "This will create a ${CYAN}${INSTALL_DIR}${NC} folder here:"
|
|
echo -e " ${BOLD}$PWD/${INSTALL_DIR}${NC}"
|
|
echo ""
|
|
echo "What will be installed:"
|
|
if [ -n "$REF_LABEL" ]; then
|
|
echo -e " - Tiny${YELLOW}🔥Torch${NC} learning modules ${YELLOW}[${REF_LABEL}]${NC}"
|
|
else
|
|
echo -e " - Tiny${YELLOW}🔥Torch${NC} learning modules"
|
|
fi
|
|
echo " - Python virtual environment (.venv/)"
|
|
echo " - tito CLI tool"
|
|
echo ""
|
|
if [ "$REF_KIND" = "release" ]; then
|
|
echo -e "${DIM}Source: ${REPO_SHORT} (release ${INSTALL_REF})${NC}"
|
|
else
|
|
echo -e "${DIM}Source: ${REPO_SHORT} (${REF_LABEL})${NC}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
do_install() {
|
|
echo ""
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Step 1: Download from GitHub using sparse checkout
|
|
# This downloads only the tinytorch/ subdirectory, not the entire repo
|
|
# -------------------------------------------------------------------------
|
|
echo -e "${BLUE}[1/4]${NC} Downloading from GitHub..."
|
|
|
|
TEMP_DIR=$(mktemp -d)
|
|
|
|
# A shallow sparse clone of one small folder should take a few seconds.
|
|
# If it's still running after CLONE_TIMEOUT, something is wrong
|
|
# (dead network, proxy silently dropping the connection, etc.) --
|
|
# better to fail with a clear message than leave the spinner running
|
|
# forever with no way to tell it apart from a slow-but-working download.
|
|
local clone_err_file="$TEMP_DIR/clone.err"
|
|
# clone_install_ref is a shell function; export what the subshell needs so
|
|
# run_with_timeout (timeout/gtimeout exec a new process) can run it.
|
|
export -f clone_install_ref
|
|
export REF_KIND INSTALL_REF REPO_URL SPARSE_PATH
|
|
run_with_timeout "$CLONE_TIMEOUT" bash -c 'clone_install_ref "$1"' _ "$TEMP_DIR/repo" \
|
|
>/dev/null 2>"$clone_err_file" &
|
|
local clone_pid=$!
|
|
if [ -n "$REF_LABEL" ]; then
|
|
spin $clone_pid "Cloning repository (${REF_LABEL})..."
|
|
else
|
|
spin $clone_pid "Cloning repository (${INSTALL_REF})..."
|
|
fi
|
|
# `|| clone_status=$?` keeps a failed clone from tripping `set -e` here, so
|
|
# the fallback and the error messages below are actually reachable.
|
|
local clone_status=0
|
|
wait $clone_pid || clone_status=$?
|
|
|
|
# The default release tag can be briefly absent (the site is deployed a
|
|
# few minutes before the tag is pushed) or missing after a partial publish.
|
|
# Fall back to main rather than failing a student's install.
|
|
if [ $clone_status -ne 0 ] && [ $clone_status -ne 124 ] && [ "$REF_KIND" = "release" ] \
|
|
&& grep -qi "not found" "$clone_err_file" 2>/dev/null; then
|
|
print_warning "Release tag ${INSTALL_REF} not found; installing from main instead."
|
|
rm -rf "$TEMP_DIR/repo"
|
|
INSTALL_REF="main"; REF_KIND="branch"; REF_LABEL="branch: main"
|
|
export REF_KIND INSTALL_REF
|
|
run_with_timeout "$CLONE_TIMEOUT" bash -c 'clone_install_ref "$1"' _ "$TEMP_DIR/repo" \
|
|
>/dev/null 2>"$clone_err_file" &
|
|
clone_pid=$!
|
|
spin $clone_pid "Cloning repository (branch: main)..."
|
|
clone_status=0
|
|
wait $clone_pid || clone_status=$?
|
|
fi
|
|
|
|
if [ $clone_status -eq 124 ]; then
|
|
print_error "Download timed out after ${CLONE_TIMEOUT}s"
|
|
echo " This usually means a firewall, VPN, or proxy is silently blocking"
|
|
echo " the connection to GitHub rather than rejecting it outright."
|
|
echo " Try a different network, or disable VPN/proxy, then try again."
|
|
exit 1
|
|
elif [ $clone_status -ne 0 ]; then
|
|
print_error "Failed to download from GitHub"
|
|
if [ -f "$clone_err_file" ] && grep -qiE "Remote branch.*not found|couldn't find remote ref|not our ref" "$clone_err_file"; then
|
|
echo " '$INSTALL_REF' was not found in $REPO_SHORT."
|
|
echo " Please verify the branch, tag, or commit and try again."
|
|
else
|
|
echo " Check your internet connection and try again."
|
|
if [ -f "$clone_err_file" ] && [ -s "$clone_err_file" ]; then
|
|
echo -e " ${DIM}$(cat "$clone_err_file")${NC}"
|
|
fi
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
local original_dir="$PWD"
|
|
cd "$TEMP_DIR/repo"
|
|
git sparse-checkout set "$SPARSE_PATH" 2>/dev/null
|
|
|
|
# Capture commit hash for provenance tracking
|
|
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
|
cd "$original_dir"
|
|
|
|
# Move to final location
|
|
mv "$TEMP_DIR/repo/$SPARSE_PATH" "$INSTALL_DIR"
|
|
rm -rf "$TEMP_DIR"
|
|
TEMP_DIR=""
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Clean up dev-only files that students don't need
|
|
#
|
|
# KEEP (students need these):
|
|
# src/ - Module source notebooks
|
|
# tinytorch/ - Package where student code goes
|
|
# tito/ - CLI tool source
|
|
# milestones/ - Historical ML recreations
|
|
# modules/ - Working directory (cleared, populated by tito)
|
|
# tests/ - Test suites for student code
|
|
# datasets/ - Sample datasets (tinydigits, tinytalks)
|
|
# bin/ - CLI entry point script
|
|
# requirements.txt, pyproject.toml - Package dependencies
|
|
# settings.ini - nbdev config (needed for exports)
|
|
# README.md, LICENSE - Documentation
|
|
#
|
|
# REMOVE (dev-only):
|
|
# -------------------------------------------------------------------------
|
|
rm -rf "$INSTALL_DIR/paper" \
|
|
"$INSTALL_DIR/book" \
|
|
"$INSTALL_DIR/guide" \
|
|
"$INSTALL_DIR/quarto" \
|
|
"$INSTALL_DIR/site" \
|
|
"$INSTALL_DIR/instructor" \
|
|
"$INSTALL_DIR/scripts" \
|
|
"$INSTALL_DIR/tools" \
|
|
"$INSTALL_DIR/binder" \
|
|
"$INSTALL_DIR/etc" \
|
|
"$INSTALL_DIR/assignments" \
|
|
"$INSTALL_DIR/benchmark_results" \
|
|
"$INSTALL_DIR/vscode-ext" \
|
|
"$INSTALL_DIR/.git-hooks" \
|
|
"$INSTALL_DIR/.vscode" \
|
|
"$INSTALL_DIR/Makefile" \
|
|
"$INSTALL_DIR/activate.sh" \
|
|
"$INSTALL_DIR/setup-dev.sh" \
|
|
"$INSTALL_DIR/setup-environment.sh" \
|
|
"$INSTALL_DIR/CONTRIBUTING.md" \
|
|
"$INSTALL_DIR/INSTRUCTOR.md" \
|
|
"$INSTALL_DIR/MANIFEST.in" \
|
|
"$INSTALL_DIR/MODULE_ANATOMY.md" \
|
|
"$INSTALL_DIR/NBGRADER_RELEASE_TIERS.md" \
|
|
"$INSTALL_DIR/nbgrader_validation_results.json" \
|
|
"$INSTALL_DIR/.pre-commit-config.yaml" \
|
|
"$INSTALL_DIR/.shared-ai-rules.md" \
|
|
"$INSTALL_DIR/.tinyrc" \
|
|
"$INSTALL_DIR/.editorconfig" \
|
|
"$INSTALL_DIR/.gitattributes" \
|
|
"$INSTALL_DIR/.all-contributorsrc" \
|
|
"$INSTALL_DIR/settings.json" \
|
|
"$INSTALL_DIR/.tinytorch" \
|
|
"$INSTALL_DIR/uv.lock" \
|
|
2>/dev/null || true
|
|
|
|
# Clear modules/ folder - students populate this via tito CLI exports
|
|
if [ -d "$INSTALL_DIR/modules" ]; then
|
|
find "$INSTALL_DIR/modules" -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + 2>/dev/null || true
|
|
fi
|
|
|
|
# Reset progress tracking - students start fresh
|
|
rm -f "$INSTALL_DIR/progress.json" 2>/dev/null || true
|
|
rm -rf "$INSTALL_DIR/.tito" 2>/dev/null || true
|
|
|
|
# Clear exported module implementations in tinytorch/ - students build these
|
|
# Keep __init__.py files (package structure) and hand-written tinytorch/extensions/
|
|
if [ -d "$INSTALL_DIR/tinytorch/core" ]; then
|
|
find "$INSTALL_DIR/tinytorch/core" -name "*.py" ! -name "__init__.py" -type f -delete 2>/dev/null || true
|
|
fi
|
|
if [ -d "$INSTALL_DIR/tinytorch/perf" ]; then
|
|
find "$INSTALL_DIR/tinytorch/perf" -name "*.py" ! -name "__init__.py" -type f -delete 2>/dev/null || true
|
|
fi
|
|
if [ -n "$REF_LABEL" ]; then
|
|
print_success "Downloaded TinyTorch ${DIM}(${REF_LABEL}, ${COMMIT_HASH})${NC}"
|
|
else
|
|
print_success "Downloaded TinyTorch ${DIM}(${INSTALL_REF}, ${COMMIT_HASH})${NC}"
|
|
fi
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Step 2: Create Python virtual environment
|
|
# -------------------------------------------------------------------------
|
|
echo -e "${BLUE}[2/4]${NC} Creating Python environment..."
|
|
cd "$INSTALL_DIR"
|
|
|
|
# Use the detected 3.10+ command explicitly. A generous timeout guards
|
|
# against a corrupted Python install hanging during its internal
|
|
# ensurepip bootstrap; venv creation itself is a local, fast operation.
|
|
if ! run_with_timeout "$VENV_CREATE_TIMEOUT" "$PYTHON_CMD" -m venv .venv; then
|
|
print_error "Could not create the virtual environment"
|
|
echo " This usually means the detected Python ($PYTHON_CMD) is broken or"
|
|
echo " incomplete. Try reinstalling Python from https://python.org/downloads"
|
|
echo " and running this installer again."
|
|
exit 1
|
|
fi
|
|
|
|
# Activate venv (handle Windows Git Bash vs Unix) and resolve venv python binary
|
|
local venv_python
|
|
if [ -f ".venv/Scripts/activate" ]; then
|
|
# Windows (Git Bash)
|
|
source .venv/Scripts/activate
|
|
venv_python=".venv/Scripts/python.exe"
|
|
else
|
|
# macOS/Linux
|
|
source .venv/bin/activate
|
|
venv_python=".venv/bin/python"
|
|
fi
|
|
print_success "Created virtual environment using $PYTHON_CMD"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Step 3: Install dependencies
|
|
# Uses isolated venv python for pip to avoid host PEP 668 restrictions
|
|
# -------------------------------------------------------------------------
|
|
echo -e "${BLUE}[3/4]${NC} Installing dependencies..."
|
|
|
|
# A generous timeout (PIP_INSTALL_TIMEOUT) rather than a short one:
|
|
# a slow-but-working install on a bad connection can legitimately take
|
|
# a few minutes and should be allowed to finish. What this actually
|
|
# guards against is a true hang -- e.g. a global pip config pointing at
|
|
# a private package index that silently waits for credentials nobody
|
|
# can type in a piped `curl | bash` session.
|
|
pip_install_step() {
|
|
local description="$1"
|
|
shift
|
|
run_with_timeout "$PIP_INSTALL_TIMEOUT" "$venv_python" -m pip "$@" -q 2>/dev/null &
|
|
local step_pid=$!
|
|
spin $step_pid "$description"
|
|
wait $step_pid
|
|
local status=$?
|
|
if [ $status -eq 124 ]; then
|
|
print_error "Timed out after ${PIP_INSTALL_TIMEOUT}s: $description"
|
|
echo " This can happen with a very slow connection, or if pip is configured"
|
|
echo " to use a private/internal package index that needs credentials."
|
|
echo " Check for a pip.ini/pip.conf pointing at a custom index, or try again"
|
|
echo " on a different network."
|
|
exit 1
|
|
elif [ $status -ne 0 ]; then
|
|
print_error "Failed: $description"
|
|
echo " Check your internet connection and try again. If it keeps failing,"
|
|
echo " re-run with pip's quiet flag removed to see the underlying error:"
|
|
echo " $venv_python -m pip $*"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
pip_install_step "Upgrading pip..." install --upgrade pip
|
|
|
|
if [ -f "requirements.txt" ]; then
|
|
total_pkgs=$(grep -c -E "^[^#]" requirements.txt 2>/dev/null || echo "?")
|
|
pip_install_step "Installing $total_pkgs packages..." install -r requirements.txt
|
|
fi
|
|
|
|
pip_install_step "Installing TinyTorch..." install -e .
|
|
|
|
print_success "Installed dependencies"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Step 4: Verify installation
|
|
# -------------------------------------------------------------------------
|
|
echo -e "${BLUE}[4/4]${NC} Verifying installation..."
|
|
if command -v tito >/dev/null 2>&1; then
|
|
print_success "Verified tito CLI"
|
|
else
|
|
print_warning "Installation completed but tito not found in PATH"
|
|
echo " This is normal - activate the venv first."
|
|
fi
|
|
}
|
|
|
|
print_success_message() {
|
|
local install_path="$PWD"
|
|
|
|
# Determine correct activation command for the platform
|
|
local activate_cmd="source .venv/bin/activate"
|
|
if [ -f ".venv/Scripts/activate" ]; then
|
|
# Windows (Git Bash)
|
|
activate_cmd="source .venv/Scripts/activate"
|
|
fi
|
|
|
|
echo ""
|
|
if [ -n "$REF_LABEL" ]; then
|
|
echo -e "${GREEN}✓${NC} Tiny${YELLOW}🔥Torch${NC} (${YELLOW}${REF_LABEL}${NC}) installed successfully!"
|
|
else
|
|
echo -e "${GREEN}✓${NC} Tiny${YELLOW}🔥Torch${NC} installed successfully!"
|
|
fi
|
|
echo ""
|
|
echo -e "${BOLD}Next steps:${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}cd $install_path${NC}"
|
|
echo -e " ${CYAN}$activate_cmd${NC}"
|
|
echo -e " ${CYAN}tito setup${NC}"
|
|
echo ""
|
|
echo -e "${BOLD}Then start building:${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}tito module start 01${NC}"
|
|
echo ""
|
|
echo -e "${DIM}Documentation: https://tinytorch.ai${NC}"
|
|
echo ""
|
|
}
|
|
|
|
# ============================================================================
|
|
# Command-Line Argument Parsing & Help
|
|
# ============================================================================
|
|
|
|
show_help() {
|
|
cat << EOF
|
|
TinyTorch Installer
|
|
|
|
Usage:
|
|
install.sh [OPTIONS]
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- [OPTIONS]
|
|
|
|
Options:
|
|
--dev Install latest development version from the 'dev' branch
|
|
-b, --branch BRANCH Install from a specific Git branch, e.g. main or dev
|
|
(default: the latest release tag, tinytorch-v<version>)
|
|
--ref REF Install an exact tag or full 40-character commit SHA
|
|
-d, --dir DIR Target installation directory (default: tinytorch)
|
|
-v, --version VERSION Install release tag tinytorch-v<VERSION>
|
|
-y, --yes, --non-interactive
|
|
Skip interactive prompts and use defaults (for CI)
|
|
-h, --help Show this help message
|
|
|
|
Environment Variables:
|
|
TINYTORCH_BRANCH Target branch (e.g. dev, main); overrides the release tag
|
|
TINYTORCH_REF Exact tag or full commit SHA
|
|
TINYTORCH_DEV Set to 1 to target dev branch
|
|
TINYTORCH_INSTALL_DIR Target installation directory
|
|
TINYTORCH_VERSION Release to install (tag tinytorch-v<version>)
|
|
TINYTORCH_NON_INTERACTIVE Set to 1 to skip interactive prompts
|
|
TINYTORCH_REPO GitHub repository (default: harvard-edge/cs249r_book)
|
|
|
|
Examples:
|
|
# Standard installation (latest release):
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash
|
|
|
|
# Untagged tip of main:
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --branch main
|
|
|
|
# Install latest development version from dev branch:
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dev
|
|
|
|
# Install from a specific branch:
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --branch dev
|
|
|
|
# Non-interactive installation to a custom directory:
|
|
curl -sSL mlsysbook.ai/tinytorch/install.sh | bash -s -- --dev --dir ./my-tinytorch -y
|
|
EOF
|
|
}
|
|
|
|
parse_args() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dev)
|
|
BRANCH="dev"
|
|
shift
|
|
;;
|
|
-b|--branch)
|
|
if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then
|
|
BRANCH="$2"
|
|
shift 2
|
|
else
|
|
echo -e "${RED}✗${NC} Option '$1' requires a branch name argument."
|
|
exit 1
|
|
fi
|
|
;;
|
|
--branch=*)
|
|
BRANCH="${1#*=}"
|
|
shift
|
|
;;
|
|
--ref)
|
|
if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then
|
|
REF_OVERRIDE="$2"
|
|
shift 2
|
|
else
|
|
echo -e "${RED}✗${NC} Option '$1' requires a tag or commit SHA argument."
|
|
exit 1
|
|
fi
|
|
;;
|
|
--ref=*)
|
|
REF_OVERRIDE="${1#*=}"
|
|
shift
|
|
;;
|
|
-d|--dir|--directory)
|
|
if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then
|
|
INSTALL_DIR="$2"
|
|
shift 2
|
|
else
|
|
echo -e "${RED}✗${NC} Option '$1' requires a directory path argument."
|
|
exit 1
|
|
fi
|
|
;;
|
|
--dir=*|--directory=*)
|
|
INSTALL_DIR="${1#*=}"
|
|
shift
|
|
;;
|
|
-v|--version)
|
|
if [ -n "${2:-}" ] && [ "${2:0:1}" != "-" ]; then
|
|
TINYTORCH_VERSION="$2"
|
|
shift 2
|
|
else
|
|
echo -e "${RED}✗${NC} Option '$1' requires a version argument."
|
|
exit 1
|
|
fi
|
|
;;
|
|
--version=*)
|
|
TINYTORCH_VERSION="${1#*=}"
|
|
shift
|
|
;;
|
|
-y|--yes|--non-interactive)
|
|
NON_INTERACTIVE=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
show_help
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo -e "${RED}✗${NC} Unknown option: $1"
|
|
echo "Run with --help to see available options."
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# ============================================================================
|
|
# Main Entry Point
|
|
# ============================================================================
|
|
|
|
main() {
|
|
# Parse CLI flags (e.g. --dev, --branch, --dir, -y, --help)
|
|
parse_args "$@"
|
|
|
|
# Fetch version from GitHub (single source of truth: pyproject.toml via tags or branch)
|
|
fetch_latest_version
|
|
|
|
# Decide the exact ref to install (release tag by default)
|
|
resolve_install_ref
|
|
|
|
print_banner
|
|
|
|
# Pre-flight checks
|
|
check_write_permission
|
|
check_not_in_venv
|
|
|
|
echo "Checking prerequisites..."
|
|
check_prerequisites
|
|
check_internet
|
|
|
|
# Ask where to install
|
|
prompt_install_directory
|
|
|
|
# Check directory doesn't exist (after user chooses)
|
|
check_existing_directory
|
|
|
|
# Show plan and confirm (skip in non-interactive mode)
|
|
if [ -z "$NON_INTERACTIVE" ] && { [ -t 0 ] || [ -e /dev/tty ]; }; then
|
|
show_plan_and_confirm
|
|
|
|
printf "Continue? [Y/n] "
|
|
# See prompt_install_directory for why this needs a timeout: a tty
|
|
# that exists but never delivers input must not hang forever.
|
|
if ! read -r -t "$TTY_READ_TIMEOUT" REPLY </dev/tty; then
|
|
echo ""
|
|
print_warning "No response after ${TTY_READ_TIMEOUT}s, continuing automatically."
|
|
REPLY="Y"
|
|
fi
|
|
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
|
print_info "Installation cancelled"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
# Run installation
|
|
do_install
|
|
|
|
# Success message with next steps
|
|
print_success_message
|
|
}
|
|
|
|
main "$@"
|