unsloth/.gitignore
Daniel Han b535eca250
Windows on ARM: install the native ARM64 CUDA stack on NVIDIA hosts (#10282)
* Windows on ARM: install the native ARM64 CUDA stack on NVIDIA hosts

NVIDIA's Windows-on-ARM parts (GB10 / N1X, the RTX Spark laptops) run CUDA
natively, but the installer treated every ARM64 Windows host as "no GPU wheels
here" and fell back to x64 Python under emulation. That fallback looks like it
works: torch loads and sees the GPU. It then fails on the first kernel, because
triton-windows' bundled ptxas is built for the x64 CUDA 12.8 toolkit and cannot
target the sm_121 these parts report, so every Unsloth kernel fails to compile.
llama.cpp separately picked a CPU-only bundle regardless of the GPU.

Detection is evidence based rather than arch based: the installer asks the wheel
indexes what actually exists for this interpreter, and only takes the native path
when both a win_arm64 CUDA torch and an obtainable pyarrow are found. Anything
else keeps the historical x64 path, so a host that cannot be served natively is
never left with a half configured venv. The torch index is probed in order:
download.pytorch.org first, so this special case ends by itself the day PyTorch
publishes these wheels, then NVIDIA's GA channel, then its nightly.

Requirement pins with no win_arm64 wheel are split on
platform_machine == "ARM64" rather than loosened. Every other platform
(linux x86_64 and aarch64, win x64, macOS arm64 and x86_64) evaluates to the
pins it had before, checked by evaluating the markers per platform as well as by
the test suite.

Also:

* llama.cpp prebuilt selection learns a windows-arm64-cuda install kind and
  falls through to upstream's win-cuda-13.4-arm64 asset, so these hosts get a
  CUDA bundle instead of the CPU one. When neither is hash gated the fallback is
  still taken, but says so in the log rather than silently.
* setup.ps1 keeps an installed cu134 torch on win_arm64 instead of "repairing"
  it to a cu130 that has no wheel for this platform, and asks the chosen index
  whether it publishes torchaudio instead of assuming it does not.
* install.ps1 checks before the studio handoff that the installed package
  understands this host, and stops with an explanation if it does not. The two
  halves ship through different channels (the script is fetched from unsloth.ai,
  the package comes from PyPI), and an older setup.ps1 handed a working +cu134
  torch will reinstall a cu130 that does not exist here, leaving the environment
  with no torch at all. UNSLOTH_WOA_NATIVE=0 opts back out to the x64 path.

Verified on an RTX Spark N1X laptop: clean install into an empty
UNSLOTH_STUDIO_HOME ends EXIT=0 with torch 2.14.0+cu134, torchvision 0.29.0,
torchaudio 2.11.0, triton-windows 3.8.0.post28 and the
llama-b10715-bin-win-cuda-13.4-arm64 bundle; LoRA and QLoRA both train; Studio
serves and reports CUDA; Qwen3.8-27B-GGUF UD-Q4_K_XL answers at 8.1 tok/s with
the GPU at 94-95 percent through generation.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: key the new filters on the interpreter, not the machine

Review of the branch turned up one real regression and several sharp edges. All of
them are on paths this PR added; none change what a host did before it.

The regression. `_is_windows_arm64()` answers for the MACHINE, and says yes even
under an emulated x64 Python -- its docstring says so, since that is what it is for.
But the requirement markers this PR adds key on `platform_machine == "ARM64"`, which
is the INTERPRETER's arch, and every Windows-on-ARM install predating native support
is running an emulated x64 interpreter, because install.ps1 deliberately fetched one
there. So on exactly those existing installs the two mechanisms disagreed: the marker
rows correctly selected the x64 pins while the new skip list dropped ten packages
(xformers, tiktoken, tensorboard, librosa, openai-whisper, hf_transfer, sqlite-vec,
mecab and the rest) that have working win_amd64 wheels and install fine today. Anyone
taking the new UNSLOTH_WOA_NATIVE=0 escape hatch would have hit it too.

`_is_win_arm64_interpreter()` answers the other question, from
`sysconfig.get_platform()` -- the same authority pip and uv tag wheels with -- and
the two call sites this PR added now use it. The pre-existing uses of the machine
predicate are left alone, so nothing outside this PR shifts.

The rest:

* setup.ps1 answers `Test-WinArm64Venv` from the machine arch before launching any
  interpreter. The callers sit in the stale-venv path, which on x64 previously
  executed nothing at all; probing a venv's python there to learn something already
  known gave a corrupt or lock-blocked interpreter a chance to hang the install.
* `$expectedKinds` gains windows-arm64-cuda only on the arch that can carry it.
  Accepting it on x64 would keep an install that machine cannot run, where today it
  is removed as mismatched.
* The wheelhouse reuses a staged wheel only if it opens as a zip. An interrupted run
  leaves a truncated file, and skipping on mere existence handed the resolver that
  same corpse on every retry. Both downloads also take a timeout now.
* UV_OVERRIDE / UV_FIND_LINKS / PIP_FIND_LINKS are cleared when they point into this
  StudioHome's own woa directory, so a second `irm | iex` in one shell cannot inherit
  a previous run's wheelhouse. Matched by path rather than cleared outright: an
  inherited UV_FIND_LINKS that a user set for their own wheelhouse is left alone,
  since clearing it would break air-gapped installs on every Windows host.
* hf_transfer in no-torch-runtime.txt and cryptography in studio.txt (and its mirror
  in pyproject) get the same marker split as the other rows. That file installs
  --no-deps and the extra is installable directly, so neither was covered by the
  constraints file.
* `windows_cuda_arch_for_host` tests is_windows as well as is_arm64. Every caller is
  already inside a Windows branch, but `is_arm64` is also true on macOS arm64 and
  Linux aarch64, so the answer should not depend on the caller remembering that.
* The published legacy-minor scan uses the arch being resolved instead of a
  hardcoded x64, which was harmless only while no arm64 CUDA artifact existed.
* No `windows_cuda_selection` log line on x64, where the arch is the unspoken
  default and the line would be new output in every existing install log.

Verified: requirement markers resolve identically to origin/main across 65
environments (linux x86_64/aarch64/arm64/armv7l/ppc64le/s390x, win32
AMD64/x86/ARM64EC, darwin arm64/x86_64, freebsd, python 3.10 to 3.14), differing
only on win32/ARM64; the studio-extra mirroring guard passes; both PowerShell files
parse; and a simulated interpreter matrix confirms the win_arm64 skip list applies
on a native ARM64 interpreter and on nothing else, including the emulated x64 case
that regressed.

* Keep the cross-platform parity greps matching

Two parity tests read install.ps1 and setup.ps1 as text, and both changes in this
PR moved the strings they look for. The behaviour they guard is intact in each
case, so the fix is on this side rather than in the assertions.

test_install_ps1_bounds_unknown_leaf_pinned_torch pins the literal
"@_torchSpecs --default-index $TorchIndexUrl", which is how it checks that the
bounded companion specs actually reach uv. The Windows on ARM branch inserted
@_torchExtraArgs between the two halves. Those arguments are all flags and their
values, never positional packages, so uv accepts them anywhere on the line;
moving them after the index restores the literal and changes nothing about the
resolve.

test_setup_ps1_scrub_covers_pip_fallback slices a fixed 2500 character window
from "function Fast-Install" and requires the pip fallback to appear before the
finally that restores the scrub. The flag stripping loop added here is sixteen
lines and pushed the finally out of that window: the fallback is still inside the
try, but the test could no longer see it. Lifting the loop into
Remove-UvOnlyResolverFlags puts Fast-Install back near its original size, with
the finally at offset 1695 against a 1629 baseline on main.

The helper was exercised against the inlined version it replaces on all six
shapes: no flags, --index-strategy in both spellings, --prerelease in both
spellings, and an empty list. Output is identical, including the empty-in
empty-out case that every caller in the tree takes today.

* Exempt the Windows on ARM drop list from the xFormers pin check

test_installer_never_installs_an_unpinned_xformers flags every quoted xformers
literal in install.ps1 that is not the exact pin. The Windows on ARM block added
one, but it is a removal list: those names are written into the requirement
overrides so the package vanishes from the resolve, since win_arm64 has no
xFormers wheel and no buildable sdist. A version pin there would be meaningless,
and the name has to match the requirement line exactly for the override to bind.

The test already carves out wheel filenames on the same grounds, that a filename
is not a spec. This adds the drop list beside it rather than loosening the
assertion. Checked that it stays narrow: a bare "xformers" appended at end of
file is still caught, and so is a loose "xformers>=0" placed on the line
immediately above the drop list, while the real file is clean.

* Windows on ARM: fix six ways the native stack picks the wrong thing

Six defects found in review, all on the new WoA path and none reachable on any
other host.

Probe the interpreter that was actually selected. Initialize-WoaNativeCudaTorch
answered for $PythonVersion, but Find-CompatiblePython falls back to another
supported minor whenever the requested one is not installed. Every native
decision is keyed to an interpreter tag, so a 3.13 answer carried into a 3.12
venv staged a cp313 pyarrow wheel and then pinned pyarrow== to it, forcing the
Arrow sdist this path exists to avoid. Re-probe once the interpreter is settled,
key the staged tags on the venv's own minor, and clear every script-scope answer
on entry so a second probe cannot inherit the first one's.

Redact the selected torch index. The status line printed
$script:WoaTorchIndexUrl verbatim, and it can be UNSLOTH_TORCH_INDEX_URL with
userinfo or a token query. It was the only index line in the file that did not
redact. Remove-IndexUrlCredentials moves up to the other helpers, since
PowerShell resolves a function only once its definition has executed.

Hand uv a space-free override path. uv reads UV_OVERRIDE as a space-separated
list of files, which this script splits on \s+ itself, and the default
StudioHome sits under %USERPROFILE%. An overrides file under a path with a space
was read as two paths, neither of which exists, and every later uv call failed
with "File not found". Same defect and same remedy as
backend/utils/uv_path_safety.py (issue #6503), which the Python half already
applies. pip splits its repeatable options the same way; UV_FIND_LINKS is
comma-separated and is untouched.

Do not enable the native stack for an x64 venv. The migration branches keep a
healthy legacy environment as it is, and on this host those are x64 by design.
Get-TorchIndexUrl returns the win_arm64 index on the flag alone while the spec
lift is gated on the venv's platform tag, so a migrated venv was asked for the
bounded x64 specs from an ARM64-only index and aborted with no fallback. Ask the
venv the same question the spec branch asks.

Accept every spelling of a local wheelhouse. UNSLOTH_WOA_WHEELHOUSE is
documented as taking a directory or a base URL, but only drive-plus-backslash
and UNC counted as local, so C:/wheels went to Invoke-RestMethod and the native
path was dropped on a host whose wheels were present. Ask the filesystem instead.

Count a wheelhouse wheel only for the interpreter it was built for. Staging
copies every win_arm64 wheel, cp311 through cp314, but both availability checks
keyed on the distribution name alone. A cp311 tiktoken in a cp313 venv cleared
its skip and its requirement override, and the resolve fell to an unbuildable
sdist. Filter on PEP 425 tags on both sides. WINDOWS_ARM64_SKIP_UNBLOCKED_BY
takes the whole blocker set rather than one name, since librosa needs numba as
well as llvmlite and whisper needs tiktoken too; its openai-whisper key was also
never matched, being looked up canonically.

Let an explicit CPU pin through the CUDA preservation guard. It ran ahead of
every pin check on both sides, so UNSLOTH_TORCH_INDEX_URL=.../cpu was dropped
even though download.pytorch.org publishes win_arm64 /cpu torch and torchvision.
In setup.ps1 it also left PinChangedForceReinstall false, which skips the
dependency pass entirely, so the run kept the CUDA build and reported success.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pin that an emulated x64 interpreter still gets the torch flavor repair

The CUDA preservation shortcut reads the interpreter, not the machine, and nothing
in the suite held it to that. Swapping _is_win_arm64_interpreter for
_is_windows_arm64 at the guard leaves 223 of 224 tests here passing, which is the
whole margin between a correct guard and one that strands every Windows on ARM
install predating native support. Those run an emulated x64 python against
ordinary win_amd64 wheels from download.pytorch.org, so the repair has somewhere
to go and must still reach them.

The fixture drove both predicates from one flag, which is right for every case
already here, since a native ARM64 venv is the only place both are true. They
separate on an ARM64 machine running an x64 interpreter, so win_arm64_interpreter
overrides just that half and defaults to the existing flag.

Verified by mutation: with the guard reading the machine instead, the new test is
the only one of the 224 that fails.

* Windows on ARM: pick the retained llama.cpp kind from the venv arch

$expectedKinds asked Get-HostMachineArch, which answers for the machine. A
Windows-on-ARM host on the x64 fallback runs the prebuilt helper under an
emulated x64 python and installs windows-cuda, so the machine's answer would
call that bundle mismatched and discard a working runtime. Test-WinArm64Venv is
the same predicate the torch path uses, and it short-circuits free on x64.

The block stays inert either way: write_prebuilt_metadata persists no
install_kind, so $existingKind is always null.

* Windows on ARM: pin the marker split, drop a write-only variable

Cross-platform verification of the requirement splits, plus one dead variable
the earlier fix left behind.

WoaPythonMinor was written in three places and read in none. It was the variable
the staging tag was meant to use; that moved to WoaVenvMinor, which is keyed on
the venv's own interpreter. Leaving a write-only "the minor we probed" next to
the real one only invites a future reader to take the stale answer.

test_windows_arm64_requirement_markers pins the mechanism the whole split rests
on. Every package needing a different version on win_arm64 is two rows whose
markers are exact complements, so exactly one is live everywhere. Both ways of
breaking that are silent: an overlap makes pip intersect two specifiers and can
render the row unsatisfiable, and a gap drops the package on a platform nobody
tested. The tests evaluate every row of every requirements file and every
pyproject extra across 60 environments.

Two of the checks exist because a scope-limited test skips itself when the bug
appears. Spell the machine "arm64" rather than "ARM64" and the conjunction with
sys_platform == "win32" becomes unsatisfiable, the split stops existing, and any
check that looks for a split finds nothing to look at; Windows on ARM quietly
takes the x64 pins and source-builds. That is caught semantically, by asserting
no row is dead in every environment, rather than by matching marker text --
torchcodec legitimately pairs 'arm64' with sys_platform == 'darwin' in a
separate disjunct. And the expected rows are asserted per source file, because
studio.txt and pyproject[studio] mirror each other, so a global check stays
green when only one of them loses a row.

Verified by mutation: overlapping rows, a lowercase arm64 in either file,
aarch64 for ARM64, a deleted WoA row, and either deliberate exclusion
(xformers, hf_transfer) losing its marker are each caught, and each by a
different assertion.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: close seven gaps the last round left open

All seven are on code the previous rounds added, which is where a fresh pass
should look.

The resolver paths have to be readable by uv or the native stack is not viable.
uv splits UV_OVERRIDE on whitespace and accepts no quoting of any kind -- bare,
"quoted", 'quoted' and back\ slashed all fail identically on uv 0.10.7 -- so the
8.3 short form was the whole mitigation, and 8.3 generation can be disabled on
the volume. A spaced StudioHome then has no space-free spelling and every later
uv call dies on a truncated path. Decide that up front and take the x64 path,
which never sets these variables, rather than staging wheels into a venv that
cannot resolve. Same rule the pyarrow probe already applies.

That short form then has to be recognised on the way back in. The block that
drops a previous run's resolver variables matched only the long spelling, so
after a native install under a spaced home it no longer recognised its own
values and the next run in that shell inherited them. Both spellings are
checked; a wheelhouse the user set for themselves is still left alone.

The probe follows the interpreter that ends up owning the venv. Re-probing for
the minor Find-CompatiblePython settled on was not enough: the ARM64 bootstrap
below it installs $PythonVersion, which can be a third minor again, and the
index and pyarrow source would both have been confirmed for one no longer in
play.

setup.ps1 is told which index install.ps1 used. It otherwise derives the CUDA
index from the driver, and download.pytorch.org publishes no win_arm64 CUDA
wheel at all, so a repair or a missing companion on a native venv resolved
against an index that cannot serve it. Passed as UNSLOTH_WOA_SELECTED_TORCH_INDEX
rather than UNSLOTH_WOA_TORCH_INDEX_URL, which is the user's input override: a
second run in the same shell would otherwise read this run's choice as a pin.

A wheel's ABI has to agree, not just its interpreter tag. cp313-cp313t is the
free-threaded build and uv rejects it on a regular cp313, so counting it as
available dropped the package's override and sent the resolve to the sdist the
override existed to avoid. The Python half already checked this; the PowerShell
half did not.

Blockers decide even when the package's own wheel is hosted. tensorboard and
librosa publish py3-none-any wheels that the staging step copies, so a
wheelhouse can hold the package and still lack grpcio or numba; unskipping it
there walks straight into the blocker's sdist.

UNSLOTH_LLAMA_ARM64_CUDA=0 is honoured on the direct-upstream path too. It was
checked only in the fork resolver, so the documented escape hatch silently did
nothing with --published-repo ggml-org/llama.cpp.

Each fix is mutation-tested: reverting either Python-side change fails a
specific new test, and the PowerShell changes are asserted structurally and by
executing the extracted helpers.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: build probe URLs with Join-UrlPath

A tokenized mirror is a supported form for UNSLOTH_TORCH_INDEX_URL and for the
wheelhouse, and "$base/$leaf" puts the leaf inside the token value:
https://mirror/whl?token=abc became .../whl?token=abc/torch/, so the preflight
read the mirror root or an error and disabled the native stack on a host whose
wheels were there. Join-UrlPath already solves this for the xFormers URLs; it
moves up next to the other Windows-on-ARM helpers, since PowerShell resolves a
function only once its definition has executed and these probes run far above
its old position. Applied to the torch/torchaudio preflight and to all three
wheelhouse URLs, which had the same shape.

* Windows on ARM: abandon the native route before building, and carry the index across shells

Two review findings on the previous head.

Abandon the native route earlier. The check for a StudioHome uv cannot read --
a path containing a space on a volume with 8.3 short names disabled, which uv
splits UV_OVERRIDE on -- sat next to the resolver files it guards, which is
after the ARM64 venv has been created. The install therefore paid for a venv it
then tore down. Test-WoaResolverPathsUsable now runs inside
Initialize-WoaNativeCudaTorch, immediately after the GPU probe, so the route is
never chosen in the first place and every re-probe honours it. The verdict is
cached, so a user with several interpreters reads the explanation once.

Carry the chosen index across shells. install.ps1 exports the index it probed,
but a later `unsloth studio update` runs in a fresh shell where that variable is
gone, and Windows on ARM is the one platform whose CUDA wheels are not on
download.pytorch.org at all, so the index cannot be re-derived from the driver.
The manifest now records it, and setup.ps1 falls back to it when the handover
variable is absent.

That is a deliberate exception to the rule install_manifest.py documents for
itself -- the flavor, never the index URL it came from, since a pinned index can
carry a token -- so it is narrowed to the case that motivated it. Only
credential-free https://pypi.nvidia.com URLs are written, with no userinfo,
query or fragment, and setup.ps1 re-checks the same thing on read so a
hand-edited manifest cannot redirect a torch install to another host. A mirror
the user pinned is deliberately not persisted; they supply it through their own
environment as before.

tests/studio/install/test_woa_torch_index_persistence.py covers both halves of
the guard, the Python that writes and the PowerShell that reads, including the
older manifests that carry no such key and the truncated ones a killed installer
leaves behind.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: restore the generated requirement overrides on a fresh-shell update

The torch index was not the only thing install.ps1 hands over through a
process-scoped variable. It also writes StudioHome\woa\overrides.txt, stages a
win_arm64 wheelhouse beside it, and exports both through UV_OVERRIDE /
UV_FIND_LINKS / PIP_FIND_LINKS. A direct `unsloth studio update` runs in a fresh
shell where all three are gone, so the dependency pass resolved without them.

That matters because studio.txt pulls ddgs, which requires
httpx[brotli,http2,socks], whose brotli extra is Brotli on CPython, and neither
Brotli nor brotlicffi publishes a win_arm64 wheel. Confirmed against the
published metadata and against the resolver: uv pip compile for
httpx[brotli]==0.28.1 targeting aarch64-pc-windows-msvc resolves 8 packages
including brotli==1.2.0, and 7 with the overrides applied. With no wheel to
install, those 8 mean an sdist build of a C extension -- the exact failure this
path exists to avoid. Only reachable since this PR: an emulated x64 venv has a
win_amd64 brotli wheel.

Restore-WoaResolverEnvironment sits next to the torch-index recovery so both
halves of the lost handover are put back in one place, before the torch installs
and the dependency pass, which is where install.ps1 has them set for its own run.
It restores rather than re-derives: both files persist under StudioHome\woa\, and
the generated file is the only accurate record of what was dropped, since the
drops depend on what the wheelhouse turned out to hold. No-op unless
Test-WinArm64Venv, and it returns untouched if any of the three variables is
already set, so install.ps1 driving the resolve in the same process is never
clobbered. A native venv whose overrides file was deleted warns and points at
install.ps1 rather than guessing the list.

Get-UvSafePath is duplicated from install.ps1 because neither script can
dot-source the other; a test asserts the two copies stay identical, since a copy
that drifts would hand uv a path install.ps1 had already ruled unreadable.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: re-export the recovered index, restore each resolver variable, and cover no-torch

Three follow-ups on the fresh-shell recovery.

Re-export the recovered index. install_python_stack.py rewrites the manifest at
the end of every run from UNSLOTH_WOA_SELECTED_TORCH_INDEX, so recovering the
index into a local variable only meant the first fresh-shell update wrote a
manifest without it -- erasing the one thing the manifest exists to carry, and
making the whole mechanism work exactly once. It is now put back in the
environment right after it is recovered, guarded so an empty recovery exports
nothing. The value is always one install.ps1 chose or one write_manifest's
credential-free allowlist already accepted, so re-exporting widens nothing.

Restore each resolver variable independently. Returning when ANY of the three
was set was aimed at the install.ps1 case, where all three arrive together, but
it also fired for a shell carrying an unrelated UV_FIND_LINKS or PIP_FIND_LINKS
-- a corporate wheel mirror has nothing to do with the win_arm64 drop list, and
should not cost the user the brotli exclusions. Each is now restored on its own
and a caller's value is still never overwritten, so the install.ps1 path is
unchanged: every branch no-ops there.

Cover no-torch mode. Both recoveries sat inside `if (-not $NoTorchMode)`, and
neither is about torch. install_python_stack.py installs studio.txt at function
scope with no NO_TORCH gate, and ddgs is not in NO_TORCH_SKIP_PACKAGES, so
UNSLOTH_NO_TORCH=1 resolves the same brotli chain; the manifest is rewritten in
that mode too. Both have moved above the guard. The torch-index USES stay inside
it, where they belong.

The new tests assert the placement rather than the text, and one asserts the
studio.txt pass is still unconditional so this reasoning cannot go stale
unnoticed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: read the manifest before dropping it, publish the index torch came from

Three follow-ups, the first of which made the persistence added earlier a no-op.

Read the index before the manifest is deleted. setup.ps1 drops
unsloth_install_manifest.json before it replaces pip, torch and triton, so a run
killed in those leaves the venv marked half-built rather than behind a marker
that verifies. The recovery was placed below that drop and read a file that no
longer existed, so it returned empty every time -- on precisely the fresh-shell
path the persisted index exists to serve, and silently, since empty is also the
correct answer for an older install that recorded nothing. The recovery now runs
as soon as the dependency guard opens, before the drop. It stays inside that
guard: a run that installs nothing has nothing to recover for.

Publish the index torch was actually installed from. The native path resolves
from the channel install.ps1 probed, while $TorchInstallIndexUrl still names the
driver-derived family, so the handoff sent install_python_stack.py's repair to an
index with no win_arm64 CUDA wheel and named the flavor cu130 when the installed
wheel is +cu134 -- a repair could have tried to correct a working venv to a build
that does not exist for this platform. $_effectiveTorchIndexUrl is initialised to
$TorchInstallIndexUrl and overwritten only by the branch that installs torch, so
every other path publishes exactly what it published before. On the NVIDIA channel
the leaf is nvtorch_oot, not a CUDA family name, so no flavor tag is published at
all -- the honest answer, and what the block already documents for an unknown leaf.

Gate the llama.cpp ARM64 CUDA branch on the opt-out. resolve_upstream_asset_choice
is reached through resolve_asset_choice on the fallback paths, so leaving it
ungated meant UNSLOTH_LLAMA_ARM64_CUDA=0 worked in direct_upstream_release_plan
and silently did not work here. The same gate is already on the equivalent
branches at lines 1126 and 3897.

The new tests assert ordering and wiring rather than the function in isolation,
which is what let the first of these through.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: rebuild a migrated x64 venv as ARM64, and widen the llama.cpp CUDA opt-out

Rebuild a migrated legacy environment. The migration branches keep a healthy
~/.unsloth/studio/.venv exactly as it is, and on a Windows-on-ARM host those are
x64 by design: every install predating the native stack deliberately
bootstrapped an emulated x64 interpreter. The venv-platform guard then saw
win-amd64 and stood down to that same stack, so an upgrade left an NVIDIA user
on a Triton that cannot compile for sm_121. Only a SECOND installer run
recovered, because migrating makes the layout new and the new-layout branch
already does preserve-and-recreate.

Do that in the first run instead. Just above the venv-creation block, a migrated
environment that is not win-arm64 is preserved with the same
Start-StudioVenvRollback and the existing creation path builds an ARM64 venv.
Scoped to a venv THIS run migrated: a new-layout venv was already moved aside,
and one created moments ago came from the interpreter this run chose.

$_Migrated is cleared on success. It gates an upgrade-in-place far below that
installs unsloth with --no-deps --reinstall-package; left set, that would run
against a freshly created empty venv and leave an unsloth with no dependencies.
If the rollback throws, the environment is kept and the existing guard disables
native exactly as before -- a user's environment is not worth trading for a
native stack, so the platform guard keeps the final say.

Widen UNSLOTH_LLAMA_ARM64_CUDA to every ARM64 CUDA bundle. This is a deliberate
scope change, not only a bug fix. The helper documented itself as governing the
upstream bundle when the fork publishes no checksum, but what the flag promises
the user is the CPU bundle, and gating only the unverified tail meant the escape
hatch would switch itself off the day the fork published an approved
windows-arm64-cuda artifact, since the published branch returns first. The
docstring now says what the code does. The inner check inside that branch is
unreachable once the branch is gated, so it is gone rather than left reading as
a fallback that no longer exists.

The opt-out tests walk the AST rather than grepping, because the same attribute
guards the x64, Linux and macOS paths, which must not be gated: they assert both
that every Windows-ARM64 NVIDIA branch is gated and that no other one is.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: gate the pandas 3 row on Python 3.11, and keep the CPU llama bundle when CUDA is opted out

pandas 3 needs Python 3.11. Every pandas 3.0.x is requires-python >=3.11 and its
win_arm64 wheels start at cp311, which is why the Windows-on-ARM row exists at
all -- but the row carried no python_version bound while requires-python still
admits 3.9 and 3.10, so `pip install unsloth[studio]` was unsatisfiable on an
ARM64 3.9 or 3.10 interpreter. Split on python_version as well as platform,
which keeps the partition exact: below 3.11 an ARM64 host takes 2.3.3 and
source-builds, slow but resolvable, as it did before win_arm64 wheels existed.
Narrowing requires-python instead would drop 3.9/3.10 everywhere to fix one
platform. The same two rows appear in studio.txt and single-env/constraints.txt
with the same defect, so all three move together.

The 603-case marker suite passed throughout this bug's life: the partition was
exact and every environment had exactly one live row -- that row just could not
install anywhere. So the suite gains the axis it was missing, asserting that a
live row is installable on the interpreter it was selected for, checked against
the package's own requires-python, plus the complement that no ARM64 Python is
left with no pandas row at all.

Keep the ARM64 CPU llama.cpp bundle when CUDA is opted out. With
UNSLOTH_LLAMA_ARM64_CUDA=0 the selector installs a windows-arm64 CPU bundle on an
NVIDIA Windows-on-ARM host, but the mismatch check expected only
windows-arm64-cuda there, so it deleted a correct install on every setup and
update -- and an update that cannot download then leaves no llama.cpp at all.
The previous commit made this reachable in practice by widening the opt-out to
the published artifact.

The expected kind now follows the flag, INSTEAD of the CUDA kind rather than as
well as it: accepting both would keep a bundle installed before the flag was set,
so the opt-out would appear not to work on exactly the machine that already had
CUDA. Falsy spellings are _upstream_arm64_cuda_allowed's, with a test asserting
the two stay identical, since this is now one contract in two languages. The x64
arm is untouched and covered as a negative control.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: pair torchaudio with torch, prove wheels are CUDA, gate prereleases on nightly

Take torchaudio only as a matched pair. The GA channel serves torch 2.14.0+cu134
beside torchaudio 2.11.0+cu134, torchaudio 2.11 dropped the exact torch== pin
that 2.10 still carried, and the native specs are torch>=2.4 / torchaudio>=2.4 --
so nothing stopped the resolver installing that pair and leaving torchaudio's
extension to load against a libtorch three minors newer. install.ps1 already
names this hazard on the bounded-trio branch; the native branch was not covered
by it. The probe now reads versions rather than existence and enables torchaudio
only when its major.minor pairs with torch's, the relationship the old pin
encoded. No matching pair means no torchaudio, which is what every
Windows-on-ARM install did before this channel published one; it lights up on
its own the day NVIDIA ships a matching pair.

Establish CUDA positively. The probe rejected a literal "+cpu", which misses two
things: PEP 503 hrefs encode "+" as %2B, and PyPI's own win_arm64 torch wheels
carry no local version at all, so they are not "+cpu" and were read as CUDA --
sending the host native on CPU-only torch, which is worse than staying emulated
because the GPU then goes unused with no fallback. Filenames are unescaped and
must carry a +cuNNN local version. A private index whose CUDA wheels carry no
+cu tag now reads as unknown and takes the x64 path: a behaviour change, and the
safe direction, since that path works.

Gate --prerelease=allow on the nightly channel, as install.ps1 already does.
setup.ps1 passed it unconditionally and its comment ("the wheels are nightlies")
predated the GA channel. "allow" means every prerelease, and it rode on a command
carrying unsafe-best-match and an explicit PyPI extra index, so a prerelease of
torch or of any shared dependency could outrank the stable GA build on every
setup and update. The other two flags stay unconditional; the GA channel needs
them.

Verified against both live NVIDIA channels (temp/sim10282/probe_test.ps1) and
covered offline with stubbed index pages for the parsing and pairing rules.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: five resolver and validation fixes

Reject abi3 wheels on free-threaded interpreters. The exact-minor branch
accepted "abi3" unconditionally, shadowing the guarded branch below it, so
cp313-abi3 was installable on 3.13t while cp311-abi3 was correctly refused.
Free-threaded builds do not implement the stable ABI (CPython #111506, PEP 703),
so this marked a blocker available, dropped its skip and sent the resolver at a
wheel it cannot use. PEP 803's abi3t is 3.15 and outside requires-python, so it
is documented rather than matched. The test simulates both builds instead of
reading whichever interpreter runs the suite.

Validate UNSLOTH_PYARROW_WHEEL. Every other branch of Get-WoaPyarrowSource
checks the interpreter and platform tags; this one took any file that existed,
so an x64 wheel, a wheel for another minor or a truncated download selected the
native path and then failed at resolution, having already given up the working
x64 stack. Same checks as the siblings plus the zip signature, read two bytes
off a stream rather than loading tens of megabytes.

Keep the caller's resolver configuration. The purge above deliberately preserves
anything that is not this StudioHome's own, and the assignments then overwrote
it. Folded, not appended: uv combines override files rather than letting a later
one win, and two files naming one package without distinguishing markers is an
error -- and this file declares torch and torchvision. Same shape as
New-UnslothTorchOverridesFile. Find-links have no such conflict semantics, so
those are appended with the caller's kept and ours first. install_python_stack.py
split find-links on os.pathsep alone, which would have read an appended
"dirA,dirB" as one unusable path; it now splits on comma, whitespace and
os.pathsep. The generated file is UTF-8 now that a caller's line can be
non-ASCII.

Accept the automatic ARM64 CPU fallback. resolve_asset_choice falls through to
the published windows-arm64 bundle when no ARM64 CUDA asset exists, so demanding
the CUDA kind deleted a healthy fallback every run. Widening strands nobody: the
installer's already-satisfied check is per candidate and CUDA is attempted
first, which a test now pins. The opt-out arm stays exclusive.

Make the manifest writer and reader accept the same URLs. urlsplit().hostname
strips the port, so https://pypi.nvidia.com:443/... was persisted and then
refused by setup.ps1, losing the index on the next fresh-shell update. The
writer now requires netloc == hostname, admitting exactly what the reader admits
and dropping userinfo with it. Declining is preferred to silently rewriting a
caller's URL; the default channels carry no port.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: probe and stage the interpreter's ABI tag, not just its minor

A free-threaded build installs cp313-cp313t wheels, but every probe here derived
its tag from the minor alone and searched for cp313-cp313. The two halves then
failed in opposite directions: the index probe found the ordinary GIL wheels and
enabled the native stack, while the wheelhouse staging kept exactly those wheels
and discarded the cp313t ones the venv could have installed -- all after the x64
fallback had been given up, so the run failed at resolution with nowhere to go.

Reachable: Find-CompatiblePython's python3/python PATH scan and its `py -0p`
enumeration both accept any interpreter whose --version matches, which a
free-threaded build does, and the native path ranks ARM64 builds first.

Test-PythonFreeThreaded asks the selected interpreter for Py_GIL_DISABLED and
Get-WoaAbiTag turns the answer into cp313 or cp313t, threaded through the torch,
torchaudio and pyarrow probes and through the staging scan, which also records
the venv's own ABI. A free-threaded host therefore declines the native path
today, because no index publishes those wheels, and starts working by itself the
day one does -- better than refusing such interpreters outright. Unknown answers
GIL, the historical assumption, so nothing moves for a GIL host: there the two
tags are the same string.

The staging scan also stops accepting abi3 on a free-threaded venv, the rule
already applied to _wheel_matches_interpreter, so the two now agree.

Verified against the live GA channel: a GIL 3.13 still resolves torch
2.14.0+cu134 and a free-threaded 3.13 finds nothing.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: re-probe on ABI change, merge caller overrides, honour an explicit pin

Re-probe when the ABI changes, not only the minor. The previous round threaded
the interpreter's ABI through every probe but left the re-probe guard keyed on
the minor alone -- so the one case where the minor cannot change, a 3.13t chosen
for the default 3.13 request, was exactly the case that never reached the call
passing -FreeThreaded. The initial probe's GIL assumption stood, native mode was
enabled on cp313 wheels, and the venv was then built from an interpreter that
installs cp313t. $WoaProbedFreeThreaded is tracked beside $WoaProbedMinor and
either changing re-probes, including after Install-PythonFromPythonOrg, where
the ABI can change while the minor does not. Detection is scoped to an ARM64
host so no other Windows host spends a subprocess on an answer that cannot
matter there.

Merge caller overrides on an update instead of standing down. install.ps1 folds;
setup.ps1 skipped the generated file entirely whenever UV_OVERRIDE was set, which
keeps the caller's file and loses the win_arm64 drop list -- so ddgs ->
httpx[brotli] -> Brotli went back to an sdist with no win_arm64 wheel. uv
combines override files rather than letting one win, and errors on a duplicate
package, so the two cases differ: disjoint files are both handed over, needing no
rewriting and leaving each file's relative references resolving against its own
directory, and only an actual conflict is merged, with this platform's entries
winning because they name what it cannot build. Get-RequirementName(s) do PEP 503
normalisation so Brotli and brotli_cffi compare as one name, and a merge that
cannot be written falls back to the caller's file with a warning.

Let an explicit pin outrank the persisted index. UNSLOTH_TORCH_INDEX_URL and
UNSLOTH_TORCH_INDEX_FAMILY both land in $PinnedTorchIndexUrl, and the recovered
WoA index was winning over both -- so a user switching to another CUDA mirror was
silently still served by the previously recorded channel. The recovery is a
memory of what install.ps1 chose, not a decision the user made, so it is now
consulted only when nothing is pinned.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: keep caller resolver entries, survive a bad handoff URL, drop a stale torchaudio, check wheelhouse versions

- install.ps1: the resolver purge removes only the entries pointing into this
  StudioHome's woa directory instead of the whole variable, so a caller's
  mirror appended behind ours survives a second run in one shell.
- install_manifest.py: parse the WoA index URL inside a try/except ValueError;
  a malformed authority no longer aborts the whole manifest write.
- setup.ps1: on a native ARM64 install that leaves torchaudio out of the trio,
  uninstall an already-installed torchaudio whose major.minor no longer
  matches torch.
- install_python_stack.py: the find-links scan records versions, and a hosted
  wheel clears a win_arm64 skip only when one of its versions satisfies the
  pin in the requirements file being installed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: match wheel tags as fields, evaluate markers, bound the purge, version the drop list

- install.ps1: one Test-WoaWheelTags parses the PEP 425 fields and expands each
  dot-separated tag set, replacing seven substring probes. "*cp313-cp313*" also
  matched cp313-cp313t, so a free-threaded wheel could select and then be staged
  for a GIL interpreter, failing only after the ARM64 venv was committed to.
- install.ps1: the resolver purge treats only the prefix itself or a descendant
  past a path boundary as owned, so <StudioHome>\woa-mirror survives.
- install.ps1: the override drop list records wheel versions and consults the
  floor the released metadata puts on the name (xformers>=0.0.22.post7) before
  removing a drop; a lower hosted version keeps it.
- install_python_stack.py: requirement markers are evaluated for the running
  interpreter, so the inactive MeCab row no longer overwrites the active one.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: honour any explicit pin, preflight PyAV, keep caller override files in place

- setup.ps1: the CUDA-preservation shortcut exempts any explicit torch index
  pin, not only a /cpu one. It distrusts the inferred host-family comparison; a
  pin at a cu129 mirror is a stated instruction, and the narrower test skipped
  the dependency pass and kept cu134 while reporting success.
- install.ps1: on a free-threaded interpreter, native mode requires an
  installable win_arm64 av (PyAV) wheel. The wheel constraints.txt asks for is
  cp311-abi3, which no free-threaded build can use; PyAV publishes cp314-cp314t
  but no cp313t, so the index is asked rather than assumed. GIL builds are
  unchanged and never run the probe.
- install.ps1: a caller UV_OVERRIDE file that declares none of our packages is
  passed to uv where it sits, so its relative -r and wheel paths keep resolving.
  Only a conflicting file is folded, and its relative references are rebased
  onto the directory they were written for.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: read the machine arch from the registry, not from the process

Fresh install of this branch on the GB10 laptop went down the x64 path: plain
"NVIDIA GPU detected" and cu130 from download.pytorch.org, which publishes no
win_arm64 wheel, with no line saying why. Running the probe's gates one at a time
found it: Get-HostMachineArch answered x86_64, so Initialize-WoaNativeCudaTorch
returned on its first check before printing anything, and every later gate,
nvidia-smi, the NVIDIA index, the pyarrow wheel, would have passed.

Every signal it trusted follows the process, and the install ran from an x64
emulated shell. There PROCESSOR_ARCHITECTURE is AMD64, PROCESSOR_ARCHITEW6432 is
empty because it is a WOW64 variable and x64 emulation on ARM64 is not WOW64, and
.NET Framework's OSArchitecture reports X64, a bug fixed only in .NET 5. VS Code's
integrated terminal is often x64 on these machines, so this is a real user path,
not a test artefact. The machine-scope PROCESSOR_ARCHITECTURE from the registry
is the one value emulation cannot misreport, and it now leads the signal list in
install.ps1, in setup.ps1, and in setup.ps1's Get-UvHostArch so the uv an update
picks matches the one install.ps1 chose. The per-process signals stay as
fallbacks; on x64 hardware every answer is unchanged.

setup.ps1 is the one that made this urgent. Test-WinArm64Venv short-circuits on
the same function, so from an x64 terminal an update of a native ARM64 venv would
have skipped the CUDA preservation guard and repaired a working cu134 torch to a
cu130 that cannot install there.

The python side had the identical hole: _is_windows_arm64 read the same three
signals and its docstring promised ARCHITEW6432 would be set under emulation,
which it is not. It reads the registry first now. Two of its three callers drop
torchaudio on the grounds that no win_arm64 wheel exists, which is a property of
the interpreter, not the machine: an emulated x64 venv on an ARM64 box installs
win_amd64 wheels and download.pytorch.org publishes torchaudio for those. Both
move to _is_win_arm64_interpreter. The third caller prints the Vulkan note, a
machine fact, and stays.

Verified with the real functions lifted from install.ps1 and run under both an x64
emulated and a native ARM64 PowerShell: both now answer arm64 and select the
nvtorch_oot index. Under the x64 and the ARM64 CPython on the same box the machine
predicate is True for both and the interpreter predicate is True only for the
ARM64 one, which is the disagreement the wheel filters rely on.

Tests: the arch-probe cases patch the registry helper so they answer the same on
an ARM64 host as on x64 CI, and gain the emulated case with every per-process
signal saying AMD64. The Vulkan-advice fixture takes the host as a parameter
instead of reading the box it runs on; three of its tests had failed on every
ARM64 dev box for that reason.

* Make the marker-evaluation test independent of the host running it

test_pins_are_read_canonically_and_markers_evaluated wrote a row under
`sys_platform != 'win32'` and asserted the pin survived because "its marker holds
on this host". It holds on a Linux runner and on no Windows machine, which is
every machine this file is about; on the GB10 laptop it fails with a KeyError.

The reader answers True, False or None for a marker, and only False drops the
row, so the test now uses one marker that holds everywhere and one that holds
nowhere, and asserts both directions. Same coverage, no dependence on the box.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: prepend the managed wheelhouse, rebase merged overrides, tolerate a self-copy

- setup.ps1: the resolver recovery prepends $StudioHome\woa\wheels to an existing
  UV_FIND_LINKS / PIP_FIND_LINKS instead of standing down. find-links are
  additional search locations, so skipping ours because a caller had set a mirror
  left the staged win_arm64 wheels out of the search and sent pyarrow to an sdist.
- setup.ps1: Resolve-WoaOverrideLine is carried over from install.ps1 and every
  line folded into overrides.merged.txt is rebased against the caller file's
  directory, so a relative -r, -c, -f or wheel path keeps resolving.
- install.ps1: both wheel staging copies compare normalised paths first. A
  wheelhouse pointed at the managed wheel directory made Copy-Item overwrite an
  item with itself, which under ErrorActionPreference Stop aborted the install.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: open the supplied wheel, and honour a configured win_arm64 mirror

- install.ps1: UNSLOTH_PYARROW_WHEEL is validated with Test-ZipArchiveReadable
  rather than a two-byte PK sniff. An interrupted download still carries that
  signature, and accepting one selected the native path, staged the broken file
  and failed the pyarrow resolve with the x64 stack already given up.
- setup.ps1: UNSLOTH_WOA_TORCH_INDEX_URL is read ahead of the handover variable
  and the manifest. write_manifest persists only NVIDIA's own channels, since
  any other URL could carry a credential, so a corporate win_arm64 mirror
  survives a fresh shell nowhere else; without this a direct update fell back to
  the driver-derived index, which publishes no win_arm64 CUDA wheel.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: guard the third staging copy against a self-copy

UNSLOTH_PYARROW_WHEEL may point at the already-cached wheel under
$StudioHome\woa\wheels, which is how an offline run reuses the managed cache.
Copy-Item refuses to overwrite an item with itself; here the throw is caught, so
instead of aborting it disabled native mode after the ARM64 interpreter and venv
had already been chosen, leaving the run on a CUDA index with no win_arm64 torch.
Test-WoaSamePath now guards all three staging copies, and a counting test keeps a
fourth from being added without one.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the new marker and path tests independent of the host running them

Same defect as the pin-reading test: a fixture marker of
sys_platform != 'darwin' or python_version < '3.14' picks the other row on a
macOS 3.14 box, and literal POSIX expectations for Resolve-WoaOverrideLine are
wrong on Windows, where GetFullPath returns a drive-qualified backslash path.
The marker fixtures now use one marker that holds everywhere and one that holds
nowhere, and the rewriter expectations are derived with os.path.abspath.

* Windows on ARM: trim the commentary

Comment-only pass over the branch: collapse wrapped explanations, drop
restatements of the line below them, and shorten test docstrings that
carried more than their first sentence. Every fact a comment was carrying
is kept.

The comment-only property is checked two ways: comment_tools.py for the
Python files, and a PowerShell tokeniser comparison for install.ps1 and
setup.ps1 (32293 and 29539 non-comment tokens, identical before and after).

One test changes: test_no_staging_copy_is_left_unguarded anchored its
search on a comment, so a comment pass could break it. It now anchors on
the code the region starts with, and still fails when a guard is removed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: six review fixes

- _version_satisfies now answers with packaging when it is importable, with
  prereleases off unless the specifier asks for one, which is the policy uv
  resolves under. A wheelhouse tiktoken-0.13.0rc1 reduced to (0, 13, 0) and
  read as satisfying ==0.13.0, so tiktoken was unskipped and the resolve fell
  to the ARM64 sdist. The numeric fallback now refuses a version it cannot
  model rather than guessing.

- Every pyarrow candidate is floored at the ARM64 constraint (>=21.0.0). A
  tag-compatible 19.x in the wheelhouse selected the native path, and staging
  then wrote an exact pyarrow== override that the constraint rejects, after
  the ARM64 venv had been built. A test keeps the two numbers in step.

- --prerelease=allow is decided by the version of the wheel the probe
  selected, not by whether the index URL says nightly. A mirror of NVIDIA's
  prerelease channel need not say so, and without the flag uv takes the
  stable win_arm64 CPU torch from the PyPI extra index instead of the CUDA
  build. install.ps1 hands the answer to setup.ps1, which cannot probe.

- The ARM64 CUDA-preservation shortcut in install_python_stack.py exempts
  every explicit pin, not only a /cpu one. A user asking for cu129 by URL or
  family was silently left on the cu134 build. setup.ps1 already did this.

- Both requirement scanners follow a nested -r. uv reads those includes, so a
  conflicting package one level down made two override files look disjoint;
  handing uv both is an error, and the caller's capped torch could win. The
  fold flattens the include, rebasing each line onto the file it was read
  from.

- install.ps1's torchaudio and prerelease flags apply only while the index
  they were measured on is still in force. Change UNSLOTH_TORCH_INDEX_URL and
  re-run an update in the same shell and the pin outranks the handover for
  the install, while a stale 1 asked the new index for a torchaudio wheel it
  does not publish and aborted the whole torch update.

Each fix is mutation-tested. Suite: 5902 passed, four pre-existing failures
unrelated to this branch (managed node runtime, tokenizers negative control).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: a version floor has to order prereleases

Test-WoaVersionAtLeast compared the numeric release and then .postN, so
21.0.0rc1 cleared a 21.0.0 floor. Staging writes an exact pyarrow==
override from whichever candidate it picks, and pyarrow>=21.0.0 rejects
that rc, which is the failure the floor was added to prevent.

A marker attached to the release now sorts below it. A larger release is
unaffected, so a wheelhouse nightly still clears an older floor: the
pyarrow 24.0.0.dev260 an end-to-end GB10 run staged is named in a test,
since a floor that rejected it would break a verified install.

The eleven-row ordering table is cross-checked against packaging in the
same class rather than asserted from memory, because the PowerShell
cannot import it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* GPU driver: an all-[N/A] compute-apps listing is "cannot attribute", not "nothing appeared"

Ran the Kaggle GPU payload against a fresh native install on the GB10 laptop.
Every workflow passed except cli_run, which said `unsloth run` served from the
CPU because no process appeared on the card holding more than 0 MiB. It had:
with the exact flags the CLI passes (-ngl -1 --threads 2) the same llama-server
generates at 7.7 to 8.3 ms per token, inside the --gpu-layers 99 band (6.5 to
7.1) and far from a two-thread CPU control (18.4). llama-server --list-devices
shows CUDA0 with 46 GB.

The verdict was blind, not wrong about the rule. On Windows (WDDM) and on
unified-memory parts nvidia-smi lists every CUDA process with used_gpu_memory
[N/A], and parse_compute_apps drops those rows, so a server that did appear
during the window looked like nothing appeared and the device-delta fallback
never engaged, because that branch only runs when nvidia-smi cannot enumerate
at all.

Listing alone is not evidence either: a -ngl 0 server is listed too, since a
CUDA build creates a context regardless. Measured here: the device counter
reads 132 MiB idle, 272 MiB with that bare context, 560 MiB with the 270M model
offloaded, so the existing 200 MiB fallback threshold separates the two.

count_listed_pids tells listed-but-unattributed apart from empty, and
nvidia_compute_apps returns None for it, which is what the verdict already
treats as "judge on the device delta". Readable listings are unchanged, and
parse_compute_apps keeps dropping [N/A] rows, so its tests stand. Tests added
for the count, for the None, and for the verdict on the numbers measured on
this box.

test_a_box_without_nvidia_smi_reports_no_gpu_rather_than_crashing fails on this
host on unmodified main too, since a real nvidia-smi leaks through its
simulation; not touched here.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: sqlite-vec cannot resolve in the studio extra

pyproject pinned sqlite-vec==0.1.9 unconditionally, so a native win_arm64
interpreter running pip install "unsloth[studio]" hit a row it cannot
satisfy: sqlite-vec publishes win_amd64 wheels only and no sdist at all,
so there is nothing to build and no version to floor to. The runtime skip
list already covers the name, but that filtering never runs for a direct
extra install, which resolves pyproject's rows.

Dropped on win_arm64 with the same negative marker cryptography and
pymupdf use, mirrored into studio.txt so the two stay in step. Nothing
regresses: rag_db.py guards the import and the router mounts either way,
reporting RAG unavailable.

Guarded generally for the studio extra: no skip-list package may be left
live on win_arm64, and a marker that drops one must drop it nowhere else.
Scoped to studio on purpose, since the other extras are x64 recipes
pinning win_amd64 wheel URLs and were never a claim about this platform.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: three review fixes

- The resolver purge split every variable on [,\s] and rejoined each with
  its own separator. uv reads UV_FIND_LINKS comma-separated, so a caller
  path like C:\private wheels is ONE directory there: the shared split
  tore it into two fragments, dropped the managed entry, and rejoined the
  pieces with commas, quietly losing an air-gapped mirror. Each variable
  is now split the way the tool that reads it splits it, in install.ps1
  and in _find_links_wheel_versions, where the same class of split made
  wheels in a space-containing directory invisible to the skip list.

- The native ARM64 index arguments were gated on $UseUv, so when uv could
  not be obtained at all the pip fallback got only --index-url pointing at
  a channel that publishes just the torch trio, leaving their shared
  dependencies unresolvable. Remove-UvOnlyResolverFlags already exists to
  make that list safe for pip, dropping --index-strategy and rewriting
  --prerelease=allow as --pre, so the arguments are built either way.

- The manifest was the only durable record of the WoA index, and the
  dependency pass deletes it before rebuilding it. A run that died in that
  window left the next update with no handover and no manifest, so it fell
  to the driver-derived cu130 and failed on an index with no win_arm64
  wheel, on every retry. A marker beside overrides.txt, written before the
  manifest is dropped, now carries it. It persists exactly what the
  manifest persists, checked on write as well as on read, so a mirror
  pinned with a token is not written to disk and a hand-edited file cannot
  redirect a torch install.

Seven mutations, all killed. The first attempt at the marker tests passed
with the write-side guard removed, since the read guard alone defends a
round-trip; a test that inspects the file on disk covers it now.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: two review fixes

- New-UnslothTorchOverridesFile kept relative references working by writing
  the merge into the caller's override directory, which only helped when
  every override file shared one. Round 19 made two the normal case on the
  native path, since a non-conflicting caller file is now kept where it
  sits, so the merge fell through to %TEMP% and every relative include or
  wheel path resolved against nothing. Each folded line is rebased against
  the file it came from instead, and nested includes are flattened for the
  same reason, so where the merge lands no longer changes its meaning.

- The dependency pass fell back to pip whenever uv failed or was missing.
  On the native stack that does not recover: the overrides lift the
  released torch cap, which no win_arm64 CUDA wheel satisfies, and drop the
  packages with no win_arm64 build, and pip has no override mechanism to
  translate them into. Constraints cannot stand in, they narrow a
  requirement rather than replace one. Both fallback routes now refuse with
  the reason and name the remedy instead of resolving the wrong stack.

Five mutations, all killed. Restored one behaviour the new scanner had
changed: comment lines are carried through a fold again, which
test_unsloth_torch_override.ps1 was right to be checking.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: four review fixes

- An include's inline comment is not part of its path. "-r nested.txt
  # corporate pins" is a valid line, and capturing the comment meant the
  file never opened, so a torch conflict inside it went unseen and the two
  override files were handed to uv as disjoint. Stripped only where
  whitespace precedes the hash, so "-r a#b.txt" keeps its filename.

- An index the marker may not record now CLEARS it rather than leaving the
  last one in place. A host that moved from NVIDIA's channel to a
  credentialed corporate mirror kept a marker naming the old public
  channel, and since the manifest does not record the mirror either, the
  next fresh-shell update read the marker and put torch back on a source
  the user had left. An unrecordable index inherits nothing.

- The local wheelhouse mirror counted a wheel on its filename, which is all
  _find_links_wheel_versions reads, so a truncated archive took its package
  off the ARM64 skip list and uv failed the whole dependency pass on it.
  It is opened before it is copied now, the same check the remote branch
  already applied.

- The GPU harness read a mixed nvidia-smi listing as proof of CPU: a
  readable row on one card keeps the mapping nonempty, so the all-[N/A]
  guard stays quiet and the newly launched server, reported as [N/A] on a
  WDDM or unified-memory card, is simply missing from it. A pid that
  appeared but carries no figure now defers to the device-wide delta, as
  the all-[N/A] case does. Both samples come from one nvidia-smi call, so
  the attributed mapping and the listed pids describe the same moment.

Seven mutations, all killed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: two review fixes

- The local wheelhouse pyarrow was selected and staged on its filename.
  Last round's archive check covered the OPTIONAL mirror, where a bad wheel
  costs one feature; this is the mandatory one, and staging writes an exact
  pyarrow== override from whichever file it picks, so a truncated wheel
  chose the native route and then failed the resolve with x64 already given
  up. The probe and the staging now apply the same filter, tags and
  readability, so they cannot pick different files.

- The device-wide VRAM total is only a measurement of this process when
  this process owns the card. Under --studio-concurrent a training leg
  shares it and both allocates and frees inside the window, which is how
  one run read the delta as -182.0 MiB while the server held 2.6 GB, so
  accepting a rise there would pass a CPU-served run on somebody else's
  memory. Both fallbacks to the total now refuse when anything was on the
  card before the launch. Decided on evidence rather than configuration:
  the payload is not told which half it is, but a pid already present says
  so, and an empty card keeps the fallback the [N/A] parts need.

Six mutations, all killed. Two tests from last round asserted the mixed
listing case with a co-tenant present, which is exactly the scenario that
must not pass; they now use an owned card, and the shared variants are
their own cases.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: quote a rebased override path that needs it

Resolve-WoaOverrideLine stripped the quotes off an -r/-c/-f target and
wrote the absolute path back bare. These options take one file argument,
so a space truncated it and the dependency pass failed opening a path cut
at its first space.

Two ways in, and only one is about quoting. A caller who quoted the value
had the quotes discarded. A caller who had no reason to quote a plain
relative name gets a space anyway when it rebases onto a directory that
has one, which no amount of preserving would have caught. The rebased
value is quoted whenever it contains whitespace, so both are covered, and
setup.ps1's parity copy is kept identical by a test.

The first version of this had the option token disappear: -match on the
rebased value replaces , so reading the groups afterwards returned
a bare path where -c <file> had been. The file already carries a comment
warning about exactly that. The groups are copied out first now, and a
test asserts the option survives.

Four mutations, all killed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: four review fixes

- The marker recorded the WoA chain, which never consults the generic
  UNSLOTH_TORCH_INDEX_URL pin the torch steps actually prefer. A run pinned
  elsewhere installed from the pin and then recorded an NVIDIA channel it
  had not used, and the next fresh shell went back to it. The pin decides
  what is recorded now, and an unrecordable one clears the marker.

- _find_links_wheel_versions read PIP_FIND_LINKS as well as UV_FIND_LINKS.
  uv does not consume it, so a wheel hosted only there was counted as
  available, dropped off the skip list, and then invisible to the uv
  resolve that followed. pip cannot be the resolver on this path either,
  since pip_install refuses the fallback once the overrides are in force.

- A transitive blocker had no direct row to satisfy, so any hosted version
  counted. tensorboard 2.21.0 requires grpcio>=1.74.0 and librosa 0.11.0
  requires numba>=0.51.0: a wheelhouse holding an older one lifted the
  skip, and the optional package's own metadata then failed the whole
  extras pass. The floors are recorded with the release they were read
  from, and a test fails if extras.txt moves off that pin. llvmlite has no
  entry deliberately: nothing states a floor for it.

- The PyPI pyarrow route cleared native mode on a wheel it then forgot, so
  no exact override was written. With only pyarrow>=21.0.0 in force, uv
  takes the newest release, and one that ships an sdist for this
  interpreter builds Arrow from source. The matched wheel is pinned now,
  and the probe clears the name on entry so a re-probe cannot inherit it.

Six mutations, all killed. The first attempt at the second fix keyed on
USE_UV, which is False until the bootstrap runs, so it read the wrong
variable everywhere else; the list is unconditional now, which is correct
because uv is the only resolver that reaches it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: open the downloaded pyarrow wheel before keeping native mode

A mirror can serve a truncated body with a 200, and Invoke-WebRequest
reports that as success. This is the mandatory wheel: the exact pyarrow==
override is written from it, so an unreadable download kept the native
route and then failed uv on that override, after x64 had been given up.
The optional remote wheels below already opened what they kept; this one
was the last route that did not.

It throws into the existing catch, so the failure falls back to x64 rather
than continuing, and the bad file is removed first -- left in the managed
directory it would be read by the resolver on every later run, and by
_find_links_wheel_versions as proof the package is available.

Three mutations, all killed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Record the effective torch index in the manifest, not just the marker

install_python_stack.py writes UNSLOTH_WOA_SELECTED_TORCH_INDEX into the
manifest as woa_torch_index, and the read chain in setup.ps1 prefers the
manifest over the marker. Exporting the WoA chain while saving the pin left
the manifest shadowing a marker that had already been corrected, so the next
fresh shell went back to the index the run had not installed from.

Resolve the value once from Get-PinnedTorchIndexUrl, falling back to the WoA
chain, and write both records from it. The torchaudio and prerelease flags
install.ps1 leaves in the shell describe the index it probed, so drop them
when the resolved index differs from that one.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop the generated requirement snapshots that reached the branch

_filter_requirements writes its skip-filtered copy beside the source so relative
-r/-c includes still resolve, and pip_install unlinks them in a finally. A test
that calls the helper directly, or an install killed mid-run, leaves them in the
checkout, where a git add -A picks them up. Nine were committed that way.

They are a snapshot of a file already in the tree, so a stale one reads as a
second, silently divergent copy of the pins. Remove them, ignore the pattern, and
add two tests: one that no tracked file matches the generated shape, one that the
ignore rule covers it. The shape is matched exactly rather than by a leading dot,
so a real hidden requirements file such as .constraints.txt stays in scope.

* Persist an explicit pin, install hosted optionals, scope the GPU samples

Three separate holes.

setup.ps1 ran the index persistence block only when the WoA chain had something
in it. A venv installed through a credentialed mirror has nothing to recover, so
a later run pinned at an NVIDIA channel installed from the pin and recorded
neither record, and the next fresh shell fell back to the driver-derived index
that publishes no win_arm64 CUDA wheel. Either record now opens the block.

install.ps1 reported that a hosted optional wheel was keeping its package, but
only declined to emit the removal override. hf_transfer and xformers are
excluded on win_arm64 by marker, so no requirement survived for the wheel to
satisfy, and torchcodec's only requirement line was filtered out here
unconditionally. Make that filter conditional on a hosted wheel, and install the
distributions the metadata cannot ask for, --no-deps and best effort.

The Kaggle GPU harness sampled every physical card while the payload was pinned
to one, so an unrelated process on a hidden card could supply the memory that
carried a CPU-served run past the assertion. Both samplers now pass
CUDA_VISIBLE_DEVICES to nvidia-smi -i.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pin the probed CUDA trio, and validate the wheelhouse xformers

uv documents --index-strategy unsafe-best-match as selecting the best version
from the combined candidate set of every index, and the PyPI extra index is
there because NVIDIA's channel publishes only torch, torchvision and torchaudio.
So torch>=2.4 takes PyPI's stable win_arm64 CPU wheel the moment it is one
release ahead of the selected channel, and the native GPU path is replaced by a
CPU build that imports perfectly. Probe torchvision as well as torch and
torchaudio, keep all three versions, and install them pinned exactly, local +cu
tag included. An unreadable version keeps the floor it had before.

The optional wheelhouse installs added last commit took the bare name, so any
tag-compatible wheel landed: below the declared xformers floor, or built for
another torch and CUDA pair, whose extension then fails to load while the step
reports success. Select the newest hosted version that clears the floor,
install that version, and for xformers compare cpp_lib.json against the resident
torch, removing it on a mismatch the way the post-repair resync already does.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pin the repair path too, pair the companion wheels, fix two availability rules

The forced-repair path in setup.ps1 installs the trio with the same
unsafe-best-match and public PyPI extra index as the install path, so open-ended
specs there had the defect the install path was just fixed for. Re-probe the
effective index at repair time and pin what it publishes; a repair can happen in
a fresh shell long after the install, so a remembered version would be stale.
The probe is a parity copy of install.ps1's, compared body for body by a test.

Pinning both torch and torchvision exactly made the pair mandatory, and NVIDIA's
nightly channel publishes each project on its own schedule while nightly
torchvision metadata pins its exact torch. Select the companions as builds of the
torch that was chosen, matching on the dev stamp and the local tag, and fall back
to the floor when the index carries no companion from that build.

Test-WoaVersionAtLeast read .dev as a pre-release of the release, so
0.0.22.post7.dev0 came out at or above 0.0.22.post7. PEP 440 hangs .devN off
whatever precedes it. Track it separately and as an ordinal.

The win_arm64 skip list was decided from the local wheelhouse alone, so a native
CPython 3.14 host dropped librosa even though llvmlite and numba publish cp314
win_arm64 wheels. Record what the public index resolves for this interpreter.

Also removes a file named - from the root: a workspace simulation ran
uv pip compile -o -, which writes a file rather than to stdout.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten the Windows on ARM comments

Post-convergence comment pass over the three files that carry the added prose:
collapse the multi-line rationales to the fact each one is there to state, and
delete the ones that restate the line below them. 782 added comment lines to
569. Comments, docstrings and whitespace only, verified with comment_tools.

* Stop the override file from undoing the CUDA pins, and two probe fixes

uv's --overrides replace a requirement's version even when it is named directly
on the command line: verified on 0.10.7, an override of packaging>=20 beat a CLI
packaging==24.0. The generated file carries torch>=2.4 and torchvision>=0.19, so
it discarded the exact CUDA pins the probe selected and best-match then took
PyPI's newer CPU wheel. Build the value for that one command without the trio,
passing every other file through, and restore it in a finally so the later
unsloth resolve still gets the drop list.

nvidia-smi failing to answer is not evidence the GPU is gone. A transient
failure during a direct update dropped windows-arm64-cuda from the expected
kinds, deleted the working llama.cpp tree and installed the CPU bundle. Accept a
win_arm64 venv on a persisted WoA CUDA index as evidence too; only NVIDIA's own
channels are ever persisted, so a cpu pin cannot claim it.

The public-index wheel table is only availability if the resolve looks at PyPI.
Offline, or pointed at an exclusive corporate index, unblocking librosa dropped
the skip and then failed the extras pass on an unavailable numba chain.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Take a wheel from PyPI when PyPI builds it, not from the wheelhouse

The Windows on ARM wheelhouse exists for distributions PyPI has no win_arm64 build
of. regex is not one of them: PyPI has published win_arm64 regex since 2025.7.29, and
the wheelhouse copy is the same version. The staging directory is first in
UV_FIND_LINKS, so ours won the tie and the venv got our binary of a wheel the project
releases itself. Nothing needed it there either: regex is in no skip list, no drop
list and no constraint, so it unblocked nothing.

Staging now asks PyPI first and skips the copy when PyPI publishes that project at or
above the version being staged. Version-aware in both directions, because the reverse
is a downgrade: an upstream release BEHIND the wheelhouse leaves ours in place.

The probe is PyPI-only on purpose. Test-WoaWheelAvailable falls back to the wheelhouse,
and a wheel is by definition present in the wheelhouse it is being staged from, so
reusing it would answer $true every time and the guard would never fire. Its PyPI leg
became Test-WoaPyPIWheel and it now calls that, so there is still one place that knows
how to read the simple index.

Tag matching is relaxed for this caller alone. hf_transfer ships cp38-abi3 and
sqlite_vec ships py3-none, both of which import on cp313; the exact-tag test would call
them foreign and go on shipping ours the day upstream publishes one. Free-threaded
venvs keep the exact test, which is what it was written for. A wheel this venv could
not have used is never judged: a cp312 wheel is not made redundant by a cp313 wheel.

pyarrow keeps its own path. Get-WoaPyarrowSource already probes PyPI before the
wheelhouse and reports which one it used, which is why the generic loop skips it.

Two comments were wrong and are corrected. pyarrow has never published a win_arm64
wheel at any version, so the constraints floor is the floor of the wheels we stage
rather than of anything upstream ships, and Get-WoaPyarrowSource checks an explicitly
supplied UNSLOTH_PYARROW_WHEEL before PyPI, not after.

* Fix uninstall.ps1 Split-Path calls that throw on Windows PowerShell 5.1

Split-Path puts -LiteralPath in its own parameter set, which carries only
-Resolve and -Credential. -Parent belongs to the -Path set, so naming both
resolves no parameter set and the call throws AmbiguousParameterSet. It
throws at runtime rather than at parse time, which is how eight of these
shipped in scripts/uninstall.ps1.

Two of them sat outside a try/catch, so a real uninstall printed a red
error and left the derived path null: the legacy sibling build at
<parent>\stable-diffusion.cpp was never removed, and the empty path then
tripped the deny list into "refusing to remove unsafe path:" with nothing
after the colon. The other six were swallowed by their surrounding
try/catch and failed silently. That left _RootFromConf returning nothing,
so studio.conf-based root discovery never worked, and it disabled the
parent-of-USERPROFILE rule in _IsUnsafeRoot, so C:\Users read as a safe
recursive-delete target.

Drop -Parent rather than switching to -Path. -LiteralPath on its own
already returns the parent, and -Path globs, so an install root containing
[ ] would be read as a wildcard.

Verified on this box under Windows PowerShell 5.1.28000.2704 against a
real env-mode install: the root, its studio.db, auth, outputs and exports
all go, an Unsloth-owned sibling stable-diffusion.cpp now goes with them,
and an unowned checkout at the same path is still kept.

The new test is static because CI has no Windows PowerShell 5.1 to run
the scripts under. It covers install.ps1 and studio/setup.ps1 too, which
are already clean.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Give the uninstaller's remove retries a long enough budget

Uninstalling a Studio that had actually been used left the whole install
tree behind. The stop pass kills the server, but torch inductor's compile
workers keep handles on the .py files they wrote under
<root>\TORCHINDUCTOR_CACHE_DIR for several seconds after that. Those are
plain data handles on files the worker never loaded as a module, so
neither the ExecutablePath pass nor the loaded-module pass in
_StopProcessesLockingRoots has a process to kill, and the only thing left
to do is wait.

_RemovePath waited 700ms three times, so it gave up after about 2.1s and
reported 'could not remove', leaving the tree and studio.db on disk with
a summary telling the user to delete them by hand. Replace the flat sleep
with an escalating backoff that tops out near 20s, which covers the
teardown with room to spare and still bounds the run.

* Say that a Windows on ARM desktop build needs clang

Building studio/src-tauri for aarch64-pc-windows-msvc with only MSVC BuildTools
installed fails with

    ring@0.17.14: failed to find tool "clang": program not found

because ring and aws-lc-sys assemble their aarch64 sources through cc-rs, which
calls clang rather than cl.exe on that target. The error names the crate rather
than the missing toolchain, and the prerequisite was written down nowhere: not in
the workflows, not in CONTRIBUTING.md, not in the manifest.

Recorded in build.rs, which is the file whose job this is and the one a reader
reaches for when the build fails. No behaviour change.

Not a hypothetical: the desktop app was built and launched on a Windows ARM64 box
for this branch, and this was the only thing standing in the way. It builds and
runs natively there once clang is present, PE machine 0xAA64, window up and the
frontend rendering off tauri.localhost.

* Fix the two CI reds: a dead import and a pid-file race

Import hoist lint: run_studio_gpu.py still imported count_listed_pids after the
listing helpers grew up around it. nvidia_compute_apps_listing now returns the
pids themselves via listed_pids, because a MIXED listing needs names rather than
a count, so the count import had no caller left and only survived in a comment.
Dropped both. count_listed_pids itself stays: gpu_assert still exports it, its
tests still cover it, and listed_pids documents itself against it.

Tauri rust tests: stop_update_kills_descendants_after_the_group_leader_exits
panicked with ParseIntError { kind: Empty }. The test child runs
`echo $! > "$1"`, and the redirection creates that file before writing to it, so
waiting on is_file() can win the race and read an empty string. It is a real
flake rather than anything about this branch, and it does not reproduce
reliably: the same test passed on main minutes earlier. It surfaced here only
because the Tauri workflow is path filtered on studio/src-tauri and the clang
note is the first change this branch makes there, so this was its first run.

Wait for a pid that parses instead of for a path that exists, over the same kind
of budget. A child that never writes one now fails with a sentence rather than
an unwrap panic.

* Close three holes in the PyPI-first wheelhouse guard

Review of the guard I added in 7fe37c6 turned up three ways it could do harm,
one of them worse than the duplication it was written to fix.

The bad one: skipping a wheel made it stop counting as available. $WoaWheelNames
is rebuilt by scanning the staging directory, and a $WoaDropCandidates name that
is missing from it gets emitted as `name ; platform_machine == "AMD64"`, which
EXCLUDES the package on ARM64. So the day PyPI published a win_arm64 hf_transfer
or brotli, the guard would have turned "installed from our wheelhouse" into "not
installed at all" -- the opposite of preferring upstream, firing on exactly the
event the guard exists for. A skipped wheel is now recorded in
$script:WoaPyPIProvided with the version PyPI matched, and that is merged into
$WoaWheelNames, so availability reads the same whether the wheel came from the
wheelhouse or from PyPI. The xformers floor still applies to the PyPI version,
so a release below it keeps the drop.

abi3 was treated as universally compatible. It is forward compatible from the
version it was built against, so a cp314-abi3 wheel does not import on cp313,
and calling ours redundant against one would have left the package
uninstallable. Test-WoaWheelTagsUsable now applies the same minimum rule the
staging scan already used for abi3 and py3N, and free-threaded venvs keep the
exact-tag test, which is what it was written for.

PyPI publishing a wheel is only availability if the resolve will look at PyPI.
UV_INDEX_URL and UV_DEFAULT_INDEX REPLACE the default index, and offline flags
remove it, so on a corporate mirror the guard was dropping our wheel and leaving
it obtainable from nowhere. Test-WoaResolveReachesPyPI mirrors
_public_pypi_is_reachable in studio/install_python_stack.py, which reasons about
the same five variables for the same reason. An unreachable pypi.org already
failed safe; reachable but unused did not.

The new tests execute the helpers under pwsh instead of reading install.ps1 as
text, because two of these three were wired correctly and answered wrongly, which
is precisely what a source-shape test cannot see. Hermetic: the live PyPI leg is
exercised on hardware, not in CI.

* Don't recursively delete ~/.unsloth/studio unless it is ours

The custom-root branch refuses a path that carries none of the four ownership
sentinels ("refusing to remove non-Unsloth path"), and then the default root a
few lines down is removed with no check at all. So a bare run, the documented
one-liner with no UNSLOTH_STUDIO_HOME set, recursively deletes whatever happens
to sit at ~/.unsloth/studio, and the empty-dir prune takes ~/.unsloth with it.

That path is not far-fetched to own. On a machine where Studio was only ever
installed in env mode, ~/.unsloth/studio is an ordinary place for someone to
keep notes or a checkout, and nothing warns them that running the uninstaller
for their custom install will also clear it out. Hit it here for real: a run
against a disposable env-mode install removed an unrelated directory that held
only a log file, and reported nothing unusual while doing it.

Gate it on _IsStudioRoot, the same test the custom root gets. An interrupted
install that lost every sentinel is now left on disk with a message instead of
removed, which is the failure direction that does not destroy data.

Both scripts had it; uninstall.sh line 588 was the same bare call.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pair stable companions by release line, honour uv config files, and clean up the override swap

Stable torch, torchvision and torchaudio releases all carry an empty dev stamp, so
the CUDA local tag alone paired a companion from any release the index still
served and the exact-pin install then asked for a pair that does not exist.
Test-WoaWheelPairsWithTorch now applies the release mapping for stable builds:
torchvision 0.(M+15) pairs with torch 2.M and torchaudio agrees on major.minor.
Nightlies still pair by dev stamp. The setup.ps1 parity copy is identical.

Only environment variables were consulted before assuming a resolve reaches
public PyPI. uv also discovers uv.toml and pyproject [tool.uv] from the current
directory and its parents, then the user and system files, so a no-index or an
exclusive default-index set there still answered "PyPI". Both the PowerShell
probe (Test-WoaResolveReachesPyPI, now also gating the pyarrow source probe)
and the Python one (_public_pypi_is_reachable) read that configuration, honour
UV_NO_CONFIG and UV_CONFIG_FILE, let an index in the environment decide alone,
and resolve doubt to "not PyPI".

The filtered torch-step override was written with GetTempFileName(), which
follows %TEMP% and so could produce a quoted, uv-rejected path, and the copies
were never deleted. New-WoaTorchStepOverrideValue now writes under the already
validated WoA directory, passes every path through Get-UvSafePath, and reports
the copies so the caller removes them in finally. setup.ps1 never performed
this swap around its own CUDA trio install, so the restored overrides.txt
floors undid its exact pins too; it now swaps the same way.

On the manifest fast path the dependency block is skipped, so the llama.cpp
NVIDIA-evidence check read an unset index and a transient nvidia-smi failure
deleted a working CUDA bundle. The check now reads the pin, manifest or marker
itself when the pass did not run.

sqlite-vec joins the explicit ARM64 optional install map: its requirement is
marker-excluded on win_arm64 everywhere the torch path applies, so a staged
wheel was ignored. The Kaggle harness stops the Studio server before the CLI
baseline regardless of skip_ui, so its own llama-server is not counted as a
co-tenant.

Tests cover each change and the free-threaded probe test inlines the helpers
Test-WoaWheelAvailable now routes through.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Decide "public PyPI" by host, not by substring

An exclusive index whose URL merely contains the name, such as
https://pypi.org.corp.example/simple or .../api/pypi/pypi.org/simple, passed
the substring test in both the PowerShell and the Python reachability probes,
so the pyarrow route preferred a wheel the resolve could never fetch. Both now
parse the URL and require the host to be exactly pypi.org. TestPyPI, which does
not carry these packages, is therefore not public PyPI either.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Rank uv's [pip] index settings above the top-level ones, and drop a redundant wheel's managed copy

uv pip gives [pip].no-index and [pip].index-url precedence over their top-level
twins, and an [[index]] entry with default = true beats [pip].index-url
(verified on uv 0.10.7 with dry-run resolves). Both readers assigned whichever
value came first in the file, so a uv.toml that set the top-level key ahead of
the [pip] one could report public PyPI as reachable while the real resolve went
to an exclusive index. Each value is now collected and ranked at the end, in
both install.ps1 and install_python_stack.py.

A wheelhouse wheel that PyPI now publishes was skipped at staging time but a
copy already in the managed directory stayed there, and that directory is
first in UV_FIND_LINKS, so it still won the tie: the offline-cache mode points
the wheelhouse at that very directory, and a URL wheelhouse leaves the copy an
earlier install staged. The managed copy is removed in both modes; an external
wheelhouse file is never touched.

test_woa_wheelhouse_guard_behaviour inlines every helper the reachability probe
now calls, so a missing one no longer answers True from inside a non-terminating
error.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Run the two uninstaller tests that no workflow ran

cross-platform-parity-ci already triggered on uninstall*.ps1 and
scripts/uninstall*.ps1, so editing the uninstaller queued a run that then
asserted nothing about it. test_uninstall_prebuilt_parity.ps1 and
test_uninstall_reparse_stop_roots.ps1 were referenced by no workflow at all.
Both are hermetic -- they parse scripts/uninstall.ps1 with the AST and execute
only the helper under test -- so they cost a few seconds on a row that is
already running.

Worth being precise about what this does not buy, because the gap is wider than
the wiring. Neither test executes the uninstaller BODY, and neither does any
other: every uninstaller test we have asserts source shape. A runtime
parameter-binding fault is therefore invisible to all of them, which is exactly
how `Split-Path -LiteralPath $p -Parent` reached users. It binds to no parameter
set and throws on every uninstall, on 5.1 and on 7 alike, but it parses, so the
board stayed green.

The reparse test also could not run unprivileged. It created its home with
New-Item -ItemType SymbolicLink, which needs elevation or Developer Mode, so it
died on NewItemSymbolicLinkElevationRequired before the first subtree check. It
now runs over both reparse kinds. A junction needs no privilege and is the shape
a user is likelier to have -- mklink /J is what gets reached for when moving a
studio home to another volume -- and _ManagedPathsUnderReparseTargets reads
nothing but .Target, which both kinds supply rooted. A kind that cannot be
created is reported as SKIP rather than passed over, and a final check fails the
run if neither kind was exercised, so an environment that can make no links
cannot report a clean pass having asserted nothing.

* Read a uv config the way TOML is actually written

Test-WoaResolveReachesPyPI decides whether PyPI is in the resolve at all, and a
wrong "yes" is the expensive answer: it lets the redundancy guard delete the
wheelhouse copy of a wheel nothing can then fetch, and makes Get-WoaPyarrowSource
answer "pypi" and skip a usable local pyarrow, which gates the whole native path.
The subset parser behind it answered "yes" for six shapes of config that had
replaced or disabled PyPI. All six failed in that direction.

Held against tomllib over 41 fixtures, the parser and a correct reading now agree
everywhere; before this they disagreed on exactly these six:

  no-index = true# offline lab                 a comment needs no space in front
  index-url = "https://corp/simple"#corp       same, on the value side
  "index-url" = "https://corp/simple"          a quoted key is the same key
  pip.no-index = true                          a dotted key is [pip]'s no-index
  pip.index-url = "https://corp/simple"        same
  [tool.uv] + pip.no-index = true              same, in pyproject.toml

The comment cut was `(^|\s)#`, which misses a `#` with nothing before it. That
cannot be widened to a bare `#`, though, because the same line may carry one
inside a string: `index-url = "https://host/simple#frag"` is a fragment, not a
comment, and the old pattern got that right only by accident. Both halves need a
scan that knows where the quotes are, so Remove-WoaTomlComment does that, and
Split-WoaTomlKey does the matching job for keys -- unquoting them and splitting
on dots, so `pip.no-index` is routed into the [pip] scope instead of being
compared, whole, against the literal `no-index` and never matching.

The two new helpers are added to the behaviour test's HELPERS tuple in the same
commit. Leaving them out would have been the bug that tuple exists to prevent: a
helper the prelude does not extract is a command-not-found inside
Test-WoaResolveReachesPyPI, which aborts the statement, skips the branch, and
answers True.

Coverage was the reason these survived. The PowerShell side had no config-file
fixtures at all -- test_pypi_counts_only_when_the_resolve_would_reach_it drives
environment variables only -- while the Python twin has had file fixtures and a
real tomllib all along. Sixteen cases now cover both directions of the scan, the
[pip] precedence both ways, a lookalike host, and the shapes that must keep
answering True so this cannot be mistaken for a blanket "not reachable".

* Put the uv user config where uv would look for it on Windows

The user-file row of test_a_configured_exclusive_source_is_not_pypi failed on
Windows and always would have. The fixture deletes APPDATA and then points only
XDG_CONFIG_HOME at a directory, but _uv_config_files reads %APPDATA%\uv\uv.toml
on Windows and $XDG_CONFIG_HOME/uv/uv.toml elsewhere, so on Windows the file was
written somewhere uv never looks, nothing was discovered, and the assertion that
a user-level no-index takes PyPI out of the resolve could not hold. It stayed
green because tests/python runs on the Linux row, where XDG is the right answer.

Both names now point at one directory, so the case is real on every platform.

test_project_outranks_user_for_a_scalar needed the same treatment for a sharper
reason: it wrote the user file to the old xdg path, which after this change is
not a config location anywhere, so it would have passed on the project file
alone while asserting nothing about precedence. It now writes to the same place
and first asserts the file is one _uv_config_files discovers, because a
precedence test whose lower-ranked file was never found is a test that cannot
fail.

* Don't strand an old install behind the new ownership gate

bcc0ffd stopped the uninstaller recursively deleting ~/.unsloth/studio unless
one of install.ps1's sentinels says the directory is ours. That was right, and
the list it used was too short.

On Windows share\studio.conf is never written -- only install.sh writes it --
so the three sentinels that actually decide a Windows root all postdate the
bin\ shim dir and the unsloth_studio rename. An install from before those has
neither: the venv lived at <root>\.venv, the venv's Scripts dir went on PATH in
place of a shim dir, and .unsloth-studio-owned did not exist yet. install.ps1
still migrates exactly that layout, at "found legacy Unsloth environment", so
it is not hypothetical. Uninstalling one refused, printed the user's own
install as a non-Unsloth path, and left the tree and studio.db on disk.

tests/sh/test_uninstall_arg_guard.sh has been asserting this all along -- its
fixture is a pre-marker unsloth_studio venv and "no arguments removes the
install" fails at eda3801.

So accept two more things Unsloth put there: the owner marker inside the legacy
.venv, and either venv dir carrying the unsloth console script pip generates for
the CLI. That is the property the gate is testing. A bare .venv, a venv merely
named unsloth_studio and a hand-made studio directory still have none of them
and are still refused.

(cherry picked from commit 05c6cdf9a23273bb7a5e9fe4900b59dbe6cda3a1)

* Recognise the abi3 pyarrow upstream is going to publish

pyarrow is the mandatory gate for the whole native path: staging turns the chosen
wheel into an exact `pyarrow==` override, so whichever file we select is what the
resolve gets. Today that is our own 24.0.0.dev260, a pre-release, while upstream
stable is 25.0.1 -- acceptable only because PyPI publishes no win_arm64 pyarrow
at all yet.

It will. apache/arrow#48539 adds the wheel and is held behind apache/arrow#50398,
whose plan is an abi3 floor of 3.11, so the first one on PyPI will be named
pyarrow-<v>-cp311-abi3-win_arm64.whl. Every pyarrow candidate went through
Test-WoaPyarrowWheelUsable, which gated on Test-WoaWheelTags -- exact PEP 425
fields -- and an exact test answers false for cp311-abi3 on every interpreter we
build. The day upstream shipped, we would have gone on selecting our own dev
build and pinning it, and nothing anywhere would have said so. Silent, permanent,
and pointing the wrong way for a PR whose whole aim is that a new user on this
hardware installs from upstream wheels.

Test-WoaWheelTagsUsable is the predicate that already answers this correctly, and
it keeps both properties exactness was buying: an abi3 wheel built against a
NEWER interpreter than the venv is still refused, because abi3 reaches forward
from its build version and not backward, and abi3 is refused outright on
free-threaded builds, which have no stable ABI (CPython #111506).

One function changed, because all six call sites already funnel through it -- the
supplied wheel, the PyPI scan, the wheelhouse directory and index, and the two in
staging. test_every_pyarrow_candidate_goes_through_the_floor counts them and
still reads 7.

Four cases cover the shape upstream will ship and the two directions it must not
loosen, and a fifth asserts the free-threaded refusal on its own. The preludes
that lift Test-WoaPyarrowWheelUsable out of install.ps1 gained
Test-WoaWheelTagsUsable in the same commit, for the reason that keeps recurring
in this file: a helper the prelude does not lift is a command-not-found, which
aborts the statement rather than answering, and the caller then reports success.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Windows on ARM: make the purge and merge tests speak the host's path separator

These tests are gated on pwsh being present, not on the platform, so on a Windows
box with PowerShell installed they run and 11 of them fail. None of it is the
product: the purge builds its owned prefix with Join-Path, which uses the host
separator, and the tests hand it a hardcoded POSIX home, so "C:\...\unsloth\woa"
was compared against ".../unsloth/woa" and never matched. In a real run both
sides come from the same Join-Path, so the purge is correct either way.

Build the fake home and the entries the way the host does. The same cases now
cover the separator the machine actually uses, and on POSIX the values are
byte-identical to before.

The two merge tests failed for a second reason: their fakepython stub is a
shebang script, which Windows cannot execute, so the merge got no pins back and
the failure was an empty path rather than anything about rebasing. Write a .cmd
stub there instead; PowerShell's & runs it and the stub ignores -c regardless.

473 passed, 0 skipped on win-arm64 with PowerShell 5.1.

(cherry picked from commit 8dc13a7692c325408a9cb56eb94b612e71f45a2f)

* Test the Windows on ARM install path on Windows on ARM

Everything this repo knows about win_arm64 has been tested only on x64 runners:
ubuntu-latest and windows-latest. That is the wrong machine for all of it, and it
is why several real bugs sat behind a green board.

Three of them could not have failed anywhere else. The purge builds its owned
prefix with Join-Path, which uses the HOST separator, so a test hardcoding a POSIX
home compared "C:\...\woa" against ".../woa" and never matched -- eleven tests
passed on Linux and failed on Windows for a reason that had nothing to do with the
purge. The uv config parser, which exists because Windows PowerShell 5.1 ships no
TOML reader, read six shapes of config wrongly, every one of them answering "PyPI
is reachable" for a config that had taken PyPI out of the resolve. And the
uninstaller's ownership gate refused genuinely old installs, leaving the user's
tree and studio.db on disk while calling their own install a non-Unsloth path.

windows-11-arm has been free for public repositories since 2025-08-07, and this
leg is cheap because none of this needs a GPU, CUDA or a compiler: it is
arithmetic over wheel tags, path handling and PowerShell parsing. A full install
or a Tauri build needs much more and is deliberately not attempted here.

Two rows, because the shells are not interchangeable and each is what something
real uses. pwsh 7 is what CI has always run. Windows PowerShell 5.1 is what the
CLI launches setup.ps1 with, and it is the only shell on a stock Windows box, so a
developer on this hardware runs 5.1 whether or not anyone planned for it.
cross-platform-parity-ci already keeps a 5.1 row for #8335 on the same reasoning.
The suites locate their shell with shutil.which("pwsh"), which 5.1 does not
provide, so that row puts a copy of powershell.exe on PATH under that name --
without it the row would SKIP everything and report green having run nothing. A
copy rather than a .cmd shim, because CreateProcess will not launch a .cmd without
a shell, and Python is what starts them.

Both architectures are asserted rather than assumed. A leg that silently ran on
x64 would be worse than no leg, since it would report green for precisely the
platform it does not cover, and setup-python needs an explicit architecture: arm64
because the image ships an x64 Python that runs under emulation and reports
win-amd64 from sysconfig -- half of these assertions are about wheel tags, so an
emulated interpreter would quietly test the wrong platform. A third check pins
that the shell resolved for the row is really the major version that row is for.

Dry run of this exact selection on win_arm64 under PowerShell 5.1: 671 passed, 0
skipped.

One note recorded in the workflow for later: GitHub moves the windows-11-arm image
to Visual Studio 2026 between 2026-09-21 and 2026-09-30. This job invokes no
compiler so it cannot be affected, but a leg that builds studio/src-tauri would
be, and would also need clang on PATH for ring and aws-lc-sys.

* Only ask for a junction where junctions exist

My own regression, caught by the parity job the same commit wired these tests
into: parity (ubuntu-latest) went red on

  FAIL  Junction : a linked root yields the sd.cpp tree under its physical target
  FAIL  Junction : ... and the venv under it

A junction is a Windows-only reparse type, and New-Item -ItemType Junction does
not throw on Linux pwsh. It quietly produces a plain directory with no .Target,
so the SKIP guard -- which only catches a failure -- never fired, and the row ran
and failed every positive assertion. The negative ones passed for the wrong
reason: _ManagedPathsUnderReparseTargets returns nothing for a path with no
target, which is also what "the bare physical target is NOT in scope" wants.

Choose the kinds by platform instead of by catching an exception that never
comes, and treat $IsWindows being undefined as Windows, since that is Windows
PowerShell 5.1 and 5.1 runs nowhere else.

Also verify the link after creating it. Created is not the same as usable: the
helper reads nothing but .Target, so a link without one cannot exercise
anything, and reporting that as a failure blames the helper for the filesystem's
answer. A kind that produces no target is now a SKIP, and the existing check that
at least one kind ran still fails a run that exercised nothing.

Windows unprivileged: Junction all pass, SymbolicLink SKIP (needs elevation),
overall pass. Linux keeps the SymbolicLink row, which passed on that job already.

* Assert the uninstaller's ownership gate on POSIX too

The gate that decides whether ~/.unsloth/studio is recursively deleted exists in
both uninstallers, and when it regressed it regressed in both. Verified on real
Linux (aarch64, bash 5.2.21): against scripts/uninstall.sh before the fix, three
layouts Unsloth itself created were refused --

  FAIL: legacy .venv carrying the owner marker (got foreign, want own)
  FAIL: pre-marker unsloth_studio venv        (got foreign, want own)
  FAIL: pre-marker legacy .venv               (got foreign, want own)

-- and against the current one all thirteen checks pass. So this was never a
Windows problem: a Linux or macOS user with an install predating the
unsloth_studio rename was told their own install was a non-Unsloth path and left
with the tree and studio.db on disk.

There was no POSIX test for it. tests/studio/test_uninstall_legacy_layout_gate.ps1
covers the Windows side; this is its twin, following the extract-via-sed pattern of
test_uninstall_shared_icon.sh so the uninstaller body, which kills processes and
deletes trees, is never executed.

Both directions are asserted, because a gate is only as good as what it refuses.
The negative cases include a venv that merely carries an unrelated console script,
so accepting bin/unsloth cannot quietly become "any venv with anything in it", and
a bare .venv, which is the case the gate was added for and which stays refused
because it is genuinely indistinguishable from a user's own project venv.

Wired into the parity workflow's Linux row. Worth noting how it went unnoticed:
the shell uninstaller tests are named individually in workflows and four of them
are named in none, while consolidated-tests-ci excludes tests/sh outright. A test
nobody runs is indistinguishable from a test that passes.

* Cut the comment volume in the Windows on ARM installer

* Cut the comment volume in the studio setup script

* Cut the comment volume in the python installers and the CI workflows

* Cut the comment volume in the uninstaller and its tests

* Cut the comment volume in the tests, requirements and tauri sources

* Second comment reduction sweep across the Windows on ARM changes

* Third comment reduction sweep over the Windows on ARM changes

* Rebase editable and bare local-directory override lines too

New-UnslothTorchOverridesFile used to write its merge beside the caller's
UV_OVERRIDE file when there was exactly one, so relative references in it kept
resolving. The merge moved to %TEMP% with per-line rebasing, which also fixes
the multi-directory case that was always broken, but Resolve-WoaOverrideLine
did not know -e/--editable or a bare ./pkg directory requirement, so those two
forms started pointing at nothing. Reached every Windows host with UV_OVERRIDE
set, not only Windows on ARM.

* Fold 64 copies of the PowerShell invocation into one helper

Every test that runs a lifted PowerShell function spelled out the same five-line
subprocess.run and the same `assert done.returncode == 0, done.stderr`, 64 times.
_ps and _ps_ok replace them, and the timeout stays a parameter because five sites
need 180 and one needs 60.

369 lines out of the file, no test added or removed: 502 collected before and
after. Mechanically substituted, then checked the tests still catch what they are
for, by breaking install.ps1 three ways and comparing which tests fail. The
failing node ID sets are identical before and after (27 for a Split-WoaTomlKey
that never splits, 8 for a Test-WoaWheelTagsUsable that always says yes), so the
tests still execute PowerShell rather than passing vacuously.

* Test the default wheelhouse URL, which nothing tested

Found by mutation: changing the default to a working but wrong host left all
1492 tests passing. It is load bearing twice over. It is the only source for the
pyarrow that gates the native path, so a typo silently sends every Windows on
ARM host back to the emulated x64 stack with no error anywhere, and it is
fetched over the network, so a wrong host is a wrong download.

Pins the literal, that it is an https resolve/main URL under our own org rather
than a repo page that would serve HTML the staging code saves as a .whl, and
runs the assignment for eight override shapes: unset, empty, a mirror, one and
several trailing slashes, surrounding whitespace, and a local directory whose
trailing backslash TrimEnd('/') must not touch.

The same mutation now fails 3 tests instead of 0.

* Keep windows-vulkan in the ARM64 kind lists main just widened

main added windows-vulkan to every branch of the llama.cpp kind check, because
any x64 Windows host can land on a Vulkan bundle and a guard without it deletes
a working install on every setup run. That reasoning holds for the ARM64 arms
this branch adds, so it goes in $_nvidiaKinds rather than being appended at the
one call site: the opted-out arm needs it as much as the CUDA one.

Test expectations and the source-slice anchors move with it.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Move the uninstaller fixes out to their own PR

The ownership gate, the ~/.unsloth/studio guard, the Split-Path call that throws
and the remove-retry budget are not about Windows on ARM, and two of them change
behaviour on Linux and macOS. Reviewing them as a footnote to a Windows
installer feature was the wrong shape. They are now #10471, against main, with
the parity CI wiring that runs them.

These files go back to main exactly. Also drops this workflow's references to
test_uninstall_legacy_layout_gate.ps1, which now lives in that PR; the runner
loop already skips a test file it cannot find, so it can be listed again once
#10471 lands.

* Move the Kaggle GPU harness fix out to its own PR

An all-[N/A] compute-apps listing means nvidia-smi cannot attribute memory, not
that the card is idle, which is how WDDM and unified-memory parts report. That is
a GPU attribution fix in the test harness and touches no installer code, so it
reviews better on its own: #10472.

These files go back to main exactly.

* Read the two installer scripts once, not 117 times

62 tests re-read install.ps1 and 55 re-read setup.ps1, each on its own line, and
none of them mutate what they get back. Two module constants replace all of it,
and _ps_function serves from them rather than opening the file again on each of
its 60 calls. The wheelhouse-tag tests get the same treatment for
sys.version_info and the cp tag derived from it, restated in nearly every test.

130 lines out, no test added or removed: 1502 collected before and after.

Substituted on NAME tokens through tokenize, never on raw text, so an occurrence
of "text" inside a PowerShell string literal could not be caught by it. Verified
by mutation, not by the suite going green: breaking install.ps1 three ways fails
exactly the same node IDs as before, 3 for the default wheelhouse URL, 27 for a
Split-WoaTomlKey that never splits, 8 for a Test-WoaWheelTagsUsable that always
says yes.

Formatted with scripts/run_ruff_format.py and confirmed idempotent.

* Lower the win_arm64 floors to the first release that actually has a wheel

Three floors were set one release too high, and each one excludes a wheel that
exists.

scikit-learn is the one that bites. 1.8.0 publishes six win_arm64 wheels
including cp313-cp313t; 1.9.0 publishes five and dropped the free-threaded one.
So >=1.9.0 leaves a free-threaded 3.13 with no binary candidate and an sdist to
compile, which is what the ARM64 rows exist to avoid. The comment two lines up
already said "from 1.8 only" and contradicted its own pin.

av first ships win_arm64 at 17.0.0 (cp311-abi3, plus a cp314t), not 17.1.
No release has a 3.13t wheel, so that half of the comment now says so.

bitsandbytes is a comment fix only: the wheel starts at 0.50.0, and the pin was
already below both.

tests/studio/install/test_diffusers_pin.py pins all three against the index
rather than against an audit, and rejects a second ARM64 row for the same
project so a future edit cannot leave two floors disagreeing. It fails on each
of the three old floors.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Translate every spelling of --prerelease for the pip fallback, not one

Remove-UvOnlyResolverFlags handled --prerelease two ways, and both were wrong
away from the single spelling the two call sites use.

The space-separated form dropped the flag and its value and emitted nothing, so
a pip fallback given `--prerelease allow` resolved without prereleases and could
not see the torch build the run had just asked for. uv accepts that form
everywhere it accepts the = form.

The = form emitted --pre for any value. Of uv's five, only allow means what pip's
--pre means: disallow is pip's default and if-necessary, explicit and
if-necessary-or-explicit have no pip flag at all. So --prerelease=disallow became
--pre and inverted the caller's intent.

Both are latent today, since install.ps1 and setup.ps1 both pass
--prerelease=allow, which is the one case the old code got right. The helper
takes a caller-supplied argument list, so the grammar it accepts should be uv's.

The test walks ten argument lists through the real function: both spellings of
both flags, four --prerelease values, a bare value that must not be swallowed by
a flag it does not belong to, and the two flags combined. Four fail on the old
implementation.

* Stop the llama.cpp gate deleting what the selector just installed on ARM64

setup.ps1 deletes an existing llama.cpp tree whose recorded install_kind is not
in a set it computes for the host, then reinstalls via
install_llama_prebuilt.py. Nothing made those two agree, and where they do not
the disagreement is permanent: every update deletes the tree and refetches the
identical bundle, forever, on a machine that is working.

Walking the product of what either side branches on finds two disagreements on
main, both only reachable from Windows ARM64, which is why they lasted:

  arm64 + nvidia  selector installs windows-arm64, gate expected windows-cuda
  arm64 + rocm    selector installs windows-arm64, gate expected windows-rocm

The first is every Windows ARM64 machine with an NVIDIA GPU, and this branch
already fixed it by widening the gate to the ARM64 kinds.

The second is fixed here. No ROCm bundle exists for Windows ARM64 at all:
upstream ships hip-radeon-x64 and we publish none, so the selector falls through
to the ARM64 CPU bundle while the gate went on expecting windows-rocm or
windows-hip. The two ROCm kinds stay in the set for the day one is published.

tests/studio/install/test_llama_kind_gate_matches_selector.py asserts the only
relationship that matters between the two sets, over the twelve host
combinations, rather than the text of either. Offline:
direct_upstream_release_plan takes a release dict, and the gate's own block is
lifted out of setup.ps1 with its inputs replaced. It fails 3 checks against the
gate before this commit.

While confirming this, the x64-fallback churn loop I had noted earlier turned
out not to exist. An emulated x64 Python on an ARM64 box reports AMD64, so that
venv is an x64 host to the selector, it installs windows-cuda, and the gate
expects windows-cuda. All four x64 rows agree, before and after.

* Fold the repeated script and source boilerplate in the WoA persistence tests

Four whole installer and stack files were still being re-read per test; they
are read once now. The 62 copies of the join-a-list-of-lines idiom and the 48
run-then-take-the-last-line pairs become _script and _ps_last, and the three
copies of the parity normaliser become one _ps_copies helper.

* Give the wheelhouse tag tests one cache fixture and one read of each source

The find-links listing was cleared by hand in eleven try/finally blocks and a
dozen bookend pairs; an autouse fixture now empties it before and after every
test, and the clears that sit between two states inside a test stay. install.ps1,
install_python_stack.py and extras.txt are read once, and the wheelhouse builder
becomes a fixture rather than a helper taking three fixtures per call.

* Read each source file once in the marker and guard behaviour tests

install_python_stack.py was exec'd twice in the marker tests and install.ps1 was
re-read and re-scanned for ten helper bodies on every one of the guard tests'
thirty-five pwsh calls.

* Use the hoisted source constants directly rather than aliasing them

Sixteen tests opened with a one line rebinding of a module constant to a shorter
local name; the constant is now named at the point of use.

* Let the prerelease gate test tell producing the flag from comparing to it

Remove-UvOnlyResolverFlags now names --prerelease=allow to translate it into
pip's --pre. That is not a call site adding the flag, it runs after the gate has
already allowed it, so the array-shape rule the gate test enforces does not
apply to it.

* Verify the Windows ARM64 CUDA bundle against GitHub's asset digest

This was the one archive the installer would install unchecked. Every other
prebuilt goes through apply_approved_hashes, which drops an attempt our own
checksum manifest does not cover. The ARM64 CUDA branch could not: we publish no
windows-arm64-cuda artifact yet, so nothing covers upstream's zip, and refusing
outright would mean no CUDA llama.cpp on this hardware at all. So it logged that
it was installing without a hash, and did.

GitHub's release API now reports a per-asset digest. Checked against
ggml-org/llama.cpp b10853: 27 of 27 assets carry one, including
llama-b10853-bin-win-cuda-13.4-arm64.zip and the cudart archive it pairs with.

That is weaker than our manifest, which we compute ourselves rather than read
from the same host that serves the bytes, and the comment says so. It still pins
the download to what the API listed, which is enough for this branch to stop
being the exception. An asset GitHub states no digest for is now refused and the
install falls through to the ARM64 CPU bundle, so no path installs an unverified
archive.

Three things kept deliberately narrow:

  * A separate github_release_asset_digests rather than a second return value
    from github_release_assets. That one is mocked in a dozen suites and used on
    every platform; this is read on one branch.
  * Any failure fetching the digests is an empty mapping, which that branch reads
    as refuse. A rate limit costs the CUDA bundle, not the install, and cannot be
    mistaken for "no digest needed".
  * release_asset_digests requires the sha256: prefix rather than stripping it
    for convenience. normalize_sha256_digest accepts a bare 64-hex string, and an
    unprefixed digest is one whose algorithm GitHub did not state.

tests/studio/install/test_arm64_cuda_digest_verification.py covers the digest
parser over seven rejected shapes, the drop and unpair rules, the fail-closed
fetcher, and asserts against the source that every return in the resolver is
hash gated. That last one fails on the branch before this commit, on the literal
`return upstream_arm64_cuda`.

143 failures before and after across the four llama selection suites, identical
sets, so nothing here changes any other platform's routing.

* Bound the early GPU probes, gate the wheel on the driver, and tighten the resolver and index checks

install.ps1
- Test-WoaNvidiaPresent and the new Get-WoaDriverCudaVersion run nvidia-smi through the bounded runner.
- A CUDA wheel whose major exceeds the driver's falls back to the x64 stack with a driver-update message.
- The av probe is gated on the resolver reaching PyPI; public PyPI supplied as an extra index (env or uv config) keeps the probe in play.
- A managed wheel is redundant only when PyPI has the exact version, and the managed scan lifts win_arm64 or any wheels only.
- On an equal numeric release a final build outranks a .dev one; later stamps win among dev builds.
- The exact CUDA trio pin ignores UV_EXCLUDE_NEWER for that one install, since the index carries no upload dates.
- A pyarrow staging failure after the venv exists is a hard stop with the opt-out named.
- Rebased file: references become file URIs, so spaces are encoded.

studio/setup.ps1
- The merged override file is recorded, removed at the end of the run and on failure, and any stale copy is removed first.
- WinArm64NoAudio follows the effective-index probe; retention pairs torchaudio with the new torch by both predicates.
- Same tie-break, file URI and exclude-newer handling as the installer.

studio/install_python_stack.py, install_manifest.py, install_llama_prebuilt.py
- Extra indexes that are public PyPI count; the resident xformers check runs after a failed refresh.
- The NVIDIA index is persisted with scheme and host normalised.
- Legacy bundle ordering synthesises names for the arch being resolved and passes it through.

.github/workflows/windows-arm64-ci.yml
- The ARM job fails when a required test is reported as skipped.

Tests cover each change, including a real Exit-SetupFailure run under UNSLOTH_TAURI_MODE.

* Make a paired torchvision part of the native gate, and read a supplied wheel's name from its archive

install.ps1
- An index qualifies for the native path only when it publishes a torchvision paired with its torch; otherwise the next candidate is tried, and with none pairing the x64 stack is kept with the reason logged. The vision pin is the version the gate accepted.
- UNSLOTH_PYARROW_WHEEL saved under another name (.bin, no extension) has its wheel name read from the archive before the project and tag checks, as staging already does.

studio/setup.ps1
- When the effective index pairs no torchvision with its newest torch, the fresh-shell probe keeps the installed torch and torchvision pair instead of pinning a torch whose torchvision would resolve against another build.

Tests cover the gate order, the x64 fallback, the kept pair, and archives named .bin or without an extension.

* Rank prereleases in PEP 440 order, probe a reselected interpreter, and put the caller's resolver variables back

install.ps1
- Within one numeric release the wheel pick follows PEP 440 order in full: dev below a below b below rc below the final build, later numbers winning within a kind. Same in setup.ps1's copy.
- After the re-probe flips native mode, the reselected interpreter is probed before it is taken; one with no stack of its own is declined and the accepted interpreter keeps its answer.
- UV_OVERRIDE, UV_FIND_LINKS and PIP_FIND_LINKS are snapshotted before the first assignment and restored in the script-level finally, so an `irm | iex` session does not keep resolving with Studio's overrides and wheelhouse.

studio/backend/plugins/data-designer-unstructured-seed
- The plugin admits pandas 3, which constraints.txt installs on Windows on ARM; it only reads and writes parquet.

Tests cover the ordering, the reselection, the restore, and the plugin range against the constraints.

* Format the three files this branch owns with the ruff the hooks actually pin

.pre-commit-config.yaml pins ruff v0.15.18. I had been running
scripts/run_ruff_format.py against a local ruff 0.6.9, and the two disagree
about where an assert's message goes:

  0.6.9     assert (
                cond
            ), "message"

  0.15.18   assert cond, (
                "message"
            )

So every local format pass wrote the older shape and pre-commit.ci rewrote it on
push. This puts the three files that are ours at the pinned formatter's fixed
point instead.

Verified rather than assumed: with 0.15.18, running the wrapper twice leaves
every file byte-identical, so the wrapper is idempotent and it was only the
version that was wrong.

Four more files still differ under 0.15.18 and are left alone, because they
differ identically on origin/main and so are not this branch's drift:
studio/prebuilt_core.py, tests/studio/install/test_cuda_repair.py,
tests/studio/install/test_rdna1_unsupported_message_8529.py and
tests/studio/install/test_windows_torch_flavor_invariant.py.

855 tests pass across the three.

* Let the dependency index follow the resolver policy, widen the ARM CI filters, and survive an upstream listing failure

install.ps1 and studio/setup.ps1
- torch's shared dependencies resolve from the index the caller's resolver policy names (environment, then uv config), from public PyPI when it names none, and from the find-links wheelhouse alone under no-index. Invoke-InstallCommand clears the inherited index settings whenever --default-index is passed, so the policy is restated on the command line. setup.ps1 carries the readers as parity copies.

.github/workflows/windows-arm64-ci.yml
- The path filters name pyproject.toml and the whole requirements tree the ARM tests read.

studio/install_llama_prebuilt.py
- A failure listing the upstream release assets on a Windows ARM64 NVIDIA host costs the CUDA bundle only; the planner falls through to the published ARM64 CPU bundle.

Tests cover the policy readers in both scripts, the filters, and the fall-through.

* Judge index reachability and the dependency index for the resolver that runs the pass

uv reads UV_* and its configuration files and ignores PIP_*; pip reads PIP_* alone. Reading both sets together reported public PyPI reachable from a PIP_EXTRA_INDEX_URL that uv, the resolver in use, never consults, and then dropped the librosa and tensorboard skip on wheels the resolve could not fetch.

- studio/install_python_stack.py: the probe branches on USE_UV; the uv path also honours UV_NO_INDEX.
- install.ps1: the probe reads uv's policy only, since this script resolves with uv; Get-WoaDependencyIndexArgs takes the resolver and reads that resolver's variables, uv's configuration files for uv alone.
- studio/setup.ps1: the parity copy, with the resolver it will use passed in.

Tests cover the cross-resolver case in all three places and the pip fallback's own policy.

* Keep markers out of rebased file references, clean the merged overrides on every exit, and stop on an unwritable wheel directory

install.ps1 and studio/setup.ps1
- A PEP 508 marker on a `pkg @ file:...` override is kept aside while the path is rebased and re-attached after the URI.
- setup.ps1 traps a terminating error after the resolver restore, removes the merged override file, and rethrows.

install.ps1
- A wheel directory that cannot be created after the ARM64 venv exists is a stop with the reason, not a stand-down into an index with no win_arm64 wheel. The directory is verified after New-Item, which can return without creating it when a file is in the way.
- The trio command names the staged wheelhouse with --find-links, since the inherited UV_FIND_LINKS is cleared for a pinned index; UV_NO_INDEX yields for that one command and is restored after it.
- The two probe warnings that print an index URL redact its credentials.

studio/backend/requirements/no-torch-runtime.txt
- sqlite-vec carries the negative Windows ARM64 marker, as it does in studio.txt.

Tests cover each change; the marker table claims the new row.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Make the five Windows-on-ARM tests actually run on Windows

All five assert Windows-on-ARM behaviour and none of them could execute their
own fake on Windows, so they failed on the one platform they are about while
passing on Linux. windows-arm64-ci.yml runs this file on a win-arm64 runner
under both shells and asserts sysconfig.get_platform() first, so these were red,
not skipped.

Test-WoaNvidiaPresent. The fake was an extensionless file containing #!/bin/sh.
Windows has no shebang handling and PATHEXT covers no extensionless name, so the
probe found nothing at all. Now written as nvidia-smi.cmd on Windows, which is
the idiom the rest of this suite already uses, and the sh script elsewhere. The
hung-probe case shared the same fake and so was a false pass: it asserted the
call returns, and a probe that finds no executable returns immediately. It now
actually blocks, via ping rather than timeout, since timeout /t needs a console.

The installed-pair probe. The block runs <VenvDir>/Scripts/python.exe by that
exact name, and the fake was a shell script called python.exe, which Windows
will not execute because it is not a PE. All three cases therefore asserted the
floor rather than the branch they were written for. Replaced with a real venv
carrying real .dist-info metadata, so importlib.metadata answers the way it
would after a real install and the interpreter answers the tag query itself.
Confirmed it now bites: breaking the installed-pair read fails
test_the_installed_pair_is_kept, which it could not do before.

The file: URL row. Its expectation was built by pasting a rebased path after
"file://". A rebased Windows path is C:\opt\corp\ov\dist\a.whl, and "file://"
plus that is not a file URL: the authority would be C: and the separators are
wrong. The rewriter emits file:///C:/opt/corp/ov/dist/a.whl, which is correct,
so the code was right and the expectation was not. Split into its own test that
builds the URL with pathlib as_uri instead of concatenating.

26 pass here on native Windows ARM64 under PowerShell 5.1.

* Add soxr to librosa's unblock set: it has no win_arm64 wheel either

librosa is on the win_arm64 skip list and comes off it once its blockers are
installable. The blocker set was llvmlite and numba, but librosa 0.11.0, which is
what extras.txt pins, requires soxr>=0.3.2 as well, and soxr has published no
win_arm64 wheel in any release up to 1.1.0. Verified against the PyPI JSON API
for the pinned version rather than for latest, since only the pinned release's
metadata is the one that applies.

So hosting cp313 numba and llvmlite would have un-skipped librosa, and the extras
pass would then have gone looking for soxr and found only an sdist. Building that
needs the toolchain the whole skip list exists to avoid. Nobody has hit it
because numba and llvmlite publish win_arm64 for cp314 only, so librosa has never
actually un-skipped on a cp313 host, which is what kept it latent.

soxr also gets a floor entry, read from librosa 0.11.0's own metadata, matching
the provenance convention the other two floors already follow.

openai-whisper is left alone deliberately: its metadata asks for numba and
tiktoken and does not mention soxr.

Upstream is dofuuz/python-soxr PR 36, "Add Windows ARM64 wheels", still open.

The three existing tests that drove librosa off the skip list now stub the index
answer for soxr, so they stay about what they were written for: the interpreter
tag deciding availability, the empty-wheelhouse early return, and losing the
index offline. The new truth gets its own test, which fails when soxr is taken
back out of the tuple.

165 pass.

* Name the wheelhouse on the setup.ps1 trio command, read pip's config files, evict a stale xformers without a hosted wheel for PR #10282

setup.ps1: Fast-Install clears UV_FIND_LINKS and PIP_FIND_LINKS beside the other inherited index settings whenever --index-url is given and leaves UV_NO_INDEX alone, so a native CUDA update under --no-index saw neither the staged wheelhouse nor the CUDA index. $WinArm64IndexArgs now carries --find-links for the wheelhouse when the directory exists, and UV_NO_INDEX yields for the trio command through the existing cutoff save and restore, as install.ps1 already does.

install_python_stack.py: the pip path of the PyPI reachability check read PIP_* only. It now consults `pip config list` when the environment is silent: an `[install]` or `[global]` no-index or exclusive index-url set in pip's files means PyPI is not in play, a PyPI extra-index-url keeps it in play, and a pip config that cannot be read keeps the skip. :env: rows are skipped since the variables were already read.

install_python_stack.py: the resident xformers compatibility check ran only after a hosted install attempt, so a copy an earlier run installed against another torch stayed resident whenever the wheelhouse stopped offering a usable wheel. The check is now a helper that also runs when nothing usable is hosted.

Tests: setup.ps1 twin of the no-index trio class (find-links on the command line, UV_NO_INDEX yield and restore, off-ARM64 untouched), the pip fallback rows carry --find-links through the pip translation, a pip configuration class with stubbed and real `pip config list` output, and the wheelhouse optional rows for eviction without a hosted wheel. Twelve mutants on the three changes are killed.

* Search every supported nvidia-smi location for the driver version

Test-WoaNvidiaPresent searched PATH, System32 and the NVSMI folder, but
Get-WoaDriverCudaVersion searched PATH and System32 only. On a host that carries
nvidia-smi.exe under %ProgramFiles%\NVIDIA Corporation\NVSMI alone, the presence
probe reported a GPU and the version probe returned nothing, so
Initialize-WoaNativeCudaTorch skipped its CUDA-major compatibility guard and could
select a cu13x ARM64 wheel against a driver that only advertises CUDA 12.

Factor the candidate list into Get-WoaNvidiaSmiPath and have both probes call it, so
the two cannot disagree. Same two locations the main GPU detection already searches.

* Inject Get-WoaNvidiaSmiPath into the scripts that paste the probes' real bodies

The pwsh tests compose real function bodies into a bare script. Routing both nvidia-smi
probes through the shared lookup gave those bodies a call the compositions did not
inject, leaving it unresolved.

One of the two would have failed loudly. The other passes for the wrong reason:
PowerShell writes the unresolved-command error and continues with $null, so
Get-WoaDriverCudaVersion returns $null before it ever calls nvidia-smi and the '[]'
assertion still holds, with the timeout the test exists to bound never exercised.

Both are marked requires_pwsh, so on a host without pwsh they skip and the suite is
green either way. Add a guard that needs no pwsh: it fails if a composition injects a
body calling Get-WoaNvidiaSmiPath without also injecting the helper.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Read uv's inline index spelling, and stop guessing when it cannot be read

Read-WoaUvTomlIndexKeys returned $null on sight of an `index` key, so
`index = [{ url = "...", default = true }]` -- valid, documented uv config -- read as
Unreadable. Get-WoaDependencyIndexArgs never looked at Unreadable and fell through to
its https://pypi.org/simple default, silently replacing whatever mirror the file
configured. Test-WoaResolveReachesPyPI had honoured the same flag all along, so the two
consumers of one signal disagreed, and the unsafe one built the install command.

Parse the inline form, conservatively: a single-line, brace-balanced array of flat
inline tables only. A multi-line array, a nested value, a bare entry beside a table, a
missing url, a non-boolean default or an explicit index all still return $null and stay
Unreadable, because guessing is the only outcome worse than not parsing.

Then make the consumers agree, per call site, since they genuinely differ. With no env
default and an unreadable policy, name no dependency index rather than substituting
PyPI. install.ps1's trio step calls uv directly, so uv reads the file itself and the
empty answer is strictly better than today: no substitution and no failure.
setup.ps1's runs under Fast-Install with --index-url, which sets UV_NO_CONFIG and
scrubs UV_*, so uv cannot read it and an empty answer would leave the trio index as the
only source; that one stops with a message naming the file, the spelling, and both ways
out. Get-WoaUvConfigIndexPolicy now records which file it could not read so the message
can name it.

Blast radius, traced before writing: a previously-$null read now returns a value, which
moves Test-WoaResolveReachesPyPI and therefore wheel pruning. A mirror URL still gives
false and still disables pruning, so that case is unmoved. A genuine pypi.org URL now
gives true and enables it, which is correct and was previously suppressed by a parse
failure rather than by a decision.

Verified against pwsh 7.4.6 rather than skipped: the inline reader is exercised on five
accepted shapes and six ambiguous ones. Running them for real caught a bare string entry
beside a table being accepted, because the scanner entered string mode before testing
depth. Full tests/studio/install plus the wheelhouse tags suite gives an identical
failure set to HEAD, 173 either side, none new.

* Apply the pinned formatter's kwarg spacing to the new inline-index cases

The ruff-format-with-kwargs hook pins ruff==0.6.9, which wraps the new parametrize
tuples one element per line. Formatting only; the 733 tests in the two files are
unchanged and still pass, and a second run of the hook is a no-op.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Default the Windows on ARM wheelhouse to the published one for PR #10282

The default named unsloth/windows-arm64-wheels, which does not exist, so no host took the native path without UNSLOTH_WOA_WHEELHOUSE set. The wheelhouse now lives under danielhanchen/unsloth-blackwell-docker: pyarrow 25.0.1 for cp311 through cp314, built from the Arrow release tag with apache/arrow#48539's build script on a windows-11-arm runner, and sqlite-vec 0.1.9 repacked with an ARM64 DLL built from its release tag. Every wheel was smoke tested on the ARM64 runner, every native image inside is ARM64, and install.ps1 ran its native path against the published URL end to end (pyarrow 25.0.1, torch 2.14.0+cu134, sqlite_vec 0.1.9 all importing in the win-arm64 venv). The test that pins the URL follows.

* Share the PowerShell test harness and parametrize the Windows on ARM persistence tests for PR #10282

* Shorten the comments the Windows on ARM installer added for PR #10282

* Parametrize and share fixtures across the Windows on ARM wheelhouse tests for PR #10282

* Keep explicit uv indexes out of the dependency index, prune wheels an earlier wheelhouse left for PR #10282

Explicit indexes: uv serves an index marked explicit = true only to packages pinned to it via [tool.uv.sources], so it is neither the default nor an extra for the trio's shared dependencies. The block form [[index]] reader in install.ps1 and setup.ps1 read url and default only and flushed such an entry as a general extra, which Get-WoaDependencyIndexArgs then passed as --extra-index-url to the native torch resolve; the inline table reader refused the key outright, which turned the documented PyTorch pattern into an unreadable policy. Both readers and the Python reader now skip an explicit entry; explicit together with default = true also removes PyPI as the default, which is not modelled, so that combination stays doubt.

Stale wheels: the managed woa\wheels directory persists across runs, so after UNSLOTH_WOA_WHEELHOUSE changed a wheel the earlier wheelhouse staged stayed in it, the hosted scan read it as current and UV_FIND_LINKS installed it from a source no longer configured. Staging now reconciles the directory against the wheelhouse listing it just read, keeping the listed wheels and the selected pyarrow wheel; an unreadable listing (offline) keeps the staged copies, and the directory used as its own wheelhouse lists everything it holds.

Tests: block-form and inline rows on both scripts plus the Python reader, the dependency args never naming an explicit index, and the reconciliation across a local wheelhouse, a URL wheelhouse, an offline listing and the self-sourced directory. Ten mutants on the change are killed.

* Shorten the comments the explicit-index and wheelhouse reconciliation changes added for PR #10282

* Hand PyPI-provided wheels to the skip list, install a hosted torchcodec, order xformers after the final torch repair, and key the pip refusal to the generated overrides for PR #10282

* Keep folded caller overrides in a per-run file rather than the persistent WoA overrides for PR #10282

* Backtrack to the newest paired torch and keep inline comments when rebasing override lines for PR #10282

* Form the rebase test's expected paths per host and let the ARM64 CI print its pytest output for PR #10282

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add a signed Windows on ARM wheelhouse workflow that builds, Authenticode signs and publishes the pyarrow and sqlite-vec win_arm64 wheels for PR #10282

* Publish the Windows on ARM wheelhouse under the Windows-ARM64 release tag for PR #10282

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothshared@gmail.com>
2026-09-16 04:32:53 -07:00

274 lines
6.2 KiB
Text

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.class
unsloth_compiled_cache/
# Notebook-validator runtime PyPI metadata cache (CI repopulates).
scripts/data/pypi_cache/
# ML artifacts (large files)
feature/
outputs/
exports/
/datasets/
studio/backend/assets/datasets/
# Generated async worker / reviewer transcripts (never part of the product).
studio/backend/async_task_outputs/
unsloth_training_checkpoints/
*.gguf
*.safetensors
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
/lib/
/lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.venv_overlay/
.venv_t5/
environment.yaml
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
.pre-commit-cache/
# PyPI configuration file and IDE/Editors
.pypirc
.vscode
.idea/
.claude/
*.swp
*.swo
# oh-my-codex
.omx/
# Firebase
firebase-debug.log
# Other
resources/
tmp/
**/node_modules/
auth.db
# Tauri local build/generated output
studio/src-tauri/target/
studio/src-tauri/gen/
studio/src-tauri/artifacts/
studio/src-tauri/icons/android/
studio/src-tauri/icons/ios/
studio/src-tauri/icons/128x128@2x.png
studio/src-tauri/icons/64x64.png
studio/src-tauri/icons/Square*Logo.png
studio/src-tauri/icons/StoreLogo.png
studio/src-tauri/icons/squarehq.png
# Local working docs
**/CLAUDE.md
**/claude.md
**/AGENT.md
**/agent.md
docs/canvas-lab-architecture.md
log_rtx.txt
log.txt
setup_leo.sh
server.pid
*.log
# Ignore stray lockfiles; real npm projects opt back in below (npm ci needs them).
package-lock.json
!studio/frontend/package-lock.json
!studio/backend/core/data_recipe/oxc-validator/package-lock.json
!studio/package-lock.json
llama.cpp/
async_task_outputs/
individual_reviews/
# Stray "~" dir some tools create from a literal ~ TMPDIR; never part of the repo.
~/
# Agent workspace runtime/scratch artifacts, never part of the repo.
async_task_outputs/
temp/
logs/
# Audio fixture downloaded by tests/saving/text_to_speech_models at import time.
Speech_12dB_s16.flac
# Generated at test time by tests/studio/install/test_selection_logic.py, which points
# storage_roots.studio_root at it. It is a mutable SQLite runtime database, not a fixture,
# so a test run or an Unsloth start rewrites it and dirties the checkout.
.studio-test-root/
# _filter_requirements() in studio/install_python_stack.py writes its skip-filtered copy beside
# the source file so relative -r/-c includes still resolve. A killed install leaves them behind.
studio/backend/requirements/.*-filtered-*.txt
# launch.py --outdir scratch from a local run
/out/
# Built at session start by tests/security/conftest.py from fixtures/_build.py, which is
# deterministic, so these are generated artifacts rather than fixtures. They are not committed
# because the two malicious ones embed the May-12 IOC literal and are therefore true positives
# for other vendors: on VirusTotal the sdist scores 2/60 and the wheel 2/65, and their presence in
# GitHub's repository archive is what makes Panda report Exploit/CVE-2014-6271 against
# unsloth-main.zip (discussion #9577). The scanner tests need the archives; the repository does
# not need to ship them.
tests/security/fixtures/malicious_wheel.whl
tests/security/fixtures/malicious_sdist.tar.gz
tests/security/fixtures/clean_wheel.whl