Skip to content

Installer: stop requiring a developer toolchain on the consumer path - #7547

Merged
danielhanchen merged 12 commits into
mainfrom
fix/installer-no-toolchain-on-consumer-path
Jul 29, 2026
Merged

Installer: stop requiring a developer toolchain on the consumer path#7547
danielhanchen merged 12 commits into
mainfrom
fix/installer-no-toolchain-on-consumer-path

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

The problem

A brand new Mac cannot start the install. Both entry points fail the same way:

==> Xcode Command Line Tools are required.

Linux has the same shape: any non-apt distro exits 1 over cmake git build-essential libcurl4-openssl-dev, with the message "These are needed to build the GGUF inference engine."

Nothing under either gate needs a toolchain

Component How it arrives Compiler?
uv prebuilt binary tarball no
Python uv-managed python-build-standalone no
llama.cpp prebuilt download no
whisper.cpp prebuilt, sha256-verified no
Node pinned nodejs.org archive no
triton skipped on macOS n/a
unsloth-zoo via git+https --local only git, dev only

unslothai/llama.cpp release b10107-mix-1911198 publishes macos-arm64, macos-x64, linux-x64 and linux-arm64 builds covering cpu, cuda12, cuda13, rocm and vulkan. PR #6617 already removed the Homebrew/cmake hard stop on macOS for exactly this reason and left the CLT stop behind.

What changes

  • macOS: absent CLT warns and continues instead of exit 1 plus a GUI dialog.
  • Linux/WSL: only a download transport (curl or wget) is fatal. Build tooling warns and the prebuilt path proceeds. The non-apt exit 1 is gone.
  • Both: --local keeps a hard git requirement, and says why.
  • Caps av<16. av 16+ ships no cp313 macOS arm64 wheel and is a C extension over FFmpeg, so uv would silently fall back to a source build needing a compiler and FFmpeg headers.

Both gates move into _check_macos_deps() and _check_linux_deps() so tests/sh can extract them. The old inline form could not be reached by the tests/sh convention, which is why this shipped broken and stayed broken.

Tests

tests/sh/test_macos_clt_gate.sh (19 assertions) and tests/sh/test_linux_deps_gate.sh (25) cover the clean machine, the real virgin-Mac shape where /usr/bin/git exists but fails, the non-apt distro, the missing-transport case, and both --local paths. Backend CI globs tests/sh/test_*.sh, so they run automatically.

Writing the Linux test caught a latent bug: the gate trimmed its list with $(echo ... | sed ...), so on a minimal image without sed the substitution yields empty and it reports "all system dependencies found" on a machine that has none of them. Now parameter expansion.

Verification

Run on GitHub-hosted macOS runners with /var/db/xcode_select_link, /Library/Developer/CommandLineTools, /Applications/Xcode*.app and Homebrew moved aside, so xcode-select -p, git, cc and clang genuinely do not work:

Leg main this branch
macos-14 pipe fail pass
macos-14 file fail pass
macos-15 pipe fail pass
macos-26 file fail pass
macos-14 file --no-torch fail pass

The recorded tool-invocation trace for a full successful install is a single line, xcode-select -p, which is the detection probe this change makes non-fatal. Nothing compiled and nothing installed a toolchain.

The CI that produced those numbers is in a separate PR so this one stays reviewable.

A brand new Mac cannot install Studio at all. install.sh gates on
`xcode-select -p` and exits 1 with 'Xcode Command Line Tools are required',
and Linux exits 1 on any non-apt distro over cmake/gcc/git/libcurl headers.

Nothing under either gate needs a toolchain. uv is a prebuilt binary, CPython
comes from uv's managed python-build-standalone, llama.cpp and whisper.cpp are
prebuilt downloads, Node is a pinned nodejs.org archive, and triton is skipped on
macOS. unslothai/llama.cpp b10107-mix-1911198 publishes macos-arm64, macos-x64,
linux-x64 and linux-arm64 builds covering cpu, cuda12, cuda13, rocm and vulkan.
PR #6617 already dropped the Homebrew/cmake stop on macOS for this reason and
just left the CLT stop behind.

macOS: warn and continue when the CLT are absent. Linux: only a download
transport (curl or wget) is fatal; build tooling warns. Both keep a hard git
requirement for --local, which installs unsloth-zoo from a git+https URL.

Both gates move into functions so tests/sh can extract them. The old inline form
could not be reached by the tests/sh convention, which is why this shipped broken
and stayed broken. test_macos_clt_gate.sh (19 assertions) and
test_linux_deps_gate.sh (25) cover the clean machine, the CLT-stub shape where
/usr/bin/git exists but fails, the non-apt distro, and the --local paths.

Writing the Linux test caught a latent bug: the gate trimmed its list with
$(echo ... | sed ...), so on a minimal image without sed the substitution yields
empty and it reports 'all system dependencies found' on a machine with none of
them. Replaced with parameter expansion.

Also caps av<16 in the single-env constraints. av 16+ ships no cp313 macOS arm64
wheel, and it is a C extension over FFmpeg, so uv would silently fall back to a
source build needing both a compiler and FFmpeg headers.

Verified on GitHub-hosted macOS runners with /var/db/xcode_select_link,
/Library/Developer/CommandLineTools, /Applications/Xcode*.app and Homebrew moved
aside. macos-14, macos-15 and macos-26 fail on main and install cleanly with
this; the recorded tool-invocation trace for the whole install is a single
`xcode-select -p`, so nothing compiled and nothing installed a toolchain.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 577fa1ff48

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh
# minimal image a failed $(... | sed ...) yields "" -> "all dependencies found".
_optional_missing="${_optional_missing# }"

if [ "$STUDIO_LOCAL_INSTALL" = true ] && ! _has_working_git; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Supply git before installing Triton kernels

On a normal Linux/WSL install with curl or wget but no git, this condition lets setup proceed, yet studio/install_python_stack.py:3141-3150 unconditionally installs requirements/triton-kernels.txt, whose dependency is a git+https URL. Reproducing with uv 0.11.33 and git unavailable fails with Git operation failed while invoking git init, and the pip fallback also requires git for VCS dependencies, so the exact minimal/non-apt consumer environments this change intends to support still abort later. Either retain/install git on Linux or fetch Triton kernels without a VCS dependency.

Useful? React with 👍 / 👎.

…els without it

Making git optional on Linux was too broad. studio/backend/requirements/
triton-kernels.txt line 2 is a git+https URL, so step 6/14 died with 'Cannot find
command git' and failed the whole setup on ubuntu2404-root, ubuntu2404-arm-root
and fedora41, all of which had been passing. The claim that nothing on the
consumer path needs git holds on macOS, where triton is skipped, but not here.

install.sh now auto-installs git through apt with the other optional tooling, so
Debian and Ubuntu are unchanged. The triton kernels step skips with a message
when git is absent instead of failing: they are a training speedup, not a boot
requirement, and a GGUF chat install has no use for them.

Six more assertions pin both halves.
@danielhanchen

Copy link
Copy Markdown
Member Author

Pushed a correction. The first version of the Linux half was too broad and regressed three legs that had been passing.

studio/backend/requirements/triton-kernels.txt line 2 is:

triton_kernels @ git+https://github.com/triton-lang/triton.git@release/3.6.x#subdirectory=python/triton_kernels

so with git no longer guaranteed, step 6/14 died with Cannot find command 'git' and took the whole setup down on ubuntu2404-root, ubuntu2404-arm-root and fedora41.

My "nothing on the consumer path needs git" reasoning held for macOS, where triton is skipped outright, and I carried it to Linux without checking that. It does not hold there.

Fix

  • install.sh auto-installs git through apt alongside the other optional tooling, so Debian and Ubuntu behave exactly as before.
  • The triton kernels step skips with a message when git is absent rather than failing the install. Those kernels are a training speedup, not a boot requirement, and a GGUF chat-only install has no use for them.

The gate change itself was still correct

fedora41 used to exit at the dependency check having done no work at all. It now installs through to step 6/14, which is the part this PR was meant to unblock. With the triton skip it should complete, with that one speedup absent and stated.

tests/sh/test_linux_deps_gate.sh is up to 31 assertions, six of them new and covering exactly this: apt present means git gets fetched, no apt means the install proceeds while naming what is lost.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eb1192156

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh Outdated
# costs the features named below, but on Debian/Ubuntu we can just get them.
if [ -n "$_optional_missing" ] && command -v apt-get >/dev/null 2>&1; then
step "deps" "installing optional build tools: $_optional_missing" "$C_DIM"
_smart_apt_install $_optional_missing || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make optional apt failures nonfatal

On an apt-based non-root or headless install where any optional tool is absent and apt/sudo cannot install it—or the user declines—the new optional path still aborts setup. _smart_apt_install handles these cases with exit 1 (and Tauri elevation with exit 2), so the trailing || true never gets a chance to convert the failure into the promised warning; common minimal Debian/Ubuntu installs therefore continue to require the developer toolchain this change intends to make optional.

Useful? React with 👍 / 👎.

Comment thread studio/install_python_stack.py Outdated
req = REQ_ROOT / "triton-kernels.txt",
constrain = False,
)
if shutil.which("git") is None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify Git works before installing Triton kernels

