From af64da874fed556a6e4091b65df043a169282c6a Mon Sep 17 00:00:00 2001 From: yiliang114 <1204183885@qq.com> Date: Wed, 13 May 2026 18:53:45 +0800 Subject: [PATCH] feat(installer): restore hosted PowerShell entrypoint --- scripts/build-hosted-installation-assets.js | 4 +++ scripts/installation/INSTALLATION_GUIDE.md | 25 ++++++++------ scripts/installation/install-qwen.ps1 | 37 +++++++++++++++++++++ scripts/tests/install-script.test.js | 27 +++++++++++++-- 4 files changed, 80 insertions(+), 13 deletions(-) create mode 100644 scripts/installation/install-qwen.ps1 diff --git a/scripts/build-hosted-installation-assets.js b/scripts/build-hosted-installation-assets.js index b30805729..d5e3f45fc 100644 --- a/scripts/build-hosted-installation-assets.js +++ b/scripts/build-hosted-installation-assets.js @@ -27,6 +27,10 @@ const HOSTED_INSTALLATION_ASSETS = [ output: 'install-qwen.bat', lineEndings: 'crlf', }, + { + sourcePath: ['scripts', 'installation', 'install-qwen.ps1'], + output: 'install-qwen.ps1', + }, ]; const HOSTED_INSTALLATION_ASSET_NAMES = HOSTED_INSTALLATION_ASSETS.map( ({ output }) => output, diff --git a/scripts/installation/INSTALLATION_GUIDE.md b/scripts/installation/INSTALLATION_GUIDE.md index 33607431c..4acf5a870 100644 --- a/scripts/installation/INSTALLATION_GUIDE.md +++ b/scripts/installation/INSTALLATION_GUIDE.md @@ -23,7 +23,7 @@ are only required when the installer falls back to npm or when ## Installation Scripts - Linux/macOS: `install-qwen.sh` -- Windows: `install-qwen.bat` +- Windows: `install-qwen.ps1` ## Release Artifacts @@ -37,7 +37,7 @@ GitHub releases publish these standalone archives: - `SHA256SUMS` The installer scripts (`install-qwen.sh`, -`install-qwen.bat`) are not republished per release. They are +`install-qwen.ps1`) are not republished per release. They are served from a hosted installation endpoint and accept `--version` to pin a specific standalone release. This keeps the public install command on a stable hosted entrypoint while still allowing version pinning, rather than using @@ -58,16 +58,15 @@ curl -fsSL https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/in ``` ```cmd -powershell -Command "Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path $env:TEMP 'install-qwen.bat')" +powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex" ``` -To pin a release with the hosted Windows entrypoint, download `install-qwen.bat` -and pass `--version`: +To pin a release with the hosted Windows entrypoint, set +`QWEN_INSTALL_VERSION` before invoking `install-qwen.ps1`: ```powershell -$installer = Join-Path $env:TEMP 'install-qwen.bat' -Invoke-WebRequest 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' -OutFile $installer -& $installer --version vX.Y.Z +$env:QWEN_INSTALL_VERSION = 'vX.Y.Z' +irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex ``` `QWEN_INSTALL_VERSION` is the equivalent environment variable when arguments @@ -76,7 +75,9 @@ cannot be passed through. Hosted installer assets are staged separately from GitHub Release archives: - `install-qwen.sh` is the Linux/macOS hosted entrypoint. -- `install-qwen.bat` is the Windows hosted entrypoint (also runnable directly). +- `install-qwen.ps1` is the Windows hosted entrypoint for `irm | iex`. +- `install-qwen.bat` is the Windows installer implementation used by + `install-qwen.ps1` and can also be downloaded and run directly. Build them with: @@ -84,9 +85,11 @@ Build them with: npm run package:hosted-installation -- --out-dir dist/installation ``` -The staged `install-qwen.sh` and `install-qwen.bat` files map to the fixed +The staged `install-qwen.sh`, `install-qwen.ps1`, and `install-qwen.bat` files +map to the fixed hosted URLs shown above. Upload their contents byte-for-byte to -`installation/install-qwen.sh` and `installation/install-qwen.bat`; the staging +`installation/install-qwen.sh`, `installation/install-qwen.ps1`, and +`installation/install-qwen.bat`; the staging command also writes `SHA256SUMS` for upload verification. The hosted installers intentionally default to `latest`; use `--version` or `QWEN_INSTALL_VERSION` to pin a standalone release. OSS/CDN upload automation is still a follow-up release diff --git a/scripts/installation/install-qwen.ps1 b/scripts/installation/install-qwen.ps1 new file mode 100644 index 000000000..8fcf0b113 --- /dev/null +++ b/scripts/installation/install-qwen.ps1 @@ -0,0 +1,37 @@ +# Qwen Code Windows hosted PowerShell entrypoint. +# Pairs with install-qwen.bat: this shim downloads the .bat into TEMP and runs +# it, so the documented one-liner can use the standard irm | iex pattern. +# +# Usage: +# powershell -ExecutionPolicy Bypass -c "irm https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.ps1 | iex" +# +# To pin a specific release, set $env:QWEN_INSTALL_VERSION before invoking, +# e.g. $env:QWEN_INSTALL_VERSION = 'vX.Y.Z'. This is equivalent to passing +# --version vX.Y.Z to install-qwen.bat directly. + +$ErrorActionPreference = 'Stop' + +$qwenInstallerUrl = 'https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/install-qwen.bat' +$qwenInstallerPath = Join-Path $env:TEMP 'install-qwen.bat' + +try { + Invoke-WebRequest -Uri $qwenInstallerUrl ` + -OutFile $qwenInstallerPath ` + -UseBasicParsing ` + -MaximumRedirection 10 +} catch { + Write-Error "Failed to download Qwen Code installer from ${qwenInstallerUrl}: $($_.Exception.Message)" + exit 1 +} + +$qwenInstallerExitCode = 0 +try { + & $qwenInstallerPath @args + $qwenInstallerExitCode = $LASTEXITCODE +} finally { + Remove-Item -Path $qwenInstallerPath -Force -ErrorAction SilentlyContinue +} + +if ($qwenInstallerExitCode -ne 0) { + exit $qwenInstallerExitCode +} diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index 72973f033..3fc771314 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -264,6 +264,7 @@ describe('standalone release packaging', () => { expect(hostedInstallScript).toContain('HOSTED_INSTALLATION_ASSETS'); expect(hostedInstallScript).toContain("output: 'install-qwen.sh'"); expect(hostedInstallScript).toContain("output: 'install-qwen.bat'"); + expect(hostedInstallScript).toContain("output: 'install-qwen.ps1'"); expect(hostedInstallScript).not.toContain("output: 'install'"); const releaseVerifyScript = readScript( @@ -409,6 +410,15 @@ describe('standalone release packaging', () => { ); expect(installBatchSource).toContain('"%~1"=="--version"'); expect(installBatchSource).toContain('--version requires a value'); + + const installPowerShellSource = readScript( + 'scripts/installation/install-qwen.ps1', + ); + expect(installPowerShellSource).toContain('install-qwen.bat'); + expect(installPowerShellSource).toContain('Invoke-WebRequest'); + expect(installPowerShellSource).toContain('QWEN_INSTALL_VERSION'); + expect(installPowerShellSource).toContain('--version vX.Y.Z'); + expect(installPowerShellSource).toContain('@args'); }); it('stages hosted installation assets with checksums', async () => { @@ -425,12 +435,14 @@ describe('standalone release packaging', () => { const installSh = path.join(tmpDir, 'install-qwen.sh'); const installBat = path.join(tmpDir, 'install-qwen.bat'); + const installPs1 = path.join(tmpDir, 'install-qwen.ps1'); const checksums = readScript(path.join(tmpDir, 'SHA256SUMS')); const checksumLines = checksums.trim().split('\n'); expect(HOSTED_INSTALLATION_ASSET_NAMES).toEqual([ 'install-qwen.sh', 'install-qwen.bat', + 'install-qwen.ps1', ]); expect(HOSTED_INSTALLATION_ASSETS.map(({ output }) => output)).toEqual( HOSTED_INSTALLATION_ASSET_NAMES, @@ -444,14 +456,18 @@ describe('standalone release packaging', () => { '\r\n', ), ); + expect(readScript(installPs1)).toBe( + readScript('scripts/installation/install-qwen.ps1'), + ); expect(existsSync(path.join(tmpDir, 'install'))).toBe(false); - expect(existsSync(path.join(tmpDir, 'install-qwen.ps1'))).toBe(false); expect(checksumLines.map((line) => line.split(' ')[1])).toEqual([ 'install-qwen.bat', + 'install-qwen.ps1', 'install-qwen.sh', ]); expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.sh$/m); expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.bat$/m); + expect(checksums).toMatch(/^[0-9a-f]{64} {2}install-qwen\.ps1$/m); if (process.platform !== 'win32') { expect(lstatSync(installSh).mode & 0o111).not.toBe(0); } @@ -485,6 +501,10 @@ describe('standalone release packaging', () => { path.join(sourceDir, 'install-qwen.bat'), '@echo off\r\nset "VERSION=latest"\r\n', ); + writeFileSync( + path.join(sourceDir, 'install-qwen.ps1'), + "# --version vX.Y.Z\n$env:QWEN_INSTALL_VERSION = 'latest'\n", + ); await expect( buildHostedInstallationAssets(tmpDir, { root: tmpRoot }), @@ -598,7 +618,7 @@ describe('standalone release packaging', () => { ]); } for (const [url] of fetchedUrls) { - expect(url).not.toMatch(/install-qwen\.(sh|bat)$/); + expect(url).not.toMatch(/install-qwen\.(sh|bat|ps1)$/); expect(url).not.toMatch(/\/install$/); } }); @@ -831,6 +851,7 @@ describe('standalone release packaging', () => { expect(workflow).not.toContain('package:installation-assets'); expect(workflow).not.toContain('install-qwen.sh'); expect(workflow).not.toContain('install-qwen.bat'); + expect(workflow).not.toContain('install-qwen.ps1'); expect(workflow).not.toContain('verify_node_checksum()'); expect(workflow).not.toContain('download_node()'); expect(workflow).toContain('dist/standalone/qwen-code-*.tar.gz'); @@ -855,6 +876,8 @@ describe('standalone release packaging', () => { expect(guide).toContain('package:hosted-installation'); expect(guide).toContain('installation/install-qwen.sh'); expect(guide).toContain('installation/install-qwen.bat'); + expect(guide).toContain('installation/install-qwen.ps1'); + expect(guide).toContain('irm https://qwen-code-assets'); expect(guide).toContain('release operators must sync these staged files'); expect(guide).toContain('Hosted endpoint status'); expect(guide).toContain('legacy NVM-based installer');