mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-19 16:28:28 +00:00
fix(installer): make Windows standalone shim available in cmd
This commit is contained in:
parent
564f899359
commit
74130fc79e
2 changed files with 186 additions and 0 deletions
|
|
@ -91,6 +91,146 @@ function Get-ParentProcessName {
|
|||
}
|
||||
}
|
||||
|
||||
function Get-NormalizedPath {
|
||||
param([string]$PathValue)
|
||||
|
||||
if ([string]::IsNullOrEmpty($PathValue)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$trimmed = $PathValue.Trim().Trim('"')
|
||||
if ([string]::IsNullOrEmpty($trimmed)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
return [IO.Path]::GetFullPath($trimmed).TrimEnd('\')
|
||||
} catch {
|
||||
return $trimmed.TrimEnd('\')
|
||||
}
|
||||
}
|
||||
|
||||
function Test-PathContainsDirectory {
|
||||
param([string]$PathValue, [string]$Directory)
|
||||
|
||||
$target = Get-NormalizedPath -PathValue $Directory
|
||||
if ([string]::IsNullOrEmpty($target)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($entry in @($PathValue -split ';')) {
|
||||
$normalizedEntry = Get-NormalizedPath -PathValue $entry
|
||||
if ([string]::Equals($normalizedEntry, $target, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-WritableDirectory {
|
||||
param([string]$Directory)
|
||||
|
||||
if ([string]::IsNullOrEmpty($Directory)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Directory -PathType Container)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$probe = Join-Path $Directory ('.qwen-write-test-' + [IO.Path]::GetRandomFileName())
|
||||
try {
|
||||
[IO.File]::WriteAllText($probe, '')
|
||||
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
||||
return $true
|
||||
} catch {
|
||||
Remove-Item -LiteralPath $probe -Force -ErrorAction SilentlyContinue
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Add-PathCandidate {
|
||||
param(
|
||||
[System.Collections.Generic.List[string]]$Candidates,
|
||||
[string]$Directory
|
||||
)
|
||||
|
||||
$normalizedDirectory = Get-NormalizedPath -PathValue $Directory
|
||||
if ([string]::IsNullOrEmpty($normalizedDirectory)) {
|
||||
return
|
||||
}
|
||||
|
||||
foreach ($candidate in $Candidates) {
|
||||
$normalizedCandidate = Get-NormalizedPath -PathValue $candidate
|
||||
if ([string]::Equals($normalizedCandidate, $normalizedDirectory, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
[void]$Candidates.Add($Directory.Trim().Trim('"'))
|
||||
}
|
||||
|
||||
function Install-CurrentCmdPathShim {
|
||||
param([string]$QwenCommand, [string]$PathValue)
|
||||
|
||||
$pathEntries = @($PathValue -split ';' | Where-Object { -not [string]::IsNullOrEmpty($_) })
|
||||
$candidates = [System.Collections.Generic.List[string]]::new()
|
||||
$preferredDirectories = @()
|
||||
|
||||
if (-not [string]::IsNullOrEmpty($env:LOCALAPPDATA)) {
|
||||
$preferredDirectories += Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps'
|
||||
}
|
||||
if (-not [string]::IsNullOrEmpty($env:APPDATA)) {
|
||||
$preferredDirectories += Join-Path $env:APPDATA 'npm'
|
||||
}
|
||||
if (-not [string]::IsNullOrEmpty($env:USERPROFILE)) {
|
||||
$preferredDirectories += Join-Path $env:USERPROFILE '.bun\bin'
|
||||
}
|
||||
|
||||
foreach ($preferredDirectory in $preferredDirectories) {
|
||||
$preferredNormalized = Get-NormalizedPath -PathValue $preferredDirectory
|
||||
foreach ($entry in $pathEntries) {
|
||||
$entryNormalized = Get-NormalizedPath -PathValue $entry
|
||||
if ([string]::Equals($entryNormalized, $preferredNormalized, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Add-PathCandidate -Candidates $candidates -Directory $entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$userRoot = Get-NormalizedPath -PathValue $env:USERPROFILE
|
||||
foreach ($entry in $pathEntries) {
|
||||
$entryNormalized = Get-NormalizedPath -PathValue $entry
|
||||
if (
|
||||
-not [string]::IsNullOrEmpty($userRoot) -and
|
||||
-not [string]::IsNullOrEmpty($entryNormalized) -and
|
||||
$entryNormalized.StartsWith($userRoot, [StringComparison]::OrdinalIgnoreCase)
|
||||
) {
|
||||
Add-PathCandidate -Candidates $candidates -Directory $entry
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if (-not (Test-WritableDirectory -Directory $candidate)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$shimPath = Join-Path $candidate 'qwen.cmd'
|
||||
if (Test-Path -LiteralPath $shimPath -PathType Leaf) {
|
||||
$existingShim = Get-Content -LiteralPath $shimPath -Raw -ErrorAction SilentlyContinue
|
||||
if ($existingShim -notmatch 'Qwen Code current-session shim') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
$shim = "@echo off`r`nREM Qwen Code current-session shim. Generated by install-qwen-standalone.ps1.`r`ncall `"$QwenCommand`" %*`r`n"
|
||||
[IO.File]::WriteAllText($shimPath, $shim, [Text.UTF8Encoding]::new($false))
|
||||
return $shimPath
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Update-CurrentShell {
|
||||
$qwenInstallBinDir = Get-QwenInstallBinDir
|
||||
$qwenCommandPath = Join-Path $qwenInstallBinDir 'qwen.cmd'
|
||||
|
|
@ -98,15 +238,32 @@ function Update-CurrentShell {
|
|||
return
|
||||
}
|
||||
|
||||
$inheritedPath = $env:Path
|
||||
Update-CurrentSessionPath -BinDir $qwenInstallBinDir
|
||||
|
||||
Write-Output "Run: qwen"
|
||||
$parentProcessName = Get-ParentProcessName
|
||||
if ($parentProcessName -ieq 'cmd.exe') {
|
||||
if (Test-PathContainsDirectory -PathValue $inheritedPath -Directory $qwenInstallBinDir) {
|
||||
Write-Output "qwen is ready to use after this installer command returns."
|
||||
return
|
||||
}
|
||||
|
||||
$shimPath = Install-CurrentCmdPathShim -QwenCommand $qwenCommandPath -PathValue $inheritedPath
|
||||
if (-not [string]::IsNullOrEmpty($shimPath)) {
|
||||
Write-Output "INFO: Added qwen.cmd to a directory already on this cmd.exe PATH:"
|
||||
Write-Output "INFO: ${shimPath}"
|
||||
Write-Output "qwen is ready to use after this installer command returns."
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "WARNING: Windows does not allow this PowerShell child process to update the parent cmd.exe PATH directly."
|
||||
Write-Output "Or, for this cmd.exe window, run:"
|
||||
Write-Output " set `"PATH=${qwenInstallBinDir};%PATH%`""
|
||||
return
|
||||
}
|
||||
|
||||
Write-Output "qwen is ready to use in this PowerShell session."
|
||||
}
|
||||
|
||||
$qwenDefaultInstallerUrl = 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen-standalone.bat'
|
||||
|
|
|
|||
|
|
@ -603,6 +603,35 @@ describe('standalone release packaging', () => {
|
|||
expect(installPowerShellSource).toContain('@args');
|
||||
});
|
||||
|
||||
it('PowerShell hosted entrypoint refreshes the current Windows shell', () => {
|
||||
const installPowerShellSource = readScript(
|
||||
'scripts/installation/install-qwen-standalone.ps1',
|
||||
);
|
||||
const installBatchSource = readScript(
|
||||
'scripts/installation/install-qwen-standalone.bat',
|
||||
);
|
||||
|
||||
expect(installPowerShellSource).toContain('Update-CurrentSessionPath');
|
||||
expect(installPowerShellSource).toContain('Install-CurrentCmdPathShim');
|
||||
expect(installPowerShellSource).toContain('Test-WritableDirectory');
|
||||
expect(installPowerShellSource).toContain('Qwen Code current-session shim');
|
||||
expect(installPowerShellSource).not.toContain('doskey.exe');
|
||||
expect(installPowerShellSource).toContain(
|
||||
'qwen is ready to use in this PowerShell session.',
|
||||
);
|
||||
expect(installPowerShellSource).toContain(
|
||||
'Added qwen.cmd to a directory already on this cmd.exe PATH:',
|
||||
);
|
||||
expect(installPowerShellSource).toContain(
|
||||
'Windows does not allow this PowerShell child process to update the parent cmd.exe PATH directly.',
|
||||
);
|
||||
|
||||
expect(installBatchSource).toContain('QWEN_INSTALLER_PARENT_POWERSHELL');
|
||||
expect(installBatchSource).toContain(
|
||||
'Final PATH refresh is handled by the PowerShell entrypoint.',
|
||||
);
|
||||
});
|
||||
|
||||
it('stages hosted installation assets with checksums', async () => {
|
||||
const {
|
||||
HOSTED_INSTALLATION_ASSET_NAMES,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue