diff --git a/install.sh b/install.sh index f0af60e2d6..532203a51a 100755 --- a/install.sh +++ b/install.sh @@ -1530,17 +1530,51 @@ if [ -x "$VENV_DIR/bin/python" ]; then : > "$VENV_DIR/.unsloth-studio-owned" 2>/dev/null || true fi -# Guard against Python 3.13.8 torch import bug on Apple Silicon -# (skip when the user explicitly chose a version via --python) +# Guard against two independent Apple Silicon venv problems, in order: +# 1. uv may create the venv from a cached x86_64 (Rosetta) Python when a +# same-version x86_64 build is already cached (often because uv itself +# is an x86_64 build). That venv reports x86_64 to wheel resolvers, and +# PyTorch ships no macOS wheels on the CPU index for any architecture, +# so the torch install can never resolve. Recreate it with an +# arch-explicit arm64 CPython. +# 2. Python 3.13.8 has a known torch import bug. +# The two are independent: a venv may be x86_64 and, once recreated, still +# land on 3.13.8. So we re-inspect the interpreter between the checks instead +# of chaining them with elif, guaranteeing both invariants hold on whatever +# venv we end up with. Skip both when the user explicitly chose an interpreter +# via --python. if [ -z "$_USER_PYTHON" ] && [ "$OS" = "macos" ] && [ "$_ARCH" = "arm64" ]; then - _PY_VER=$("$VENV_DIR/bin/python" -c \ - "import sys; print('{}.{}.{}'.format(*sys.version_info[:3]))" 2>/dev/null || echo "") + _inspect_venv() { + "$VENV_DIR/bin/python" -c \ + "import platform, sys; print(platform.machine(), '{}.{}.{}'.format(*sys.version_info[:3]))" \ + 2>/dev/null || echo " " + } + _info=$(_inspect_venv) + _VENV_ARCH=${_info%% *} + _PY_VER=${_info##* } + + if [ "$_VENV_ARCH" = "x86_64" ]; then + echo " WARNING: venv was created with an x86_64 (Rosetta) Python on Apple Silicon." + echo " Recreating venv with native arm64 Python ${PYTHON_VERSION}..." + rm -rf "$VENV_DIR" + run_install_cmd "recreate venv (arm64)" uv venv "$VENV_DIR" \ + --python "cpython-${PYTHON_VERSION}-macos-aarch64-none" + if [ -x "$VENV_DIR/bin/python" ]; then + : > "$VENV_DIR/.unsloth-studio-owned" 2>/dev/null || true + fi + # Re-inspect: the recreated arm64 venv may still be 3.13.8. + _info=$(_inspect_venv) + _VENV_ARCH=${_info%% *} + _PY_VER=${_info##* } + fi + if [ "$_PY_VER" = "3.13.8" ]; then echo " WARNING: Python 3.13.8 has a known torch import bug." echo " Recreating venv with Python 3.12..." rm -rf "$VENV_DIR" PYTHON_VERSION="3.12" - run_install_cmd "recreate venv" uv venv "$VENV_DIR" --python "$PYTHON_VERSION" + run_install_cmd "recreate venv" uv venv "$VENV_DIR" \ + --python "cpython-${PYTHON_VERSION}-macos-aarch64-none" if [ -x "$VENV_DIR/bin/python" ]; then : > "$VENV_DIR/.unsloth-studio-owned" 2>/dev/null || true fi diff --git a/tests/sh/test_mac_intel_compat.sh b/tests/sh/test_mac_intel_compat.sh index be9f756f90..673fd58cd6 100644 --- a/tests/sh/test_mac_intel_compat.sh +++ b/tests/sh/test_mac_intel_compat.sh @@ -563,6 +563,80 @@ else FAIL=$((FAIL + 1)) fi +echo "" +echo "=== Apple Silicon x86_64 (Rosetta) venv rebuild ===" + +# Extract the real guard block from install.sh so we exercise the shipped logic +# (comment header down to its column-0 closing fi). +_GUARD_FILE=$(mktemp) +awk '/Guard against two independent Apple Silicon venv problems/{f=1} f{print} f&&/^fi$/{exit}' \ + "$INSTALL_SH" > "$_GUARD_FILE" + +if [ ! -s "$_GUARD_FILE" ]; then + echo " FAIL: could not extract Apple Silicon venv guard from install.sh" + FAIL=$((FAIL + 1)) +else + # Runner: stub uv (via run_install_cmd) + a fake venv python, source the + # guard, then print " | ". + # The stub maps a uv arm64 selector to the interpreter uv would produce: + # cpython-3.12-* -> arm64 3.12.7, cpython-3.13-* -> arm64 $REBUILD_313_VERSION. + _RUNNER=$(mktemp) + cat > "$_RUNNER" << 'RUNNER_EOF' +GUARD="$1"; VENV_DIR="$2" +make_python() { # dir machine version + mkdir -p "$1/bin" + printf '#!/usr/bin/env bash\necho "%s %s"\n' "$2" "$3" > "$1/bin/python" + chmod +x "$1/bin/python" +} +RECREATE_LOG=$(mktemp); : > "$RECREATE_LOG" +run_install_cmd() { + shift # drop the human label + if [ "$1" = "uv" ] && [ "$2" = "venv" ]; then + dir="$3"; sel=""; shift 3 + while [ $# -gt 0 ]; do [ "$1" = "--python" ] && { sel="$2"; shift; }; shift; done + echo "$sel" >> "$RECREATE_LOG" + case "$sel" in + *3.12-macos-aarch64*) make_python "$dir" arm64 "3.12.7" ;; + *3.13-macos-aarch64*) make_python "$dir" arm64 "${REBUILD_313_VERSION:-3.13.3}" ;; + *) make_python "$dir" arm64 "$sel" ;; + esac + fi +} +[ "$INIT_ARCH" != none ] && make_python "$VENV_DIR" "$INIT_ARCH" "$INIT_VER" +PYTHON_VERSION="3.13" +. "$GUARD" >&2 # guard's user-facing echoes go to stderr; keep stdout clean +final="none"; [ -x "$VENV_DIR/bin/python" ] && final="$("$VENV_DIR/bin/python" -c x)" +printf '%s | %s\n' "$final" "$(paste -sd, "$RECREATE_LOG" 2>/dev/null)" +rm -f "$RECREATE_LOG" +RUNNER_EOF + + _run_guard() { # _USER_PYTHON OS _ARCH INIT_ARCH INIT_VER REBUILD_313_VERSION + _vd=$(mktemp -d) + env _USER_PYTHON="$1" OS="$2" _ARCH="$3" INIT_ARCH="$4" INIT_VER="$5" \ + REBUILD_313_VERSION="$6" bash "$_RUNNER" "$_GUARD_FILE" "$_vd/venv" + rm -rf "$_vd" + } + + assert_eq "clean arm64 venv left untouched" \ + "arm64 3.13.3 | " "$(_run_guard '' macos arm64 arm64 3.13.3 '')" + assert_eq "x86_64 venv rebuilt as arm64" \ + "arm64 3.13.3 | cpython-3.13-macos-aarch64-none" \ + "$(_run_guard '' macos arm64 x86_64 3.13.3 '')" + assert_eq "x86_64 venv that lands on 3.13.8 is rebuilt then downgraded to 3.12" \ + "arm64 3.12.7 | cpython-3.13-macos-aarch64-none,cpython-3.12-macos-aarch64-none" \ + "$(_run_guard '' macos arm64 x86_64 3.13.3 3.13.8)" + assert_eq "arm64 3.13.8 venv downgraded to 3.12" \ + "arm64 3.12.7 | cpython-3.12-macos-aarch64-none" \ + "$(_run_guard '' macos arm64 arm64 3.13.8 '')" + assert_eq "--python override skips the guard entirely" \ + "x86_64 3.13.3 | " "$(_run_guard 3.11 macos arm64 x86_64 3.13.3 '')" + assert_eq "x86_64 host (Intel/Rosetta shell) is a no-op here" \ + "x86_64 3.13.3 | " "$(_run_guard '' macos x86_64 x86_64 3.13.3 '')" + + rm -f "$_RUNNER" +fi +rm -f "$_GUARD_FILE" + echo "" echo "Results: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ] || exit 1