On non-apt Linux systems where a git executable exists but is nonfunctional, _check_linux_deps correctly classifies Git as missing via _has_working_git, but this downstream check only tests whether its path exists. It consequently attempts the git+https Triton requirement and pip_install terminates the whole installation instead of taking the advertised optional-skip path; probe git --version here as well rather than relying on shutil.which alone.

Useful? React with 👍 / 👎.

The Intel clean-machine leg installed with the toolchain masked, then died in
studio setup:

    subprocess.CalledProcessError: Command '['cmake', ...]' returned non-zero
    ERROR: Failed building wheel for pytorch_tokenizers

pytorch_tokenizers publishes wheels for macOS arm64, linux x86_64, linux aarch64
and windows, but none for macOS x86_64 at any Python version, so uv falls back to
an sdist that shells out to cmake. Nothing passes --only-binary, so the
compiler-free property was an assumption rather than a contract, and Intel is
where it broke.

Marked so it installs everywhere except Intel macOS. Apple Silicon is unaffected.
@danielhanchen

Copy link
Copy Markdown
Member Author

One more from the clean-machine runs, in scope for this PR because it is the same "the consumer path must not need a compiler" property.

With the toolchain masked, the Intel macOS leg got past the dependency gate and then died inside studio setup:

subprocess.CalledProcessError: Command '['cmake', ...]' returned non-zero exit status 1
ERROR: Failed building wheel for pytorch_tokenizers

Measured with uv, --only-binary :all:, cp312 and cp313:

target wheel
macOS arm64 yes
macOS x86_64 none, any version
linux x86_64 / aarch64 yes
windows x86_64 yes

So uv silently fell back to an sdist that shells out to cmake. Nothing in the tree passes --only-binary, which is why compiler-free was an assumption rather than a contract, and Intel is where it broke.

Marked to install everywhere except Intel macOS. Apple Silicon is unaffected, and the file already uses environment markers so this follows the existing convention. I verified the marker evaluates correctly for Intel mac, Apple Silicon, linux x86_64, linux arm64 and Windows.

This is also the argument for adding --only-binary with a narrow allowlist as a follow-up: it would have failed loudly here with "no wheel for pytorch_tokenizers" instead of quietly compiling.

shimmyshimmer and others added 2 commits July 28, 2026 13:43
_smart_apt_install exits rather than returns, and `|| true` does not catch an
exit, so a box missing cmake or git aborted at the gate added to let it
continue. Verified in sh, dash and bash. Run it in a subshell and re-raise only
code 2, the NEED_SUDO handshake install.rs answers with an elevation prompt.

install.sh treats a present-but-broken git as missing, but the Python side
tested only shutil.which, so it promised to skip the git+https triton
requirement and then fetched it anyway. Same check on both sides now.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6420d68f1a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh Outdated
Comment on lines +2108 to +2109
( _smart_apt_install $_optional_missing ) || _sai_rc=$?
if [ "$_sai_rc" -eq 2 ]; then exit 2; fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not elevate for optional build tools

On an apt-based Linux desktop install running as a normal user, any missing cmake, git, compiler, or curl headers makes _smart_apt_install emit NEED_SUDO and exit 2, which is re-raised here. The production Tauri entry point passes --tauri (studio/src-tauri/src/install.rs:201-202) and treats this exit as a mandatory elevation request (install.rs:674-686), while canceling returns the user to the not-installed state. Consequently, the consumer path still cannot complete without granting permission to install the very developer toolchain this change classifies as optional; optional-install failure, including exit 2, should instead fall through to the prebuilt warning.

Useful? React with 👍 / 👎.

