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.
This commit is contained in:
parent
d6d2408ce6
commit
f09a1e71a9
11 changed files with 620 additions and 680 deletions
22
.github/scripts/assert-nobuild.ps1
vendored
22
.github/scripts/assert-nobuild.ps1
vendored
|
|
@ -1,12 +1,11 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
# The `nobuild` contract from clean-machine-assert.sh, for Windows.
|
||||
#
|
||||
# A port and not `shell: bash`: the clean-machine scrub drops every `*\Git\*` PATH
|
||||
# entry, and the bash version needs sed/grep/tr/sort out of Git's usr/bin. It also runs
|
||||
# inside the servercore container, which has no bash at all. Both Windows lanes call
|
||||
# this one file so the sdist allowlist cannot drift.
|
||||
# The `nobuild` contract from clean-machine-assert.sh, for Windows. A port and not
|
||||
# `shell: bash`: the scrub drops every `*\Git\*` PATH entry and the bash version needs
|
||||
# sed/grep/tr/sort out of Git's usr/bin, and it also runs inside the servercore
|
||||
# container, which has no bash. Both Windows lanes call this one file so the sdist
|
||||
# allowlist cannot drift.
|
||||
#
|
||||
# Usage: assert-nobuild.ps1 -LogPath logs/install.log (exit 1 = a source build)
|
||||
[CmdletBinding()]
|
||||
|
|
@ -17,9 +16,10 @@ if (-not (Test-Path -LiteralPath $LogPath)) {
|
|||
exit 1
|
||||
}
|
||||
|
||||
# "Built an sdist" is NOT "needed a compiler": every name here has a
|
||||
# setuptools.build_meta backend, no ext_modules and no .c/.cpp/.pyx/.rs file, so its
|
||||
# PEP 517 build is a pure-Python copy step. Identical to clean-machine-assert.sh.
|
||||
# "Built an sdist" is NOT "needed a compiler": every name here 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. Identical to
|
||||
# clean-machine-assert.sh, which carries the per-name rationale.
|
||||
$allow = @('openai-whisper', 'argbind', 'randomname', 'antlr4-python3-runtime', 'triton-kernels')
|
||||
if ($env:UNSLOTH_ALLOW_SDIST) {
|
||||
$allow += ($env:UNSLOTH_ALLOW_SDIST -split '\s+' | Where-Object { $_ })
|
||||
|
|
@ -34,8 +34,8 @@ $esc = [char]27
|
|||
$text = (Get-Content -LiteralPath $LogPath -Raw) -replace "$esc\[[0-9;]*[A-Za-z]", ''
|
||||
$built = @()
|
||||
foreach ($line in ($text -split "`r?`n")) {
|
||||
# A local-path build is something the caller pointed at (the CI source overlay),
|
||||
# never something resolution chose; index dependencies always print `==<version>`.
|
||||
# A local-path build is one the caller pointed at (the CI source overlay), never
|
||||
# one resolution chose; index dependencies always print `==<version>`.
|
||||
if ($line -imatch 'building [a-z0-9._-]+ @ file://') { continue }
|
||||
# pip prints `Building wheel for <pkg>`, uv prints `Building <pkg>==<ver>`
|
||||
# (astral-sh/uv#11165); the `==` or ` @ ` requirement keeps this off the
|
||||
|
|
|
|||
81
.github/scripts/clean-machine-assert.sh
vendored
81
.github/scripts/clean-machine-assert.sh
vendored
|
|
@ -5,13 +5,12 @@
|
|||
# Assert the clean-machine contract after an install attempt.
|
||||
#
|
||||
# absent The toolchain really was absent for the whole run. Catches a leg that
|
||||
# "passed" only because masking silently failed, or because the installer
|
||||
# "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 The wheels-only contract: 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
|
||||
# and there is nothing to read.
|
||||
# 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.
|
||||
|
|
@ -40,9 +39,9 @@ for check in "$@"; do
|
|||
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 survives their
|
||||
# removal, 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.
|
||||
# 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"
|
||||
|
|
@ -74,7 +73,7 @@ for check in "$@"; 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 the question is not USE.
|
||||
# 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
|
||||
|
|
@ -93,17 +92,16 @@ for check in "$@"; do
|
|||
;;
|
||||
|
||||
nobuild)
|
||||
# "Built an sdist" is NOT "needed a compiler". Every name below was checked
|
||||
# "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 subdirectory: 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
|
||||
# the step on Windows and macOS).
|
||||
# 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
|
||||
|
|
@ -116,10 +114,10 @@ for check in "$@"; do
|
|||
# <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 something the
|
||||
# caller pointed at (--local, or the editable overlay), never something
|
||||
# resolution chose. Index dependencies always print `<name>==<version>`, so a
|
||||
# genuine PyPI sdist is still caught, including one named unsloth.
|
||||
# `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://" \
|
||||
|
|
@ -154,10 +152,10 @@ for check in "$@"; do
|
|||
# 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, including the .venv_t5_510/_530/_550 sidecars.
|
||||
# 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.
|
||||
# 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
|
||||
|
|
@ -165,18 +163,17 @@ for check in "$@"; do
|
|||
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 executed
|
||||
# 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.
|
||||
# `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.
|
||||
# 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"
|
||||
|
|
@ -195,11 +192,11 @@ for check in "$@"; do
|
|||
*) bad_arch="$bad_arch $f [$desc]" ;;
|
||||
esac
|
||||
|
||||
# Signature: MAIN EXECUTABLES ONLY. Asserting it for every Mach-O failed the
|
||||
# Signature: MAIN EXECUTABLES ONLY. Asserting it on every Mach-O failed the
|
||||
# mask/pipe leg on 29 ordinary PyPI extension modules plus libportaudio.dylib:
|
||||
# those are MH_BUNDLE/MH_DYLIB images dlopen'd without library validation and
|
||||
# ship unsigned, and that run had already imported them with the installer
|
||||
# exiting 0. macOS enforces on main executables and gatekept .app bundles.
|
||||
# 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
|
||||
|
|
@ -218,14 +215,14 @@ for check in "$@"; do
|
|||
# ("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
|
||||
# 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 status is what the pipeline
|
||||
# returns even on a match.
|
||||
# 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" ;;
|
||||
|
|
@ -235,8 +232,8 @@ for check in "$@"; do
|
|||
fi
|
||||
done < <(_macho_targets | sort -u)
|
||||
if [ "$n" = "0" ]; then
|
||||
# An empty scan reads exactly like a clean one, so the check would pass on a
|
||||
# wrong root and prove nothing.
|
||||
# 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
|
||||
|
|
|
|||
22
.github/scripts/clean-machine-env.sh
vendored
22
.github/scripts/clean-machine-env.sh
vendored
|
|
@ -98,8 +98,8 @@ if [ "$MODE" = "mask" ]; then
|
|||
|
||||
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;
|
||||
# `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
|
||||
|
|
@ -120,9 +120,9 @@ if [ "$MODE" = "mask" ]; then
|
|||
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.
|
||||
# `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
|
||||
|
|
@ -133,9 +133,9 @@ if [ "$MODE" = "mask" ]; then
|
|||
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. Runs before the Homebrew block below so /usr/local/Homebrew is stashed
|
||||
# once, with one restore line, in the right order.
|
||||
# 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"
|
||||
|
|
@ -175,9 +175,9 @@ if [ "$MODE" = "mask" ]; then
|
|||
|
||||
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.
|
||||
# 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.
|
||||
|
|
|
|||
18
.github/scripts/ensure-docker-daemon.ps1
vendored
18
.github/scripts/ensure-docker-daemon.ps1
vendored
|
|
@ -4,14 +4,12 @@
|
|||
# Waits for the Windows Docker daemon on a hosted runner, starting the service if
|
||||
# it is installed but not running.
|
||||
#
|
||||
# Docker is installed on every windows-2022 runner image (runner-images installs it
|
||||
# via Microsoft's install-docker-ce.ps1, without -HyperV, so the daemon serves
|
||||
# WINDOWS containers) but it is not always already RUNNING when a job starts. A
|
||||
# spike run died 21 seconds in with
|
||||
# Docker is installed on every windows-2022 image (runner-images uses Microsoft's
|
||||
# install-docker-ce.ps1 without -HyperV, so the daemon serves WINDOWS containers) but
|
||||
# is not always RUNNING when a job starts: a spike run died 21s in with
|
||||
# failed to connect to the docker API at npipe:////./pipe/docker_engine
|
||||
# while a sibling job on a different runner was fine. Without this wait that flake
|
||||
# reads as "Windows containers are not available on hosted runners", which is the
|
||||
# wrong conclusion entirely.
|
||||
# while a sibling job was fine. Without this wait that flake reads as "Windows
|
||||
# containers are not available on hosted runners", the wrong conclusion entirely.
|
||||
|
||||
[CmdletBinding()]
|
||||
param([int] $TimeoutMinutes = 5)
|
||||
|
|
@ -36,8 +34,8 @@ while ($true) {
|
|||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
||||
# The failing `docker info` probes leave $LASTEXITCODE non-zero, and the runner
|
||||
# appends `exit $LASTEXITCODE` to every pwsh step (actions/runner#351), so without
|
||||
# this reset a successful wait still fails the step.
|
||||
# The failing `docker info` probes leave $LASTEXITCODE non-zero and the runner appends
|
||||
# `exit $LASTEXITCODE` to every pwsh step (actions/runner#351), so without this reset a
|
||||
# successful wait still fails the step.
|
||||
$global:LASTEXITCODE = 0
|
||||
exit 0
|
||||
|
|
|
|||
42
.github/scripts/virgin-windows-install.ps1
vendored
42
.github/scripts/virgin-windows-install.ps1
vendored
|
|
@ -1,9 +1,9 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
# Runs INSIDE a Windows container, after virgin-windows-probe.ps1 has proved the
|
||||
# environment has no toolchain. Runs install.ps1 the way a real user on a bare
|
||||
# Windows box would, then asserts the same things the hosted Windows leg asserts.
|
||||
# Runs INSIDE a Windows container, after virgin-windows-probe.ps1 has proved there is no
|
||||
# toolchain: install.ps1 the way a real user on a bare Windows box runs it, then the same
|
||||
# assertions the hosted Windows leg makes.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
|
|
@ -20,16 +20,16 @@ function Section($t) { Write-Host ""; Write-Host "=== $t ===" }
|
|||
# ── Environment the installer needs to be non-interactive ─────────────────────
|
||||
Section 'install environment'
|
||||
# install.ps1:2885-2888 prompts `Start Unsloth Studio now? [Y/n]` when
|
||||
# [Environment]::UserInteractive is true and stdin is not redirected. Both hold in a
|
||||
# `docker exec` session, so without this the installer BLOCKS FOREVER on Read-Host
|
||||
# and the job dies on timeout with no diagnosis.
|
||||
# [Environment]::UserInteractive is true and stdin is not redirected -- both hold under
|
||||
# `docker exec` -- so without this the installer BLOCKS FOREVER on Read-Host and the job
|
||||
# dies on timeout with no diagnosis.
|
||||
$env:UNSLOTH_SKIP_AUTOSTART = '1'
|
||||
# install.ps1:254/258 joins $env:USERPROFILE with no null guard. Setting the install
|
||||
# root explicitly also keeps the container's state entirely under one directory.
|
||||
# install.ps1:254/258 joins $env:USERPROFILE with no null guard. An explicit root also
|
||||
# keeps the container's state under one directory.
|
||||
$env:UNSLOTH_STUDIO_HOME = 'C:\studio-home'
|
||||
$env:UNSLOTH_STUDIO_DISABLE_PUBLIC_CHECK = '1'
|
||||
# Without this, uv's output is discarded on success and the nobuild check below can
|
||||
# only ever report "built: none".
|
||||
# Without this uv's output is discarded on success and the nobuild check below can only
|
||||
# ever report "built: none".
|
||||
$env:UNSLOTH_VERBOSE = '1'
|
||||
if ($Overlay) {
|
||||
$env:UNSLOTH_CI_SOURCE_OVERLAY = $Overlay
|
||||
|
|
@ -73,8 +73,7 @@ Section 'assert: the install produced something usable'
|
|||
if ($rc -ne 0) {
|
||||
$failures += "installer exited $rc"
|
||||
} else {
|
||||
# Mirrors the Linux leg's "Assert the install is actually usable": an installer
|
||||
# that exits 0 having done nothing must not pass.
|
||||
# As the Linux leg: an installer that exits 0 having done nothing must not pass.
|
||||
if (-not (Test-Path -LiteralPath $venvPy)) {
|
||||
$failures += "installer exited 0 but left no managed Python at $venvPy"
|
||||
Get-ChildItem -Path $env:UNSLOTH_STUDIO_HOME -ErrorAction SilentlyContinue | Format-Table | Out-String | Write-Host
|
||||
|
|
@ -89,11 +88,10 @@ if ($rc -ne 0) {
|
|||
}
|
||||
|
||||
Section 'assert: torch imports'
|
||||
# On the hosted runner this proves less than it looks like: the runner image ships
|
||||
# the VC++ 2015-2022 runtime in System32, so Test-VCRedistInstalled (setup.ps1:875)
|
||||
# short-circuits before it needs winget. THIS container is the first environment in
|
||||
# which that is not true, so a failure here is a genuine finding about bare Windows,
|
||||
# not a CI artefact.
|
||||
# On the hosted runner this proves less than it looks: the image ships the VC++
|
||||
# 2015-2022 runtime in System32, so Test-VCRedistInstalled (setup.ps1:875)
|
||||
# short-circuits before it needs winget. THIS container is the first environment where
|
||||
# that is not true, so a failure here is a genuine finding about bare Windows.
|
||||
if (Test-Path -LiteralPath $venvPy) {
|
||||
foreach ($dll in 'vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll') {
|
||||
$p = Join-Path $env:WINDIR "System32\$dll"
|
||||
|
|
@ -111,9 +109,9 @@ if (Test-Path -LiteralPath $venvPy) {
|
|||
|
||||
Section "assert: the installer took the no-winget path"
|
||||
if (Test-Path -LiteralPath $LogPath) {
|
||||
# install.ps1:1098, the no-winget branch. A container has no Microsoft Store and
|
||||
# therefore no App Installer, so this is the fallback path (python.org + astral.sh)
|
||||
# under test -- the whole reason a container is a good harness.
|
||||
# install.ps1:1098, the no-winget branch. A container has no Microsoft Store and so
|
||||
# no App Installer, which puts the fallback path (python.org + astral.sh) under
|
||||
# test -- the whole reason a container is a good harness.
|
||||
$noWinget = 'will require Python + uv to be already installed'
|
||||
if (Select-String -Path $LogPath -Pattern $noWinget -SimpleMatch -Quiet) {
|
||||
Write-Host "confirmed: installer reported winget as unavailable and used the fallback path"
|
||||
|
|
@ -132,8 +130,8 @@ if ($Overlay -and $rc -eq 0) {
|
|||
}
|
||||
|
||||
Section 'assert: no non-allowlisted source build'
|
||||
# Shared with the hosted Windows legs so the sdist allowlist lives in one place; the
|
||||
# script prints its own diagnosis, so only the verdict is folded in here.
|
||||
# Shared with the hosted Windows legs so the sdist allowlist lives in one place; it
|
||||
# prints its own diagnosis, so only the verdict is folded in here.
|
||||
$nobuild = Join-Path $PSScriptRoot 'assert-nobuild.ps1'
|
||||
if (-not (Test-Path -LiteralPath $nobuild)) {
|
||||
$failures += "assert-nobuild.ps1 is missing next to this script, so the no-build contract went unchecked"
|
||||
|
|
|
|||
56
.github/scripts/virgin-windows-probe.ps1
vendored
56
.github/scripts/virgin-windows-probe.ps1
vendored
|
|
@ -1,14 +1,12 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
||||
|
||||
# Runs INSIDE a Windows container. Proves the environment is genuinely virgin
|
||||
# BEFORE anything is installed into it.
|
||||
#
|
||||
# This script is the entire point of the container lane. The hosted-runner Windows
|
||||
# legs of clean-machine-install-ci.yml simulate absence (rename the toolcache Python
|
||||
# directory, scrub the Machine and User registry PATH); this one asserts real
|
||||
# absence on an OS image that never had a toolchain. If these assertions do not
|
||||
# run, the lane proves nothing that the masked legs did not already prove.
|
||||
# Runs INSIDE a Windows container, proving the environment is genuinely virgin BEFORE
|
||||
# anything is installed into it. This is the entire point of the container lane: the
|
||||
# hosted-runner Windows legs of clean-machine-install-ci.yml only simulate absence
|
||||
# (rename the toolcache Python dir, scrub the Machine and User registry PATH), while
|
||||
# this asserts real absence on an image that never had a toolchain. Without it the lane
|
||||
# proves nothing the masked legs did not already prove.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$failures = @()
|
||||
|
|
@ -45,10 +43,9 @@ Write-Host "USERPROFILE : $env:USERPROFILE"
|
|||
Write-Host "LOCALAPPDATA : $env:LOCALAPPDATA"
|
||||
Write-Host "PROCESSOR_ARCH : $env:PROCESSOR_ARCHITECTURE"
|
||||
|
||||
# install.ps1 line 254/258 does Join-Path $env:USERPROFILE ".unsloth\studio" with no
|
||||
# null guard, so an unset USERPROFILE aborts under ErrorActionPreference=Stop.
|
||||
# The lane sets UNSLOTH_STUDIO_HOME, but record whether a bare container would have
|
||||
# survived without it.
|
||||
# install.ps1:254/258 does Join-Path $env:USERPROFILE ".unsloth\studio" with no null
|
||||
# guard, so an unset USERPROFILE aborts under ErrorActionPreference=Stop. The lane sets
|
||||
# UNSLOTH_STUDIO_HOME, but record whether a bare container would have survived without.
|
||||
if ([string]::IsNullOrWhiteSpace($env:USERPROFILE)) {
|
||||
Write-Host "::warning::USERPROFILE is unset in this container; install.ps1's default install root would abort"
|
||||
}
|
||||
|
|
@ -66,27 +63,26 @@ foreach ($t in $mustBeAbsent) {
|
|||
}
|
||||
|
||||
Section 'informational: present but not a developer toolchain'
|
||||
# These are OS components, not a toolchain. curl.exe and tar.exe ship in System32 on
|
||||
# Server 2022 and are the only transport into a container with no git; naming them
|
||||
# keeps the premise honest rather than silently relying on them.
|
||||
# OS components, not a toolchain. curl.exe and tar.exe ship in System32 on Server 2022
|
||||
# and are the only transport into a container with no git; naming them keeps the
|
||||
# premise honest rather than silently relying on them.
|
||||
foreach ($t in 'cmd', 'powershell', 'curl', 'tar', 'certutil', 'msiexec', 'reg', 'where', 'pwsh', 'node', 'npm', 'msbuild', 'dotnet', 'gcc') {
|
||||
$c = Get-Command $t -ErrorAction SilentlyContinue
|
||||
Write-Host (" {0,-10} {1}" -f $t, $(if ($c) { $c.Source } else { 'ABSENT' }))
|
||||
}
|
||||
|
||||
Section 'virginity: no toolchain on disk either'
|
||||
# A binary can be absent from PATH and still be found by uv's own interpreter
|
||||
# discovery or by py.exe's registry view -- that is exactly how the hosted Windows
|
||||
# leg once reported `python ABSENT` and then installed with the runner's 3.13.14.
|
||||
# Check the disk and the registry, not just PATH.
|
||||
# A binary can be off PATH and still be found by uv's interpreter discovery or py.exe's
|
||||
# registry view -- exactly how the hosted Windows leg once reported `python ABSENT` and
|
||||
# then installed with the runner's 3.13.14. So check disk and registry too.
|
||||
$badPaths = @(
|
||||
'C:\Python27', 'C:\Python3*', 'C:\Program Files\Python*', 'C:\Program Files (x86)\Python*',
|
||||
'C:\Program Files\Git', 'C:\Program Files\CMake', 'C:\Program Files\Microsoft Visual Studio',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio', 'C:\hostedtoolcache', 'C:\ProgramData\chocolatey'
|
||||
)
|
||||
foreach ($p in $badPaths) {
|
||||
# Wildcards can match several directories; take the first so the message names a
|
||||
# real path instead of stringifying an array.
|
||||
# Wildcards can match several dirs; take the first so the message names a real
|
||||
# path instead of stringifying an array.
|
||||
$hit = @(Get-Item -Path $p -ErrorAction SilentlyContinue) | Select-Object -First 1
|
||||
if ($hit) {
|
||||
Write-Host " PRESENT $($hit.FullName)"
|
||||
|
|
@ -115,12 +111,12 @@ foreach ($scope in 'Machine', 'User') {
|
|||
|
||||
# ── The VC++ runtime question the hosted leg cannot answer ────────────────────
|
||||
Section 'VC++ runtime (honest measurement)'
|
||||
# clean-machine-install-ci.yml carries an explicit HONESTY NOTE that the hosted image
|
||||
# ships the VC++ 2015-2022 runtime in System32 and it cannot be removed without
|
||||
# breaking the runner, so `import torch` succeeding there does NOT prove a no-winget
|
||||
# machine has the runtime. This container is the only environment in CI that can
|
||||
# answer it, so their absence is asserted, not merely recorded: if a future base image
|
||||
# starts shipping them the lane silently degrades into another masked leg.
|
||||
# The hosted image ships the VC++ 2015-2022 runtime in System32 and cannot lose it
|
||||
# without breaking the runner (see the HONESTY NOTE in clean-machine-install-ci.yml),
|
||||
# so `import torch` succeeding there does NOT prove a no-winget machine has the
|
||||
# runtime. This container is the only environment in CI that can answer it, so their
|
||||
# absence is asserted, not merely recorded: if a future base image starts shipping
|
||||
# them the lane silently degrades into another masked leg.
|
||||
foreach ($dll in 'vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll') {
|
||||
$p = Join-Path $env:WINDIR "System32\$dll"
|
||||
$present = Test-Path $p
|
||||
|
|
@ -135,9 +131,9 @@ foreach ($k in 'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64',
|
|||
|
||||
# ── Can the installer's transport work at all here? ───────────────────────────
|
||||
Section 'outbound HTTPS and TLS'
|
||||
# install.ps1 never sets [Net.ServicePointManager]::SecurityProtocol, so it inherits
|
||||
# the .NET Framework default. Test the DEFAULT first: if that fails and Tls12 works,
|
||||
# the installer has a real portability bug on hardened images, not a container quirk.
|
||||
# install.ps1 never sets [Net.ServicePointManager]::SecurityProtocol, so it inherits the
|
||||
# .NET Framework default. Test the DEFAULT first: if that fails and Tls12 works, the
|
||||
# installer has a real portability bug on hardened images, not a container quirk.
|
||||
Write-Host "default SecurityProtocol: $([Net.ServicePointManager]::SecurityProtocol)"
|
||||
$probeUrls = @(
|
||||
'https://www.python.org/ftp/python/',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue