unsloth/studio/setup.bat
Daniel Han 201790e691
WSL installer: stop running the C# compiler on the user's Windows machine (#10978)
* WSL installer: stop running the C# compiler on the user's Windows machine

`install.sh` generates a PowerShell script and runs it on the Windows side to create the WSL
shortcut. Two constructs in that path were removed from `install.ps1` in earlier passes and left
live here, where nothing looked:

- `install.sh:2249` declared `SHChangeNotify` with `Add-Type -MemberDefinition`. On Windows
  PowerShell 5.1 that writes C# to `%TEMP%` and runs `csc.exe`, producing a DLL on the user's
  machine. #10540 traced exactly that chain to Bitdefender `Gen:Variant.MSILHeracles.272113`:
  `unsloth-stud... -> powershell.exe -> csc.exe -> %TEMP%\<random>.dll`. It replaced the
  declaration in `install.ps1` with reflection emit and missed this copy.
- `install.sh:2273` launched the generated script with `-ExecutionPolicy Bypass`, the flag #7822
  removed everywhere else.

Neither was caught because `test_the_installer_never_runs_the_c_sharp_compiler` parametrised over
`("install.ps1", "studio/setup.ps1")` only, and `windows-no-compiler-ci.yml` did not list
`install.sh` in its path triggers. A shell script that emits PowerShell can put a compiler on a
Windows machine without touching a `.ps1`.

## The compile

`SHChangeNotify` is now emitted, mirroring `install.ps1:3485-3499`: a `DefineDynamicAssembly` /
`DefineDynamicModule` / `DefineType` / `DefinePInvokeMethod` / `CreateType` block inlined, since the
generated script is standalone and cannot call `New-StudioEmittedNativeType`. The enclosing
`try {} catch {}` stays, so a failure still costs only a stale icon, and both notifications stay:
per-item `SHCNE_UPDATEITEM` + `SHCNF_PATHW` per `.lnk`, then the global `SHCNE_ASSOCCHANGED`. The
per-item call is not redundant, and the `ALLOWED_PINVOKES` entry records why: `ie4uinit.exe -show`
does not recover a stale same-name `.lnk`.

I ran the emitted type rather than assuming it: under pwsh it produces
`void SHChangeNotify(Int32, UInt32, String, IntPtr)`, identical to the `Add-Type` declaration,
`PreserveSig` set, and `Assembly.Location` empty, which is the point. Nothing reaches disk.

## The execution policy

`Bypass` was load-bearing, for one reason: `wslpath -w /tmp/unsloth-shortcut-XXXXXX.ps1` yields a
`\\wsl.localhost\<distro>\...` UNC path, PowerShell treats a script there as remote, and
`RemoteSigned` refuses an unsigned remote script. So rather than relax the policy, the script now
goes to the Windows `%TEMP%` (`cmd.exe /c echo %TEMP%`, CR stripped, `wslpath -u`), which is a local
volume and therefore `MyComputer` zone, where `RemoteSigned` loads it unsigned. Same generated
script, same launch, same result.

When `%TEMP%` cannot be resolved there is deliberately **no fallback to WSL's `/tmp`**, because that
is the case that needed the relaxed policy in the first place. Nothing is allocated and the caller
takes the notice it already prints when interop is unavailable. That population is very nearly the
population where interop is already broken.

## Tripwires, with messages that say what is wrong

`tests/sh/test_wsl_shortcut_script_location.sh` evals the real resolution block out of `install.sh`
with `cmd.exe` and `wslpath` stubbed, covering resolved, no-interop, unexpanded `%TEMP%`, and a
`%TEMP%` that is not a directory, then asserts statically that the launch uses `RemoteSigned`, that
the file relaxes the policy nowhere, that it compiles no C#, and that both refreshes survive. A
failed extraction is reported as a failure rather than exiting early, so the output names every
problem at once, and it still fails closed.

