mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-17 04:43:52 +00:00
* 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.
83 lines
3.7 KiB
PowerShell
83 lines
3.7 KiB
PowerShell
#!/usr/bin/env pwsh
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
# Unit test for the PSModulePath normalization in studio/setup.ps1 and install.ps1.
|
|
#
|
|
# Windows PowerShell 5.1 cannot load its own Microsoft.PowerShell.Security when it
|
|
# inherits PowerShell 7's PSModulePath, which happens whenever a process sits
|
|
# between pwsh and powershell.exe (PowerShell/PowerShell#18681). Astral's uv
|
|
# installer calls Get-ExecutionPolicy out of that module, so the install dies.
|
|
#
|
|
# Two properties have to hold together, and the second is easy to lose: the
|
|
# system module directory must be PREPENDED (appending still finds the PS7 copy
|
|
# first), and Refresh-Environment must not reload PSModulePath from the registry
|
|
# afterwards, because most of its callers run before the uv installer.
|
|
#
|
|
# Run: pwsh -NoProfile -File tests/studio/test_psmodulepath_normalization.ps1
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$repoRoot = (Resolve-Path ([System.IO.Path]::Combine($PSScriptRoot, "..", ".."))).Path
|
|
$setupPath = [System.IO.Path]::Combine($repoRoot, "studio", "setup.ps1")
|
|
$installPath = [System.IO.Path]::Combine($repoRoot, "install.ps1")
|
|
|
|
$failures = 0
|
|
function Check($name, $cond) {
|
|
if ($cond) { Write-Host " PASS $name" }
|
|
else { Write-Host " FAIL $name" -ForegroundColor Red; $script:failures++ }
|
|
}
|
|
|
|
function Get-Ast($path) {
|
|
$tokens = $null; $errors = $null
|
|
$ast = [System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors)
|
|
if ($errors) { $errors | ForEach-Object { $_.ToString() }; throw "$path has parse errors" }
|
|
return $ast
|
|
}
|
|
|
|
Write-Host "the normalization block prepends, in both entry points"
|
|
foreach ($pair in @(@{ Name = "studio/setup.ps1"; Path = $setupPath }, @{ Name = "install.ps1"; Path = $installPath })) {
|
|
$ast = Get-Ast $pair.Path
|
|
$text = $ast.Extent.Text
|
|
Check "$($pair.Name) parses" $true
|
|
Check "$($pair.Name) guards on PSEdition" ($text -match "PSVersionTable\.PSEdition -ne 'Core'")
|
|
Check "$($pair.Name) targets the 5.1 system module dir" ($text -match "System32\\WindowsPowerShell\\v1\.0\\Modules")
|
|
# Prepended, not appended: @($sys) + $kept, never $kept + @($sys).
|
|
Check "$($pair.Name) puts the system dir first" (
|
|
$text -match '\(@\(\$_UnslothSystemModules\)\s*\+\s*\$_UnslothKept\)'
|
|
)
|
|
Check "$($pair.Name) does not append it instead" (
|
|
-not ($text -match '\(\$_UnslothKept\s*\+\s*@\(\$_UnslothSystemModules\)\)')
|
|
)
|
|
}
|
|
|
|
Write-Host "Refresh-Environment leaves PSModulePath alone"
|
|
$setupAst = Get-Ast $setupPath
|
|
$fn = $setupAst.FindAll({ param($n)
|
|
$n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq "Refresh-Environment"
|
|
}, $true)
|
|
Check "exactly one Refresh-Environment" ($fn.Count -eq 1)
|
|
$fnText = $fn[0].Extent.Text
|
|
Check "it skips PSModulePath as well as Path" ($fnText -match "\`$key -eq 'PSModulePath'")
|
|
|
|
# Behavioural: the registry reload must not clobber a value already normalized.
|
|
# On non-Windows GetEnvironmentVariables('Machine') is empty, so this leg only
|
|
# proves the function is callable there; the AST check above is what holds the
|
|
# line cross-platform.
|
|
$savedPath = $env:Path
|
|
$savedModulePath = $env:PSModulePath
|
|
try {
|
|
Invoke-Expression $fnText
|
|
$sentinel = "C:\__unsloth_sentinel__;C:\Windows\System32\WindowsPowerShell\v1.0\Modules"
|
|
$env:PSModulePath = $sentinel
|
|
Refresh-Environment
|
|
Check "a normalized PSModulePath survives a refresh" ($env:PSModulePath -eq $sentinel)
|
|
} finally {
|
|
$env:Path = $savedPath
|
|
$env:PSModulePath = $savedModulePath
|
|
}
|
|
|
|
Write-Host ""
|
|
if ($failures -gt 0) {
|
|
Write-Host "Results: $failures failed" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
Write-Host "Results: all passed"
|