Comment on lines +3171 to +3172
if not _has_working_git():
_safe_print(" no working git -- skipping triton kernels (training speedup only)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Advance progress when skipping triton kernels

When git is unavailable on the newly supported Linux consumer path, this branch skips both the install and _progress(), but _TOTAL still includes the Linux triton step. Every subsequent step is therefore off by one and a successful install finishes with an incomplete progress bar such as 14/15; advance a labeled skipped step here, as the other conditional skips do, or adjust the total for this condition.

Useful? React with 👍 / 👎.

# antlr4-python3-runtime pinned at 4.9.3) are pure Python and build with no compiler,
# which is why they are allowlisted in .github/scripts/clean-machine-assert.sh instead
# of being pinned here.
av<16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the PyAV wheel on Windows ARM64

This unconditional cap also applies to the supported Windows ARM64 path (install_python_stack.py:59-62 and install.ps1:1184-1188). PyAV 15.1.0 publishes Windows wheels only for win_amd64, whereas PyAV 18 publishes a win_arm64 wheel, so constraining ARM64 to <16 makes the clean install resolve an sdist and attempt the compiler-and-FFmpeg build that this change is intended to avoid. Scope the cap to the affected macOS target, or explicitly retain a wheel-bearing version on Windows ARM64.

Useful? React with 👍 / 👎.

Re-raising code 2 turned the optional set into a NEED_SUDO handshake, so a box
missing cmake or gcc got the desktop's mandatory permission dialog, whose Cancel
drops back to not-installed. That re-imposes through a prompt the build-tool
requirement this gate removes, and none of those tools are needed to run.
Suppress the handshake for optional callers; a required package still elevates.
Verified in sh, dash and bash.

Also advance the progress bar on the no-git triton skip, which otherwise ends at
14/15.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e8c3568cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh
Comment on lines +2067 to +2070
command -v cmake >/dev/null 2>&1 || _optional_missing="$_optional_missing cmake"
_has_working_git || _optional_missing="$_optional_missing git"
command -v gcc >/dev/null 2>&1 || _optional_missing="$_optional_missing build-essential"
command -v curl-config >/dev/null 2>&1 || _optional_missing="$_optional_missing libcurl4-openssl-dev"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep build tools required when no prebuilt can match

On Linux hosts outside x86_64/aarch64, or GPU configurations without a compatible published bundle, this classifies the compiler, CMake, Git, and curl headers as optional even though the downstream resolver deliberately falls back to a source build. studio/install_llama_prebuilt.py:3426 rejects architectures without a prebuilt policy, while studio/setup.sh:1506-1513 selects the source fallback, setup.sh:1616-1622 skips it when CMake or Git is absent, and setup.sh:2248-2254 makes a fresh install exit nonzero. Retain the hard dependency gate for targets without an eligible prebuilt, or determine prebuilt eligibility before relaxing these requirements.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not taking this one, and it reverses the P1 accepted two rounds ago (3666107810), which asked for exactly the opposite: that an optional-install failure fall through to the prebuilt warning rather than gate the install. Restoring the hard requirement re-breaks that.

The degrade is not silent. install.sh:2125-2129 prints a C_WARN step naming the missing tools, using prebuilt llama.cpp (missing: ...), plus Not required to run: Unsloth downloads a prebuilt inference engine. and a specific note when git is the one missing. setup.sh:1617 and :1620 print skipped (cmake not found) / skipped (git not found).

The cited resolver is also not on the Linux path: setup.sh:1281 pins _HELPER_RELEASE_REPO to unslothai/llama.cpp, which equals DEFAULT_PUBLISHED_REPO, so resolve_simple_install_release_plans returns the fork-manifest plans and Linux goes through _linux_published_attempts. Release b10107-mix-1911198 publishes linux-x64 cpu, cuda12 and cuda13 variants, rocm per-gfx, and vulkan, plus linux-arm64. Non-apt distros are strictly better off than before this PR, which exited 1 whenever any of cmake, git, gcc or curl-config was missing.

Comment thread install.sh
substep " xcode-select --install"
substep "Then re-run this script. A normal (non---local) install needs no compiler"
substep "and no git -- it uses prebuilt binaries and wheels only."
tauri_log "NEED_XCODE_CLT" "git"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle the Xcode requirement marker in the desktop installer

On a clean Mac running a debug desktop build, studio/src-tauri/src/install.rs:199-220 automatically invokes this script with --tauri --local, so this branch is reached and emits NEED_XCODE_CLT. However, a repo-wide search shows that the Rust reader at install.rs:430-442 only recognizes NEED_SUDO; this new marker has no consumer, and the subsequent exit 1 is handled as an ordinary installation failure rather than an actionable Command Line Tools request. Add protocol handling for this marker or otherwise surface the specific remediation in the desktop flow.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct that install.rs:431 parses only NEED_SUDO, but nothing is dropped: there is no final else in that chain, so install.rs:468-471 forwards the raw line to diagnostics, info! and the progress event. Unconsumed markers are already normal here (ROCM_WSL_AVAILABLE at install.sh:3228 and TAURI:DONE at :4339 have no consumers on main).

The branch is also dev-only: --local is only pushed under cfg!(debug_assertions), and STUDIO_LOCAL_INSTALL has no source other than --local. On main this same scenario emitted no marker at all and also exited 1, so there is no regression to fix.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9e8c3568cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh Outdated
Comment on lines +781 to +787
if [ "$TAURI_MODE" = true ]; then
# Optional callers never elevate. The desktop turns NEED_SUDO into a mandatory
# "Permission needed" dialog whose Cancel drops the user back to not-installed,
# so prompting for cmake, gcc or the libcurl headers would re-impose through a
# permission prompt the very build-tool requirement this gate removes. Nothing
# in that set is needed to run: the caller falls through to prebuilt llama.cpp.
if [ "${_SMART_APT_OPTIONAL:-false}" = true ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor optional mode before invoking sudo

When a regular Linux/WSL CLI install runs on an apt-based system as a non-root user and optional tools are missing, _SMART_APT_OPTIONAL=true has no effect because this check is nested under TAURI_MODE. Execution therefore falls through to the sudo branch, opens /dev/tty, and defaults to installing cmake, git, gcc, and libcurl headers when the user presses Enter; headless installs likewise attempt passwordless sudo. Check optional mode before the Tauri-specific branch so consumer installs never request elevation for these non-required packages.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right that _SMART_APT_OPTIONAL is only read inside the TAURI_MODE block, but the CLI path needs no suppression: the caller is ( _SMART_APT_OPTIONAL=true; _smart_apt_install $_optional_missing ) || true (install.sh:2106), so any exit dies in the subshell and the install continues. Verified in sh, dash and bash. The apt prompt on the CLI is the intended behaviour rather than a stop (install.sh:2050, auto-installed on apt, a warning elsewhere, never a stop); suppressing it would lose git and cmake on apt hosts for nothing. The accepted item 3666107810 was specifically about the desktop modal whose Cancel leaves the user not-installed, which is a different path.

Comment thread install.sh Outdated
Comment on lines +2049 to +2053
# Only a download transport is genuinely required. cmake, gcc and the libcurl headers
# exist solely for a llama.cpp SOURCE build, and the consumer path never does one --
# unslothai/llama.cpp publishes linux-x64 and linux-arm64 prebuilts covering cpu,
# cuda12, cuda13, rocm and vulkan. Requiring them turned every non-apt distro into a
# hard exit 1 (the Fedora/Arch/openSUSE message below) for tooling nothing downstream

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep build tools required on architectures without prebuilts

On Linux architectures other than x86_64 and arm64, treating the toolchain as optional leaves no usable inference engine: studio/install_llama_prebuilt.py:5447-5451 deliberately produces no CPU prebuilt attempt for ppc64le, riscv64, or s390x and falls back to a source build, while studio/setup.sh:1616-1623 skips that build when cmake or git is absent. Restrict this relaxed gate to architectures covered by published prebuilts, or retain the build prerequisites on these source-build-only hosts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already answered in 3666948524 for 3666794583. Also unreachable: torch 2.13.0 publishes wheels only for manylinux_2_28 x86_64 and aarch64, win_amd64 and macosx_14_0_arm64 (PyPI files API), so ppc64le, riscv64 and s390x cannot complete an install with or without a toolchain. On apt hosts those tools are still installed at install.sh:2106.

# antlr4-python3-runtime pinned at 4.9.3) are pure Python and build with no compiler,
# which is why they are allowlisted in .github/scripts/clean-machine-assert.sh instead
# of being pinned here.
av<16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope the PyAV cap to the affected macOS target

The wheel gap described here is specific to CPython 3.13 on macOS arm64, but this unconditional constraint is added to every pip_install() invocation on Linux, Windows, Intel macOS, and other Python versions as well. Consequently an update on an unaffected host with PyAV 16–18 already installed resolves it back to 15.1.0 solely because of the Mac workaround. Add an appropriate PEP 508 platform/Python marker so unaffected installations retain their supported newer PyAV release.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ask as 3666107820. av<16 is a constraint rather than a pin, nothing under studio/backend/requirements requests av>=16 (extras.txt:13 asks for bare av), and av 15.1.0 ships wheels for cp310 through cp314 on manylinux, musllinux, both macOS arches and win_amd64, so no host is pushed to a source build. One correction to our own comment though, which I will fix: 16.0.0 does ship cp313-cp313-macosx_14_0_arm64; it is 17.0.0+ that drops it, and 16.0.0's wheel is macosx_14_0 so it is unusable below macOS 14 regardless.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 135764b674

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh
# would not catch it and a missing optional tool aborted the install. And
# _SMART_APT_OPTIONAL suppresses the NEED_SUDO handshake, so no install hinges
# on a permission prompt for tools nothing here needs.
( _SMART_APT_OPTIONAL=true; _smart_apt_install $_optional_missing ) || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require build tools when source compilation is requested

When the desktop installer is launched with UNSLOTH_LLAMA_FORCE_COMPILE=1 and any build tool is missing, this optional call suppresses the Tauri elevation request and swallows the failure. The checked studio/setup.sh flow then explicitly skips the prebuilt at lines 1410-1412, but skips or fails the source build when CMake/Git/compiler support is unavailable at lines 1616-1622, ultimately returning a failed installation. Keep these dependencies optional for the normal prebuilt path, but make them mandatory when the force-compile mode is selected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNSLOTH_LLAMA_FORCE_COMPILE=1 without cmake or git ends at studio/setup.sh:1616-1621 with skipped (cmake not found) and _LLAMA_CPP_DEGRADED=true, so an advanced user who opted in by env var gets an explicit warning. Making it mandatory again reinstates the NEED_SUDO gate accepted in 3666107810.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 135764b674

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

16.0.0 does ship cp313-cp313-macosx_14_0_arm64; the comment claimed no cp313
wheel exists. The actual reason is the deployment target: 15.1.0 is macosx_13_0
and 16+ is macosx_14_0, so the cap is what keeps macOS 13 off a source build.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: af98dabe23

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

danielhanchen and others added 3 commits July 28, 2026 20:24
cryptography 49.0.0 (2026-06-12) dropped the macosx_10_9_universal2 wheel
and now ships macosx_11_0_arm64 only, so x86_64 macOS has no wheel and uv
falls back to the sdist. That build calls maturin, which pulls Rust and
then fails at 'linking with cc failed' on a clean Mac without the Xcode
Command Line Tools. It surfaced in the clean-machine leg mac macos-15-intel
/ mask / file, several minutes into the studio dependency step, which is
exactly the up-front toolchain requirement this branch removes.

48.0.1 is the newest release carrying a universal2 wheel, and its
cp39-abi3 / cp311-abi3 tags cover the 3.12 and 3.13 interpreters the
installer creates. The cap is marker-scoped to darwin + x86_64, so arm64
macOS and every other platform still resolve to the latest. Lift it when
cryptography ships an x86_64-capable macOS wheel again.

Resolution of studio/backend/requirements/studio.txt under this
constraints file gives 48.0.1 on x86_64-apple-darwin and 49.0.0 on
aarch64-apple-darwin and x86_64-unknown-linux-gnu, on both 3.12 and 3.13.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88d52a0d77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh
Comment on lines +2016 to +2021
if [ "$_clt_missing" = true ]; then
# Not fatal, and no GUI dialog: firing xcode-select --install and exiting is
# what stranded clean Macs.
step "deps" "no Xcode Command Line Tools (not required)" "$C_WARN"
substep "Unsloth installs prebuilt binaries and wheels, so no compiler is needed."
substep "Install them only for a llama.cpp source build: xcode-select --install"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require CLT when an explicit source build is requested

When UNSLOTH_LLAMA_FORCE_COMPILE=1 or UNSLOTH_LLAMA_PR is set on a clean Mac, this branch still treats missing CLT as non-fatal. studio/setup.sh later skips the prebuilt path for those inputs and requires both Git and CMake to build, so the installer performs the Python/package installation before ultimately failing with no llama.cpp server. Keep the relaxed behavior for the normal prebuilt path, but fail early with the CLT remedy when either source-build option is active.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

studio/setup.sh guards the source build on the toolchain itself: lines 1616-1621 skip it with "skipped (cmake not found)" or "skipped (git not found)" and set _LLAMA_CPP_DEGRADED, so UNSLOTH_LLAMA_FORCE_COMPILE=1 or UNSLOTH_LLAMA_PR on a CLT-less Mac ends in a named skip, the "limited: llama.cpp unavailable" footer, and the non-zero exit at setup.sh:2253, not a confusing late compiler failure. cmake ships with neither macOS nor the CLT, and brew itself requires the CLT, so that gate fires before any compile is attempted. install.sh:2021 already prints the exact remedy for this user: "Install them only for a llama.cpp source build: xcode-select --install". Hard-failing the prereq check instead would abort the Python stack as well, which is the behaviour this PR exists to remove.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88d52a0d77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread install.sh Outdated
Comment on lines +785 to +786
if [ "${_SMART_APT_OPTIONAL:-false}" = true ]; then
return 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor optional apt mode outside Tauri

On a normal interactive Debian/Ubuntu install run as a non-root user, missing optional tools still pass through this Tauri-only check into the sudo branch, which presents a default-yes prompt and installs cmake, GCC, and development headers. Thus the consumer path still requests elevation and installs the developer toolchain it now claims is unnecessary; _SMART_APT_OPTIONAL should bypass escalation regardless of TAURI_MODE while preserving escalation for required packages such as curl.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. I extracted _smart_apt_install and drove it with a stubbed apt-get (always fails, as for a non-root user) and an elevation binary that records its invocation. With TAURI_MODE=false and _SMART_APT_OPTIONAL=true it printed "WARNING: We require sudo elevated permissions to install: cmake" plus the default-yes Accept? [Y/n] prompt on a pty, and tried sudo -n -k apt-get headless. The bypass at install.sh:785 was inside the TAURI_MODE block, so only the desktop path honoured it.

Fixed in afcab59: the optional check now sits above the mode split and returns 2 in both modes, while required packages such as curl escalate unchanged. tests/sh/test_linux_deps_gate.sh gained four cases over that matrix; they fail on 88d52a0 and pass on the new commit.

The optional bypass sat inside the TAURI_MODE branch, so a plain curl | sh
install on a non-root Debian or Ubuntu box still fell through to the
escalation branch and showed the default-yes permission prompt for cmake,
GCC and the libcurl headers. That is exactly the toolchain this change set
declared unnecessary on the consumer path, so the prompt asked for a
password to install packages nothing here uses, and a headless run failed
the same way instead of falling through to prebuilt llama.cpp.

Move the check above the mode split so optional callers return 2 in both
modes. Required packages such as curl still escalate unchanged.
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: afcab59461

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danielhanchen
danielhanchen merged commit df63522 into main Jul 29, 2026
59 of 66 checks passed
@danielhanchen
danielhanchen deleted the fix/installer-no-toolchain-on-consumer-path branch July 29, 2026 01:50
danielhanchen added a commit that referenced this pull request Jul 29, 2026
…ws container lane

Three red checks, two of which test something this branch does not own.

desktop linux deb / appimage run the SHIPPED bundle's own install.sh, and
desktop-v0.1.50-beta was cut on 2026-07-21, before #7547 merged on 07-29. That
bundle still carries the old optional-dependency gate, so on a stripped runner it
exits 2 at [TAURI:NEED_SUDO] cmake git build-essential libcurl4-openssl-dev and
never creates a venv. Current main's _check_linux_deps runs the same set through
_SMART_APT_OPTIONAL, which suppresses every escalation path, so only a new release
can change this. The step now pins that exact outcome: the exit code must be 2 and
the log must carry exactly that package list, anything else still fails, and
finding _SMART_APT_OPTIONAL in the extracted install.sh (the guard #7547 added)
turns into a hard error saying to delete the pin. The venv and torch assertions
stay and still run whenever the installer succeeds.

win windows-11-arm gets a native ARM64 CPython, and torchaudio publishes no
win_arm64 wheel at any version, so the PyTorch step cannot resolve. The fix is in
install.ps1 on #7549, still open. Same treatment: the Install step is
continue-on-error and a new step requires all three of the PyTorch step, the
torchaudio resolution error and the missing win_arm64 platform tag, so any other
failure is red. The row leaves experimental so the job is required, and the pin
errors out as soon as the venv interpreter reports anything but win-arm64, which
is what #7549 landing looks like.

Adds the virgin Windows container lane as two jobs here rather than a sibling
workflow: same premise as the win legs, same path filters, and masked-versus-real
reads better side by side. The hosted Windows legs cannot test the VC++
2015-2022 runtime (it ships in the runner image's System32) or a Windows with no
Microsoft Store, and a servercore:ltsc2022 container on windows-2022 answers both.
The probe asserts no python, py, git, cmake, cl, winget or uv on PATH, on disk or
in the registry, and now also asserts vcruntime140.dll, vcruntime140_1.dll and
msvcp140.dll are absent, which is the one thing the hosted runner cannot un-ship.

Both container install rows stop at studio/setup.ps1's winget-only git gate on
this branch, since #7549 is what relaxes it, so both are pinned the same way. The
overlay row additionally requires the UNSLOTH_CI_SOURCE_OVERLAY hook to have
fired, unconditionally: without that it would be indistinguishable from the
released-wheel row, and the hook is this branch's own feature.

Container notes carried over from the spike: never docker pull when the image is
cached, since MCR has shipped an image ahead of the runner host before; wait for
the Docker daemon, because one leg died in 21s on npipe:////./pipe/docker_engine
and that flake misreads as "Windows containers unavailable"; drive docker from a
run: step, because the job-level container: key is Linux-only. The root CA store
is seeded after the virginity assertion, restoring what a real Windows already
has, because studio/install_node_prebuilt.py downloads Node with bare
urllib.request.urlopen and hits CERTIFICATE_VERIFY_FAILED against the empty
container ROOT store. That product bug is left alone here.
danielhanchen added a commit that referenced this pull request Jul 29, 2026
The Linux rows already pin the shipped bundle's own install.sh exiting 2 at the
NEED_SUDO handshake. macos-15 and macos-26 fail the same way for the same reason:
desktop-v0.1.50-beta predates #7547, so the bundled installer still hard-exits on
the Xcode CLT gate that #7547 turned into a warning.

Accept exit 1 plus that exact gate line, and nothing else. _check_macos_deps is
the function #7547 added, so its presence in the bundle means the release caught
up and the block errors out asking for the pin to be deleted.
danielhanchen added a commit that referenced this pull request Jul 29, 2026
#7551)

* CI: prove the installer works on a machine with no developer toolchain

No job has ever run the installer on a machine without one.
studio-mac-install-matrix.yml is the only macOS installer job and it runs
'bash install.sh --local --no-torch' on runners that already have the Xcode CLT
selected and setup-python preinstalled, so the CLT gate never fires there, and
--local is precisely the mode that legitimately needs git. Repo-wide there was
zero coverage of xcode-select or CommandLineTools outside install.sh itself.

clean-machine-install-ci.yml runs the installer on a genuinely stripped machine.
macOS legs move /var/db/xcode_select_link, /Library/Developer/CommandLineTools,
/Applications/Xcode*.app and Homebrew aside, so xcode-select -p, git, cc and
clang really do fail, and restore unconditionally afterwards. Removing the
select-link alone is not enough: xcode-select falls through to a full Xcode.app
and re-arms /usr/bin/git. Linux legs use containers, which are genuinely clean.
Windows legs cover winget visible and masked, plus windows-11-arm. A WSL leg
covers the 126 lines of WSL-specific install.sh logic that had no runtime test.

Each macOS leg runs four deliveries: pipe (the advertised command, and the shape
that turns an early exit into curl (56)), file (separates installer logic from
pipe delivery), no-torch, and tauri (stdin closed, no tty, as the desktop app
invokes it). One leg records every toolchain invocation and asserts the trace,
which is the real deliverable: proof the installer never reached for a compiler
rather than proof it happened to succeed.

The asserts test that tools do NOT WORK rather than that they are absent from
PATH. On a real virgin Mac /usr/bin/git and /usr/bin/cc exist as CLT stubs, so
'command -v git' succeeds and only running it tells the truth.

desktop-app-clean-machine-ci.yml installs and launches the SHIPPED desktop app
release on a stripped machine, covering Gatekeeper and quarantine on macOS, NSIS
silent install on Windows, and Xvfb with WebKit2GTK on Linux.

Known limit, stated plainly: hosted macOS runners are developer machines. Masking
reproduces this bug and proves the installer does not invoke a toolchain, but it
cannot prove no hidden dependency exists on a truly virgin Mac. An ephemeral-VM
lane is the follow-up.

* Point the llama assert at the right root, and name the Intel limitation

The tauri leg installs to the legacy root because --tauri refuses a custom
UNSLOTH_STUDIO_HOME. Its install succeeds end to end, but llama.cpp lives at
<root>/llama.cpp while the venv is at <root>/studio, so the assert was pointed one
level too deep.

On macos-15-intel /usr/bin/git keeps working once the CLT are gone, so it is not
CLT-provided there and no masking can remove it, while cc and clang do become
stubs. Calling that 'masking failed' was wrong. That leg allowlists git
explicitly and says why, so the assert stays strict everywhere else.

* Make the clean-machine legs able to fail

The toolchain strip never ran on the automatic triggers: inputs exists only for
workflow_dispatch, and GitHub coerces '' and false alike to 0, so
`inputs.strip_toolchain != false` was false. Confirmed on a pull_request run
where the strip step reports skipped. Gate on the event instead.

Also: scrub the Machine and User registry PATH, since install.ps1 rebuilds
$env:Path from them mid-install and the toolchain came back; stop dropping
WindowsApps unconditionally, which removed winget on the winget=visible leg too;
fail rather than annotate when a bundle ships no installer or no CLI; run the
bundled installer, which a headless launch never reaches; resolve the newest
desktop-v* release instead of a pinned immutable tag; and give the two macOS
matrix rows distinct artifact names.

* Make the Windows and Linux clean-machine legs honest

The Windows scrub only touched PATH, so the legs were green while not clean: run
30365014702 logged "python ABSENT" and then "Python 3.13 already installed"
with uv resolving C:\hostedtoolcache\windows\Python\3.13.14\arm64\python.exe.
py.exe lives in C:\Windows and uv discovers interpreters itself, so take the
toolcache off disk and fail when tooling survives, instead of only printing it.

The Linux desktop legs never stripped anything, and the tauri.log step was all
|| true so it could not fail. Run the bundled installer the way install.rs does,
with --tauri alone, and assert torch: passing --no-torch skipped the slowest
half of first launch and let the venv check pass over it.

Pin the WSL rootfs to a dated build; current/ is a rolling alias and the digest
next to it is fixed.

* Give the Linux and WSL legs an assertion that can fail

The Linux rows' only post-install gate was nobuild, a log grep, so an installer
exiting 0 having produced nothing kept a required leg green. The WSL job and the
Windows job both already check the install runs; the Linux job now does too.

The WSL detection half only printed its Select-String, and the alternation also
matches "platform linux", so a regression that skipped every WSL-specific
branch would still pass as a plain-Linux install. Assert the exact marker,
stripping ANSI first since step writes the label in reverse video. Probed against
three fixtures: real wsl log passes, platform linux fails, missing log fails.

* Tighten the clean-machine comments

Compress the comment blocks across the clean-machine workflows and
scripts. The explanations of why each check is written the way it is
stay; the padding, restatement and duplication go.

No code or workflow logic changes.

* Point the nightly at the repo that publishes, and let its checks fail

REL_REPO defaulted to unsloth-test/unsloth-test, which holds one release frozen
at 2026-07-27, while release-desktop.yml publishes into github.repository. The
schedule was re-testing the same fixture forever and could never see a broken
production bundle.

The windows job carried a blanket continue-on-error, so its NSIS assertions
could not gate. lipo -archs prints and exits 0 for a thin binary and `|| true`
swallowed even that, so the architecture was never checked; fall back to file,
which survives the CLT mask. And require the preflight disposition line rather
than the mere existence of tauri.log, which setup_logging creates at process
start regardless.

* Stop four clean-machine checks from passing over a real failure

Re-run `absent` after the install on the masked macOS legs. It only ran
before, so an installer that quietly selected the Xcode CLT or installed a
compiler left the leg green while every later source build could succeed,
which is the one thing clean-machine-assert.sh says `absent` guards the whole
run against.

Fail the Windows simulation when py.exe can still start an interpreter. The
launcher binary itself may stay, but Find-CompatiblePython probes `py` first
(install.ps1:1130-1153), so an interpreter registered outside the two renamed
toolcache directories gets reused and Python bootstrap is never exercised.
Exempting `py` without ever running it left that unchecked.

Propagate the WSL installer exit code. It was printed and discarded, and the
CLI check does not compensate: install.sh links the `unsloth` shim (4174-4182)
before it reports a failing studio/setup.sh (4219-4230), so a late setup
failure leaves a shim whose --version succeeds.

Run the bundled installer in the Linux desktop jobs. The launch step only
proves the process stayed alive, and on a fresh home preflight reports
not_installed and the app waits on the install screen, so both required rows
passed after 90 seconds without ever touching the shipped install.sh. Locate
the resource in the deb payload or the extracted AppImage, run it the way
install.rs does, and require a managed venv that can import torch.

* Prove the trace wrapper records before trusting an empty trace

The `notools` check reads an absence: it passes when the trace file contains no
compiler, git or brew invocation. A shim directory that never reached PATH
produces exactly the same empty file as an installer that touched nothing, so
the single leg carrying that assertion would stay green no matter what the
installer did. "Verify the simulation actually took effect" only ran for mask
mode, which left the trace leg with nothing checking its own instrumentation.

Call git explicitly after sourcing the environment and require it to appear in
the trace, then truncate the file so the self-test entry does not count against
the install. The call has to be explicit because macOS reaches _has_working_git
only under STUDIO_LOCAL_INSTALL (install.sh:2026), so no consumer leg on that
platform probes git on its own.

* Stop the Windows clean-machine check failing on its own probe exit code

All three Windows legs failed "Verify the simulation took effect" with no
::error:: printed at all. The check itself was right: the mask step logged
"masked toolcache python: C:\hostedtoolcache\windows\Python", python/git/cmake/cl
were ABSENT, no `py -3.x` probe started an interpreter, and the winget assertions
were satisfied. The step still exited 1.

The cause is $LASTEXITCODE leaking out of the step. The last external command is
the `py -3.13` probe, which is SUPPOSED to fail; Get-Command and Write-Host are
cmdlets and never reset $LASTEXITCODE, and the runner appends
`if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE }`
to every pwsh step (actions/runner#351). So a clean machine reported failure,
and because this step runs before Install, no Windows leg has ever reached the
installer. Clear $LASTEXITCODE after the probe loop and end with an explicit
exit 0. The leak detection is untouched: a surviving python/git/cmake/cl, or a
`py -3.x` that actually starts, still exits 1.

Also print each probe's exit code and output, so the next failure here explains
itself instead of being silent, and label `py -0p` as what it is. The launcher
reads the registry, which the on-disk toolcache rename cannot rewrite, so -0p
keeps naming paths that no longer exist. Unlabelled it reads like a leak.

Accept the Fedora leg's real outcome instead of a message that can be absent

The fedora assertion only accepted the unsupported-package-manager hard exit.
That is still what this ref's install.sh does, but the pending installer change
replaces it with a warning that lets the install continue, at which point the
old grep matches nothing and the step fails for the wrong reason.

Handle both, strictly. If the log shows the newer "using prebuilt llama.cpp
(missing:" warning, the Linux gate demonstrably did not hard-stop, and the only
tolerated failure past that point is release lag: install.sh comes from this ref
while unsloth comes from PyPI, and the released studio/install_python_stack.py
has no "skip triton kernels when git is missing" guard, so it still fetches the
git+https triton_kernels requirement on a machine with no git. Anything else
after that warning fails the step. Otherwise the old hard-exit message is still
required. A missing log, a bootstrap outage or any unrecognised failure all
remain errors, and the step retires to a plain success assertion once a release
ships the no-git skip.

* Make the AppImage Linux row actually extract, and hold Linux to the macOS preflight bar

The appimage row invoked the extractor by bare filename, and a command word
with no slash is resolved through PATH rather than the working directory, so
the extraction exited 127 and the bundled-installer assertion below it never
ran. Prefix it with ./ so the row exercises what it claims to.

The Linux log step also asserted nothing: it skipped a missing log with
continue and discarded the grep with || true. The launch step only proves the
process stayed alive for 90 seconds, and the bundled-installer checks do not
exercise the Rust preflight path, so an app that hung before preflight
completed passed both required Linux rows. Require the same
desktop_preflight completed disposition= record the macOS rows already do.

* Put the branch's own Python under test on the clean-machine legs

install.sh and install.ps1 come from the ref under test, but they install
unsloth from PyPI, which is the consumer path and has to stay that way. That
left everything Python-side coming out of the released wheel: studio/setup.sh,
studio/setup.ps1, studio/install_python_stack.py, and every requirements and
constraints file those resolve through Path(__file__). A branch that changes
constraints.txt or setup.ps1 therefore got a green run that proved nothing
about the change, and some legs proved less than they looked. The Fedora
assertion was already carrying a hand-written workaround for exactly this,
tolerating a triton/git failure on the grounds that the released package lags
the ref.

Legs marked overlay: true now re-point the venv at the ref just before studio
setup runs, through UNSLOTH_CI_SOURCE_OVERLAY: a --no-deps editable install of
the checkout. That makes import studio resolve to the working tree, so the
existing setup-script lookup finds the ref's setup.sh / setup.ps1 and
install_python_stack reads the ref's constraints, with no other change to
either installer.

Not --local: --local additionally installs unsloth-zoo from a git+https URL,
which genuinely needs git, and git absence is the whole point of the masked
legs. The overlay resolves no dependencies and clones nothing, so it holds up
with git, cmake and the compilers all gone. It is not a consumer knob either:
no flag, no usage entry, ignored unless the variable names a directory with a
pyproject.toml in it.

Four legs stay on the released package deliberately, each for its own reason,
recorded in the header: the mac pipe legs keep an end-to-end signal on what a
user actually runs; the trace leg would otherwise answer its own question,
since the editable build calls git through setuptools-scm's file finder; the
non-root Linux leg dies before a venv exists; and WSL only ever receives
install.sh, not a source tree.

Two supporting fixes the overlay depends on or exposes:

install_python_stack.py discarded uv's output whenever a step succeeded, so
the nobuild assertion, which reads the install log, could not see a source
build in the dependency phase at all. That is the phase that installs
studio.txt, where an sdist-only dependency actually turns up, and it reported
"built: none" regardless. It now echoes successful output under
UNSLOTH_VERBOSE, matching what install.sh's run_install_cmd already does.

nobuild now ignores "Building <name> @ file://" lines. A local-path build is
something the caller pointed at, never a dependency resolution chose, and
index dependencies always print <name>==<version>, so a real sdist from PyPI
is still caught, including one named unsloth.

Each overlaid leg also asserts it really was overlaid, so an unset variable
cannot quietly put the whole matrix back on the released wheel.

* Allowlist the triton-kernels pure-Python sdist, and record why Windows on ARM is red

The two ubuntu2404 root legs went red at "Assert no source build" reporting
triton-kernels. That is not a regression in what the installer does. Those
builds have always happened; they only became visible now that pip_install
stopped discarding uv's output on success, which is what finally let the
nobuild check read the dependency phase at all.

So the question was whether each build actually needs a compiler. Checked
against the real artifacts rather than assumed:

  openai-whisper 20250625, randomname 0.2.1, argbind 0.3.9 -- no version of
  any of the three has ever published a wheel; antlr4-python3-runtime is
  pinned at 4.9.3, below the first release that ships one. All four sdists
  use setuptools.build_meta, declare no ext_modules, and contain no
  .c/.cpp/.pyx/.rs file. Already allowlisted, correctly.

  triton-kernels is the same category and was the only name failing. It is
  pinned to the triton repo's python/triton_kernels subdirectory; that tree
  is 75 files of Python, a four-line pyproject.toml, no setup.py and no
  native source at all. The kernels are Triton DSL compiled at runtime, not
  at install time. It is also a direct URL the installer names itself rather
  than something resolution picked, and only Linux reaches it. It belongs in
  the allowlist, so add it with that reasoning written down.

The allowlist match is now lowercased and underscore-folded on both sides.
The requirement spells the package triton_kernels while uv prints
triton-kernels, and an allowlist that matched only one spelling would pass
by luck rather than by intent. A plain pyarrow sdist is still caught.

The two data-designer @ file:// plugin builds needed nothing: they are
in-tree local paths, already dropped by the same rule that exempts the
source overlay's own build.

Separately, the windows-11-arm leg fails for a real reason and should keep
failing. The ARM handling itself works, the log shows torchaudio being
skipped and torch plus torchvision installing from wheels. What stops it is
that pyarrow and hf-transfer publish no win_arm64 wheel at all, so uv falls
back to their sdists and they fail on CMake configure and on openssl-sys
wanting perl. That is a product gap on the platform, not a gap in the
simulation, so the leg stays experimental and keeps reporting it. Record
that above the matrix entry so the next reader does not re-diagnose it.

* Exercise the bundled Windows installer, and stop mislabelling installer sources

Four things that let a leg go green while proving nothing.

The desktop Windows job installed the bundle and launched it, and that was all.
On a fresh profile preflight reports not_installed and the app sits on the
install screen waiting for a click, so the process happily stays alive for 90
seconds without the bundled install.ps1 ever running. A bundle that shipped no
install.ps1 resource, or a broken one, passed this job -- which is the packaged
app failure the workflow exists to catch. macOS and Linux already invoke their
bundled script directly; Windows now does the same, via the resource NSIS laid
down next to the exe, invoked the way install.rs invokes it, then asserts the
managed venv exists and can import torch. Its timeout goes to 60 minutes
because a full torch install on a Windows runner is the slowest of the three.

A manual run that selects installer_source: published only redirected the macOS
and Linux jobs. WSL kept copying the checked-out install.sh and Windows kept
running the checked-out install.ps1, so a run asking whether the script on
unsloth.ai works reported on this ref under the published label. Both now honor
the selection; install.ps1 advertises its own unsloth.ai URL, so published has a
meaning on Windows too. Both branches stay empty on pull_request and push, so
automatic runs are unchanged.

The push-to-main filter listed only install.sh, install.ps1 and this workflow,
while the PR filter also covers setup.sh, setup.ps1, install_python_stack.py and
the clean-machine helpers. A direct push touching those skipped the workflow
entirely, so the post-merge backstop never ran for the files the source overlay
was added to cover. The two lists now match.

Neither filter covered studio/backend/requirements, even though the overlay
exists precisely so a constraints change is resolved on a machine with no
compiler and no cached wheels. The update-smoke workflows cannot stand in: they
start from a preinstalled Python and full developer tooling.

* Make the Linux and Windows desktop legs clean, and honour published on every macOS delivery

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.

* Run the Windows installer under PowerShell 5.1, the only shell a clean machine has

The Windows Install step ran `& $script` inside a `shell: pwsh` step, so
install.ps1 was executing under PowerShell 7. A genuinely clean Windows box
does not have PowerShell 7: Windows ships powershell.exe (Windows PowerShell
5.1) and pwsh is a separate install that the hosted runner image happens to
preinstall. So the one workflow whose premise is a machine that has never seen
a developer toolchain was testing the installer under a shell that machine
would not have, and no other Windows job anywhere in .github exercises
install.ps1 under 5.1.

Invoke it the way the desktop does (install.rs:325-339, and the bundled
installer step in desktop-app-clean-machine-ci.yml): powershell.exe with
-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File. The pwsh
step wrapper stays, since it is only the installer that has to be under 5.1.
Calling powershell.exe with `&` keeps the output in the pipeline, so
Tee-Object still fills logs/install.log, and $LASTEXITCODE after the pipeline
is the child's real exit code, so $rc and `exit $rc` are unchanged.

install.ps1 and studio/setup.ps1 hold no PowerShell 7-only constructs: no
`#Requires` above 5.1, no `&&`/`||` chain operators, no ternary, no
null-coalescing, no ForEach-Object -Parallel, no $IsWindows/$PSStyle, and no
6+ cmdlets or parameters. setup.ps1 declares `#Requires -Version 5.1`, and its
three $PSVersionTable branches gate a 7-only preference on the 7 side with a
5.1 fallback. Every Invoke-WebRequest already passes -UseBasicParsing, which
5.1 needs because it otherwise reaches for the IE engine.

* Assert the Windows desktop strip actually took effect

The desktop job's Windows masking renamed the toolcache Python, scrubbed the
Machine and User registry PATH, and probed `py`, but nothing checked that
`python`, `git`, `cmake` or `cl` were gone. The drop list is heuristic path
fragment matching, so a runner image that moves any of those outside those
fragments leaves the bundled install.ps1 reusing hosted developer tooling while
the job still reports a clean machine. PATH written to $GITHUB_ENV only applies
to later steps, so the check has to live in a step of its own; it carries the
same event gate as the strip, exempts `py` (it lives in C:\Windows and stays,
which is why the start probe is the real evidence), and resets $LASTEXITCODE
before exiting 0 so an intentionally failing probe cannot fail a clean machine.

Also correct the no-winget matrix note: that leg is not failing for an unfixed
product reason. It stops at the unconditional git gate in setup.ps1 only on this
ref, and with that gate relaxed it passes along with every other leg, so the row
is a merge order dependency and stays required.

* Resolve the desktop release including drafts, the convention this repo ships

All three desktop legs died at the download step with an empty REL_TAG. The
resolver passed --exclude-drafts while REL_REPO now defaults to
github.repository, and every desktop-v* release in unslothai/unsloth is a draft:
desktop-v0.1.50-beta and desktop-v0.1.471-beta are both drafts carrying the .dmg,
.deb, .AppImage and setup.exe, while only the non-desktop tags like v0.1.501-beta
are published. Excluding drafts therefore matched nothing and no leg could ever
run against a production bundle.

Drop --exclude-drafts so the newest desktop-v* release is found. A draft has no
tag ref, so releases/tags/<tag> 404s for one, but gh resolves drafts over GraphQL
and gh release download <tag> fetches their assets normally, so the download call
is unchanged. Listing drafts requires push access, which for GITHUB_TOKEN means
contents: write, so the workflow permission is raised from read and annotated.

When nothing resolves the leg still fails hard rather than skipping: with no
bundle to install there is nothing to prove, so a green run would be a lie. The
error now names both causes, no release cut yet or a token that cannot see drafts.

Also stop the restore step swallowing its own failure. `bash
.clean-machine/restore.sh || true` printed "No such file or directory" whenever an
earlier step failed before the toolchain was stripped, and hid a genuinely broken
restore just the same. Skip explicitly when the file is absent and let a real
restore failure surface. Same fix in clean-machine-install-ci.yml, which had the
identical line.

* Skip the desktop jobs on fork PRs instead of failing them

Every desktop-v* release in this repo is a draft, and GitHub lists drafts only
to a token with push access, which is why resolving one needs contents: write.
A pull request from a fork receives a read-only token no matter what the
workflow declares, so on those runs the resolver cannot see any release and the
job died on "no desktop-v* release visible", accusing the repo of having no
bundle when the real cause is the trigger.

This workflow runs on pull_request for changes to itself and the stripping
scripts, so an outside contributor editing either would have hit that. Guard the
three jobs on the head repo not being a fork. A skipped job is honest here: it
does not claim to have tested a bundle it was never able to download, and it is
not reported as a pass.

* Close the free headroom in the clean-machine simulation

Assert arch and signature on every downloaded Mach-O. This is the one genuine
gap the simulation had: Rosetta 2 is preinstalled on hosted runners and absent
from a factory-fresh Mac, so an x86_64-only llama.cpp, whisper.cpp, Node or uv
payload runs green here and dies with "bad CPU type in executable" for the
user. llama-server launching under `assert-llama-loads.sh` does not rule that
out, because Rosetta makes it launch. The new `macho` check reads `file -b`
(`lipo` is an xcrun shim and is gone after masking, as the desktop lane already
notes) and keys the expected arch off `uname -m`, so macos-15-intel expects
x86_64. It also requires at least an ad-hoc signature on arm64, which closes
the AMFI "Killed: 9" class that uv has already been bitten by; the check is
skipped on x86_64, where unsigned code loads fine and so is not the same
defect. It fails when the scan finds nothing, since an empty scan reads exactly
like a clean one.

Make absence real rather than PATH-hidden. uv probes well-known interpreter
locations and the framework loader ignores PATH entirely, so hiding the
toolcache only hid it from `command -v`. Empty /usr/local (it EXISTS on a
factory-fresh Mac as a SIP-exempt firmlink, and is empty; it is /usr/local/bin
that is absent, so the directory itself stays), move the hosted toolcache and
/Library/Frameworks/Python.framework aside, and clear the developer dotdirs and
caches. A populated uv or pip cache can also satisfy a resolution that would
fail on a user's machine. Every removal goes through --remove and is recorded
in the generated restore.sh, guarded so a path the install recreated is not
buried inside its own restore.

Unset CI, GITHUB_* and RUNNER_* for the installer process only. An installer
branching on CI=true is a hidden dependency no consumer exercises. Scoped to
the child so the step's own $GITHUB_OUTPUT still resolves.

Record spctl --status and csrutil status. Neither is documented for these
images and both change what a binary is allowed to do.

* Pin the two failures no change here can fix, and add the virgin Windows container lane

Three red checks, two of which test something this branch does not own.

desktop linux deb / appimage run the SHIPPED bundle's own install.sh, and
desktop-v0.1.50-beta was cut on 2026-07-21, before #7547 merged on 07-29. That
bundle still carries the old optional-dependency gate, so on a stripped runner it
exits 2 at [TAURI:NEED_SUDO] cmake git build-essential libcurl4-openssl-dev and
never creates a venv. Current main's _check_linux_deps runs the same set through
_SMART_APT_OPTIONAL, which suppresses every escalation path, so only a new release
can change this. The step now pins that exact outcome: the exit code must be 2 and
the log must carry exactly that package list, anything else still fails, and
finding _SMART_APT_OPTIONAL in the extracted install.sh (the guard #7547 added)
turns into a hard error saying to delete the pin. The venv and torch assertions
stay and still run whenever the installer succeeds.

win windows-11-arm gets a native ARM64 CPython, and torchaudio publishes no
win_arm64 wheel at any version, so the PyTorch step cannot resolve. The fix is in
install.ps1 on #7549, still open. Same treatment: the Install step is
continue-on-error and a new step requires all three of the PyTorch step, the
torchaudio resolution error and the missing win_arm64 platform tag, so any other
failure is red. The row leaves experimental so the job is required, and the pin
errors out as soon as the venv interpreter reports anything but win-arm64, which
is what #7549 landing looks like.

Adds the virgin Windows container lane as two jobs here rather than a sibling
workflow: same premise as the win legs, same path filters, and masked-versus-real
reads better side by side. The hosted Windows legs cannot test the VC++
2015-2022 runtime (it ships in the runner image's System32) or a Windows with no
Microsoft Store, and a servercore:ltsc2022 container on windows-2022 answers both.
The probe asserts no python, py, git, cmake, cl, winget or uv on PATH, on disk or
in the registry, and now also asserts vcruntime140.dll, vcruntime140_1.dll and
msvcp140.dll are absent, which is the one thing the hosted runner cannot un-ship.

Both container install rows stop at studio/setup.ps1's winget-only git gate on
this branch, since #7549 is what relaxes it, so both are pinned the same way. The
overlay row additionally requires the UNSLOTH_CI_SOURCE_OVERLAY hook to have
fired, unconditionally: without that it would be indistinguishable from the
released-wheel row, and the hook is this branch's own feature.

Container notes carried over from the spike: never docker pull when the image is
cached, since MCR has shipped an image ahead of the runner host before; wait for
the Docker daemon, because one leg died in 21s on npipe:////./pipe/docker_engine
and that flake misreads as "Windows containers unavailable"; drive docker from a
run: step, because the job-level container: key is Linux-only. The root CA store
is seeded after the virginity assertion, restoring what a real Windows already
has, because studio/install_node_prebuilt.py downloads Node with bare
urllib.request.urlopen and hits CERTIFICATE_VERIFY_FAILED against the empty
container ROOT store. That product bug is left alone here.

* Check signatures on Mach-O main executables only

The macho check asserted a valid signature for every Mach-O under the studio
home, and failed the macos-15 mask/pipe leg on 29 files: lxml, charset_normalizer,
cygrpc, _upb, fontTools, caio, brotli and a bundled libportaudio.dylib. Those are
MH_BUNDLE and MH_DYLIB images dlopen'd into a process without library validation,
they ship unsigned in the wheels, and the same run had already installed and
imported them with the installer exiting 0.

Key the signature half off the Mach-O filetype and run it only on main
executables. Report an absent seal separately from one that fails to verify, and
capture codesign output instead of piping it into grep, which returned the
unsigned exit status through pipefail and called every unsigned binary broken.

The architecture half is unchanged and still a hard failure: it is what closes
the Rosetta 2 gap. The zero-Mach-O guard is unchanged. The .venv_t5_* sidecars
stay in scope; setup.sh creates them during a normal install and
transformers_version.py puts them on sys.path, so they are payload.

* Make the WSL job gate, assert Windows installed no toolchain, strip before the .deb

* Assert the root Linux legs did not compile llama.cpp with the apt-installed toolchain

* Pin the macOS desktop legs on the same pre-7547 release lag

The Linux rows already pin the shipped bundle's own install.sh exiting 2 at the
NEED_SUDO handshake. macos-15 and macos-26 fail the same way for the same reason:
desktop-v0.1.50-beta predates #7547, so the bundled installer still hard-exits on
the Xcode CLT gate that #7547 turned into a warning.

Accept exit 1 plus that exact gate line, and nothing else. _check_macos_deps is
the function #7547 added, so its presence in the bundle means the release caught
up and the block errors out asking for the pin to be deleted.

* Pin the WSL pipe truncation and the masked-winget git gate

The WSL leg dies at install.sh:2082 with an unterminated quoted string.
Nothing is wrong with that line: piping the script into sh is not atomic.
dash reads it from the pipe in 8192-byte blocks and runs each command as
it parses, and install.sh:2007 calls _maybe_reroute_strixhalo_to_2404,
which on WSL alone shells out to Windows interop; interop relays the
stdin it inherited and drains the pipe. dash has 11 blocks buffered at
that point, ending at byte 90112, which falls inside
"$STUDIO_LOCAL_INSTALL" on line 2082. Truncating install.sh at 90112
and parsing it reproduces the message verbatim, and running the whole
file under a stdin-draining interop stub reproduces the exit code too.
#7548 wraps the body in _unsloth_main so sh parses everything before
running anything, and the same reproduction against its head is clean.

The eight green staging runs cited when this job's continue-on-error came
off were all on trees that already carried #7548, so that evidence never
covered this branch. Pin the exact signature instead: exit 2 plus the
shell's own unterminated-quoted-string error, with the _unsloth_main
marker read back out of the distro as the flip condition.

Pin winget=masked the same way. studio/setup.ps1:1655-1669 gates on git
unconditionally and can only fetch it through winget, so masking winget
leaves no way to satisfy it. #7549 relaxes the gate, and its wording
appearing in the tree retires the pin.

* Retire the WSL pipe pin now that #7548 is in main

The pin flipped exactly as designed: it looks for _unsloth_main in the installer
it actually ran, and #7548 put it there. Delete the pin and the CLI waiver, and
assert the opposite instead.

WSL is the only platform whose install shells out to Windows interop mid-script,
and interop relays the stdin it inherited, so this job is the one that can catch
the pipe being drained again. A truncation here is now a hard failure.

* Gate the no-elevation Linux install and split off the no-transport case

* Assert no source build on the hosted Windows legs and keep winget for the desktop lane

* Retry the container root CA seeding instead of failing on one Windows Update timeout

* Run the clean-machine workflow for the prebuilt installer helpers it overlays

* Narrow the container pin to its own gates and scan uv and the venv interpreter for arch

* Tighten the clean-machine comments

* Re-assert toolchain absence after the desktop .deb pulls its dependencies

* Retire the #7549 pins and add a wget-only Linux leg

#7549 is in main, so the three known-outcome pins that were waiting on it are
stale and would now hard-error by design. Each is replaced by the assertion it
was standing in for rather than deleted:

win windows-11-arm now gates. The x64-on-ARM64 resolver is asserted as an
outcome: the venv interpreter reports win-amd64 from its own sysconfig, and
torchaudio (no win_arm64 wheel at any version) is installed. Measured on the
integration branch before #7549 merged: "only a native ARM64 Python 3.13 was
found" -> "installing x64 Python" -> torchaudio 2.10.0+cpu, install green.

win windows-latest / winget=masked now gates. The relaxed git gate is asserted
from both sides: the old unconditional message must be absent, the no-git
branch must have been reached (so the row cannot pass because git leaked back
onto PATH), and setup.ps1 must report git as absent-but-not-required.

Both Windows rows, and the visible one, gained the usability check the Linux
legs have had and Windows never did: a managed interpreter, an unsloth CLI on
disk, and that CLI actually running. nobuild and the toolchain check only read
the log, so an installer that exited 0 having produced nothing satisfied them.
The torch assert also loses its fallback to whatever `python` resolves to.

The virgin container overlay row gates, and asserts what only that lane can:
it is the one environment whose System32 does not already ship the VC++
2015-2022 runtime, so it is the only place Ensure-VCRedist's direct aka.ms
download can be proved to run rather than be short-circuited. The overlay=false
row keeps a pin, with a new reason: it installs unsloth from PyPI on purpose,
and setup.ps1 inside 2026.7.5 (uploaded the 23rd) predates #7549, so it still
stops at the old gate. That is release lag, it flips on the next release, and
the pinned signature is now the old wording rather than "#7549 has not landed".

Also adds linux ubuntu2404-nonroot-wget. install.sh's download() takes curl or
wget and _transport_missing is true only when both are gone, so a wget-only box
is supported on paper, but the gating nonroot leg provisions ca-certificates
AND curl, so curl won every probe and the wget branch had never run. Same image,
same no-sudo user, same asserts, wget instead of curl, and curl proved absent on
disk for root and for tester before AND after the install, so the claim is that
every download went through wget rather than that curl happened to be unused.

* 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.

* Clean machine CI: strip Strawberry, make the Fedora pin gating, run the Linux CLI

desktop windows failed the strip verification because windows-latest ships a MinGW
toolchain under C:\Strawberry\c\bin, which matches none of the drop fragments; the
installer workflow already scrubs it.

Fedora sat behind job-level continue-on-error, so its outcome pin could not fail the
run. Tolerate the install step instead, as the no-transport row does.

The Linux usable-install check only tested the executable bit; Windows and WSL already
execute the CLI. The macho scan now fails when no venv interpreter was scanned, rather
than letting uv alone satisfy the outside-root guard.

* Clean machine CI: tighten the comments

Round 12 comment reduction: compress wording, keep every reason. Comments only,
verified with a YAML leaf walk (differences only inside run: scalars, only on # lines),
bash --pretty-print -n byte comparison, a PowerShell token-stream diff and a Python AST
comparison.

* Clean machine CI: dereference the venv interpreter, pin the deb deps and the Windows disposition

file did not follow the <venv>/bin/python symlink find -L printed, so it answered
'symbolic link to ...' and the Mach-O test dropped the one interpreter the Rosetta scan
exists to check. Read with file -Lb and count what was classified, not what was found.

apt treats a toolchain package the strip only renamed as already installed, so a .deb
that started declaring git or cmake would never restore it and the absent re-check would
still pass. Assert the declared Depends instead.

The Windows lane accepted any preflight disposition although the bundled installer was
already required to build a working venv; NotInstalled or ManagedStale there means the
app cannot boot what it just installed.

* Clean machine CI: assert every masked tool, and re-select the developer dir last

clean-machine-env.sh moves ten tools aside and only warns when a move fails, but absent
checked four of them, so a surviving gcc -- which install.sh probes for build-essential
-- went unnoticed.

restore.sh ran xcode-select --switch before the line that moved CommandLineTools back,
so it named a still-masked directory, failed into || true and left the selection link
unrestored. Capture the original selection and re-apply it after both directory
restores.

---------

Co-authored-by: danielhanchen <unslothai@gmail.com>
danielhanchen added a commit that referenced this pull request Aug 6, 2026
…h default

Two things the pin retirement left behind.

The dispatch default for release_tag was desktop-v0.1.50-beta, a bundle that
predates #7547. With the outcome pins gone a manual dispatch that took the
default would fail all four POSIX legs at the installer step, on a bundle
nobody meant to test. Empty instead, which falls through to the same
newest-desktop-v* resolution the schedule already uses.

The ready-disposition assertion was Windows-only, and the reason given for
that was the install root. The source says otherwise: preflight/managed.rs
env_removes UNSLOTH_STUDIO_HOME and STUDIO_HOME unconditionally, not under a
cfg(windows), with a comment saying Tauri uses the legacy root regardless. So
all three platforms read the same root and the asymmetry was unjustified.

It also mattered more once the pins went: a post-#7547 bundle can install a
working venv and still have preflight regress to NotInstalled, and the app
then sits on the Install screen -- alive, log written, a disposition recorded,
and nothing it just built recognised. macOS and Linux accepted that. All four
staging legs record ManagedReady today, so this tightens the bar without
moving any leg from green to red.
danielhanchen added a commit that referenced this pull request Aug 6, 2026
…pins

The nightly has been red on main since 2026-08-05, all six legs, for two
unrelated reasons.

macOS and Windows died at the download with 'no assets match the file
pattern'. The patterns were Tauri's default bundle names, '*aarch64.dmg' and
'*setup.exe'. The release pipeline moved to its own scheme at
desktop-v0.1.512-beta, so the assets are now Unsloth-Desktop-<ver>-MacOS.dmg
and Unsloth-Desktop-<ver>-Windows.exe. The last green run, on 2026-08-04,
resolved desktop-v0.1.50-beta and downloaded
Unsloth.Studio.Desktop._0.1.50-beta_aarch64.dmg, which is the last release that
carried the old names. Matching on the extension instead removes the coupling
to a naming scheme; a release ships exactly one .dmg and one .exe, and the
arm64 assertion further down is what actually checks the architecture.

Linux died on its own tripwire, which fired exactly as designed:

  the bundled install.sh now carries #7547; delete this pin block and let the
  venv + torch assertions below run unconditionally

Both outcome pins are gone. macOS no longer accepts exit 1 plus the Xcode CLT
gate line, Linux no longer accepts exit 2 plus the NEED_SUDO optional set, and
all three legs now require the installer to exit 0, leave a venv and import
torch -- the assertion Windows already made.
danielhanchen added a commit that referenced this pull request Aug 6, 2026
…h default

Two things the pin retirement left behind.

The dispatch default for release_tag was desktop-v0.1.50-beta, a bundle that
predates #7547. With the outcome pins gone a manual dispatch that took the
default would fail all four POSIX legs at the installer step, on a bundle
nobody meant to test. Empty instead, which falls through to the same
newest-desktop-v* resolution the schedule already uses.

The ready-disposition assertion was Windows-only, and the reason given for
that was the install root. The source says otherwise: preflight/managed.rs
env_removes UNSLOTH_STUDIO_HOME and STUDIO_HOME unconditionally, not under a
cfg(windows), with a comment saying Tauri uses the legacy root regardless. So
all three platforms read the same root and the asymmetry was unjustified.

It also mattered more once the pins went: a post-#7547 bundle can install a
working venv and still have preflight regress to NotInstalled, and the app
then sits on the Install screen -- alive, log written, a disposition recorded,
and nothing it just built recognised. macOS and Linux accepted that. All four
staging legs record ManagedReady today, so this tightens the bar without
moving any leg from green to red.
danielhanchen added a commit that referenced this pull request Aug 6, 2026
…pins (#8026)

* Fix the desktop clean-machine CI: asset patterns and the retired #7547 pins

The nightly has been red on main since 2026-08-05, all six legs, for two
unrelated reasons.

macOS and Windows died at the download with 'no assets match the file
pattern'. The patterns were Tauri's default bundle names, '*aarch64.dmg' and
'*setup.exe'. The release pipeline moved to its own scheme at
desktop-v0.1.512-beta, so the assets are now Unsloth-Desktop-<ver>-MacOS.dmg
and Unsloth-Desktop-<ver>-Windows.exe. The last green run, on 2026-08-04,
resolved desktop-v0.1.50-beta and downloaded
Unsloth.Studio.Desktop._0.1.50-beta_aarch64.dmg, which is the last release that
carried the old names. Matching on the extension instead removes the coupling
to a naming scheme; a release ships exactly one .dmg and one .exe, and the
arm64 assertion further down is what actually checks the architecture.

Linux died on its own tripwire, which fired exactly as designed:

  the bundled install.sh now carries #7547; delete this pin block and let the
  venv + torch assertions below run unconditionally

Both outcome pins are gone. macOS no longer accepts exit 1 plus the Xcode CLT
gate line, Linux no longer accepts exit 2 plus the NEED_SUDO optional set, and
all three legs now require the installer to exit 0, leave a venv and import
torch -- the assertion Windows already made.

* Let the early-exit branch actually print its diagnostic

The launch steps run under `bash -e`, so `wait "$APP_PID"` returning non-zero
aborted the step on that line and neither the ::error:: annotation nor the
tail of app-stdout.log ever printed. The failure surfaced as a bare 'Process
completed with exit code 1' with no annotation and no app output, which is
precisely the information the branch exists to produce.

Found while validating this PR on staging: with the download and pin fixes in
place the Linux legs reach the launch step for the first time, it fails, and
the log said nothing about why.

* Require a ready disposition on macOS and Linux, and unpin the dispatch default

Two things the pin retirement left behind.

The dispatch default for release_tag was desktop-v0.1.50-beta, a bundle that
predates #7547. With the outcome pins gone a manual dispatch that took the
default would fail all four POSIX legs at the installer step, on a bundle
nobody meant to test. Empty instead, which falls through to the same
newest-desktop-v* resolution the schedule already uses.

The ready-disposition assertion was Windows-only, and the reason given for
that was the install root. The source says otherwise: preflight/managed.rs
env_removes UNSLOTH_STUDIO_HOME and STUDIO_HOME unconditionally, not under a
cfg(windows), with a comment saying Tauri uses the legacy root regardless. So
all three platforms read the same root and the asymmetry was unjustified.

It also mattered more once the pins went: a post-#7547 bundle can install a
working venv and still have preflight regress to NotInstalled, and the app
then sits on the Install screen -- alive, log written, a disposition recorded,
and nothing it just built recognised. macOS and Linux accepted that. All four
staging legs record ManagedReady today, so this tightens the bar without
moving any leg from green to red.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants