Merge remote-tracking branch 'origin/main' into r7552

This commit is contained in:
Daniel Han 2026-07-29 04:16:56 +00:00
commit 60d4bdcb06
4 changed files with 160 additions and 3 deletions

View file

@ -19,6 +19,17 @@
# 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 -e
# ── Why the installer lives in a function ──
# Under `curl ... | sh`, sh is the pipe READER. This file is ~150KB, so a top-level
# `exit` left most of it unread, the write end failed, and curl tacked
# "(56) Failure writing output to destination" onto our own error message. Wrapping
# the body forces sh to parse to the closing brace first, so the pipe always drains
# (install.ps1 has always had this shape).
#
# Body is deliberately NOT reindented: reflowing 4000+ lines would bury the change,
# and `exit` still exits the shell from inside a function. Do not add
# `exec < /dev/null`: for a piped shell that closes the script's own source.
_unsloth_main() {
# ── Output style (aligned with studio/setup.sh) ──
RULE=""
@ -4447,3 +4458,8 @@ else
substep "(add -H 0.0.0.0 --cloudflare for a public Cloudflare HTTPS link, or --secure to keep the raw port private; anyone with the API key can run code)"
echo ""
fi
}
# Every byte above is parsed before this line runs, which is the point.
_unsloth_main "$@"

View file

