The desktop workflow claims all three platforms are stripped, but only macOS and Windows had a strip step and the Windows one scrubbed the process PATH only. Both gaps let a bundle that needs a developer toolchain pass the one workflow whose premise is that it must not. Linux: the job ignored strip_toolchain entirely and ran the bundled install.sh with the runner's git, gcc, cmake and make in /usr/bin. clean-machine-env.sh now has a Linux --remove branch that moves the resolved tool binaries aside, recorded in restore.sh, and the job calls it plus `assert absent` after the apt step (the .deb install needs dpkg) and before the bundled installer, with a restore step to match macOS. The loop repeats per tool so a name present in both /usr/bin and /usr/local/bin is fully masked rather than half masked. Windows: rewriting $env:PATH does not survive the bundled install.ps1, which calls Refresh-SessionPath (318-337) and rebuilds $env:Path from the Machine and User registry values, and py.exe in C:\Windows reaches the toolcache whatever PATH says. Ported the on-disk toolcache rename, the Machine/User registry scrub and the py -3.11/-3.12/-3.13 start probe from clean-machine-install-ci.yml, so the strip is proven rather than assumed. Windows preflight: the log step was Test-Path, Get-Content and Select-String, none of which can fail, so an app that hangs before preflight passed on the 90 second liveness check alone. It now asserts a tauri.log exists and carries a `desktop_preflight completed disposition=` line, the same unconstrained check macOS and Linux already make. The disposition VALUE is deliberately not constrained: ManagedReady over an unbootable venv is the reported bug. installer_source on macOS: only the pipe delivery branched on it, so a `published` dispatch ran the checked-out script on six of the eight macOS rows while the run was labelled published. The script is now resolved once at the top of the Install step and used by the file and tauri deliveries; pipe still re-fetches through the live transport, because that is half of what it tests. Linux, WSL and Windows already honoured the input. Also shortened the comments across the changed files, keeping the reasoning that says why each check exists.
178 lines
7.5 KiB
Bash
Executable file
178 lines
7.5 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] $*"; }
|
|
|
|
# ── 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_link is what
|
|
# `xcode-select -p` reads, 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 does not fail, it falls 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
|
|
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"
|