Comments only, no assertion logic, pins or leg definitions touched. Reflowed every rationale block to denser wording and removed the duplication that had built up across repeated steps: the desktop workflow repeated the fork-PR skip, the desktop-v* tag resolution and the restore-runner note once per platform, and the installer workflow repeated its path-filter rationale in both the pull_request and push blocks. Those now point at the first copy. Every WHY is kept: why the masked legs avoid install.sh --local, what UNSLOTH_CI_SOURCE_OVERLAY is for, why `absent` tests "must not work" rather than command -v, why the .venv_t5_* sidecars are in the macho scan scope, why the signature check is main-executables-only, why each nobuild allowlist entry is a pure-Python sdist, why the WSL job gates and what the pipe truncation was, and why the virgin container's overlay=false row is still pinned. Proved comments-only three ways: both workflow revisions parsed with yaml.safe_load_all and every leaf walked (only `run:` scalars differ); every changed bash body and .sh compared byte-for-byte after `bash --pretty-print -n`; every changed pwsh body and .ps1 compared as a token stream with Comment and NewLine tokens dropped. A negative control injecting one non-comment line into each layer makes all of them fail.
261 lines
13 KiB
Bash
Executable file
261 lines
13 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. Catches a leg that
|
|
# "passed" 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 Wheels-only: no "Building wheel" from pip, no "Building <pkg>==<ver>"
|
|
# from uv. Needs UNSLOTH_VERBOSE=1, or run_install_cmd
|
|
# (install.sh:193-243) discards uv's output on success.
|
|
# macho Every Mach-O under $MACHO_ROOT is the host architecture, and every
|
|
# Mach-O MAIN EXECUTABLE is signed. Closes the Rosetta 2 gap, the one
|
|
# divergence masking cannot reproduce.
|
|
#
|
|
# Usage: bash .github/scripts/clean-machine-assert.sh absent notools nobuild macho
|
|
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)
|
|
# NOT `command -v`: on a virgin Mac /usr/bin/{git,cc} EXIST as CLT stubs, so it
|
|
# succeeds and only RUNNING them fails. The 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, so no masking can take
|
|
# it away. cc and clang do become stubs and the macOS consumer path needs
|
|
# no git, so report rather than fail.
|
|
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, and the fix
|
|
# is that the installer carries on without one, so it is not USE.
|
|
# `--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". Every name below was verified
|
|
# against its own sdist: setuptools.build_meta backend, no ext_modules, no
|
|
# .c/.cpp/.pyx/.rs file, so its PEP 517 build is a pure-Python copy step.
|
|
# openai-whisper, argbind, randomname -- no version ever ships a wheel
|
|
# antlr4-python3-runtime==4.9.3 -- pinned below the 4.13.2 wheel
|
|
# triton-kernels -- requirements/triton-kernels.txt pins a git URL under the
|
|
# triton repo's python/triton_kernels subdir: 75 Python files, a four-line
|
|
# pyproject.toml, no setup.py, kernels compiled at runtime. A direct URL the
|
|
# installer names itself, not something resolution chose, and only the Linux
|
|
# legs reach it (install_python_stack.py skips it on Windows and macOS).
|
|
# UNSLOTH_ALLOW_SDIST extends the allowlist.
|
|
#
|
|
# Lowercased and underscore-folded on both sides: a distribution name and the
|
|
# name uv prints can disagree on the separator (triton_kernels vs triton-kernels).
|
|
_allow="$(printf '%s' "openai-whisper argbind randomname antlr4-python3-runtime triton-kernels ${UNSLOTH_ALLOW_SDIST:-}" | tr 'A-Z_' 'a-z-')"
|
|
if [ ! -f "$LOG" ]; then
|
|
fail "nobuild requested but $LOG is missing"
|
|
else
|
|
# uv prints `Building <name>==<version>`, pip prints `Building wheel for
|
|
# <name>` (astral-sh/uv#11165), so match both; the `==` or ` @ ` requirement
|
|
# keeps this off the installer's own lowercase "building frontend..." text, and
|
|
# ANSI is stripped first so a coloured run (FORCE_COLOR) parses.
|
|
# `Building <name> @ file://...` is dropped: a local-path build is one the
|
|
# caller pointed at (--local, or the editable overlay), never one resolution
|
|
# chose. Index dependencies always print `<name>==<version>`, so a genuine
|
|
# PyPI sdist 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/(==| @ )$//' \
|
|
| tr '_' '-' \
|
|
| 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
|
|
;;
|
|
|
|
macho)
|
|
# The one thing masking cannot reproduce: Rosetta 2 is preinstalled on hosted
|
|
# runners and absent from a factory-fresh Mac, so an x86_64-only payload runs
|
|
# green here and dies with "bad CPU type in executable" for the user. `lipo` is an
|
|
# xcrun shim and gone after masking, so read `file -b`, keyed off `uname -m`
|
|
# (macos-15-intel is x86_64).
|
|
#
|
|
# SCOPE: all of $MACHO_ROOT, .venv_t5_510/_530/_550 sidecars included. Those are
|
|
# payload, not scratch: setup.sh:579-581 creates them during a normal install and
|
|
# transformers_version.py:338-348 puts them on sys.path. Any exclusion must be a
|
|
# named path rule, never a narrowed find.
|
|
root="${MACHO_ROOT:-${UNSLOTH_STUDIO_HOME:-$HOME/.unsloth}}"
|
|
want="$(uname -m)"
|
|
[ "$want" = "aarch64" ] && want=arm64
|
|
if [ ! -d "$root" ]; then
|
|
fail "macho requested but $root does not exist"
|
|
else
|
|
# SCOPE, part 2: the two payloads the install RUNS ON live outside $root.
|
|
# `uv venv` links <venv>/bin/python at its base interpreter rather than copying
|
|
# it, and the find below has no -L, so the interpreter that ran every install
|
|
# step is invisible to it; the uv that fetched it lands in $HOME/.local/bin.
|
|
# Both are exactly what Rosetta 2 hides: an x86_64 uv or managed CPython runs
|
|
# green here and dies on the factory-fresh Mac this job stands in for.
|
|
_macho_targets() {
|
|
find "$root" -type f \( -perm -u+x -o -name '*.dylib' -o -name '*.so' -o -name '*.node' \) 2>/dev/null
|
|
# -L follows the interpreter symlink; -maxdepth keeps this a bin/ lookup and
|
|
# not a second walk of site-packages through the venv's lib64 link. Depth 4
|
|
# covers <root>/unsloth_studio, the .venv_t5_* sidecars and the tauri layout's
|
|
# <root>/studio/unsloth_studio.
|
|
find -L "$root" -maxdepth 4 -type f -path '*/bin/python' 2>/dev/null
|
|
for _uv in "$HOME/.local/bin/uv" "$(command -v uv 2>/dev/null || true)"; do
|
|
[ -n "$_uv" ] && [ -f "$_uv" ] && printf '%s\n' "$_uv"
|
|
done
|
|
}
|
|
n=0 nexe=0 nout=0 bad_arch="" unsigned="" broken=""
|
|
while IFS= read -r f; do
|
|
desc="$(file -b "$f" 2>/dev/null || true)"
|
|
case "$desc" in *Mach-O*) ;; *) continue ;; esac
|
|
n=$((n + 1))
|
|
case "$f" in "$root"/*) ;; *) nout=$((nout + 1)) ;; esac
|
|
# Substring, not equality: a universal binary lists every slice it carries,
|
|
# and one that includes the host arch is fine.
|
|
case "$desc" in
|
|
*"$want"*) ;;
|
|
*) bad_arch="$bad_arch $f [$desc]" ;;
|
|
esac
|
|
|
|
# Signature: MAIN EXECUTABLES ONLY. Asserting it on every Mach-O failed the
|
|
# mask/pipe leg on 29 ordinary PyPI extension modules plus libportaudio.dylib:
|
|
# MH_BUNDLE/MH_DYLIB images dlopen'd without library validation, shipped
|
|
# unsigned, and that run had already imported them with the installer exiting
|
|
# 0. macOS enforces on main executables and gatekept .app bundles.
|
|
#
|
|
# Key off the filetype `file` reports, not the path: a .so may be a bundle or a
|
|
# dylib, and an executable may have no extension. The library veto is second so
|
|
# a mixed-type fat file counts as a library. Substring tests are
|
|
# order-independent (Apple prints `... executable arm64`, GNU `... arm64
|
|
# executable`).
|
|
_is_exe=0
|
|
case "$desc" in *executable*) _is_exe=1 ;; esac
|
|
case "$desc" in *"shared library"*|*bundle*) _is_exe=0 ;; esac
|
|
# Named rule so a failure says which path matched; the filetype test
|
|
# already covers the MH_EXECUTE at .app/Contents/MacOS/<name>.
|
|
case "$f" in *.app/Contents/MacOS/*) _is_exe=1 ;; esac
|
|
[ "$_is_exe" = 1 ] && nexe=$((nexe + 1))
|
|
|
|
# arm64 only: the kernel refuses to exec an unsigned arm64 main binary
|
|
# ("Killed: 9"), while x86_64 execs it happily, so an unsigned x86_64
|
|
# payload is not the same defect.
|
|
if [ "$want" = "arm64" ] && [ "$_is_exe" = 1 ]; then
|
|
# Ad-hoc counts as signed: arm64 linkers seal ad-hoc by default, so the test
|
|
# is "has a seal that verifies", not "has an identity". `spctl` and
|
|
# `--strict` would demand an authority and reject ad-hoc.
|
|
if ! codesign -v "$f" >/dev/null 2>&1; then
|
|
# Nothing to verify and a seal that does not match mean different things.
|
|
# Captured, not piped into grep: `codesign -dvv` exits non-zero on an
|
|
# unsigned file, and under `pipefail` that is the pipeline's status even
|
|
# on a match.
|
|
_sig="$(codesign -dvv "$f" 2>&1 || true)"
|
|
case "$_sig" in
|
|
*"not signed at all"*) unsigned="$unsigned $f" ;;
|
|
*) broken="$broken $f" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
done < <(_macho_targets | sort -u)
|
|
if [ "$n" = "0" ]; then
|
|
# An empty scan reads exactly like a clean one, so a wrong root would pass
|
|
# and prove nothing.
|
|
fail "no Mach-O found under $root; the arch/signature assertion proved nothing"
|
|
elif [ "$nout" = "0" ]; then
|
|
# Same rule for the roots added above: install.sh always bootstraps uv into
|
|
# $HOME/.local/bin, so zero hits outside $root means the extra scan matched
|
|
# nothing and uv's architecture went unproven.
|
|
fail "no Mach-O outside $root was scanned, so uv and the venv's base interpreter escaped the check"
|
|
elif [ -n "$bad_arch" ]; then
|
|
fail "Mach-O is not $want, so it runs here only under Rosetta 2, which a fresh Mac does not have:$bad_arch"
|
|
elif [ -n "$unsigned" ]; then
|
|
fail "unsigned Mach-O main executable, which arm64 macOS refuses to exec:$unsigned"
|
|
elif [ -n "$broken" ]; then
|
|
fail "Mach-O main executable carries a signature that does not verify:$broken"
|
|
else
|
|
ok "$n Mach-O files under $root, plus uv and the venv's base interpreter, are $want$([ "$want" = arm64 ] && echo "; all $nexe main executable(s) signed")"
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
fail "unknown check '$check'"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
exit "$rc"
|