`clean-machine-install-ci.yml`'s `wsl` job is the only lane that runs this branch, so it gains the
behavioural half: read the created `.lnk` back and refuse a script-host target, require the
generated script to have been cleaned up, and fail on any `.cmdline`/`.rsp` under the Windows
`%TEMP%`, which is csc.exe output and nothing else. That check plants two intermediates and requires
the search to find both before trusting a negative result, because a detector that sees nothing looks
exactly like a clean run and every reason it might see nothing is silent. If the control does not
fire the job fails as a broken detector instead of reporting a pass. Shortcut absence is a warning,
not an error, since the installer itself treats shortcut creation as best-effort.

`install.sh` is added to the compiler test's parametrise list and to
`windows-no-compiler-ci.yml`'s paths. The `DefinePInvokeMethod` assertion in that test becomes
conditional on the file declaring a native import at all, so it does not fail for being satisfied on
`scripts/uninstall.ps1` or `studio/setup.sh`.

## Verification

- The new shell suite: 16 passed on this branch, and against `main` in a worktree it exits 1 with 6
  named failures. Proved to bite rather than assumed to.
- The generated WSL script rendered and parsed with
  `[System.Management.Automation.Language.Parser]::ParseFile`: clean over 765 tokens. No existing
  gate parses that here-string, and this change edits it.
- `bash -n` and `sh -n` on `install.sh`; all of `tests/sh` passing; the parse gate clean over
  `install.ps1`, `studio/setup.ps1` and `scripts/uninstall.ps1`.
- `tests/studio/test_installer_av_shapes.py`, `tests/studio/install/`, `tests/security/`: green.
- The new CI PowerShell parsed standalone, and its positive control plus detector exercised for real
  against a planted `csc.exe`-style intermediate: control 2 of 2, simulated compile caught, clean
  directory reports zero.

I have no Windows or WSL here, so the runtime behaviour of the shortcut path rests on the
`clean-machine-install-ci.yml` WSL job, not on a local run. This is a shape fix with a confirmed
detection behind the shape; it is not a claim that any particular user report is resolved.

* Fix a test of mine that passed for the wrong reason, and could only fail on a real CI machine

`test_wsl_shortcut_script_location.sh` asserted that the generated shortcut script's path does not
start with `/tmp`, as a proxy for "not in WSL's own temp, which wslpath turns into a \\wsl.localhost
UNC path that RemoteSigned refuses". The proxy is broken: the harness builds its *fake Windows temp*
with `mktemp -d`, so on any machine with `TMPDIR=/tmp` -- which is every normal CI runner -- the
stub's own directory matched the pattern and the test failed while install.sh was behaving exactly as
intended.

It passed locally only because this workspace sets `TMPDIR` elsewhere. That is precisely the class of
accidental pass the rest of this work has been built to catch, so it is worth naming rather than
quietly correcting.

The replacement asserts the property the proxy was standing in for, structurally: the block contains
exactly one `mktemp`, so there is no second path to fall back to, and that `mktemp` allocates inside
`$_css_win_temp`. Both are true or false regardless of where anyone's temp directory happens to live.

Verified under the exact condition that broke it: `TMPDIR=/tmp` gives 17 passed, 0 failed on this
branch, and 6 failed on `main` in a clean worktree -- so the test still bites.

* WSL installer: read %TEMP% with cmd /d, so an AutoRun command cannot cost the user their shortcut

Reading the Windows %TEMP% through interop is new in this branch, and it reads it with
`cmd.exe /c echo %TEMP%`. Without /d, cmd first runs the AutoRun command from
HKLM or HKCU \Software\Microsoft\Command Processor, and `cmd /c` is not exempt from
that. AutoRun writes to the same stdout, so its output arrives ahead of the path and
`tr -d '\r\n'` glues the two into one string. wslpath then rejects it, the directory
check fails, and the branch takes its "couldn't create the Windows shortcut" notice.

Clink sets an AutoRun, so Cmder does, and so do plenty of corporate images. Every one
of those users got a working shortcut before this branch and would silently stop
getting one, while being told WSL interop is disabled, which is not what happened.

