mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* feat(installer): add standalone archive installation * fix(installer): harden standalone archive installs * fix(installer): address standalone review findings * chore(installer): clarify review followups * fix(installer): stabilize standalone script checks * chore(installer): remove internal planning docs * chore(installer): simplify standalone release review fixes * test(installer): add Windows batch install smoke * test(installer): fix Windows batch smoke quoting * test(installer): preserve Windows cmd quotes * fix(installer): use robust Windows checksum hashing * ci: narrow installer debug matrix * fix(installer): address standalone review hardening * fix(installer): avoid Windows validation parse errors * fix(installer): simplify Windows option validation * fix(installer): harden standalone review fixes * feat(installer): publish release installer assets * fix(installer): address release asset review feedback * fix(installer): avoid prerelease installer asset links * test(installer): isolate standalone dist fixture * feat(installer): add hosted install release alias * chore: no changes - code review requested Agent-Logs-Url: https://github.com/QwenLM/qwen-code/sessions/38467aec-15b9-4b76-9139-0b2cfe40477a * fix(installer): pin versioned installer assets * fix: parallelize Node.js binary downloads in standalone release build Use Promise.all instead of sequential for...of+await for the 5 independent Node.js runtime downloads, reducing CI release build time by ~4-5x. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(installer): address release asset review followups * refactor(installer): share release CLI parsing * fix(installer): address release asset review followups - sh: reject CR/LF in archive entry names before the literal `..` glob so a `..\r` entry cannot bypass path validation. - bat: prefer Tls12+Tls13 in PowerShell helpers, fall back to Tls12 alone on older .NET Framework where the Tls13 enum is missing. - bat: document the implicit `:ValidateOptions` dependency next to the qwen.cmd wrapper writer so loosening the validator stays a conscious choice. - build-standalone-release: surface the `xz-utils` host requirement for Linux Node downloads in `--help`. - release-script-utils: support `--key=value` form in `parseCliArgs`. - tests: cover the new CRLF message, TLS string, and `--key=value` parsing; register process-level signal/exit handlers in `ensureMinimalDist` so a crashed test still restores `dist/`. * fix(installer): unblock Windows CI for standalone install path Three CI failures and a few review followups in one pass. - ensureMinimalDist places its dist/ backup beside dist/ instead of under os.tmpdir(). On Windows GitHub runners the workspace lives on D: while os.tmpdir() is on C:, so renameSync raised EXDEV for every test that needed to swap dist/ in. - create-standalone-package.js and the matching test fixture build win-x64 zips with [IO.Compression.ZipFile]::CreateFromDirectory. Compress-Archive emits backslash entry names that the .bat installer's path-traversal guard then rejected, so every freshly built archive failed the standalone install path on Windows. - :ValidateArchiveContents normalizes entry separators to '/' before checking for '..', absolute paths, and drive prefixes - archives from any Windows zip tool still install while real traversal entries remain rejected. - createWindowsTraversalStandaloneArchive runs PowerShell via -File instead of a single -Command line; the joined-with-'; ' form had a function definition the runner's PowerShell refused to parse. Drive-by review followups: - replaceRequired uses replaceAll so a future duplicate placeholder cannot silently keep the trailing copy as 'latest'. - :ValidateOptions runs the unsafe-character check on SOURCE alongside the other variables. - build-installation-assets.js drops a dead INSTALLATION_ASSETS re-export; consumers already import from release-asset-config.js. - .gitignore covers the new sibling .qwen-dist-backup-* directory. * fix(installer): address release asset review findings * fix(installer): keep installer entrypoint hosted * fix(installer): reject stale hosted assets * fix(installer): refine hosted asset staging * fix(installer): tighten hosted default-version check, flag legacy URL - Replace the loose `latest` fragment check with per-format regex patterns in HOSTED_INSTALLER_DEFAULT_VERSION_PATTERNS so an unrelated occurrence of `latest` (comment, help text) cannot satisfy the staging guard. The patterns still tolerate whitespace variation, only the default-version assignment itself must be intact. - Add a "Hosted endpoint status" callout in INSTALLATION_GUIDE.md before the curl examples. The documented `--version` flow does not work against the OSS URL today because it currently serves the legacy NVM-based installer; the callout points users at a local checkout until the next release sync. - Tests: drop `latest` from the fragments equality assertion, add positive and negative regex coverage, add a failure-path case for sources whose default version is not `latest`, and pin the new guide markers so the callout cannot silently disappear. * feat(installer): verify installation release assets Adds `npm run verify:installation-release` and wires it into the release workflow after `Build Standalone Archives`, so a broken release directory fails CI before publishing. Local mode (`--dir PATH`) checks: - All five `qwen-code-{platform}.{ext}` standalone archives exist. - `SHA256SUMS` covers exactly those five — missing or unexpected entries fail. - Each archive's actual SHA256 matches its `SHA256SUMS` entry. Remote mode (`--base-url URL`) checks: - `SHA256SUMS` is downloadable, parseable, and contains exactly the expected archive entries. - Each archive URL is reachable via HEAD, with a 1-byte ranged GET fallback for hosts that disable HEAD. Hosted installer scripts (`install-qwen.sh` / `install-qwen.bat`) are intentionally out of scope here — they are served from the hosted endpoint prepared by `package:hosted-installation` (PR #3853), not from the GitHub Release surface this verifier targets. * fix(installer): tighten verifier base-url + clarify test helper Three small refinements from the second review pass: - normalizeHttpsBaseUrl rejects everything except https, since real release URLs are always HTTPS. Accepting http previously would let an operator silently target a stale or attacker-controlled mirror. - Drop EXPECTED_RELEASE_ASSET_NAMES from the public exports; it was only used internally for the verification log line. - Rename the test helper standaloneChecksumContent to placeholderChecksumContent and document that the hashes in its output are placeholders — the remote verifier does not download archives or compare hashes, it only validates that SHA256SUMS lists the expected names and that each archive URL is reachable. The non-https rejection test now also covers `http://` in addition to the existing `file://` case. * fix(installer): address standalone review follow-ups * fix(installer): repair Windows installer tests * fix(release): tighten standalone asset checks * fix(installer): stabilize Windows managed install checks * test(installer): relax Windows installer timeout * fix(test): escape release asset regex * test(cli): avoid POSIX node path in relaunch test * fix(installer): align npm fallback node gate with engines * test(installer): allow Windows archive validation more time * fix(installer): remove stale node 20 installer references * docs(installer): clarify hosted endpoint sync requirement * refactor(installer): reuse standaloneArchiveName in release verifier The verify-installation-release script was duplicating the archive name derivation logic with a hardcoded ternary instead of reusing the standaloneArchiveName helper from build-standalone-release. Export the helper and import it so the extension mapping lives in one place. * fix(scripts): address release verifier review feedback * feat(installer): add standalone archive installer with multi-platform release workflow - Add standalone archive installer (bat/sh) that downloads platform binaries from GitHub/Aliyun without requiring Node.js or npm on the target machine - Add fork-friendly release-test workflow for manual GitHub Release creation covering all 5 platforms (darwin-arm64/x64, linux-arm64/x64, win-x64) - Add OSS upload/mirror tools for staging and release distribution - Update .gitignore to exclude generated build artifacts (release-staging/, hosted-staging/) - Fix Windows PowerShell test command in copy-release-to-latest tool * feat(installer): support QWEN_INSTALL_GITHUB_REPO env var for custom repo * chore(installer): exclude local-only staging tools from PR The tools/ directory contained personal staging-OSS upload helpers (upload-staging, upload-release-mirror, copy-release-to-latest, test-upload-one) that should not ship in the public PR. They reference a personal staging bucket and only exist to validate the installer end-to-end before production release. Removes them from git tracking via `git rm --cached` (files stay on disk for the author's local use) and adds /tools/ to root .gitignore so they cannot be re-added accidentally. No runtime / installer code change. Production CI on ubuntu-latest is unaffected. * fix(installer): enforce CRLF line endings for .bat files via gitattributes cmd.exe requires CRLF in batch scripts; the global eol=lf was causing every line to be misparsed on Windows, producing errors like 'QWEN_VALIDATE_METHOD=detect is not recognized as a command'. * fix(installer): store .bat files with CRLF in git blob for raw GitHub downloads GitHub raw file serving bypasses gitattributes eol conversion and serves blob bytes directly, so eol=crlf alone was not enough. Use -text to disable normalization and commit with actual CRLF so raw downloads work on Windows. * fix(installer): follow HTTP redirects in UrlExists and RaceMirrorHead probes GitHub release asset URLs return HTTP 302 to objects.githubusercontent.com. [Net.WebRequest] with HEAD does not auto-redirect by default, so the existence check and mirror-race probe both incorrectly reported the file as missing. Set AllowAutoRedirect=true on HttpWebRequest instances. * fix(installer): surface download errors and add MaximumRedirection 10 * feat(installer): add hosted install-qwen.ps1 shim for irm|iex one-liner The previous Windows quick-install one-liner used `Invoke-WebRequest -OutFile (Join-Path $env:TEMP 'install-qwen.bat'); & (Join-Path …)`. When pasted into a narrow terminal, line wrap could land on `-OutFile`, orphaning the parameter from its value and producing the "missing argument for OutFile" failure followed by a "file not found" when the second `&` ran. PowerShell's line continuation rules cannot resolve this for parameter-name-at-EOL. Add `install-qwen.ps1` as a thin hosted entrypoint that downloads `install-qwen.bat` into TEMP, runs it, and cleans up. Documented one-liner becomes the standard pattern used by bun, uv, scoop, deno, pnpm: powershell -ExecutionPolicy Bypass -c "irm <url>/install-qwen.ps1 | iex" The `.bat` remains the source of truth for installer behavior; `.ps1` is just the modern hosted entrypoint. Version pinning via `$env:QWEN_INSTALL_VERSION` flows through unchanged. Stored with `*.ps1 -text` so CRLF survives both GitHub raw and OSS uploads, matching the existing `.bat` handling. * fix(installer): stage direct hosted install scripts * chore(installer): trim hosted release diff scope * chore(installer): narrow hosted release diff * feat(installer): restore hosted PowerShell entrypoint * chore(installer): stage standalone hosted entrypoints * fix(installer): address hosted installer review followups * fix(installer): stabilize Windows installer tests * fix(installer): make Windows option validation readable * feat(installer): wire Aliyun OSS sync, address review followups - Add Aliyun OSS sync steps to release workflow: package hosted assets, install pinned ossutil, configure credentials, upload versioned and latest paths, and verify upload via verify:installation-release plus curl probes against the hosted installer endpoint. - Document required production-release environment secrets and bucket variables in INSTALLATION_GUIDE.md. - Restructure hosted endpoint guidance to lead with the pre-sync warning, splitting "Run today" (local checkout) from "After the OSS sync" (hosted one-liners) so users no longer copy a one-liner that silently installs latest. - Distinguish mirror auto-selection timeout from successful selection in install-qwen-standalone.sh and install-qwen-standalone.bat: emit a "timed out; defaulting to github" log instead of pretending the HEAD probe picked github. - Support QWEN_INSTALLER_BAT_URL override (https only) in the PowerShell shim so staging mirrors can be exercised without forking the file. - Strip a leading UTF-8 BOM in verify-installation-release.js parseSha256Sums so BOM-prefixed SHA256SUMS reports a useful "Missing checksum entry" error instead of "Malformed SHA256SUMS line 1". - Add tests for verifier HEAD→Range fallback, partial-failure formatting, all-failure wording, and BOM tolerance. * ci(installer): add temporary OSS smoke test * fix(installer): make OSS release assets public-readable * chore(installer): remove temporary OSS smoke workflow * fix(installer): address hosted installer review gaps * feat(installer): refactor argument parsing and utility functions for release scripts * fix(installer): harden hosted release script checks * fix(installer): suppress PowerShell progress bar in hosted entrypoint shim Add $ProgressPreference = 'SilentlyContinue' to the .ps1 wrapper so Invoke-WebRequest downloads don't render a progress bar when invoked via the irm | iex one-liner. * fix(installer): suppress PowerShell progress bar in bat installer downloads Add $ProgressPreference = 'SilentlyContinue' to DownloadFile so the full-screen progress UI does not appear during archive downloads in interactive PowerShell sessions, consistent with the .ps1 shim. * fix(installer): use curl.exe -# progress bar in Windows downloads Prefer curl.exe with -# (hash-mark progress bar) for archive and installer downloads on Windows 10+. Falls back to Invoke-WebRequest (which shows its own progress bar) when curl.exe is unavailable. Matches the approach used by code-server (curl -#fL) and bun.sh (curl.exe -#SfLo). * fix(installer): suppress progress bars for small downloads and Expand-Archive - .ps1: replace curl.exe -# with silent mode, suppress Invoke-WebRequest progress bar; save/restore $global:ProgressPreference - .bat: add $ProgressPreference = 'SilentlyContinue' before Expand-Archive to prevent full-screen extraction progress UI - .sh: remove --progress-bar / --show-progress from download_file, always use silent curl/wget * fix(installer): auto-backup non-qwen directories and simplify output - ensure_managed_install_dir / :EnsureManagedInstallDir now back up non-qwen directories instead of refusing to install, so users upgrading from npm or old installers don't hit a hard error - Simplify header/footer output: remove banner bars, verbose INFO lines, and redundant "Installation completed!" message - Match bun.sh / code-server style: minimal, to the point * fix(installer): revert Expand-Archive progress suppression in bat The inline $ProgressPreference = 'SilentlyContinue' caused a cmd.exe parsing error ("此时不应有 >") on Chinese Windows. Revert to the original Expand-Archive invocation. * fix(installer): fix cmd.exe parsing error in backup fallback code The %s in the for /f fallback command string was interpreted as a variable reference by cmd.exe, causing "此时不应有 >" on Chinese Windows. Replace with a safe fallback and re-enable Expand-Archive progress suppression. * fix(installer): always persist install bin to user PATH Previously MaybeUpdateUserPath was only called when shadow qwen executables were detected. When no shadow was found, the PATH update was skipped entirely, leaving the user without qwen on PATH after restarting their terminal. Now always persist the bin directory to PATH (unless --no-modify-path is set), regardless of whether other qwen installations exist. * fix(installer): persist PATH to current terminal session on Windows Use the `endlocal & set` trick (same as bun/Rust installers) to export the install bin directory from the setlocal scope to the current cmd session. qwen is now usable immediately without restarting the terminal. * docs(installer): document cmd.exe one-liner for immediate PATH availability Add curl-based one-liner for cmd.exe users. Running the .bat directly in the current cmd session makes `qwen` available immediately via the `endlocal & set` trick. The `powershell -c "irm | iex"` path creates a child process so PATH changes cannot propagate to the parent. * feat(installer): make qwen usable immediately from PowerShell after install - .ps1: detect parent process, update current session PATH, and for cmd.exe parents emit a `set PATH=...` command - .bat: skip final instructions when called from PowerShell to avoid duplicate "Run: qwen" output * fix(installer): remove non-functional doskey approach for cmd parent doskey /exename from a child PowerShell process cannot modify the parent cmd.exe session. Replace with a simple set PATH=... command that the user can copy-paste. * fix(installer): make Windows standalone shim available in cmd * feat(installer): add standalone uninstall scripts * fix(uninstall): match shell-quoted paths when removing the wrapper The installer's write_unix_wrapper shell-quotes the binary path, so paths containing single quotes (or other shell metacharacters) appear as shell-quoted strings in the generated wrapper file. The uninstall script's literal grep -qF missed these, leaving the wrapper orphaned. Add shell_quote to the uninstall script and match against both the raw and shell-quoted forms before removing the wrapper. * fix(installer): update download commands to use progress indicators for curl and wget * fix(installer): resolve Aliyun latest via version pointer * fix(installer): cleanup mirror probe temp dirs * fix(installer): harden standalone release fallback * fix(installer): address standalone review feedback * style(installer): align standalone install output * fix(installer): print standalone uninstall commands * fix(installer): address release review follow-ups * fix(installer): harden Windows target detection * test(installer): stabilize Windows fake tool path * fix(installer): allow explicit Windows curl path * test(installer): use cmd fake curl on Windows * test(installer): cover Windows fake curl helper * test(installer): inject Windows arch overrides in cmd * test(cli): wait for prompt suggestion render * test(cli): revert prompt suggestion wait tweak * fix(installer): harden hosted release publishing * fix(installer): harden Windows latest pointer parsing * fix(installer): bound Windows download timeouts * fix(installer): bound hosted installer probes * fix(release): make ossutil download configurable * fix(installer): address hosted release review feedback * test(installer): keep dist backup on same filesystem * fix(installer): address remaining review feedback on PR #3828 - Remove REQUIRE_CHECKSUM dead code, always hard-fail on checksum issues - Add JSDoc to HOSTED_INSTALLER_BEHAVIOR_PATTERNS explaining its purpose - Add credential cleanup trap for ossutilconfig in release workflow - Add 3-attempt retry with exponential backoff for OSS uploads - Tighten findstr SOURCE regex to require leading letter * fix(release): correct OSS credentials lifetime and mirror probe fallback - release.yml: remove `trap EXIT` inside the Configure step; it deleted ${RUNNER_TEMP}/.ossutilconfig as soon as the configure shell exited, so every subsequent step (publish/sync/verify) lost the credentials. Move credential cleanup to a final `if: always()` step at the job tail. - install-qwen-standalone.sh: drop the predictable PID-based mktemp -d fallback in race_mirror_head; if mktemp fails, return "github" instead of using /tmp/qwen-mirror.$$ which a local attacker could pre-create to bias mirror selection. * fix(installer): address review feedback round 2 Workflow: - Move 'Publish Aliyun OSS Latest VERSION' to run after the hosted installer assets are uploaded and verified, so the latest/VERSION pointer only flips once every release artifact is in place. Previously a hosted-sync failure could leave the pointer ahead of the actual installer scripts. upload-aliyun-oss-assets.js: - Replace `spawnSync('sleep', ...)` retry backoff with an Atomics.wait-based cross-platform sleep so retries also work on Windows runners. install-qwen-standalone.bat: - :DetectTarget no longer emits TARGET=win-arm64 because RELEASE_TARGETS has no win-arm64 archive; ARM64 hosts now fall through to the unsupported-arch branch and (in detect mode) get the npm fallback instead of a 404. - Add QWEN_INSTALL_CURL_EXE to :ValidateRawEnvironmentOptions so this curl override is checked for shell metacharacters like every other knob. - Replace `call echo %%i>>...` with plain `echo %%i>>...` when capturing pre-install qwen.cmd paths; `call` triggered an extra parse pass that could interpret &/|/<,>/etc. inside a directory name as command separators. - Add `--retry 2` to curl.exe downloads (`:DownloadFile` / `:DownloadFileQuiet`) to match the shell installer. - Include expected vs actual hash in the checksum-mismatch error message. install-qwen-standalone.ps1: - Stage the downloaded installer at a cryptographically random temp path (`qwen-installer-<random>.bat`) so a same-user attacker cannot pre-stage a malicious .bat at a predictable path and race the verify/execute window. - Atomically install the current-session cmd shim by writing to a sibling `.new` temp file then renaming, so a partial write cannot leave a half-written shim on PATH. - Add `--retry 2` to the curl.exe download path. - Include expected vs actual hash in the checksum-mismatch error message. install-qwen-standalone.sh: - Include expected vs actual hash in the checksum-mismatch error message. uninstall-qwen-standalone.ps1: - Accept `-Purge` and `-Help` parameters; previously every CLI flag was silently dropped, so users running with `-Purge` got no purge and no error. `-Purge` maps to `QWEN_UNINSTALL_PURGE=1`. uninstall-qwen-standalone.sh: - `remove_install_wrapper` additionally requires the wrapper file to start with a `#!` shebang before it deletes it; a user-authored script that just happens to mention the install path now stays untouched. verify-installation-release.js, build-hosted-installation-assets.js: - Include expected vs actual hash in the checksum-mismatch error messages. scripts/tests/install-script.test.js: - Update assertions for the new error wording, the curl `--retry 2` flag, the dropped ARM64 detection, and the new release-step ordering. * fix(installer): address review feedback round 3 Workflow: - Configure Aliyun OSS Credentials: write the ossutil config file directly with restricted umask instead of invoking `ossutil config -k <secret>`. Passing the access-key secret via argv made it visible in /proc/<pid>/cmdline for the lifetime of that step; writing the INI file in-process keeps the secret out of the process table. upload-aliyun-oss-assets.js: - Upload assets in parallel with `Promise.all` + async `spawn` instead of a sequential `spawnSync` loop. Each asset keeps its own retry budget; failures are aggregated so one flaky upload does not mask a separate failure. - Replace the bespoke `Atomics.wait` retry sleep with `timers/promises#setTimeout` now that the loop is async. INSTALLATION_GUIDE.md: - Drop the misleading "instead of overwriting the global installation/ entrypoint objects" sentence; the workflow has always also refreshed the global versionless objects so curl|bash links keep resolving without a version segment. Document the rollback story instead. * test(installer): add parseUploadArgs unit tests and align verify derivation - scripts/tests/upload-aliyun-oss-assets.test.js: cover --help short-circuit, required-option validation (--bucket/--config/--prefix/empty assets), unknown options, missing option values, and trailing-slash prefix normalization. - scripts/verify-installation-release.js: switch the win-only zip branch from `startsWith('win-')` to the strict `=== 'win-x64'` check used by build-standalone-release.js, and add a comment recording that the two derivations must stay aligned. Without this the helpers would diverge the moment a non-x64 win target gets added. * test(installer): add uploadAssets integration tests with fake ossutil Add two integration tests that route a temp-directory ossutil shim onto PATH so uploadAssets actually spawns the real binary with the real cp argv: - happy-path test asserts the destination URI, `-c <config>`, `--acl public-read`, and per-asset cp invocations land for both inputs. - failure-path test asserts non-zero ossutil exits surface as an aggregate `asset uploads failed` error after the retry budget runs out. * revert(installer): drop over-engineered ossutil/upload changes Roll back two changes from a1ef8697b/0a5d308c9 that were not justified by the actual threat model or release-pipeline needs: - .github/workflows/release.yml: restore the supported `ossutil config -k` invocation. The earlier switch to writing the .ossutilconfig INI file in-process was meant to keep the access-key out of /proc/<pid>/cmdline, but GitHub-hosted runners are single-tenant ephemeral VMs where no other user can read that namespace. The benefit was theoretical; the cost was taking on a brittle dependency on ossutil's undocumented config format. - scripts/upload-aliyun-oss-assets.js: revert the uploadAssets parallel rewrite (Promise.all + spawn + setTimeout) back to the original sync spawnSync loop with retry. Release-time uploads of ~6 small files do not need parallelism, and the async refactor changed the public contract (sync→async) for no real wall-clock win. Kept from those commits: - The cleanup `if: always()` step that removes RUNNER_TEMP/.ossutilconfig at the end of the publish job. - The cross-platform sleepSync(ms) helper, since `spawnSync('sleep', ...)` still does not work on Windows runners. - The INSTALLATION_GUIDE.md doc fix. - All other round-2 fixes. Test assertions updated for the restored sync uploadAssets contract. * test(installer): cover Windows release script regressions * test(release): avoid Windows shim lookup in oss upload tests * test(installer): use stable fake Aliyun version on Windows * fix(installer): parse Aliyun latest version in batch * fix(installer): validate Aliyun latest version without findstr * fix(installer): normalize Aliyun latest version via PowerShell * fix(installer): avoid captured PowerShell output in batch latest parsing * fix(installer): normalize Aliyun latest pointer from file * test(installer): fix fake Windows curl output parsing * fix(installer): print checksum path on miss, gate hardcoded version pin in ps1 [skip ci] Address two narrow follow-ups from PR #3828 review: - build-hosted-installation-assets.js: add a HOSTED_INSTALLER_FORBIDDEN_PATTERNS guard for install-qwen-standalone.ps1. The ps1 shim has no VERSION variable of its own (it forwards @args to the .bat), so the existing default-version positive-match patterns don't apply. The new guard fails the build if a $env:QWEN_INSTALL_VERSION assignment or a --version flag prepended to the forwarded argument list ever lands in the shim. Patterns are line-anchored with /m so the documented usage examples in the header docstring stay valid. Two vitest cases cover the reject and allow paths. - install-qwen-standalone.sh / .bat: include the searched checksum-file path in the "SHA256SUMS not found" error. Operators triaging --archive failures could not tell from the prior message whether the fallback path (next to the archive) or the remote URL was being looked up. Existing test assertions updated to match the new wording. Local validation: npm run test:scripts -> 160 passed | 9 skipped (was 158 | 9). * fix: stamp release version in hosted installers and add Zip Slip protection [skip ci] 1. The hosted installation asset build now accepts --version and stamps it into the copied .sh/.bat installers so they default to the tagged release version instead of 'latest'. The release workflow passes the version. 2. install-qwen-with-source.bat now validates archive entries before calling Expand-Archive, rejecting paths with '..', leading '/', drive-rooted paths, empty names, or control characters — matching the protection already present in install-qwen-standalone.bat and the .sh installer. * fix(installer): add SOURCE to PowerShell unsafe-character validation [skip ci] The SOURCE variable is user-provided and used in path operations but was not included in the :ValidateOptions unsafe-character check. Add it alongside the other validated variables. * fix: correct copyright year 2025 -> 2026 in new files [skip ci] --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> Co-authored-by: yiliang114 <effortyiliang@gmail.com>
798 lines
26 KiB
Batchfile
798 lines
26 KiB
Batchfile
@echo off
|
|
REM Qwen Code Installation Script
|
|
REM Installs Qwen Code from a standalone archive when available, with npm fallback.
|
|
REM This script intentionally does not install Node.js or change npm config.
|
|
|
|
setlocal enabledelayedexpansion
|
|
|
|
set "SOURCE=unknown"
|
|
set "METHOD="
|
|
if defined QWEN_INSTALL_METHOD set "METHOD=!QWEN_INSTALL_METHOD!"
|
|
set "MIRROR=github"
|
|
if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!"
|
|
set "BASE_URL="
|
|
if defined QWEN_INSTALL_BASE_URL set "BASE_URL=!QWEN_INSTALL_BASE_URL!"
|
|
set "ARCHIVE_PATH="
|
|
if defined QWEN_INSTALL_ARCHIVE set "ARCHIVE_PATH=!QWEN_INSTALL_ARCHIVE!"
|
|
set "VERSION=latest"
|
|
if defined QWEN_INSTALL_VERSION set "VERSION=!QWEN_INSTALL_VERSION!"
|
|
set "NPM_REGISTRY=https://registry.npmmirror.com"
|
|
if defined QWEN_NPM_REGISTRY set "NPM_REGISTRY=!QWEN_NPM_REGISTRY!"
|
|
if defined LOCALAPPDATA (
|
|
set "INSTALL_BASE=!LOCALAPPDATA!\qwen-code"
|
|
) else (
|
|
set "INSTALL_BASE=!USERPROFILE!\AppData\Local\qwen-code"
|
|
)
|
|
if defined QWEN_INSTALL_ROOT set "INSTALL_BASE=!QWEN_INSTALL_ROOT!"
|
|
set "INSTALL_DIR=!INSTALL_BASE!\qwen-code"
|
|
if defined QWEN_INSTALL_LIB_DIR set "INSTALL_DIR=!QWEN_INSTALL_LIB_DIR!"
|
|
set "INSTALL_BIN_DIR=!INSTALL_BASE!\bin"
|
|
if defined QWEN_INSTALL_BIN_DIR set "INSTALL_BIN_DIR=!QWEN_INSTALL_BIN_DIR!"
|
|
|
|
REM Parse flags before any network or filesystem work.
|
|
:parse_args
|
|
if "%~1"=="" goto end_parse
|
|
if /i "%~1"=="--source" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --source requires a value
|
|
exit /b 1
|
|
)
|
|
set "SOURCE=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="-s" (
|
|
if "%~2"=="" (
|
|
echo ERROR: -s requires a value
|
|
exit /b 1
|
|
)
|
|
set "SOURCE=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--method" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --method requires a value
|
|
exit /b 1
|
|
)
|
|
set "METHOD=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--mirror" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --mirror requires a value
|
|
exit /b 1
|
|
)
|
|
set "MIRROR=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--base-url" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --base-url requires a value
|
|
exit /b 1
|
|
)
|
|
set "BASE_URL=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--archive" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --archive requires a value
|
|
exit /b 1
|
|
)
|
|
set "ARCHIVE_PATH=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--version" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --version requires a value
|
|
exit /b 1
|
|
)
|
|
set "VERSION=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--registry" (
|
|
if "%~2"=="" (
|
|
echo ERROR: --registry requires a value
|
|
exit /b 1
|
|
)
|
|
set "NPM_REGISTRY=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="-h" goto usage
|
|
if /i "%~1"=="--help" goto usage
|
|
|
|
echo ERROR: Unknown option: %~1
|
|
echo.
|
|
goto usage_error
|
|
|
|
:end_parse
|
|
|
|
call :ValidateOptions
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
echo ===========================================
|
|
echo Qwen Code Installation Script
|
|
echo ===========================================
|
|
echo.
|
|
echo INFO: Install method: !METHOD!
|
|
if /i not "!METHOD!"=="npm" (
|
|
echo INFO: Standalone mirror: !MIRROR!
|
|
if not "!BASE_URL!"=="" echo INFO: Standalone base URL: !BASE_URL!
|
|
if not "!ARCHIVE_PATH!"=="" (
|
|
echo INFO: Standalone archive: !ARCHIVE_PATH!
|
|
) else (
|
|
echo INFO: Standalone version: !VERSION!
|
|
)
|
|
)
|
|
if /i not "!METHOD!"=="standalone" echo INFO: npm registry: !NPM_REGISTRY!
|
|
if not "!SOURCE!"=="unknown" echo INFO: Installation source: !SOURCE!
|
|
echo.
|
|
|
|
REM Dispatch after validation; detect falls back to npm only when unavailable.
|
|
if /i "!METHOD!"=="standalone" (
|
|
call :InstallStandalone
|
|
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
|
|
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
|
|
endlocal
|
|
exit /b 0
|
|
)
|
|
|
|
if /i "!METHOD!"=="npm" (
|
|
call :InstallNpm
|
|
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
|
|
call :PrintFinalInstructions ""
|
|
endlocal
|
|
exit /b 0
|
|
)
|
|
|
|
call :InstallStandalone
|
|
set "STANDALONE_STATUS=!ERRORLEVEL!"
|
|
if !STANDALONE_STATUS! EQU 0 (
|
|
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
|
|
endlocal
|
|
exit /b 0
|
|
)
|
|
|
|
if !STANDALONE_STATUS! EQU 2 (
|
|
echo WARNING: Falling back to npm installation.
|
|
call :InstallNpm
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo WARNING: Standalone archive was unavailable before npm fallback; npm fallback also failed.
|
|
echo WARNING: Retry with --method standalone to debug the standalone failure, or install Node.js 20+ and rerun --method npm.
|
|
exit /b !ERRORLEVEL!
|
|
)
|
|
call :PrintFinalInstructions ""
|
|
endlocal
|
|
exit /b 0
|
|
)
|
|
|
|
echo WARNING: Standalone install failed. Retry with --method npm to use npm, or --method standalone to debug the standalone failure.
|
|
exit /b !STANDALONE_STATUS!
|
|
|
|
:usage
|
|
call :PrintUsage
|
|
exit /b 0
|
|
|
|
:usage_error
|
|
call :PrintUsage
|
|
exit /b 1
|
|
|
|
:PrintUsage
|
|
echo Qwen Code Installer
|
|
echo.
|
|
echo Usage: install-qwen-with-source.bat [OPTIONS]
|
|
echo.
|
|
echo Options:
|
|
echo -s, --source SOURCE Record the installation source.
|
|
echo Only letters, numbers, dot, underscore, and dash are allowed.
|
|
echo --method METHOD Install method: detect, standalone, or npm.
|
|
echo --mirror MIRROR Standalone archive mirror: github or aliyun.
|
|
echo --base-url URL Override standalone archive base URL.
|
|
echo --archive PATH Install from a local standalone archive.
|
|
echo --version VERSION Standalone release version. Defaults to latest.
|
|
echo --registry REGISTRY npm registry to use.
|
|
echo Defaults to QWEN_NPM_REGISTRY or https://registry.npmmirror.com
|
|
echo -h, --help Show this help message.
|
|
exit /b 0
|
|
|
|
:ValidateOptions
|
|
if "!METHOD!"=="" set "METHOD=detect"
|
|
|
|
set "QWEN_VALIDATE_METHOD=!METHOD!"
|
|
set "QWEN_VALIDATE_MIRROR=!MIRROR!"
|
|
set "QWEN_VALIDATE_BASE_URL=!BASE_URL!"
|
|
set "QWEN_VALIDATE_ARCHIVE_PATH=!ARCHIVE_PATH!"
|
|
set "QWEN_VALIDATE_VERSION=!VERSION!"
|
|
set "QWEN_VALIDATE_NPM_REGISTRY=!NPM_REGISTRY!"
|
|
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!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); foreach ($name in 'METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE') { $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_VALIDATE_METHOD="
|
|
set "QWEN_VALIDATE_MIRROR="
|
|
set "QWEN_VALIDATE_BASE_URL="
|
|
set "QWEN_VALIDATE_ARCHIVE_PATH="
|
|
set "QWEN_VALIDATE_VERSION="
|
|
set "QWEN_VALIDATE_NPM_REGISTRY="
|
|
set "QWEN_VALIDATE_INSTALL_BASE="
|
|
set "QWEN_VALIDATE_INSTALL_DIR="
|
|
set "QWEN_VALIDATE_INSTALL_BIN_DIR="
|
|
set "QWEN_VALIDATE_SOURCE="
|
|
if %PS_STATUS% NEQ 0 (
|
|
echo ERROR: installer options contain unsafe command characters.
|
|
exit /b 1
|
|
)
|
|
|
|
if "!INSTALL_BASE!"=="" (
|
|
echo ERROR: QWEN_INSTALL_ROOT must not be empty.
|
|
exit /b 1
|
|
)
|
|
if "!INSTALL_DIR!"=="" (
|
|
echo ERROR: QWEN_INSTALL_LIB_DIR must not be empty.
|
|
exit /b 1
|
|
)
|
|
if "!INSTALL_BIN_DIR!"=="" (
|
|
echo ERROR: QWEN_INSTALL_BIN_DIR must not be empty.
|
|
exit /b 1
|
|
)
|
|
if "!INSTALL_BASE:~1,2!"==":\" goto validate_install_base_ok
|
|
if "!INSTALL_BASE:~1,2!"==":/" goto validate_install_base_ok
|
|
if "!INSTALL_BASE:~0,2!"=="\\" goto validate_install_base_ok
|
|
echo ERROR: QWEN_INSTALL_ROOT must be an absolute path.
|
|
exit /b 1
|
|
:validate_install_base_ok
|
|
if "!INSTALL_DIR:~1,2!"==":\" goto validate_install_dir_ok
|
|
if "!INSTALL_DIR:~1,2!"==":/" goto validate_install_dir_ok
|
|
if "!INSTALL_DIR:~0,2!"=="\\" goto validate_install_dir_ok
|
|
echo ERROR: QWEN_INSTALL_LIB_DIR must be an absolute path.
|
|
exit /b 1
|
|
:validate_install_dir_ok
|
|
if "!INSTALL_BIN_DIR:~1,2!"==":\" goto validate_install_bin_dir_ok
|
|
if "!INSTALL_BIN_DIR:~1,2!"==":/" goto validate_install_bin_dir_ok
|
|
if "!INSTALL_BIN_DIR:~0,2!"=="\\" goto validate_install_bin_dir_ok
|
|
echo ERROR: QWEN_INSTALL_BIN_DIR must be an absolute path.
|
|
exit /b 1
|
|
:validate_install_bin_dir_ok
|
|
|
|
if /i "!METHOD!"=="detect" goto validate_method_ok
|
|
if /i "!METHOD!"=="standalone" goto validate_method_ok
|
|
if /i "!METHOD!"=="npm" goto validate_method_ok
|
|
echo ERROR: --method must be detect, standalone, or npm.
|
|
exit /b 1
|
|
|
|
:validate_method_ok
|
|
if /i "!MIRROR!"=="github" goto validate_mirror_ok
|
|
if /i "!MIRROR!"=="aliyun" goto validate_mirror_ok
|
|
echo ERROR: --mirror must be github or aliyun.
|
|
exit /b 1
|
|
|
|
:validate_mirror_ok
|
|
call :ValidateHttpsUrlVar "BASE_URL" "--base-url"
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :ValidateHttpsUrlVar "NPM_REGISTRY" "--registry"
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :ValidateVersion
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :ValidateSource
|
|
exit /b %ERRORLEVEL%
|
|
|
|
:ValidateHttpsUrlVar
|
|
set "URL_VALUE=!%~1!"
|
|
set "URL_OPTION=%~2"
|
|
if "!URL_VALUE!"=="" exit /b 0
|
|
if /i "!URL_VALUE:~0,8!"=="https://" exit /b 0
|
|
|
|
echo ERROR: !URL_OPTION! must start with https://
|
|
exit /b 1
|
|
|
|
:ValidateVersion
|
|
if /i "!VERSION!"=="latest" exit /b 0
|
|
echo(!VERSION!| findstr /R /C:"^v*[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" >nul
|
|
if %ERRORLEVEL% EQU 0 exit /b 0
|
|
echo ERROR: --version must be 'latest' or a semver string.
|
|
exit /b 1
|
|
|
|
:ValidateSource
|
|
if "!SOURCE!"=="unknown" exit /b 0
|
|
echo(!SOURCE!| findstr /R /C:"^[A-Za-z0-9._-][A-Za-z0-9._-]*$" >nul
|
|
if %ERRORLEVEL% EQU 0 exit /b 0
|
|
|
|
echo ERROR: --source may only contain letters, numbers, dot, underscore, or dash.
|
|
exit /b 1
|
|
|
|
:DetectTarget
|
|
set "TARGET="
|
|
if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" set "TARGET=win-x64"
|
|
if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" set "TARGET=win-x64"
|
|
if "!TARGET!"=="" (
|
|
echo WARNING: Standalone archive is not available for this Windows architecture.
|
|
exit /b 1
|
|
)
|
|
exit /b 0
|
|
|
|
:ReleaseVersionPath
|
|
if /i "!VERSION!"=="latest" (
|
|
set "VERSION_PATH=latest"
|
|
exit /b 0
|
|
)
|
|
set "VERSION_PATH=!VERSION!"
|
|
if /i "!VERSION_PATH:~0,1!"=="v" exit /b 0
|
|
set "VERSION_PATH=v!VERSION_PATH!"
|
|
exit /b 0
|
|
|
|
:StandaloneBaseUrl
|
|
if not "!BASE_URL!"=="" (
|
|
set "STANDALONE_BASE_URL=!BASE_URL!"
|
|
exit /b 0
|
|
)
|
|
|
|
call :ReleaseVersionPath
|
|
if /i "!MIRROR!"=="aliyun" (
|
|
set "STANDALONE_BASE_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/!VERSION_PATH!"
|
|
exit /b 0
|
|
)
|
|
|
|
if /i "!VERSION_PATH!"=="latest" (
|
|
set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/latest/download"
|
|
exit /b 0
|
|
)
|
|
|
|
set "STANDALONE_BASE_URL=https://github.com/QwenLM/qwen-code/releases/download/!VERSION_PATH!"
|
|
exit /b 0
|
|
|
|
:UrlExists
|
|
set "QWEN_CHECK_URL=%~1"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $request = [Net.WebRequest]::Create($env:QWEN_CHECK_URL); $request.Method = 'HEAD'; try { $response = $request.GetResponse(); $response.Close(); exit 0 } catch { exit 1 }" >nul 2>&1
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_CHECK_URL="
|
|
exit /b %PS_STATUS%
|
|
|
|
:DownloadFile
|
|
set "QWEN_DOWNLOAD_URL=%~1"
|
|
set "QWEN_DOWNLOAD_DEST=%~2"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $client = New-Object Net.WebClient; $client.DownloadFile($env:QWEN_DOWNLOAD_URL, $env:QWEN_DOWNLOAD_DEST); exit 0 } catch { exit 1 }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_DOWNLOAD_URL="
|
|
set "QWEN_DOWNLOAD_DEST="
|
|
exit /b %PS_STATUS%
|
|
|
|
:VerifyChecksum
|
|
set "ARCHIVE_FILE=%~1"
|
|
set "CHECKSUM_SOURCE=%~2"
|
|
set "ARCHIVE_NAME=%~3"
|
|
set "CHECKSUM_FILE=!CHECKSUM_SOURCE!"
|
|
set "TEMP_CHECKSUM="
|
|
set "REQUIRE_CHECKSUM=1"
|
|
|
|
if "!CHECKSUM_FILE!"=="" (
|
|
for %%I in ("!ARCHIVE_FILE!") do set "CHECKSUM_FILE=%%~dpISHA256SUMS"
|
|
) else (
|
|
if /i "!CHECKSUM_FILE:~0,8!"=="https://" (
|
|
set "REQUIRE_CHECKSUM=1"
|
|
set "TEMP_CHECKSUM=%TEMP%\qwen-code-checksums-%RANDOM%%RANDOM%.txt"
|
|
call :DownloadFile "!CHECKSUM_FILE!" "!TEMP_CHECKSUM!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_CHECKSUM!" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
|
|
echo ERROR: Could not download SHA256SUMS for checksum verification.
|
|
exit /b 1
|
|
)
|
|
set "CHECKSUM_FILE=!TEMP_CHECKSUM!"
|
|
)
|
|
)
|
|
|
|
if not exist "!CHECKSUM_FILE!" (
|
|
if "!REQUIRE_CHECKSUM!"=="1" (
|
|
echo ERROR: SHA256SUMS not found; cannot verify archive.
|
|
exit /b 1
|
|
)
|
|
echo WARNING: SHA256SUMS not found; skipping checksum verification.
|
|
exit /b 0
|
|
)
|
|
|
|
set "EXPECTED_HASH="
|
|
for /f "usebackq tokens=1,2" %%H in ("!CHECKSUM_FILE!") do (
|
|
set "CHECKSUM_HASH=%%H"
|
|
set "CHECKSUM_NAME=%%I"
|
|
if "!CHECKSUM_NAME:~0,1!"=="*" set "CHECKSUM_NAME=!CHECKSUM_NAME:~1!"
|
|
if "!CHECKSUM_NAME!"=="!ARCHIVE_NAME!" (
|
|
if "!EXPECTED_HASH!"=="" set "EXPECTED_HASH=!CHECKSUM_HASH!"
|
|
)
|
|
)
|
|
|
|
if "!EXPECTED_HASH!"=="" (
|
|
if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
|
|
if "!REQUIRE_CHECKSUM!"=="1" (
|
|
echo ERROR: Checksum entry for !ARCHIVE_NAME! not found.
|
|
exit /b 1
|
|
)
|
|
echo WARNING: Checksum entry for !ARCHIVE_NAME! not found; skipping checksum verification.
|
|
exit /b 0
|
|
)
|
|
|
|
set "ACTUAL_HASH="
|
|
set "QWEN_HASH_FILE=!ARCHIVE_FILE!"
|
|
for /f "delims=" %%H in ('powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; (Get-FileHash -Algorithm SHA256 -LiteralPath $env:QWEN_HASH_FILE).Hash" 2^>nul') do (
|
|
if "!ACTUAL_HASH!"=="" set "ACTUAL_HASH=%%H"
|
|
)
|
|
set "QWEN_HASH_FILE="
|
|
|
|
if not "!TEMP_CHECKSUM!"=="" del /F /Q "!TEMP_CHECKSUM!" >nul 2>&1
|
|
|
|
if "!ACTUAL_HASH!"=="" (
|
|
if "!REQUIRE_CHECKSUM!"=="1" (
|
|
echo ERROR: Could not calculate SHA-256 checksum for archive.
|
|
exit /b 1
|
|
)
|
|
echo WARNING: Could not calculate SHA-256 checksum; skipping checksum verification.
|
|
exit /b 0
|
|
)
|
|
|
|
if /i not "!EXPECTED_HASH!"=="!ACTUAL_HASH!" (
|
|
echo ERROR: Checksum verification failed for !ARCHIVE_NAME!.
|
|
exit /b 1
|
|
)
|
|
|
|
echo SUCCESS: Checksum verified for !ARCHIVE_NAME!.
|
|
exit /b 0
|
|
|
|
:InstallStandalone
|
|
set "TEMP_DIR="
|
|
set "CHECKSUM_SOURCE="
|
|
|
|
REM Resolve the archive from a local file or from the configured release mirror.
|
|
if not "!ARCHIVE_PATH!"=="" (
|
|
set "ARCHIVE_FILE=!ARCHIVE_PATH!"
|
|
for %%I in ("!ARCHIVE_FILE!") do set "ARCHIVE_NAME=%%~nxI"
|
|
if not exist "!ARCHIVE_FILE!" (
|
|
echo ERROR: Standalone archive not found: !ARCHIVE_FILE!
|
|
exit /b 1
|
|
)
|
|
) else (
|
|
call :DetectTarget
|
|
if !ERRORLEVEL! NEQ 0 exit /b 2
|
|
|
|
set "ARCHIVE_NAME=qwen-code-win-x64.zip"
|
|
call :StandaloneBaseUrl
|
|
set "ARCHIVE_URL=!STANDALONE_BASE_URL!/!ARCHIVE_NAME!"
|
|
set "CHECKSUM_SOURCE=!STANDALONE_BASE_URL!/SHA256SUMS"
|
|
|
|
if /i "!METHOD!"=="detect" (
|
|
call :UrlExists "!ARCHIVE_URL!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo WARNING: Standalone archive not found: !ARCHIVE_NAME!
|
|
exit /b 2
|
|
)
|
|
)
|
|
|
|
set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%"
|
|
mkdir "!TEMP_DIR!" >nul 2>&1
|
|
set "ARCHIVE_FILE=!TEMP_DIR!\!ARCHIVE_NAME!"
|
|
|
|
echo INFO: Downloading !ARCHIVE_URL!
|
|
call :DownloadFile "!ARCHIVE_URL!" "!ARCHIVE_FILE!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo WARNING: Failed to download standalone archive.
|
|
exit /b 2
|
|
)
|
|
)
|
|
|
|
if "!TEMP_DIR!"=="" (
|
|
set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%"
|
|
mkdir "!TEMP_DIR!" >nul 2>&1
|
|
)
|
|
|
|
REM Verify integrity before extraction or changing the install directory.
|
|
call :VerifyChecksum "!ARCHIVE_FILE!" "!CHECKSUM_SOURCE!" "!ARCHIVE_NAME!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
|
|
REM Extract into a temporary directory, then validate required entry points.
|
|
set "EXTRACT_DIR=!TEMP_DIR!\extract"
|
|
mkdir "!EXTRACT_DIR!" >nul 2>&1
|
|
call :ValidateArchiveContents "!ARCHIVE_FILE!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
set "QWEN_ARCHIVE_FILE=!ARCHIVE_FILE!"
|
|
set "QWEN_EXTRACT_DIR=!EXTRACT_DIR!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "Expand-Archive -LiteralPath $env:QWEN_ARCHIVE_FILE -DestinationPath $env:QWEN_EXTRACT_DIR -Force"
|
|
set "PS_STATUS=!ERRORLEVEL!"
|
|
set "QWEN_ARCHIVE_FILE="
|
|
set "QWEN_EXTRACT_DIR="
|
|
if !PS_STATUS! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to extract standalone archive.
|
|
exit /b 1
|
|
)
|
|
|
|
call :RejectArchiveLinks "!EXTRACT_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
|
|
if not exist "!EXTRACT_DIR!\qwen-code\bin\qwen.cmd" (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Archive does not contain qwen-code\bin\qwen.cmd.
|
|
exit /b 1
|
|
)
|
|
|
|
if not exist "!EXTRACT_DIR!\qwen-code\node\node.exe" (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Archive does not contain qwen-code\node\node.exe.
|
|
exit /b 1
|
|
)
|
|
|
|
if not exist "!INSTALL_BASE!" mkdir "!INSTALL_BASE!"
|
|
if not exist "!INSTALL_BIN_DIR!" mkdir "!INSTALL_BIN_DIR!"
|
|
for %%I in ("!INSTALL_DIR!") do set "INSTALL_PARENT=%%~dpI"
|
|
if not exist "!INSTALL_PARENT!" mkdir "!INSTALL_PARENT!"
|
|
|
|
REM Stage into .new and keep .old so failed upgrades can roll back.
|
|
set "NEW_INSTALL_DIR=!INSTALL_DIR!.new"
|
|
set "OLD_INSTALL_DIR=!INSTALL_DIR!.old"
|
|
|
|
call :EnsureManagedInstallDir "!INSTALL_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
call :EnsureManagedInstallDir "!NEW_INSTALL_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
call :EnsureManagedInstallDir "!OLD_INSTALL_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
|
|
if exist "!NEW_INSTALL_DIR!" rmdir /S /Q "!NEW_INSTALL_DIR!" >nul 2>&1
|
|
if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1
|
|
move /Y "!EXTRACT_DIR!\qwen-code" "!NEW_INSTALL_DIR!" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to stage standalone archive.
|
|
exit /b 1
|
|
)
|
|
|
|
if exist "!INSTALL_DIR!" (
|
|
move /Y "!INSTALL_DIR!" "!OLD_INSTALL_DIR!" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to back up existing install at !INSTALL_DIR!.
|
|
exit /b 1
|
|
)
|
|
)
|
|
move /Y "!NEW_INSTALL_DIR!" "!INSTALL_DIR!" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to install standalone archive to !INSTALL_DIR!.
|
|
exit /b 1
|
|
)
|
|
|
|
(
|
|
echo @echo off
|
|
echo call "!INSTALL_DIR!\bin\qwen.cmd" %%*
|
|
) > "!INSTALL_BIN_DIR!\qwen.cmd.new"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!INSTALL_DIR!" rmdir /S /Q "!INSTALL_DIR!" >nul 2>&1
|
|
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to create qwen wrapper in !INSTALL_BIN_DIR!.
|
|
exit /b 1
|
|
)
|
|
move /Y "!INSTALL_BIN_DIR!\qwen.cmd.new" "!INSTALL_BIN_DIR!\qwen.cmd" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!INSTALL_BIN_DIR!\qwen.cmd.new" del /F /Q "!INSTALL_BIN_DIR!\qwen.cmd.new" >nul 2>&1
|
|
if exist "!INSTALL_DIR!" rmdir /S /Q "!INSTALL_DIR!" >nul 2>&1
|
|
if exist "!OLD_INSTALL_DIR!" move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to create qwen wrapper in !INSTALL_BIN_DIR!.
|
|
exit /b 1
|
|
)
|
|
|
|
if exist "!OLD_INSTALL_DIR!" rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1
|
|
|
|
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
|
|
call :CreateSourceJson
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
|
|
echo SUCCESS: Qwen Code standalone archive installed successfully.
|
|
echo INFO: Installed to !INSTALL_DIR!
|
|
exit /b 0
|
|
|
|
:ValidateArchiveContents
|
|
set "QWEN_ARCHIVE_FILE=%~1"
|
|
REM Enumerate archive entries and reject any with path traversal indicators:
|
|
REM empty names, leading '/', drive-rooted paths, '..' segments, or control chars.
|
|
REM This prevents Zip Slip attacks before extraction.
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $archive = $null; try { Add-Type -AssemblyName System.IO.Compression.FileSystem; $archive = [IO.Compression.ZipFile]::OpenRead($env:QWEN_ARCHIVE_FILE); foreach ($entry in $archive.Entries) { $raw = $entry.FullName; if ($raw.IndexOfAny([char[]](10,13)) -ge 0) { [Console]::Error.WriteLine('Archive contains unsafe path with control character: ' + $raw); exit 1 }; $name = $raw -replace '\\', '/'; while ($name.StartsWith('./')) { $name = $name.Substring(2) }; if ($name -eq '' -or $name.StartsWith('/') -or $name -match '^[A-Za-z]:' -or $name -match '(^|/)\.\.(/|$)') { [Console]::Error.WriteLine('Archive contains unsafe path: ' + $entry.FullName); exit 1 } } } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 2 } finally { if ($null -ne $archive) { $archive.Dispose() } }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_ARCHIVE_FILE="
|
|
if %PS_STATUS% EQU 0 exit /b 0
|
|
if %PS_STATUS% EQU 1 (
|
|
echo ERROR: Archive contains unsafe path entries.
|
|
exit /b 1
|
|
)
|
|
if %PS_STATUS% EQU 2 (
|
|
echo ERROR: Archive could not be inspected before extraction.
|
|
exit /b 1
|
|
)
|
|
echo ERROR: Archive validation failed before extraction.
|
|
exit /b %PS_STATUS%
|
|
|
|
:RejectArchiveLinks
|
|
set "QWEN_EXTRACT_DIR=%~1"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$item = Get-ChildItem -LiteralPath $env:QWEN_EXTRACT_DIR -Recurse -Force | Where-Object { ($_.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0 } | Select-Object -First 1; if ($item) { exit 1 }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_EXTRACT_DIR="
|
|
if %PS_STATUS% NEQ 0 echo ERROR: Archive contains symlinks or reparse points; refusing to install.
|
|
exit /b %PS_STATUS%
|
|
|
|
:EnsureManagedInstallDir
|
|
set "MANAGED_DIR=%~1"
|
|
if not exist "!MANAGED_DIR!" exit /b 0
|
|
if exist "!MANAGED_DIR!\manifest.json" exit /b 0
|
|
|
|
echo ERROR: !MANAGED_DIR! exists but is not a Qwen Code standalone install.
|
|
echo ERROR: Refusing to overwrite it. Move or remove it manually, then rerun the installer.
|
|
exit /b 1
|
|
|
|
:RequireNode
|
|
where node >nul 2>&1
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: Node.js was not found.
|
|
echo.
|
|
echo Node.js 20 or newer is required before installing Qwen Code with npm.
|
|
echo Please install Node.js from https://nodejs.org/ and rerun this installer.
|
|
exit /b 1
|
|
)
|
|
|
|
for /f "delims=" %%i in ('node -p "process.versions.node" 2^>nul') do set "NODE_VERSION=%%i"
|
|
if "%NODE_VERSION%"=="" (
|
|
echo ERROR: Unable to determine Node.js version.
|
|
echo Node.js 20 or newer is required before installing Qwen Code with npm.
|
|
exit /b 1
|
|
)
|
|
|
|
for /f "tokens=1 delims=." %%a in ("%NODE_VERSION%") do set "MAJOR_VERSION=%%a"
|
|
set /a NODE_MAJOR_NUM=%MAJOR_VERSION% >nul 2>&1
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: Unable to determine Node.js version.
|
|
echo Node.js 20 or newer is required before installing Qwen Code with npm.
|
|
exit /b 1
|
|
)
|
|
|
|
if %NODE_MAJOR_NUM% LSS 20 (
|
|
echo ERROR: Node.js %NODE_VERSION% is installed, but Node.js 20 or newer is required.
|
|
echo Please install Node.js from https://nodejs.org/ and rerun this installer.
|
|
exit /b 1
|
|
)
|
|
|
|
echo SUCCESS: Node.js %NODE_VERSION% detected.
|
|
exit /b 0
|
|
|
|
:RequireNpm
|
|
where npm >nul 2>&1
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: npm was not found.
|
|
echo Please install Node.js with npm included, then rerun this installer.
|
|
exit /b 1
|
|
)
|
|
|
|
for /f "delims=" %%i in ('npm -v 2^>nul') do set "NPM_VERSION=%%i"
|
|
echo SUCCESS: npm %NPM_VERSION% detected.
|
|
exit /b 0
|
|
|
|
:InstallNpm
|
|
call :RequireNode
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :RequireNpm
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
where qwen >nul 2>&1
|
|
if %ERRORLEVEL% EQU 0 (
|
|
for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i"
|
|
echo INFO: Existing Qwen Code detected: !QWEN_VERSION!
|
|
echo INFO: Upgrading to the latest version.
|
|
)
|
|
|
|
echo INFO: Running: npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY!
|
|
call npm install -g @qwen-code/qwen-code@latest --registry "!NPM_REGISTRY!"
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: Failed to install Qwen Code.
|
|
echo.
|
|
echo This installer does not change your npm prefix or PATH.
|
|
echo If the failure is a permission error, fix your npm global package directory, then run:
|
|
echo npm install -g @qwen-code/qwen-code@latest --registry !NPM_REGISTRY!
|
|
exit /b 1
|
|
)
|
|
|
|
echo SUCCESS: Qwen Code installed successfully.
|
|
call :CreateSourceJson
|
|
exit /b 0
|
|
|
|
:CreateSourceJson
|
|
if "!SOURCE!"=="unknown" exit /b 0
|
|
|
|
set "QWEN_DIR=!USERPROFILE!\.qwen"
|
|
if not exist "!QWEN_DIR!" mkdir "!QWEN_DIR!"
|
|
|
|
(
|
|
echo {
|
|
echo "source": "!SOURCE!"
|
|
echo }
|
|
) > "!QWEN_DIR!\source.json"
|
|
|
|
echo SUCCESS: Installation source saved to !USERPROFILE!\.qwen\source.json
|
|
exit /b 0
|
|
|
|
:PrintFinalInstructions
|
|
set "EXTRA_BIN=%~1"
|
|
if not "!EXTRA_BIN!"=="" set "PATH=!EXTRA_BIN!;!PATH!"
|
|
|
|
echo.
|
|
echo ===========================================
|
|
echo Installation completed!
|
|
echo ===========================================
|
|
echo.
|
|
|
|
where qwen >nul 2>&1
|
|
if %ERRORLEVEL% EQU 0 (
|
|
for /f "delims=" %%i in ('qwen --version 2^>nul') do set "QWEN_VERSION=%%i"
|
|
echo SUCCESS: Qwen Code is ready to use: !QWEN_VERSION!
|
|
echo.
|
|
echo You can now run: qwen
|
|
echo.
|
|
echo INFO: Run qwen in your project directory to start an interactive session.
|
|
exit /b 0
|
|
)
|
|
|
|
echo WARNING: Qwen Code was installed, but qwen is not on PATH in this prompt.
|
|
echo.
|
|
echo Restart your command prompt, then run: qwen
|
|
if not "!EXTRA_BIN!"=="" (
|
|
echo.
|
|
echo Or add this directory to PATH:
|
|
echo !EXTRA_BIN!
|
|
echo Then run:
|
|
echo qwen
|
|
exit /b 0
|
|
)
|
|
|
|
for /f "delims=" %%i in ('npm prefix -g 2^>nul') do set "NPM_PREFIX=%%i"
|
|
if not "!NPM_PREFIX!"=="" (
|
|
echo.
|
|
echo Or add this npm global directory to PATH:
|
|
echo !NPM_PREFIX!
|
|
echo Then run:
|
|
echo qwen
|
|
)
|
|
exit /b 0
|