Commit graph

1 commit

Author SHA1 Message Date
Daniel Han
479b46e8a9
studio setup: let Windows PowerShell load its own Security module (#7692)
* studio setup: let Windows PowerShell load its own Security module

`unsloth studio update` failed on Windows when launched from a PowerShell 7
prompt:

    installing uv package manager...
    The 'Get-ExecutionPolicy' command was found in the module
    'Microsoft.PowerShell.Security', but the module could not be loaded.

and stopped there with exit 1 and no further output. setup.ps1 installs uv by
running astral's install.ps1, which calls Get-ExecutionPolicy, and
Invoke-SetupCommand makes a failure there fatal.

_run_setup_script spawns powershell.exe, which is Windows PowerShell 5.1, and
the child inherits the caller's PSModulePath. From a pwsh 7 prompt that path
leads with PowerShell 7's module directories, which ship their own
Microsoft.PowerShell.Security. 5.1 finds that copy first and cannot load it.

The problem is precedence, not absence, so the system directory is prepended.
Two weaker variants were tested on a windows-latest runner and both still
failed: dropping PSModulePath so 5.1 rebuilds its default (the machine-level
value on that image also leads with PS7), and appending the system directory
(the PS7 copy is still found first).

Confirmed by a patched/unpatched pair on the same runner and the same PyPI
install, differing only in this file:

    patched     update exit 0, Security module error: False
    unpatched   update exit 1, Security module error: True

Guarded on PSEdition so the block is inert under PowerShell 7 itself, and on
Test-Path so a non-standard SystemRoot is a no-op.

pwsh is what Windows Terminal's default profile and the `pwsh` command give a
modern user, and there was no Windows coverage of `unsloth studio update` in
CI, so this went unnoticed.

* Apply the same PSModulePath fix to install.ps1, which has the same exposure

install.ps1:1493 runs astral's uv installer in-process the same way setup.ps1
does, and that installer calls Get-ExecutionPolicy out of
Microsoft.PowerShell.Security. The desktop app reaches it as
Tauri -> Rust -> powershell.exe (studio/src-tauri/src/install.rs:326), and
PowerShell rewrites PSModulePath only for a direct pwsh -> powershell.exe hop,
so the Rust process in between leaves Windows PowerShell 5.1 leading with
PowerShell 7's module directories. PowerShell/PowerShell#18681 is this exact
chain through an intermediate process.

The --shortcuts-only path returns at install.ps1:1104 before reaching the uv
install, so the `unsloth studio update` shortcut refresh was never exposed.
scripts/uninstall.ps1 loads no Security cmdlet and spawns no shell, so it needs
nothing.

Also guards on $env:SystemRoot, since Join-Path throws on an empty or null Path
under ErrorActionPreference Stop and this runs before anything else, and
corrects the comment about why the failure is fatal: the uv call is wrapped in
try/catch, so what ends the run is that Invoke-Expression executes the installer
in this process.

* Record why PSModulePath is not restored

The reordering is process scope, so it outlives the script in an
interactive console. That is a deliberate trade, not an oversight, and
the reasoning was only in the PR discussion. Comment only.

* Keep PSModulePath out of the registry refresh

Refresh-Environment reloads every Machine and User variable except Path,
which put the broken machine-level PSModulePath back over the
normalization at the top of the file. Eight of its call sites run before
the uv installer at setup.ps1:3080, and that installer is what loads
Microsoft.PowerShell.Security, so the fix was undone exactly where it has
to hold. PSModulePath now joins Path as an exception.

Adds tests/studio/test_psmodulepath_normalization.ps1, which pins both
halves: the system directory is prepended rather than appended in both
entry points, and Refresh-Environment leaves PSModulePath alone. It fails
against the previous commit.
2026-08-01 03:05:44 -07:00