So: /d, which is the cheap fix rather than out-parsing whatever prints. Then the last
line rather than the whole stream, for anything that still reaches stdout by another
route, and trailing blanks trimmed, because Win32 strips them from a path and [ -d ]
does not, so a %TEMP% set with one resolved to a directory that appeared not to exist.
The || stays: this file runs under set -e, where a failed substitution ends the install.

Tests
-----
tests/sh/test_wsl_shortcut_script_location.sh grows the AutoRun and trailing-blank
cases and a static check for /d, and its wslpath stub is now input-sensitive. It was
not, and that mattered: a stub that returns the fake Windows temp whatever it is handed
cannot tell "install.sh read the path" from "install.sh read a banner glued to a path",
so both new cases passed against the unfixed file until the stub compared its argument.
Against the unfixed install.sh the four new assertions fail; with it they pass.

tests/studio/test_wsl_shortcut_script_is_valid_powershell.py is new and covers a gap
nothing else did: that what install.sh generates is PowerShell at all. bash -n sees the
here-string as a here-string, the contents only run on Windows with both streams sent to
/dev/null, and the icon-refresh block runs inside try/catch, so a parse error there is
not a crash and not a message, just no shortcut and a misleading notice. The block was
just rewritten into a twenty-line reflection-emit sequence, which is when that matters
most. The file renders the here-string with a real shell rather than keeping a copy,
parses it, and then executes the emit sequence retargeted at a symbol this host exports,
so a wrong call order or argument count fails on Linux. Five mutations of the shipped
file were tried; four fail it, and the docstring names the fifth that does not and why.

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

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

* Watch the WSL install live, instead of searching for what CodeDom already deleted

The lane looked for .cmdline and .rsp files in the Windows temp after the install had finished.
CodeDom removes its whole intermediate directory once the assembly is loaded, which
Watch-ForCompiler.ps1 records as a before-and-after diff seeing nothing at all while 4688 recorded
csc.exe, so that search could not fail for the case this lane exists to catch. Its positive
control planted persistent files, which proves the search can traverse a directory and nothing
about whether ephemeral compiler output is observable.

The install now runs inside Invoke-WithCompilerWatch, the same detector windows-no-compiler-ci.yml
uses, which unions 4688 with a live FileSystemWatcher and treats an unreadable Security log as
void rather than clean. The dropped-library half is checked too, since that is what the
Bitdefender report in #10540 keyed on. The post-hoc listing stays for what genuinely is left
behind, which is the other half of the union, and its message now says that is what it means.

Watch-ForCompiler.ps1 is in both path filters now, because this job's detection depends on it, and
the new guard is registered in the unfiltered lint job since it reads a workflow file.

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

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

* Arm the 4688 half of the compiler watch in the WSL lane

Watch-ForCompiler.ps1 requires the caller to enable process creation
auditing, and this job never did, so its Security log query returned an
empty compiler list whatever ran while the step still printed that no
compiler process was seen. Enable and verify the audit policy before the
install, and require a deliberate compile to be detected first, so a
clean verdict is distinguishable from a detector that never attached.

* Close the AV shape-guard holes, and load the installers under RemoteSigned (#10982)

* AV shape guards: watch the two shipped scripts nothing watched, and ratchet the policy sites

Stacked on the WSL compiler fix, which shares this file.

Two problems, both found while investigating #10805, and both about the guard rather than the code
it guards.

## Two shipped scripts were in no list

`PS_SCRIPTS` and `SH_SCRIPTS` between them missed `studio/setup.bat` and `scripts/uninstall.sh`. A
shipped `.bat` is scanned like any other file, and `setup.bat` launches PowerShell, so every rule
here applies to it. That is how its `-ExecutionPolicy Bypass` survived the passes in #7822 and
#8586: the guards were reading a set of files that did not include it. Both are added to
`ALL_SCRIPTS`.

## The hidden-window pair check only ever looked at one line

`test_a_hidden_window_never_pairs_with_a_bypassed_policy` iterated `splitlines()` and compared the
two flags within a single physical line. `install.ps1` assigns `$shortcutArgs` a hidden window at
:3419 and overwrites it with a relaxed policy at :3433, fourteen lines apart in the same function,
and that passed for as long as it existed. A scanner reads the file, not the line.

Three layers now:

1. Same line, unchanged, because it gives the clearest message when it is the real shape.
2. A ratchet on how many relaxed policies the file contains at all, against `KNOWN_BYPASS_SITES`,
   which records each remaining site with the reason and what would remove it. It fails **both**
   ways: too many is a new site, too few is a stale entry that has quietly stopped guarding
   anything. This is the layer that catches a policy arriving somewhere the other two do not model.
3. Same variable, any distance: the union of everything assigned to one name must not contain both
   flags. This is the layer that sees the 3419/3433 shape.

`install.ps1`'s `$shortcutArgs` is recorded in `KNOWN_SPLIT_PAIR_VARIABLES` rather than failed on.
Removing it needs `launch-studio.ps1` written to a guaranteed-local directory first, because
`%LOCALAPPDATA%` can be folder-redirected to a UNC path where RemoteSigned refuses an unsigned
script, and a shortcut that silently does nothing is worse than the token. Recording it lets the
guard land now and lock in "no new ones", which is the part that matters going forward.

## Verification

70 passed in the suite, and each new layer was confirmed to bite rather than assumed to:

- A new relaxed policy injected into `studio/setup.sh` fails layer 2, naming the file, line number,
  the offending line, and what to do about it.
- Removing the recorded site from `studio/setup.bat` without lowering its count fails layer 2 the
  other way, telling the reader to lower the count in the same commit.
- Layer 3 proved in isolation by renaming `$shortcutArgs` to `$launchArgs` in `install.ps1`: the
  relaxed-policy count is unchanged so layer 2 passes, and layer 3 fires on its own.

The tree was restored after each and re-verified clean. `tests/studio/install/` and the shape suite
are green; `test_pr10648_offline_and_status.py::test_the_pr_only_added_new_arms_to_the_setup_status_blocks`
fails identically on `main` in a separate worktree and is untouched by this change.

* Load the installer scripts under RemoteSigned instead of relaxing the policy

Removes three of the four relaxed execution policies the ratchet in the previous commit recorded.
RemoteSigned refuses an unsigned script only in the Internet or Untrusted zone, so for a local,
unmarked script it behaves identically to Bypass while dropping a token endpoint products score.

## studio/setup.bat

`powershell -ExecutionPolicy Bypass -File "%~dp0setup.ps1"` becomes RemoteSigned. setup.ps1 ships
beside the batch file inside an installed package, so it is MyComputer-zone and loads unsigned
either way.

One population could regress: a package unzipped from a download, where setup.ps1 carries a mark of
the web that RemoteSigned honours and Bypass ignored. So the mark is cleared first. Execution policy
governs script *files* and not `-Command`, so that call runs under any machine policy, and clearing
an absent stream writes nothing. The path travels in an environment variable rather than being
interpolated into the command string, because an apostrophe in an install path is a real Windows user
name and would otherwise break the quoting.

`-NoProfile` is on the unblock call and deliberately **not** on the launch. The launch has always run
setup.ps1 with profiles loaded, and `tests/studio/test_amd_venv_repair_loop.ps1` drives a profile
that sets `Set-StrictMode -Version Latest` against it. Adding the flag would have retired that
coverage with nothing failing, so the new test asserts its absence and says why.

## scripts/uninstall.ps1 and README.md

Both print `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass` as the documented way to run
a cloned script. Changed to RemoteSigned rather than dropped: the Windows client default is
`Restricted`, which blocks every script file including a local unsigned one, so the
`Set-ExecutionPolicy` itself is load-bearing and removing it would break the documented path.
RemoteSigned is sufficient because a git clone carries no mark of the web, and both now say so and
point a zip user at `Unblock-File`, which is the case where the two policies genuinely differ.

The uninstaller line is never executed -- it lives in the printed `_Usage` here-string -- but AMSI
scans the file whole and a classifier cannot tell a string from a statement.

## Where that leaves the ratchet

Four recorded sites down to one. The survivor is `install.ps1:3433`, the roaming-profile fallback for
a launcher on a share, and it stays because `%LOCALAPPDATA%` can be folder-redirected to a UNC path
where RemoteSigned would refuse the launcher, and a desktop shortcut that silently does nothing is
worse than the token. Removing it needs `launch-studio.ps1` written to a guaranteed-local directory
first, which is a larger change than this one.

## Verification

- 751 passed, 6 skipped across the shape suite, `tests/test_installer_interactive_prompts.py` and
  `tests/security/`. All of `tests/sh` passing. The parse gate clean over all three `.ps1` files.
- Both new `setup.bat` assertions confirmed to bite: adding `-NoProfile` to the launch fails with the
  reason and names the test that would lose coverage, and reverting the policy to Bypass fails naming
  the offending line. The tree was restored after each.
- Relaxed-policy sites counted directly across all seven shipped scripts afterwards: one, in
  install.ps1, as recorded.

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

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

* Step setup.bat down to Bypass for a script on a share, the way install.ps1 already does

Tightening setup.bat from Bypass to RemoteSigned had one case that is a genuine regression rather
than a theoretical one, and it needed handling rather than accepting.

Execution policy is judged on the script file's ZONE. A plain \\server\share and a mapped drive are
Intranet and load fine under RemoteSigned -- but a dotted-FQDN UNC (\\corp.example.com\...), a DFS
path or an IP-literal share is the Internet zone, where RemoteSigned refuses an unsigned script. And
Unblock-File cannot save that case: with no Zone.Identifier stream present the PATH decides, so
clearing a stream that was never there changes nothing and exits silently. Bypass worked; RemoteSigned
does not.

install.ps1:3405-3421 already solves exactly this when it writes the Start Menu shortcut -- UNC
prefix, then DriveInfo.DriveType -eq 'Network' -- and the new test's own comment calls that site "the
only one that is genuinely load-bearing" for the same reason. setup.bat got no such exemption. Now it
reuses the identical logic rather than inventing a second answer.

The probe does the Unblock-File too, so this costs one process rather than two, and it runs under
-Command, which execution policy does not govern at all.

Two details that matter more than they look:

- The batch variable DEFAULTS to RemoteSigned. A probe that fails to run therefore leaves the
  tightened policy in place rather than silently restoring the relaxed one.
- -NoProfile is on the probe and still deliberately absent from the launch.
  tests/studio/test_amd_venv_repair_loop.ps1 drives a profile that sets Set-StrictMode against
  setup.ps1, and adding it would retire that coverage with nothing failing.

Tests: the embedded probe is extracted from the batch file and PARSED, because it is one long line
inside a `for /f` backquote block and a syntax error there fails quietly -- the `for /f` captures
nothing, the RemoteSigned default stands, and the mark is never cleared, so an unzipped download gets
a refusal with no hint that the probe was what broke. The pre-existing setup.bat test located the
launch by literal filename, which is now an environment variable, so it matches on the flags instead.

5713 passed, 39 skipped across tests/studio/test_installer_av_shapes.py, tests/studio/install/ and
tests/security/. The single failure, test_pr10648_offline_and_status, is pre-existing on main.

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

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

* Group the hidden/bypass pair by casefolded variable name

PowerShell variable names are not case-sensitive, so $shortcutArgs and $ShortcutArgs are one
variable. The same-variable layer keyed its map on the captured spelling, so writing the two
assignments with different capitalisation filed them as two unrelated names, each holding only
one of the two flags, and the pair went through. The file-wide ratchet did not catch it either,
since install.ps1 already records one permitted relaxed policy.

Verified by constructing that exact evasion: renaming the two install.ps1 assignments to
$launchArgs and $LaunchArgs passes under the old grouping and fails under this one, naming
both spellings.

* Stop the installers from explaining, in prose a classifier reads, which antivirus flagged them (#10990)

* Stop the installers from explaining, in prose a classifier reads, which antivirus flagged them

PowerShell hands the entire top-level script block to AMSI at compile time, before the first
statement runs, so every byte of install.ps1 is classifier input -- comments included. VirusTotal's
analysis of the file quoted one of our own comments back as grounds for suspicion:

  "The presence of comments that suggest the script is designed to evade detection by security
  tools (e.g. "AMSI scans this file in full before a line of it runs") further adds to the
  suspicion."

Six hardening passes wrote careful explanations of which product flagged which shape directly into
the files being scanned. The explanations are worth keeping. Their location is not.

- A comment in a shipped script says **what the code does and what breaks if you change it**.
- `AV_SHAPES_RECORD`, in `tests/studio/test_installer_av_shapes.py`, says **which scanner flagged
  which shape, and when**.

The record lives in that test rather than a doc because the file already owns the guards that enforce
the split, it ships to nobody, it is not packaged, and nothing scans it. Each relocated comment keeps
its operational content verbatim and gains a pointer. The diff over the five shipped scripts contains
no non-comment line, which is checked mechanically rather than asserted.

The record is not a dumping ground. It holds the measured detections (Skyhigh
`BehavesLike.PS.Suspicious.gr` on install.ps1, Bitdefender `Gen:Variant.MSILHeracles.272113` on the
csc.exe output in #10540, Kaspersky `HEUR:Trojan.VBS.Agent.gen` on the .vbs launcher shape, the two
fixture archives that made Panda flag the repo zip), the measured before-and-after from
`virustotal_delta.py` including the part that did NOT move, why each decision is a robustness fix and
not an evasion, the five shapes we are **keeping** on purpose with the reason each is load-bearing,
and how to report a new detection.

Three things a future pass will otherwise rediscover are written down there, because each one already
cost a wrong proposal: execution policy is evaluated against the script file's zone and not against
who launched PowerShell, so a `.cmd` shim cannot launder a remote-zone script; UNC paths are a remote
zone, which is the whole reason install.sh resolves the Windows `%TEMP%`; and the Windows client
default is `Restricted`, so documentation telling a user to set a process-scoped policy is
load-bearing and only the word `Bypass` needed to go.

A banned-token test alone is satisfiable by deleting the knowledge rather than moving it, so there
are three:

- `test_no_shipped_script_names_a_detection` over every shipped script including `studio/setup.bat`
  and `scripts/uninstall.sh`, which no shape test read before.
- `test_the_record_survives_and_keeps_its_evidence`, which requires the sections AND the specific
  detection strings -- a record with headings and no evidence is the same loss with extra steps.
- `test_every_script_that_dropped_its_explanation_points_at_the_record`, because a comment reduced to
  "security software blocks this" with no forward reference is worse than the prose it replaced: the
  next maintainer cannot tell whether it is still true.

Deliberately not banned: `antivirus`, `quarantine`, `scanner`, `blocked`. Those describe a real
runtime hazard the code handles and one of them reaches the user.

Also not banned, for a more interesting reason: **`false positive`**. It was on the list, and the
test immediately found it in install.ps1 and studio/setup.ps1 -- where it means a *statistical* false
positive in a registry probe and has nothing to do with a scanner. A token list is only as good as
its words having one meaning, and that one does not.

- 14 of the new assertions fail against `main` and none against this branch, so they are proved to
  bite rather than to pass vacuously.
- No non-comment line changed in any of the five scripts (`git diff -U0` filtered for lines that are
  not comments returns empty).
- `bash -n` clean on both shell scripts; `Parser::ParseFile` clean on all three `.ps1` files.
- Existing tests and CI that reference the old detection strings are unaffected: they live in
  `tests/` and `.github/`, which ship to nobody and are scanned by nothing.
- One self-inflicted bug caught while writing this: a blanket rename of the old doc path rewrote the
  "must not still point at the doc" assertion into asserting the opposite. The stale path is now
  assembled from fragments so a future rename cannot do it again.

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

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

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

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

* Match banned vendor tokens on word boundaries, and drop the one that cannot work

The guard searched for each token as a raw substring, so it rejected ordinary installer prose:
'panda' is inside 'pandas', a real dependency name, and 'rising' is inside 'surprising',
'arising' and 'comprising'. The failure message then accuses a normal sentence of naming an
antivirus vendor, and the fix the next reader reaches for is to delete the guard.

Boundaries are conditional on the token's own edges, because 'heur:' and 'gen:variant' sit on a
colon where \b asserts the opposite of what is wanted.

'rising' is removed rather than bounded. It is a real engine and one of the two that flagged the
fixture archives, but the word is also ordinary English -- 'rising memory use' is a sentence
someone will write -- and no amount of boundary matching separates the two. A guard that fails on
valid prose is worth less than nothing. 'tencent', the other engine that flagged those archives,
has no such problem and stays.

Verified both directions against studio/setup.sh: a comment naming Bitdefender, Gen:Variant and
Panda still produces three failures, while 'rising memory use' plus 'installs pandas' now passes
and produced two misleading failures before.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

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

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

* Carry the AppDomain fallback into the WSL emit, and quote TEMP inside cmd

The generated script had only the static AssemblyBuilder spelling under a
bare catch, so on any host lacking that overload both SHChangeNotify
refreshes would stop with nothing printed. install.ps1 already tries both
for this reason; mirror it.

cmd expands a variable before it parses metacharacters, so an unquoted
echo %TEMP% on a profile holding an ampersand, which is legal in a
Windows account name, splits into two commands and the captured line is
not the temp path. Quote it inside cmd and strip the quotes back off. The
test stub now reproduces cmd's expand-then-parse order so the ordinary
path still passes and only the ampersand case moves.

* Keep the backend prewarm files as they are on main

An all-files formatter pass reflowed two studio/backend files that have
nothing to do with the WSL installer change and pulled them into this
PR's diff. Restore them so the blast radius is only the installer.

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

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

* Strip cmd's quotes before trimming trailing blanks

Trimming first found nothing to trim, because the closing quote was still
the last character, so a Windows TEMP ending in spaces came back
untrimmed and the directory check then cleared it. Win32 strips those
spaces from a path and [ -d ] does not.

The test stub was also wrong in a way that hid this: it consumed the
quotes itself, but cmd's echo prints its argument verbatim, quotes
included. With the stub corrected the new case fails against the previous
commit and the rest still pass.

* Fall through to a local Windows temp when TEMP is on a share

A script written under a redirected %TEMP% is a remote script, and
RemoteSigned refuses an unsigned one, so roaming-profile users would
silently stop getting the shortcut they used to get. Rather than relax
the policy back to Bypass, try %LOCALAPPDATA%\Temp and then
%SystemRoot%\Temp, skipping any candidate that is UNC or still
unexpanded. install.ps1 solves the same problem for the launcher by
relaxing the policy because the launcher cannot move; this script can.

* Reject mapped network drives as temporary locations too

Z:\Temp is the same share and the same remote zone as its UNC spelling,
and install.ps1 treats DriveType.Network as remote for that reason, but
the filter only rejected paths beginning with two backslashes. A mapped
drive was therefore accepted and then refused by RemoteSigned at launch.
Read the mapped letters from net use and skip candidates on them; if that
cannot be read the set is empty and only the UNC check applies, which is
the previous behaviour rather than a new failure.

* Keep shortcuts working when no Windows drive is mounted

With [automount] enabled=false, interop still works but no Windows
directory is reachable as a Linux path, so no candidate can be allocated
and the file-based launch cannot run at all. Those users got a shortcut
before the move off /tmp, so losing it silently is a regression rather
than a pre-existing gap.

The generated script is now captured once and either written to the
Windows temp and run with -File, or handed to powershell on stdin. The
stdin path needs no policy relaxation, because execution policy applies
to -File and not to -Command, and nothing has to be mounted. It is fed
from its own pipe so it cannot drink the installer's stdin under
curl | sh.

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

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

* Validate both halves of the compiler watch in the WSL control

The control checked only the process half, so a FileSystemWatcher that
never attached, or stopped reporting creations, looked exactly like a run
that dropped no library. TempLibraries is the half the Bitdefender report
in #10540 keyed on, and the install below trusts it. Require both, as
windows-no-compiler-ci.yml already does.

* Check the shell suite's failure counter after the last assertion

The only $FAIL check sat before the static half of the file. `bad` records a
failure and returns success, so an assertion that ran after it printed FAIL and
left the script exiting 0, which made the launch-path regression checks
ineffective in CI. Moved the summary and the check to the end of the file.

* Do not render install.sh with a Windows bash

The new here-string renderer drives install.sh through bash, and on a Windows
runner bash is Git Bash or the WSL stub in System32, neither of which install.sh
is ever run by: its WSL arm is gated on /proc/version naming microsoft, so the
shell reading this here-string is always a POSIX one inside the distro. The
rendered text is a pure function of install.sh's bytes, so the Linux and macOS
legs cover the subject and the Windows leg only contributed three failures.
Skipped there, the way the repository already skips its other bash-driven
installer tests.

Also report the shell's own diagnostic rather than letting CalledProcessError
replace it with a 6 KB repr of the script.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-09-15 13:28:16 -07:00

42 lines
3 KiB
Batchfile

@echo off
REM SPDX-License-Identifier: AGPL-3.0-only
REM Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
rem RemoteSigned, not Bypass. RemoteSigned refuses an unsigned script only in the Internet or
rem Untrusted zone, and setup.ps1 normally sits beside this file inside an installed package, so it
rem is MyComputer-zone and loads unsigned either way. Pairing a relaxed policy with a PowerShell
rem launch is a token endpoint products score, and it was buying nothing in that case. Which product
rem scored what: tests/studio/test_installer_av_shapes.py (AV_SHAPES_RECORD).
rem
rem Two populations could regress, and both are handled below rather than assumed away.
rem
rem 1. A package unzipped from a download, where setup.ps1 carries a mark of the web that
rem RemoteSigned honours and Bypass ignored. Unblock-File clears it. Execution policy governs
rem script FILES and not -Command, so that call runs whatever the machine policy is, and clearing
rem an absent stream is a no-op that writes nothing.
rem
rem 2. setup.ps1 living on a share. Execution policy is judged on the script file's ZONE, and a
rem dotted-FQDN UNC (\\corp.example.com\...), a DFS path or an IP-literal share lands in the
rem Internet zone -- where RemoteSigned refuses an unsigned script and Unblock-File cannot save
rem it, because with no Zone.Identifier stream present the PATH decides and there is nothing to
rem clear. install.ps1:3405-3421 already steps down to Bypass for exactly this case when it
rem writes the Start Menu shortcut; the probe below reuses that logic rather than inventing a
rem second answer. A plain \\server\share and a mapped drive are Intranet and stay RemoteSigned.
rem
rem The probe also does the Unblock-File, so this costs one process rather than two.
rem
rem The path travels in an environment variable rather than being interpolated into the command
rem string, so an apostrophe in the install path cannot break the quoting.
rem
rem -NoProfile on the probe only, and deliberately NOT on the launch below: setup.ps1 is run with
rem profiles loaded on purpose, and tests/studio/test_amd_venv_repair_loop.ps1 drives a profile that
rem sets Set-StrictMode against it.
setlocal
set "UNSLOTH_SETUP_SCRIPT=%~dp0setup.ps1"
set "UNSLOTH_SETUP_POLICY=RemoteSigned"
for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "$p = $env:UNSLOTH_SETUP_SCRIPT; Unblock-File -LiteralPath $p -Confirm:$false -ErrorAction SilentlyContinue; $remote = $p -like '\\*'; if (-not $remote) { try { $remote = ([System.IO.DriveInfo]::new([System.IO.Path]::GetPathRoot($p))).DriveType -eq 'Network' } catch {} }; if ($remote) { 'Bypass' } else { 'RemoteSigned' }"`) do set "UNSLOTH_SETUP_POLICY=%%P"
rem Defaulting to RemoteSigned above means a probe that fails to run leaves the tightened policy in
rem place rather than silently restoring the relaxed one.
powershell -ExecutionPolicy %UNSLOTH_SETUP_POLICY% -File "%UNSLOTH_SETUP_SCRIPT%" %*
exit /b %ERRORLEVEL%