diff --git a/.github/scripts/clean-machine-assert.sh b/.github/scripts/clean-machine-assert.sh index 94563bc3fc..4bf1f5957f 100755 --- a/.github/scripts/clean-machine-assert.sh +++ b/.github/scripts/clean-machine-assert.sh @@ -172,11 +172,30 @@ for check in "$@"; do if [ ! -d "$root" ]; then fail "macho requested but $root does not exist" else - n=0 nexe=0 bad_arch="" unsigned="" broken="" + # SCOPE, part 2: the two payloads the install RUNS ON live outside $root. + # `uv venv` links /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. + _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 /unsloth_studio, the .venv_t5_* sidecars and the tauri + # layout's /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 @@ -227,11 +246,16 @@ for check in "$@"; do esac fi fi - done < <(find "$root" -type f \( -perm -u+x -o -name '*.dylib' -o -name '*.so' -o -name '*.node' \) 2>/dev/null) + 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. 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 @@ -239,7 +263,7 @@ for check in "$@"; do 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 are $want$([ "$want" = arm64 ] && echo "; all $nexe main executable(s) signed")" + 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 ;; diff --git a/.github/workflows/clean-machine-install-ci.yml b/.github/workflows/clean-machine-install-ci.yml index 735fc4e20b..448b19e244 100644 --- a/.github/workflows/clean-machine-install-ci.yml +++ b/.github/workflows/clean-machine-install-ci.yml @@ -1588,6 +1588,38 @@ jobs: Write-Host '::error::the container install failed at neither the winget-only git gate nor the missing VC++ runtime; this is a new failure' exit 1 } + # `-or` on its own is too generous. virgin-windows-install.ps1:97 runs the + # torch assertion whenever the venv interpreter exists, whatever the installer + # did, and this image has no VC++ runtime, so ANY failure after venv creation + # -- a Node download, a setup step, a bad prebuilt -- arrives here carrying the + # $vcGate text and was accepted as the pinned outcome. Enumerate what the + # harness actually recorded instead: it prints one `::error::` per + # entry of its $failures list (that script:151), and every one has to be a + # pinned gate. Anchored, because it also dumps the install log tail indented + # two spaces and those copies must not count. + $recorded = @(Get-Content logs/install-outer.log | + ForEach-Object { if ($_ -match '^::error::(.+)$') { $Matches[1].Trim() } }) + Write-Host "recorded failures: $($recorded.Count)" + $recorded | ForEach-Object { Write-Host " $_" } + if ($recorded.Count -eq 0) { + Write-Host '::error::the container install failed but recorded no ::error:: line, so nothing identifies which gate stopped it' + exit 1 + } + # The git gate makes install.ps1 exit non-zero; the missing runtime makes the + # torch assert fail. Nothing else is pinned. + $pinned = @('^installer exited \d+$', '^torch failed to import from the managed Python') + $unexpected = @($recorded | Where-Object { $r = $_; -not ($pinned | Where-Object { $r -match $_ }) }) + if ($unexpected.Count -gt 0) { + Write-Host "::error::the container install recorded a failure outside the pinned gates: $($unexpected -join '; '); this is a new failure" + exit 1 + } + # And a non-zero exit has to BE the git gate: without this a post-venv failure + # that also exits 1 is indistinguishable from the pinned one. + if (($recorded | Where-Object { $_ -like 'installer exited*' }) -and + -not ($gitGate -and ($log -match 'unsloth studio setup failed \(exit code 1\)'))) { + Write-Host '::error::the installer exited non-zero somewhere other than the winget-only git gate in studio/setup.ps1; this is a new failure' + exit 1 + } if ($gitGate) { Write-Host '::notice::known outcome: winget-only git gate (studio/setup.ps1), fixed by #7549' } if ($vcGate) { Write-Host '::notice::known outcome: no VC++ runtime and Ensure-VCRedist is winget-only, fixed by #7549' }