@ -128,14 +128,19 @@ huggingfacenotorch = [
]
# torchcodec backend for Gemma audio / datasets>=4 (#7225).
# Pick the audio-torch* pin matching your torch minor (see TORCH_TORCHCODEC).
# torchcodec publishes no sdist and only manylinux_2_28_x86_64, macosx_*_arm64
# and win_amd64 wheels, so Linux aarch64, Windows ARM64 and Intel Mac have
# nothing to resolve and pip fails the whole install rather than skipping audio.
# Gate on the platforms that have a wheel, matching
# PLATFORM_LACKS_TORCHCODEC_WHEEL in studio/install_python_stack.py.
audio-torch210 = [
"torchcodec>=0.10.0,<0.11.0 ; python_version >= '3.10'",
"torchcodec>=0.10.0,<0.11.0 ; python_version >= '3.10' and (((sys_platform == 'linux' or sys_platform == 'win32') and (platform_machine == 'x86_64' or platform_machine == 'AMD64')) or (sys_platform == 'darwin' and platform_machine == 'arm64'))",
]
audio-torch290 = [
"torchcodec>=0.8.0,<0.10.0 ; python_version >= '3.10'",
"torchcodec>=0.8.0,<0.10.0 ; python_version >= '3.10' and (((sys_platform == 'linux' or sys_platform == 'win32') and (platform_machine == 'x86_64' or platform_machine == 'AMD64')) or (sys_platform == 'darwin' and platform_machine == 'arm64'))",
]
audio-torch280 = [
"torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9'",
"torchcodec>=0.6.0,<0.8.0 ; python_version >= '3.9' and (((sys_platform == 'linux' or sys_platform == 'win32') and (platform_machine == 'x86_64' or platform_machine == 'AMD64')) or (sys_platform == 'darwin' and platform_machine == 'arm64'))",
]
huggingface = [
"unsloth[huggingfacenotorch]",

View file

@ -11,12 +11,21 @@ import sys
import types
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
PYPROJECT = REPO_ROOT / "pyproject.toml"
IMPORT_FIXES_PATH = REPO_ROOT / "unsloth" / "import_fixes.py"
def _tomllib():
if sys.version_info >= (3, 11):
import tomllib
return tomllib
return pytest.importorskip("tomli")
def _load_import_fixes_module():
spec = importlib.util.spec_from_file_location(
"unsloth_import_fixes_under_test",
@ -127,3 +136,41 @@ def test_import_fixes_loads_on_python39_syntax():
"""Regression: module must import on 3.9 (postponed annotations for str | None)."""
fixes = _load_import_fixes_module()
assert callable(fixes._torchcodec_version_mismatch_hint)
def test_audio_extras_are_gated_to_platforms_with_a_torchcodec_wheel():
"""torchcodec publishes no sdist and no wheel for Linux aarch64, Windows ARM64 or
Intel Mac, so an ungated pin makes pip fail the whole install on those hosts instead
of just skipping audio -- and the cu*/rocm*/intel torch 2.10 extras pull it in.
The marker must match PLATFORM_LACKS_TORCHCODEC_WHEEL in install_python_stack.py.
"""
markers = pytest.importorskip("packaging.markers")
tomllib = _tomllib()
extras = tomllib.loads(PYPROJECT.read_text(encoding = "utf-8"))["project"][
"optional-dependencies"
]
audio = {n: d for n, d in extras.items() if n.startswith("audio-torch")}
assert audio, "expected audio-torch* extras"
supported = [
{"sys_platform": "linux", "platform_machine": "x86_64"},
{"sys_platform": "win32", "platform_machine": "AMD64"},
{"sys_platform": "darwin", "platform_machine": "arm64"},
]
unsupported = [
{"sys_platform": "linux", "platform_machine": "aarch64"},
{"sys_platform": "win32", "platform_machine": "ARM64"},
{"sys_platform": "darwin", "platform_machine": "x86_64"},
]
for name, deps in audio.items():
for dep in deps:
_, _, marker_text = dep.partition(";")
assert marker_text.strip(), f"{name}: {dep!r} has no marker"
marker = markers.Marker(marker_text.strip())
env = {"python_version": "3.12"}
for case in supported:
assert marker.evaluate({**env, **case}), f"{name} must install on {case}"
for case in unsupported:
assert not marker.evaluate(
{**env, **case}
), f"{name} has no wheel for {case} and must not be resolved there"

View file

@ -0,0 +1,89 @@
#!/bin/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
#
# Guards that `curl ... | sh` cannot report a bogus transport error.
#
# History: install.sh was ~150KB of top-level statements. A top-level `exit` left most
# of the file unread, the write end failed, and curl appended "(56) Failure writing
# output to destination" (or "(23) Failed writing body") after our own error message,
# so users read a real diagnosis as a broken download. The fix is structural: the body
# lives in _unsloth_main, so sh parses the whole file before running anything.
#
# This pins both halves of that contract: the writer must not be killed, AND the
# installer's own exit code must still reach the caller.
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_SH="$SCRIPT_DIR/../../install.sh"
PASS=0
FAIL=0
assert_eq() {
_label="$1"; _expected="$2"; _actual="$3"
if [ "$_actual" = "$_expected" ]; then
echo " PASS: $_label"
PASS=$((PASS + 1))
else
echo " FAIL: $_label (expected '$_expected', got '$_actual')"
FAIL=$((FAIL + 1))
fi
}
echo "=== structure ==="
# The wrapper must be invoked on the LAST executable line, or sh starts executing
# before it has drained the pipe.
if grep -q '^_unsloth_main() {' "$INSTALL_SH"; then
echo " PASS: _unsloth_main is defined at top level"
PASS=$((PASS + 1))
else
echo " FAIL: install.sh is not wrapped in _unsloth_main -- curl-pipe safety is gone"
FAIL=$((FAIL + 1))
fi
_last="$(grep -vE '^\s*(#|$)' "$INSTALL_SH" | tail -1)"
assert_eq "last statement invokes the wrapper" '_unsloth_main "$@"' "$_last"
# Below one pipe buffer the file would fit in the kernel's buffer and this test would
# prove nothing, so fail loudly instead of passing vacuously.
_bytes="$(wc -c < "$INSTALL_SH" | tr -d ' ')"
if [ "$_bytes" -gt 65536 ]; then
echo " PASS: install.sh ($_bytes bytes) exceeds a 64KiB pipe buffer, so this matters"
PASS=$((PASS + 1))
else
echo " FAIL: install.sh is only $_bytes bytes; re-derive whether pipe safety still applies"
FAIL=$((FAIL + 1))
fi
echo "=== behaviour: an early exit must not kill the writer ==="
# `--python` with no argument exits 1 from argument validation having done no work: no
# venv, no downloads, no filesystem writes. Deterministic and safe to run for real.
#
# PIPESTATUS must be read on the very next line, so drop errexit around the pipeline
# rather than appending `|| true`, which would clobber it with the status of `true`.
set +e
cat "$INSTALL_SH" | sh -s -- --python >/dev/null 2>&1
_pipe=("${PIPESTATUS[@]}")
set -e
_writer_rc="${_pipe[0]}"
_reader_rc="${_pipe[1]}"
# A writer rc of 141 (128 + SIGPIPE) is the failure mode curl reports as (56)/(23).
assert_eq "writer survives the early exit (not SIGPIPE)" "0" "$_writer_rc"
assert_eq "installer's own exit code still propagates" "1" "$_reader_rc"
echo "=== behaviour: the same holds for a mid-file exit ==="
# `--package '-evil'` exits from a later validation block, still before any filesystem
# work, so the property is not specific to one early branch.
set +e
cat "$INSTALL_SH" | sh -s -- --package '-evil' >/dev/null 2>&1
_pipe2=("${PIPESTATUS[@]}")
set -e
assert_eq "writer survives a later exit" "0" "${_pipe2[0]}"
assert_eq "later exit code propagates" "1" "${_pipe2[1]}"
echo ""
echo "=== $PASS passed, $FAIL failed ==="
[ "$FAIL" -eq 0 ] || exit 1