unsloth/.github/scripts/clean-machine-assert.sh
danielhanchen 3be21e87cf Put the branch's own Python under test on the clean-machine legs
install.sh and install.ps1 come from the ref under test, but they install
unsloth from PyPI, which is the consumer path and has to stay that way. That
left everything Python-side coming out of the released wheel: studio/setup.sh,
studio/setup.ps1, studio/install_python_stack.py, and every requirements and
constraints file those resolve through Path(__file__). A branch that changes
constraints.txt or setup.ps1 therefore got a green run that proved nothing
about the change, and some legs proved less than they looked. The Fedora
assertion was already carrying a hand-written workaround for exactly this,
tolerating a triton/git failure on the grounds that the released package lags
the ref.

Legs marked overlay: true now re-point the venv at the ref just before studio
setup runs, through UNSLOTH_CI_SOURCE_OVERLAY: a --no-deps editable install of
the checkout. That makes import studio resolve to the working tree, so the
existing setup-script lookup finds the ref's setup.sh / setup.ps1 and
install_python_stack reads the ref's constraints, with no other change to
either installer.

Not --local: --local additionally installs unsloth-zoo from a git+https URL,
which genuinely needs git, and git absence is the whole point of the masked
legs. The overlay resolves no dependencies and clones nothing, so it holds up
with git, cmake and the compilers all gone. It is not a consumer knob either:
no flag, no usage entry, ignored unless the variable names a directory with a
pyproject.toml in it.

Four legs stay on the released package deliberately, each for its own reason,
recorded in the header: the mac pipe legs keep an end-to-end signal on what a
user actually runs; the trace leg would otherwise answer its own question,
since the editable build calls git through setuptools-scm's file finder; the
non-root Linux leg dies before a venv exists; and WSL only ever receives
install.sh, not a source tree.

Two supporting fixes the overlay depends on or exposes:

install_python_stack.py discarded uv's output whenever a step succeeded, so
the nobuild assertion, which reads the install log, could not see a source
build in the dependency phase at all. That is the phase that installs
studio.txt, where an sdist-only dependency actually turns up, and it reported
"built: none" regardless. It now echoes successful output under
UNSLOTH_VERBOSE, matching what install.sh's run_install_cmd already does.

nobuild now ignores "Building <name> @ file://" lines. A local-path build is
something the caller pointed at, never a dependency resolution chose, and
index dependencies always print <name>==<version>, so a real sdist from PyPI
is still caught, including one named unsloth.

Each overlaid leg also asserts it really was overlaid, so an unset variable
cannot quietly put the whole matrix back on the released wheel.
2026-07-28 22:25:13 +00:00

152 lines
7 KiB
Bash
Executable file

