mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(installer): auto-detect SYSTEM account and default PATH scope to machine (#4903)
* fix(installer): auto-detect SYSTEM account and default PATH scope to machine When the Windows standalone installer runs as the SYSTEM account (SID S-1-5-18), which is common for ECS Workbench SSM, WinRM, and scheduled tasks, the default --path-scope is now 'machine' instead of 'user'. Previously, persisting PATH to the SYSTEM user's HKCU hive had no effect on new sessions spawned by the SSM agent service, because the agent inherits its environment at service start time and never re-reads HKCU. Writing to the Machine PATH (HKLM) ensures new sessions pick up the qwen shim immediately. Also adds: - --repair-path flag to fix PATH for an existing install without reinstalling - --path-scope user|machine CLI option and QWEN_INSTALL_PATH_SCOPE env var - Uninstaller now cleans both User and Machine PATH entries - CI validation step for hosted installation assets Resolves #4901 * test(installer): add Windows self-hosted qwen smoke * fix(installer): address Windows PATH review feedback * refactor(installer): remove redundant YAML validation, deduplicate test fixtures - Remove windows-self-hosted-qwen-smoke.yml (operational tool, not part of fix) - Revert sync-release-to-oss.yml validate step (redundant with JS behavior guards) - Extract duplicated test fixture strings into shared constants - Pass normalized path to Remove-PathEntry to avoid double Get-NormalizedPath - Fix stale comment referencing "user PATH" when scope is now configurable * chore: drop unrelated dev.test.js changes from installer PR
This commit is contained in:
parent
01bd2b64d1
commit
968a0d1f03
5 changed files with 225 additions and 56 deletions
|
|
@ -71,6 +71,22 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = {
|
|||
name: '--version parser',
|
||||
pattern: /ARG_KEY!"=="--version"|"%~1"=="--version"/,
|
||||
},
|
||||
{
|
||||
name: '--repair-path parser',
|
||||
pattern: /if \/i "%~1"=="--repair-path"/,
|
||||
},
|
||||
{
|
||||
name: '--path-scope parser',
|
||||
pattern: /if \/i "!ARG_KEY!"=="--path-scope"/,
|
||||
},
|
||||
{
|
||||
name: 'QWEN_INSTALL_REPAIR_PATH',
|
||||
pattern: /QWEN_INSTALL_REPAIR_PATH/,
|
||||
},
|
||||
{
|
||||
name: 'QWEN_INSTALL_PATH_SCOPE',
|
||||
pattern: /QWEN_INSTALL_PATH_SCOPE/,
|
||||
},
|
||||
],
|
||||
'install-qwen-standalone.ps1': [
|
||||
{
|
||||
|
|
@ -111,7 +127,7 @@ const HOSTED_INSTALLER_BEHAVIOR_PATTERNS = {
|
|||
},
|
||||
{
|
||||
name: 'PATH cleanup',
|
||||
pattern: /Remove-UserPathEntry/,
|
||||
pattern: /Remove-PathEntryFromAllScopes/,
|
||||
},
|
||||
{
|
||||
name: 'current cmd shim cleanup',
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ The installers are intentionally lightweight:
|
|||
- They try a standalone archive first by default.
|
||||
- They do not install Node.js, NVM, or any other Node version manager.
|
||||
- They do not edit npm config. Standalone installs may update the shell profile
|
||||
or user PATH so the generated `qwen` shim is discoverable.
|
||||
or user/machine PATH so the generated `qwen` shim is discoverable.
|
||||
- They do not start `qwen` automatically after installation.
|
||||
- They store source information in `~/.qwen/source.json` or
|
||||
`%USERPROFILE%\.qwen\source.json` when `--source` is provided.
|
||||
|
|
@ -133,6 +133,14 @@ install-qwen-standalone.bat --method standalone
|
|||
install-qwen-standalone.bat --method npm
|
||||
```
|
||||
|
||||
Repair PATH for an existing standalone Windows install without downloading or
|
||||
reinstalling Qwen Code:
|
||||
|
||||
```bat
|
||||
install-qwen-standalone.bat --repair-path
|
||||
install-qwen-standalone.bat --repair-path --path-scope machine
|
||||
```
|
||||
|
||||
## Optional Native Modules
|
||||
|
||||
The standalone archives bundle Qwen Code and a private Node.js runtime. They do
|
||||
|
|
@ -211,6 +219,18 @@ Standalone installs to:
|
|||
Override with `QWEN_INSTALL_ROOT`, `QWEN_INSTALL_LIB_DIR`, or
|
||||
`QWEN_INSTALL_BIN_DIR` when needed.
|
||||
|
||||
For self-hosted Windows runners, install under the runner account once, then
|
||||
repair machine-level PATH without reinstalling:
|
||||
|
||||
```powershell
|
||||
$env:QWEN_INSTALL_REPAIR_PATH = '1'
|
||||
$env:QWEN_INSTALL_PATH_SCOPE = 'machine'
|
||||
irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.ps1 | iex
|
||||
```
|
||||
|
||||
Restart the runner service or machine after updating machine-level PATH so jobs
|
||||
inherit the refreshed environment.
|
||||
|
||||
Restart the terminal if `qwen` is not immediately available on PATH.
|
||||
|
||||
Uninstall a standalone Windows install:
|
||||
|
|
@ -220,10 +240,11 @@ powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangz
|
|||
```
|
||||
|
||||
The uninstaller removes only the standalone runtime, generated `qwen.cmd`
|
||||
wrapper, user PATH entry, and the current-session `cmd.exe` shim created by the
|
||||
hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen` by default. Set
|
||||
`QWEN_UNINSTALL_PURGE=1` to remove `%USERPROFILE%\.qwen\source.json`; other
|
||||
config and auth files are still preserved.
|
||||
wrapper, user or machine PATH entry, and the current-session `cmd.exe` shim
|
||||
created by the hosted PowerShell installer. It preserves `%USERPROFILE%\.qwen`
|
||||
by default. Set `QWEN_UNINSTALL_PURGE=1` to remove
|
||||
`%USERPROFILE%\.qwen\source.json`; other config and auth files are still
|
||||
preserved.
|
||||
|
||||
## Mirrors and Overrides
|
||||
|
||||
|
|
@ -236,6 +257,8 @@ Options:
|
|||
- `--version VERSION`
|
||||
- `--registry REGISTRY`
|
||||
- `--source SOURCE`
|
||||
- `--repair-path`
|
||||
- `--path-scope user|machine`
|
||||
|
||||
Environment variables:
|
||||
|
||||
|
|
@ -245,6 +268,8 @@ Environment variables:
|
|||
- `QWEN_INSTALL_ARCHIVE`
|
||||
- `QWEN_INSTALL_VERSION`
|
||||
- `QWEN_NPM_REGISTRY`
|
||||
- `QWEN_INSTALL_REPAIR_PATH`
|
||||
- `QWEN_INSTALL_PATH_SCOPE`
|
||||
|
||||
Use `--base-url` for private mirrors. The URL must contain
|
||||
`qwen-code-<target>` archives and `SHA256SUMS` in the same directory. Custom
|
||||
|
|
|
|||
|
|
@ -15,6 +15,16 @@ set "MIRROR=auto"
|
|||
if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!"
|
||||
set "NO_MODIFY_PATH=0"
|
||||
if defined QWEN_NO_MODIFY_PATH set "NO_MODIFY_PATH=!QWEN_NO_MODIFY_PATH!"
|
||||
set "REPAIR_PATH=0"
|
||||
if defined QWEN_INSTALL_REPAIR_PATH set "REPAIR_PATH=!QWEN_INSTALL_REPAIR_PATH!"
|
||||
set "PATH_SCOPE=user"
|
||||
REM Auto-detect SYSTEM account and default to machine scope so that new sessions
|
||||
REM spawned by service processes (SSM agent, WinRM, scheduled tasks) inherit PATH.
|
||||
if not defined QWEN_INSTALL_PATH_SCOPE (
|
||||
whoami /user 2>nul | findstr /i "S-1-5-18" >nul 2>&1
|
||||
if !ERRORLEVEL! EQU 0 set "PATH_SCOPE=machine"
|
||||
)
|
||||
if defined QWEN_INSTALL_PATH_SCOPE set "PATH_SCOPE=!QWEN_INSTALL_PATH_SCOPE!"
|
||||
set "BASE_URL="
|
||||
if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!"
|
||||
set "ARCHIVE_PATH="
|
||||
|
|
@ -194,6 +204,30 @@ if /i "%~1"=="--no-modify-path" (
|
|||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if /i "%~1"=="--repair-path" (
|
||||
set "REPAIR_PATH=1"
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if /i "!ARG_KEY!"=="--path-scope" (
|
||||
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
||||
if "!ARG_VALUE!"=="" (
|
||||
echo ERROR: --path-scope requires a value
|
||||
exit /b 1
|
||||
)
|
||||
set "PATH_SCOPE=!ARG_VALUE!"
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if "%~2"=="" (
|
||||
echo ERROR: --path-scope requires a value
|
||||
exit /b 1
|
||||
)
|
||||
set "PATH_SCOPE=%~2"
|
||||
shift
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if /i "%~1"=="-h" goto usage
|
||||
if /i "%~1"=="--help" goto usage
|
||||
|
||||
|
|
@ -206,6 +240,13 @@ goto usage_error
|
|||
call :ValidateOptions
|
||||
if %ERRORLEVEL% NEQ 0 exit /b 1
|
||||
|
||||
if /i "!REPAIR_PATH!"=="1" (
|
||||
call :RepairPath
|
||||
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
|
||||
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
call :PrintHeader
|
||||
|
||||
REM Discover all qwen executables on disk BEFORE we install. We can't
|
||||
|
|
@ -302,7 +343,9 @@ echo --base-url URL Override standalone archive base URL
|
|||
echo --archive PATH Install from a local standalone archive
|
||||
echo --version VERSION Release version (default: latest)
|
||||
echo --registry URL npm registry (default: https://registry.npmmirror.com)
|
||||
echo --no-modify-path Do not modify user PATH
|
||||
echo --no-modify-path Do not modify PATH
|
||||
echo --repair-path Repair PATH for an existing standalone install
|
||||
echo --path-scope SCOPE PATH scope: user or machine (default: user, auto machine for SYSTEM)
|
||||
echo -s, --source SOURCE Record installation source
|
||||
echo -h, --help Show this help message
|
||||
exit /b 0
|
||||
|
|
@ -329,7 +372,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "$esc = [char]27; $bar =
|
|||
exit /b 0
|
||||
|
||||
:ValidateRawEnvironmentOptions
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_REPAIR_PATH','QWEN_INSTALL_PATH_SCOPE','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0"
|
||||
if %ERRORLEVEL% EQU 0 exit /b 0
|
||||
echo ERROR: installer options contain unsafe command characters.
|
||||
exit /b 1
|
||||
|
|
@ -347,11 +390,12 @@ set "QWEN_VALIDATE_INSTALL_BASE=!INSTALL_BASE!"
|
|||
set "QWEN_VALIDATE_INSTALL_DIR=!INSTALL_DIR!"
|
||||
set "QWEN_VALIDATE_INSTALL_BIN_DIR=!INSTALL_BIN_DIR!"
|
||||
set "QWEN_VALIDATE_SOURCE=!SOURCE!"
|
||||
set "QWEN_VALIDATE_PATH_SCOPE=!PATH_SCOPE!"
|
||||
call :CreateTempFile "qwen-validate-options" ".ps1"
|
||||
if !ERRORLEVEL! NEQ 0 exit /b 1
|
||||
set "QWEN_VALIDATE_OPTIONS_SCRIPT=!TEMP_FILE!"
|
||||
> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124)
|
||||
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE')
|
||||
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE','PATH_SCOPE')
|
||||
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo foreach ($name in $names) {
|
||||
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name)
|
||||
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 }
|
||||
|
|
@ -371,6 +415,7 @@ set "QWEN_VALIDATE_INSTALL_BASE="
|
|||
set "QWEN_VALIDATE_INSTALL_DIR="
|
||||
set "QWEN_VALIDATE_INSTALL_BIN_DIR="
|
||||
set "QWEN_VALIDATE_SOURCE="
|
||||
set "QWEN_VALIDATE_PATH_SCOPE="
|
||||
if %PS_STATUS% NEQ 0 (
|
||||
echo ERROR: installer options contain unsafe command characters.
|
||||
exit /b 1
|
||||
|
|
@ -421,6 +466,18 @@ echo ERROR: --mirror must be auto, github, or aliyun.
|
|||
exit /b 1
|
||||
|
||||
:validate_mirror_ok
|
||||
if "!REPAIR_PATH!"=="0" goto validate_repair_path_ok
|
||||
if "!REPAIR_PATH!"=="1" goto validate_repair_path_ok
|
||||
echo ERROR: QWEN_INSTALL_REPAIR_PATH must be 0 or 1.
|
||||
exit /b 1
|
||||
|
||||
:validate_repair_path_ok
|
||||
if /i "!PATH_SCOPE!"=="user" goto validate_path_scope_ok
|
||||
if /i "!PATH_SCOPE!"=="machine" goto validate_path_scope_ok
|
||||
echo ERROR: --path-scope must be user or machine.
|
||||
exit /b 1
|
||||
|
||||
:validate_path_scope_ok
|
||||
call :ValidateHttpsUrlVar "BASE_URL" "--base-url"
|
||||
if %ERRORLEVEL% NEQ 0 exit /b 1
|
||||
|
||||
|
|
@ -592,17 +649,19 @@ set "GITHUB_FALLBACK_BASE_URL="
|
|||
set "MIRROR=github"
|
||||
exit /b 0
|
||||
|
||||
:MaybeUpdateUserPath
|
||||
:MaybeUpdatePath
|
||||
rem args: %~1=install_bin_dir
|
||||
rem Prepend the install dir to the user-level PATH (HKCU\Environment) via
|
||||
rem Prepend the install dir to the selected PATH scope via
|
||||
rem [Environment]::SetEnvironmentVariable. Idempotent: skips if the dir is
|
||||
rem already on the user PATH. Uses PowerShell rather than `setx` because setx
|
||||
rem already on PATH. Uses PowerShell rather than `setx` because setx
|
||||
rem truncates PATH at 1024 chars, which can silently mangle long PATHs.
|
||||
set "QWEN_NEW_BIN=%~1"
|
||||
if "!QWEN_NEW_BIN!"=="" exit /b 0
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$bin = $env:QWEN_NEW_BIN; $userPath = [Environment]::GetEnvironmentVariable('Path', 'User'); if ([string]::IsNullOrEmpty($userPath)) { $userPath = '' }; $entries = @($userPath -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { $_ -ne $bin }); if ($entries.Count -gt 0 -and $entries[0] -eq $bin -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, 'User'); exit 0"
|
||||
set "QWEN_PATH_SCOPE=!PATH_SCOPE!"
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { $bin = $env:QWEN_NEW_BIN; $scope = $env:QWEN_PATH_SCOPE; $target = if ($scope -ieq 'machine') { 'Machine' } else { 'User' }; $pathValue = [Environment]::GetEnvironmentVariable('Path', $target); if ([string]::IsNullOrEmpty($pathValue)) { $pathValue = '' }; $entries = @($pathValue -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { -not [string]::Equals($_, $bin, [StringComparison]::OrdinalIgnoreCase) }); if ($entries.Count -gt 0 -and [string]::Equals($entries[0], $bin, [StringComparison]::OrdinalIgnoreCase) -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, $target); exit 0 } catch { exit 1 }"
|
||||
set "PS_STATUS=%ERRORLEVEL%"
|
||||
set "QWEN_NEW_BIN="
|
||||
set "QWEN_PATH_SCOPE="
|
||||
exit /b %PS_STATUS%
|
||||
|
||||
:UrlExists
|
||||
|
|
@ -1014,6 +1073,35 @@ if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|||
REM Standalone archive installed to !INSTALL_DIR!
|
||||
exit /b 0
|
||||
|
||||
:RepairPath
|
||||
set "INSTALLED_BIN=!INSTALL_BIN_DIR!\qwen.cmd"
|
||||
if not exist "!INSTALLED_BIN!" (
|
||||
echo ERROR: Qwen Code standalone wrapper was not found:
|
||||
echo !INSTALLED_BIN!
|
||||
echo ERROR: Set QWEN_INSTALL_ROOT or QWEN_INSTALL_BIN_DIR to the existing install, or install Qwen Code first.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
|
||||
if /i not "!NO_MODIFY_PATH!"=="1" (
|
||||
call :MaybeUpdatePath "!INSTALL_BIN_DIR!"
|
||||
if !ERRORLEVEL! NEQ 0 (
|
||||
echo ERROR: Failed to update !PATH_SCOPE! PATH. Run as administrator for --path-scope machine, or add this directory manually:
|
||||
echo !INSTALL_BIN_DIR!
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
set "INSTALLED_VERSION=unknown"
|
||||
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
|
||||
|
||||
echo.
|
||||
echo Qwen Code !INSTALLED_VERSION! is already installed.
|
||||
echo PATH repaired for !PATH_SCOPE! scope:
|
||||
echo !INSTALL_BIN_DIR!
|
||||
echo.
|
||||
exit /b 0
|
||||
|
||||
:CreateTempDir
|
||||
set "TEMP_DIR="
|
||||
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $dir = Join-Path $env:TEMP ('qwen-code-install-' + [IO.Path]::GetRandomFileName()); New-Item -ItemType Directory -Path $dir -ErrorAction Stop | Out-Null; [Console]::Write($dir)"`) do set "TEMP_DIR=%%I"
|
||||
|
|
@ -1239,11 +1327,11 @@ if not "!INSTALLED_BIN!"=="" if exist "!INSTALLED_BIN!" (
|
|||
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
|
||||
)
|
||||
|
||||
rem Persist the install bin to user PATH unless --no-modify-path is set.
|
||||
rem Persist the install bin to PATH unless --no-modify-path is set.
|
||||
if not "!EXTRA_BIN!"=="" if /i not "!NO_MODIFY_PATH!"=="1" (
|
||||
call :MaybeUpdateUserPath "!EXTRA_BIN!"
|
||||
call :MaybeUpdatePath "!EXTRA_BIN!"
|
||||
if !ERRORLEVEL! NEQ 0 (
|
||||
echo WARNING: Failed to update user PATH. Add the directory manually:
|
||||
echo WARNING: Failed to update !PATH_SCOPE! PATH. Add the directory manually:
|
||||
echo !EXTRA_BIN!
|
||||
) else (
|
||||
set "PATH_UPDATE_APPLIED=1"
|
||||
|
|
|
|||
|
|
@ -150,22 +150,25 @@ function Test-QwenStandaloneInstallDir {
|
|||
return $true
|
||||
}
|
||||
|
||||
function Remove-UserPathEntry {
|
||||
param([string]$BinDir)
|
||||
function Remove-PathEntry {
|
||||
param(
|
||||
[string]$BinDir,
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
$target = Get-NormalizedPath -PathValue $BinDir
|
||||
if ([string]::IsNullOrEmpty($target)) {
|
||||
return
|
||||
}
|
||||
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
if ([string]::IsNullOrEmpty($userPath)) {
|
||||
$pathValue = [Environment]::GetEnvironmentVariable('Path', $Scope)
|
||||
if ([string]::IsNullOrEmpty($pathValue)) {
|
||||
return
|
||||
}
|
||||
|
||||
$kept = New-Object System.Collections.Generic.List[string]
|
||||
$removed = $false
|
||||
foreach ($entry in @($userPath -split ';')) {
|
||||
foreach ($entry in @($pathValue -split ';')) {
|
||||
if ([string]::IsNullOrEmpty($entry)) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -179,9 +182,25 @@ function Remove-UserPathEntry {
|
|||
}
|
||||
|
||||
if ($removed) {
|
||||
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), 'User')
|
||||
Write-Success "Removed $BinDir from user PATH."
|
||||
try {
|
||||
[Environment]::SetEnvironmentVariable('Path', ($kept -join ';'), $Scope)
|
||||
Write-Success "Removed $BinDir from $Scope PATH."
|
||||
} catch {
|
||||
Write-WarningMessage "Could not remove $BinDir from $Scope PATH: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-PathEntryFromAllScopes {
|
||||
param([string]$BinDir)
|
||||
|
||||
$target = Get-NormalizedPath -PathValue $BinDir
|
||||
if ([string]::IsNullOrEmpty($target)) {
|
||||
return
|
||||
}
|
||||
|
||||
Remove-PathEntry -BinDir $target -Scope 'User'
|
||||
Remove-PathEntry -BinDir $target -Scope 'Machine'
|
||||
|
||||
$current = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($entry in @($env:Path -split ';')) {
|
||||
|
|
@ -360,7 +379,7 @@ if ($installWasManaged) {
|
|||
Write-Info "Leaving $(Join-Path $installBinDir 'qwen.cmd') unchanged because no managed standalone runtime was removed."
|
||||
}
|
||||
|
||||
Remove-UserPathEntry -BinDir $installBinDir
|
||||
Remove-PathEntryFromAllScopes -BinDir $installBinDir
|
||||
Remove-SourceMarker
|
||||
if ([string]::IsNullOrEmpty($env:QWEN_INSTALL_BIN_DIR)) {
|
||||
Remove-EmptyDirectory -Directory $installBinDir
|
||||
|
|
|
|||
|
|
@ -346,7 +346,9 @@ describe('installation scripts', () => {
|
|||
expect(script).toContain('qwen-code\\node\\node.exe');
|
||||
expect(script).toContain('Archive contains symlinks or reparse points');
|
||||
expect(script).toContain('unsafe path with control character');
|
||||
expect(script).toContain('Failed to update user PATH');
|
||||
expect(script).toContain('Failed to update !PATH_SCOPE! PATH');
|
||||
expect(script).toContain("$ErrorActionPreference = 'Stop'; try");
|
||||
expect(script).toContain('catch { exit 1 }');
|
||||
expect(script).toContain('PRE_INSTALL_QWENS_LIST');
|
||||
expect(script).toContain('QWEN_INSTALL_ROOT');
|
||||
expect(script).toContain('npm fallback also failed');
|
||||
|
|
@ -574,6 +576,35 @@ describe('release-script-utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
const STUB_BAT_CONTENT =
|
||||
'@echo off\r\n' +
|
||||
'set "VERSION=%QWEN_INSTALL_VERSION%"\r\n' +
|
||||
'set "REPAIR_PATH=%QWEN_INSTALL_REPAIR_PATH%"\r\n' +
|
||||
'set "PATH_SCOPE=%QWEN_INSTALL_PATH_SCOPE%"\r\n' +
|
||||
'if "%VERSION%"=="" set "VERSION=latest"\r\n' +
|
||||
'set "VERSION=latest"\r\n' +
|
||||
'if "%~1"=="--version" set "VERSION=%~2"\r\n' +
|
||||
'if /i "%~1"=="--repair-path" set "REPAIR_PATH=1"\r\n' +
|
||||
'set "ARG_KEY=%~1"\r\n' +
|
||||
'if /i "!ARG_KEY!"=="--path-scope" set "PATH_SCOPE=%~2"\r\n';
|
||||
|
||||
const STUB_SH_CONTENT =
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
|
||||
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n';
|
||||
|
||||
const STUB_UNINSTALL_SH_CONTENT =
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'is_qwen_standalone_install_dir() { return 0; }\n' +
|
||||
'remove_shell_path_entry() { :; }\n' +
|
||||
'QWEN_UNINSTALL_PURGE=""\n';
|
||||
|
||||
const STUB_UNINSTALL_PS1_CONTENT =
|
||||
'function Test-QwenStandaloneInstallDir { return $true }\n' +
|
||||
'function Remove-PathEntryFromAllScopes { }\n' +
|
||||
'function Remove-CurrentCmdPathShim { }\n' +
|
||||
'$env:QWEN_UNINSTALL_PURGE = ""\n';
|
||||
|
||||
describe('standalone release packaging', () => {
|
||||
it('defines a standalone packaging script', () => {
|
||||
const packageJson = JSON.parse(readScript('package.json'));
|
||||
|
|
@ -1049,13 +1080,11 @@ describe('standalone release packaging', () => {
|
|||
mkdirSync(sourceDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'install-qwen-standalone.sh'),
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
|
||||
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n',
|
||||
STUB_SH_CONTENT,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'install-qwen-standalone.bat'),
|
||||
'@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n',
|
||||
STUB_BAT_CONTENT,
|
||||
);
|
||||
// The ps1 shim has every required behavior pattern but also contains
|
||||
// a hardcoded $env:QWEN_INSTALL_VERSION assignment, which must be
|
||||
|
|
@ -1070,17 +1099,11 @@ describe('standalone release packaging', () => {
|
|||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'uninstall-qwen-standalone.sh'),
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'is_qwen_standalone_install_dir() { return 0; }\n' +
|
||||
'remove_shell_path_entry() { :; }\n' +
|
||||
'QWEN_UNINSTALL_PURGE=""\n',
|
||||
STUB_UNINSTALL_SH_CONTENT,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'uninstall-qwen-standalone.ps1'),
|
||||
'function Test-QwenStandaloneInstallDir { return $true }\n' +
|
||||
'function Remove-UserPathEntry { }\n' +
|
||||
'function Remove-CurrentCmdPathShim { }\n' +
|
||||
'$env:QWEN_UNINSTALL_PURGE = ""\n',
|
||||
STUB_UNINSTALL_PS1_CONTENT,
|
||||
);
|
||||
|
||||
await expect(
|
||||
|
|
@ -1106,13 +1129,11 @@ describe('standalone release packaging', () => {
|
|||
mkdirSync(sourceDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'install-qwen-standalone.sh'),
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'VERSION="${QWEN_INSTALL_VERSION:-latest}"\n' +
|
||||
'case "$1" in --version) shift; VERSION="$1" ;; --version=*) VERSION="${1#*=}" ;; esac\n',
|
||||
STUB_SH_CONTENT,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'install-qwen-standalone.bat'),
|
||||
'@echo off\r\nset "VERSION=%QWEN_INSTALL_VERSION%"\r\nif "%VERSION%"=="" set "VERSION=latest"\r\nset "VERSION=latest"\r\nif "%~1"=="--version" set "VERSION=%~2"\r\n',
|
||||
STUB_BAT_CONTENT,
|
||||
);
|
||||
// ps1 contains the exact docstring shipped in production
|
||||
// ("$env:QWEN_INSTALL_VERSION = 'vX.Y.Z'") as a `#` comment; the
|
||||
|
|
@ -1128,17 +1149,11 @@ describe('standalone release packaging', () => {
|
|||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'uninstall-qwen-standalone.sh'),
|
||||
'#!/usr/bin/env bash\n' +
|
||||
'is_qwen_standalone_install_dir() { return 0; }\n' +
|
||||
'remove_shell_path_entry() { :; }\n' +
|
||||
'QWEN_UNINSTALL_PURGE=""\n',
|
||||
STUB_UNINSTALL_SH_CONTENT,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(sourceDir, 'uninstall-qwen-standalone.ps1'),
|
||||
'function Test-QwenStandaloneInstallDir { return $true }\n' +
|
||||
'function Remove-UserPathEntry { }\n' +
|
||||
'function Remove-CurrentCmdPathShim { }\n' +
|
||||
'$env:QWEN_UNINSTALL_PURGE = ""\n',
|
||||
STUB_UNINSTALL_PS1_CONTENT,
|
||||
);
|
||||
|
||||
// Build should succeed (only resolves; throws would fail the test).
|
||||
|
|
@ -1804,7 +1819,7 @@ describe('standalone release packaging', () => {
|
|||
expect(releaseWorkflow).not.toContain('verify_node_checksum()');
|
||||
expect(releaseWorkflow).not.toContain('download_node()');
|
||||
const createReleaseStepIndex = releaseWorkflow.indexOf(
|
||||
"name: 'Create GitHub Release and Tag'",
|
||||
"- name: 'Create GitHub Release and Tag'",
|
||||
);
|
||||
expect(createReleaseStepIndex).toBeGreaterThanOrEqual(0);
|
||||
const createReleaseStep = releaseWorkflow.slice(createReleaseStepIndex);
|
||||
|
|
@ -1844,21 +1859,25 @@ describe('standalone release packaging', () => {
|
|||
);
|
||||
|
||||
const syncStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Sync Release Assets to Aliyun OSS'",
|
||||
"- name: 'Sync Release Assets to Aliyun OSS'",
|
||||
);
|
||||
const packageHostedStepIndex = ossWorkflow.indexOf(
|
||||
"- name: 'Package Hosted Installation Assets'",
|
||||
);
|
||||
const verifyStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Verify Aliyun OSS Release Assets'",
|
||||
"- name: 'Verify Aliyun OSS Release Assets'",
|
||||
);
|
||||
const publishLatestStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Publish Aliyun OSS Latest VERSION'",
|
||||
"- name: 'Publish Aliyun OSS Latest VERSION'",
|
||||
);
|
||||
const syncHostedStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Sync Hosted Installation Assets to Aliyun OSS'",
|
||||
"- name: 'Sync Hosted Installation Assets to Aliyun OSS'",
|
||||
);
|
||||
const verifyHostedStepIndex = ossWorkflow.indexOf(
|
||||
"name: 'Verify Aliyun OSS Hosted Installation Assets'",
|
||||
"- name: 'Verify Aliyun OSS Hosted Installation Assets'",
|
||||
);
|
||||
expect(syncStepIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(packageHostedStepIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(verifyStepIndex).toBeGreaterThan(syncStepIndex);
|
||||
expect(syncHostedStepIndex).toBeGreaterThan(verifyStepIndex);
|
||||
expect(verifyHostedStepIndex).toBeGreaterThan(syncHostedStepIndex);
|
||||
|
|
@ -1988,7 +2007,9 @@ describe('standalone release packaging', () => {
|
|||
expect(uninstallPowerShellSource).toContain(
|
||||
'Test-QwenStandaloneInstallDir',
|
||||
);
|
||||
expect(uninstallPowerShellSource).toContain('Remove-UserPathEntry');
|
||||
expect(uninstallPowerShellSource).toContain(
|
||||
'Remove-PathEntryFromAllScopes',
|
||||
);
|
||||
expect(uninstallPowerShellSource).toContain('Remove-CurrentCmdPathShim');
|
||||
expect(uninstallPowerShellSource).toContain(
|
||||
'Remove-RecordedCurrentCmdPathShim',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue