unsloth/.github/scripts/clean-machine-env.sh
danielhanchen f09a1e71a9 Tighten the clean-machine CI comments
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.
2026-07-29 06:33:16 +00:00

223 lines
9.6 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.
#
# Simulate a virgin developer machine on a GitHub-hosted runner. Two modes, because
# "the tool is absent" and "the installer never called the tool" need different
# mechanisms:
#
# mask Make the toolchain genuinely ABSENT: scrub PATH to OS defaults and (with
# --remove) move the real toolchain aside so `command -v git` correctly
# FAILS. Deliberately no "poison shims": a failing shim is still FOUND by
# `command -v`, which reports the tool as present, the opposite of clean.
# trace Leave the toolchain working behind wrappers that log the call then exec
# the real binary, answering whether the installer ever REACHES for a
# compiler/git without changing behaviour.
#
# Writes shell exports to $CLEAN_ENV_FILE (default ./clean-machine.env) to `source`;
# nothing is exported globally, so other steps keep a normal environment.
#
# Usage:
# bash .github/scripts/clean-machine-env.sh mask [--remove]
# bash .github/scripts/clean-machine-env.sh trace
# source ./clean-machine.env
set -uo pipefail
MODE="${1:-}"
REMOVE=0
[ "${2:-}" = "--remove" ] && REMOVE=1
case "$MODE" in
mask|trace) ;;
*) echo "usage: $0 {mask|trace} [--remove]" >&2; exit 2 ;;
esac
OS="$(uname -s)"
WORK="${CLEAN_MACHINE_DIR:-$PWD/.clean-machine}"
ENV_FILE="${CLEAN_ENV_FILE:-$PWD/clean-machine.env}"
TRACE="$WORK/tool-invocations.log"
BIN="$WORK/bin"
RESTORE="$WORK/restore.sh"
mkdir -p "$BIN"
: > "$TRACE"
: > "$ENV_FILE"
printf '#!/usr/bin/env bash\n# Undo clean-machine-env.sh --remove. Safe to run twice.\nset -uo pipefail\n' > "$RESTORE"
chmod +x "$RESTORE"
# The toolchain we care about: a consumer install must need none of it.
TOOLS="xcode-select xcrun clang clang++ cc c++ gcc g++ git cmake make brew ninja cargo rustc"
note() { echo "[clean-machine] $*"; }
# Move a path aside and record the reverse in restore.sh. PATH scrubbing only HIDES
# these; uv, the py launcher and framework lookups find them regardless, so absence
# has to be real. The restore line is guarded: the install may have recreated the
# path, and an unguarded `mv` would bury the original inside it.
mask_aside() {
local src="$1" dst="${2:-$1.masked}" as=""
[ -e "$src" ] || return 0
[ -w "$(dirname "$src")" ] || as="sudo"
if $as mv "$src" "$dst" 2>/dev/null; then
note "moved $src aside"
printf "[ -e '%s' ] || %s mv '%s' '%s' 2>/dev/null || true\n" "$src" "$as" "$dst" "$src" >> "$RESTORE"
else
note "WARN could not move $src"
fi
}
# ── PATH scrub ────────────────────────────────────────────────────────────────
# Keep only OS-default system dirs: drops Homebrew, the hosted Python toolcache,
# setup-* shims, pipx, cargo and every other preinstalled developer dir.
scrub_path() {
local keep out=""
if [ "$OS" = "Darwin" ]; then
keep="/usr/bin:/bin:/usr/sbin:/sbin"
else
keep="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
fi
local IFS=":"
for d in $keep; do
[ -d "$d" ] && out="${out:+$out:}$d"
done
echo "$out"
}
# ── mask ──────────────────────────────────────────────────────────────────────
if [ "$MODE" = "mask" ]; then
NEWPATH="$(scrub_path)"
{
echo "export PATH='$NEWPATH'"
# UNSET, not a fake path: `xcode-select -p` honours DEVELOPER_DIR and prints it
# verbatim with exit 0, so a nonexistent dir makes the probe SUCCEED. On a clean
# Mac it is unset and the missing xcode_select_link is what makes the probe fail.
echo "unset DEVELOPER_DIR || true"
echo "unset SDKROOT CC CXX CFLAGS CXXFLAGS LDFLAGS CMAKE_GENERATOR CMAKE_PREFIX_PATH || true"
echo "export HOMEBREW_NO_AUTO_UPDATE=1"
echo "export UNSLOTH_CLEAN_MACHINE=1"
} >> "$ENV_FILE"
if [ "$REMOVE" = "1" ] && [ "$OS" = "Darwin" ]; then
# Best effort, each step independent and recorded in restore.sh so an
# `if: always()` step can put the runner back. `xcode-select -p` reads
# xcode_select_link, so removing it reproduces a virgin Mac's gate;
# `xcode-select --reset` is NOT enough, it can reselect a full Xcode.app.
if [ -e /var/db/xcode_select_link ]; then
if sudo rm -f /var/db/xcode_select_link 2>/dev/null; then
note "removed /var/db/xcode_select_link"
echo "sudo xcode-select --switch /Library/Developer/CommandLineTools 2>/dev/null || true" >> "$RESTORE"
else
note "WARN could not remove /var/db/xcode_select_link"
fi
fi
# Moving the CLT dir aside turns /usr/bin/{cc,clang,git} into dead shims, so the
# run also proves the install needs no compiler at all.
if [ -d /Library/Developer/CommandLineTools ]; then
if sudo mv /Library/Developer/CommandLineTools /Library/Developer/CommandLineTools.masked 2>/dev/null; then
note "moved CommandLineTools aside"
echo "sudo mv /Library/Developer/CommandLineTools.masked /Library/Developer/CommandLineTools 2>/dev/null || true" >> "$RESTORE"
else
note "WARN could not move CommandLineTools"
fi
fi
# Xcode.app must go too: with the link removed AND CommandLineTools moved,
# `xcode-select -p` still succeeds, falling through to the image's Xcode bundle
# (observed: /Applications/Xcode_16.4.app/Contents/Developer), which re-arms
# /usr/bin/{git,cc}. A rename is instant whatever the bundle size.
for app in /Applications/Xcode*.app; do
[ -d "$app" ] || continue
if sudo mv "$app" "${app}.masked" 2>/dev/null; then
note "moved $(basename "$app") aside"
echo "sudo mv '${app}.masked' '$app' 2>/dev/null || true" >> "$RESTORE"
else
note "WARN could not move $app"
fi
done
# /usr/local EXISTS on a factory-fresh Mac: a SIP-exempt firmlink, and empty. What
# is absent is its CONTENTS, /usr/local/bin included, so empty it rather than remove
# it. Before the Homebrew block below, so /usr/local/Homebrew is stashed once, with
# one restore line, in the right order.
if [ -d /usr/local ]; then
STASH="$WORK/usr-local"
mkdir -p "$STASH"
for entry in /usr/local/* /usr/local/.[!.]*; do
[ -e "$entry" ] || continue
base="$(basename "$entry")"
if sudo mv "$entry" "$STASH/$base" 2>/dev/null; then
note "emptied /usr/local/$base"
printf "[ -e '/usr/local/%s' ] || sudo mv '%s/%s' '/usr/local/%s' 2>/dev/null || true\n" \
"$base" "$STASH" "$base" "$base" >> "$RESTORE"
else
note "WARN could not move $entry"
fi
done
fi
# The hosted toolcache and the python.org framework are what a PATH scrub cannot
# reach: uv discovers interpreters by probing well-known locations.
mask_aside "${AGENT_TOOLSDIRECTORY:-$HOME/hostedtoolcache}"
mask_aside /Library/Frameworks/Python.framework
# Developer dotdirs and caches. A virgin $HOME has none of these, and a populated
# uv/pip cache can satisfy a resolution that would fail on a user's machine.
for d in .cargo .rustup .nvm .rbenv .pyenv .local .cache \
Library/Caches/uv Library/Caches/pip Library/Caches/Homebrew; do
mask_aside "$HOME/$d"
done
for brewdir in /opt/homebrew /usr/local/Homebrew; do
if [ -d "$brewdir" ]; then
if sudo mv "$brewdir" "${brewdir}.masked" 2>/dev/null; then
note "moved $brewdir aside"
echo "sudo mv '${brewdir}.masked' '$brewdir' 2>/dev/null || true" >> "$RESTORE"
else
note "WARN could not move $brewdir"
fi
fi
done
fi
if [ "$REMOVE" = "1" ] && [ "$OS" = "Linux" ]; then
# A hosted Linux runner keeps git, gcc, cmake and make in /usr/bin, which the PATH
# scrub has to keep, so absence must be made real: move the resolved binaries aside
# (recorded in restore.sh). Versioned siblings like gcc-11 survive, but a consumer
# install invokes the unsuffixed names, which is what `absent` checks.
for tool in $TOOLS; do
# Repeat per tool: a runner can carry the same name in /usr/bin and
# /usr/local/bin, and moving only the first leaves the second on PATH.
for _ in 1 2 3 4; do
real="$(command -v "$tool" 2>/dev/null || true)"
[ -n "$real" ] && [ -e "$real" ] || break
if sudo mv "$real" "$real.masked" 2>/dev/null; then
note "moved $real aside"
echo "sudo mv '$real.masked' '$real' 2>/dev/null || true" >> "$RESTORE"
else
note "WARN could not move $real"
break
fi
done
done
fi
fi
# ── trace ─────────────────────────────────────────────────────────────────────
if [ "$MODE" = "trace" ]; then
for tool in $TOOLS; do
real="$(command -v "$tool" 2>/dev/null || true)"
[ -n "$real" ] || continue
# Logs the call then execs the REAL binary: behaviour unchanged, so the trace
# answers "did the installer reach for this?" honestly.
cat > "$BIN/$tool" <<WRAP
#!/bin/sh
printf '%s\t%s\n' "$tool" "\$*" >> "$TRACE"
exec "$real" "\$@"
WRAP
chmod +x "$BIN/$tool"
done
{
echo "export PATH='$BIN:$PATH'"
echo "export UNSLOTH_TOOL_TRACE='$TRACE'"
echo "export UNSLOTH_CLEAN_MACHINE=trace"
} >> "$ENV_FILE"
fi
note "mode=$MODE remove=$REMOVE"
note "env file: $ENV_FILE"
note "trace: $TRACE"
note "restore: $RESTORE"