#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
#
# Assert the clean-machine contract after an install attempt.
#
# absent The toolchain really was absent for the whole run. Guards against a leg
# that "passed" only because masking silently failed, or because the
# installer quietly installed Xcode CLT behind our back.
# notools The trace recorded no compiler/git/brew invocation (trace mode).
# nobuild The wheels-only contract: no "Building wheel" from pip, no
# "Building <pkg>==<ver>" from uv. Needs UNSLOTH_VERBOSE=1, else
# run_install_cmd (install.sh:193-243) discards the uv output on success
# and there is nothing here to read.
#
# Usage: bash .github/scripts/clean-machine-assert.sh absent notools nobuild
set -uo pipefail
LOG="${INSTALL_LOG:-logs/install.log}"
TRACE="${UNSLOTH_TOOL_TRACE:-}"
rc=0
fail() { echo "::error::$*"; rc=1; }
ok() { echo "[assert] OK $*"; }
for check in "$@"; do
case "$check" in
absent)
# Deliberately NOT `command -v`: on a virgin Mac /usr/bin/{git,cc} EXIST as CLT
# stubs, so `command -v` succeeds and only RUNNING them fails ("xcrun: error:
# invalid active developer path"). The honest invariant is: must not WORK.
if xcode-select -p >/dev/null 2>&1; then
fail "xcode-select -p still resolves to $(xcode-select -p 2>/dev/null); not a clean Mac"
else
ok "xcode-select -p fails (the gate a virgin Mac hits)"
fi
for tool in git cc clang cmake; do
command -v "$tool" >/dev/null 2>&1 || { ok "$tool not on PATH"; continue; }
if "$tool" --version >/dev/null 2>&1; then
# On Intel runners /usr/bin/git is not CLT-provided and keeps working once
# the CLT are gone, so no masking can remove it. cc and clang do become
# stubs and the macOS consumer path needs no git, so report rather than
# call the simulation broken.
case " ${UNSLOTH_CLEAN_ALLOW_WORKING:-} " in
*" $tool "*)
echo "[assert] NOTE $tool still works ($(command -v "$tool")); allowed on this runner"
continue
;;
esac
fail "toolchain still usable: '$tool --version' succeeded ($(command -v "$tool")); masking failed"
else
ok "$tool present but non-functional (CLT stub), as on a clean Mac"
fi
done
# brew is a plain binary with no stub, so absence from PATH is the right test.
if command -v brew >/dev/null 2>&1; then
fail "Homebrew still on PATH at $(command -v brew); masking failed"
else
ok "brew absent"
fi
;;
notools)
if [ -z "$TRACE" ] || [ ! -f "$TRACE" ]; then
fail "notools requested but no trace file (\$UNSLOTH_TOOL_TRACE=$TRACE)"
else
# git is legitimate under --local (unsloth-zoo comes from a git URL), so that
# leg allow-lists it via UNSLOTH_ALLOW_TOOLS.
allow="${UNSLOTH_ALLOW_TOOLS:-}"
hits=""
while IFS=$'\t' read -r tool rest; do
[ -n "$tool" ] || continue
case " $allow " in *" $tool "*) continue ;; esac
# `xcode-select -p` only ASKS whether a toolchain is selected; the installer
# has to ask, and the point of the fix is that it carries on without one.
# Counting the question as toolchain USE would fail the very leg that proves
# the toolchain was never used. `--install`, which pops the CLT installer,
# stays a hit.
if [ "$tool" = "xcode-select" ]; then
case "$rest" in
-p|--print-path|-v|--version|"") continue ;;
esac
fi
hits="$hits $tool"
done < "$TRACE"
if [ -n "$hits" ]; then
fail "installer invoked toolchain:$(echo "$hits" | tr ' ' '\n' | sort -u | tr '\n' ' ')"
echo "---- tool trace ----"; sort -u "$TRACE" | head -50
else
ok "no compiler/git/brew invocation recorded"
fi
fi
;;
nobuild)
# "Built an sdist" is NOT "needed a compiler". Four packages on the macOS path
# are sdist-only PURE PYTHON (verified against cp313/macos-arm64):
# openai-whisper, argbind, randomname -- no version ever ships a wheel
# antlr4-python3-runtime==4.9.3 -- pinned below the 4.13.2 wheel
# Failing on those is a false alarm, so the contract is "nothing needing a
# COMPILER was built". UNSLOTH_ALLOW_SDIST extends the allowlist.
_allow="openai-whisper argbind randomname antlr4-python3-runtime ${UNSLOTH_ALLOW_SDIST:-}"
if [ ! -f "$LOG" ]; then
fail "nobuild requested but $LOG is missing"
else
# uv does NOT use pip's phrasing: it prints `Building <name>==<version>` to
# stderr (astral-sh/uv#11165), so the pip-only pattern left _built empty on
# every uv source build. Match both spellings. Requiring `==` or ` @ ` after
# the name keeps this off the installer's own lowercase "building frontend..."
# progress text. Strip ANSI first so a coloured run (FORCE_COLOR) parses.
#
# `Building <name> @ file://...` is dropped before the names are read: a
# local-path build is something the caller pointed at (install.sh --local,
# or the UNSLOTH_CI_SOURCE_OVERLAY editable overlay the CI legs use to put
# the branch's Python code under test), never a dependency that resolution
# chose. Dependencies from an index always print `<name>==<version>`, so
# this drops no real signal -- a genuine sdist pulled from PyPI is still
# caught, including one named unsloth.
_esc=$(printf '\033')
_built="$(sed -E "s/${_esc}\[[0-9;]*[A-Za-z]//g" "$LOG" 2>/dev/null \
| grep -viE "building [a-z0-9._-]+ @ file://" \
| grep -oiE "building wheel for [a-z0-9._-]+|building [a-z0-9._-]+(==| @ )" \
| tr 'A-Z' 'a-z' \
| sed -E -e 's/^building wheel for //' -e 's/^building //' -e 's/(==| @ )$//' \
| sort -u || true)"
_bad=""
for pkg in $_built; do
case " $_allow " in *" $pkg "*) continue ;; esac
_bad="$_bad $pkg"
done
if [ -n "$_bad" ]; then
fail "built from source:$_bad -- these must resolve to wheels on a clean machine"
else
[ -n "$_built" ] && say_built="$(echo "$_built" | tr '\n' ' ')" || say_built="none"
ok "no non-allowlisted source build (built: $say_built)"
fi
# Independent of package names: a compiler error means a toolchain was needed.
if grep -qiE "error: command '(cc|gcc|clang|cl)' failed|no such file or directory: 'cc'|clang: error|cargo: not found|error: linker \`cc\` not found" "$LOG"; then
fail "compiler invocation appears in the install log"
grep -iE "error: command '(cc|gcc|clang|cl)' failed|clang: error" "$LOG" | head -10
fi
fi
;;
*)
fail "unknown check '$check'"
;;
esac
done
exit "$rc"