feat(installer): add standalone archive installation#3776
Conversation
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
Runtime direction note: This PR intentionally uses code-server-style standalone archives that bundle the official Node.js runtime, instead of switching Qwen Code to Bun-compiled single-file executables in this pass. The goal here is to make the installer/release/checksum/mirror/offline-install flow work without depending on the user's local Node/npm environment, while keeping the runtime behavior close to today's npm package. PR #302 is still relevant as a future executable-distribution exploration. If the Bun executable path becomes mature enough, the installer and release infrastructure from this PR should still be reusable; the payload could be swapped from node + dist/cli.js to a compiled qwen executable, or added as a separate install method. |
中文说明:这个 PR 的改动范围、使用影响面,以及和 #3728 / #302 的关系这个 PR 主要做了什么这个 PR 的主线是把 Qwen Code 的一键安装流程从“依赖用户本机 Node/npm 环境”推进到“优先使用平台 standalone archive 安装”。整体设计更接近 code-server 的分发方式:release 阶段产出多个平台的压缩包,installer 根据当前 OS/arch 下载对应包并安装。 这次新增的 release artifacts 预期包括: 每个 standalone archive 里包含 Qwen Code bundle、运行所需资源、shim,以及一个私有的官方 Node.js runtime。因此用户安装 standalone 包时不需要本机已经有可用的 Node.js/npm。 合入后的使用影响面合入后,安装脚本默认会进入
也就是说,用户路径会更轻:不再由 installer 自动安装 nvm、安装 Node、修改 npm prefix、修改 shell profile 或直接启动 同时这个 PR 也支持:
和 #3728 的关系#3728 的核心目标是让 Qwen Code 支持类似 OpenCode/code-server 的一键安装体验,并减少对用户本地 Node/npm/nvm 的依赖。这个 PR 与该目标是一致的:它先落地 installer、release artifact、checksum、mirror、本地 archive 和跨平台安装流程。 但需要澄清一点:这个 PR 产出的不是 Bun/native 意义上的“单文件 executable”。它产出的是平台相关 standalone archive,里面带了官方 Node executable,再用这个私有 Node runtime 执行 Qwen Code 的 bundled JS CLI。 所以如果把 #3728 理解为“用户不需要自己装 Node/npm 就能使用 Qwen Code”,这个 PR 已经覆盖了主要目标。如果严格理解为“像 OpenCode 那样最终只发布单个编译后的可执行文件”,那还需要后续继续推进 Bun executable 或其他 binary 方案。 和 #302 的关系#302 探索的是用 Bun 把 Qwen Code 编译成多平台 executable。它和这个 PR 不直接冲突,但属于另一条 runtime payload 路线。 当前 PR 选择先使用 bundled official Node.js runtime,主要是为了保持运行行为和现有 npm 包更接近,降低一次性切 runtime 的风险。Qwen Code 当前仍有一些需要单独验证的兼容面,例如 runtime assets、optional native dependencies、Node API 行为、pty/clipboard 能力、wasm、release 校验和 installer 集成。 后续如果 Bun executable 路线成熟,这个 PR 里的 installer/release/checksum/mirror/local archive 基础设施仍然可以复用;需要替换或新增的是 archive 内部 payload,例如从 已知边界
|
| ); | ||
| } | ||
|
|
||
| fs.copyFileSync( |
There was a problem hiding this comment.
[Suggestion] Standalone archive now bundles the un-stripped root package.json (with devDependencies, scripts) instead of dist/package.json which prepare-package.js strips to a production-ready version.
The previous code preferred dist/package.json and only fell back to the root copy when it was missing. The new code unconditionally copies the root version. This means the archive carries dev-only fields that prepare-package.js intentionally removes.
| fs.copyFileSync( | |
| const distPkg = path.join(distDir, 'package.json'); | |
| fs.copyFileSync( | |
| fs.existsSync(distPkg) ? distPkg : path.join(rootDir, 'package.json'), | |
| path.join(packageRoot, 'package.json'), | |
| ); |
— deepseek-v4-pro via Qwen Code /review
| mkdir "!EXTRACT_DIR!" >nul 2>&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" |
There was a problem hiding this comment.
[Suggestion] The .bat installer calls Expand-Archive directly without pre-extraction archive entry validation. The shell installer has validate_archive_contents() that checks for path traversal (.., absolute paths, empty entries) before calling tar/unzip. The .bat relies solely on Expand-Archive's internal behavior — post-extraction checks won't catch traversal entries that land outside qwen-code/.
Consider adding an equivalent pre-extraction entry check for .zip archives (e.g., unzip -Z1 or a PowerShell listing) that mirrors validate_archive_entry_path from the shell installer.
— deepseek-v4-pro via Qwen Code /review
| 'README.md', | ||
| 'LICENSE', | ||
| 'locales', | ||
| 'examples', |
There was a problem hiding this comment.
[Suggestion] esbuild.json is not in DIST_ALLOWED_ENTRIES. When DEV=true is set, esbuild.config.js writes esbuild.json into dist/, and copyRuntimeAssets rejects it as an "Unexpected dist asset". This causes all standalone packaging tests to fail in dev environments with a persistent dist/ directory.
Production builds (CI, no DEV) are unaffected since esbuild.json is only generated in dev mode.
| 'examples', | |
| const DIST_ALLOWED_ENTRIES = new Set([ | |
| 'cli.js', | |
| 'vendor', | |
| 'bundled', | |
| 'package.json', | |
| 'README.md', | |
| 'LICENSE', | |
| 'locales', | |
| 'examples', | |
| 'esbuild.json', | |
| ]); |
— deepseek-v4-pro via Qwen Code /review
| }); | ||
| }); | ||
|
|
||
| function ensureMinimalDist() { |
There was a problem hiding this comment.
[Suggestion] ensureMinimalDist() returns false when dist/ already exists, leaving dev artifacts (like esbuild.json) in place. Tests that rely on this helper inherit environment state, causing failures in dev machines with a persistent dist/ directory.
Consider cleaning up known dev artifacts before returning false, or always creating a clean dist directory by temporarily relocating the original.
— deepseek-v4-pro via Qwen Code /review
Post-merge behavior and local validationThis PR is the first stage of #3728. It does not change the Qwen Code CLI runtime behavior for existing npm users. The main change is adding the standalone archive packaging and installer foundation. After this PR is merged:
Local validation I ran: bash scripts/installation/install-qwen-with-source.sh --help
node scripts/build-standalone-release.js --help
npm run test:scriptsObserved result: The skipped tests are platform-specific for the current local environment. Note: the stable public hosted installer entrypoint and release installer asset publication are handled in follow-up PRs. This PR provides the standalone archive packaging and installer foundation. |
| @@ -411,6 +416,8 @@ jobs: | |||
|
|
|||
There was a problem hiding this comment.
[Critical] The release job builds the new standalone archives and attaches them to the GitHub release, but nothing publishes the same qwen-code-* archives and SHA256SUMS to the Aliyun OSS/CDN path that the installer uses when --mirror aliyun is selected (https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/<version>). The installation guide also says those versioned and latest/ directories must be populated, so Aliyun standalone installs can fail or fall back to npm even though the GitHub release is healthy. Please add a release step that uploads dist/standalone/qwen-code-* and dist/standalone/SHA256SUMS to the Aliyun versioned and latest/ paths, or remove/disable the Aliyun standalone mirror path until publishing is wired up.
— gpt-5.5 via Qwen Code /review
| # 2. prefix is a system directory (would break sudo setuid binaries) | ||
| # 3. prefix directory is not writable | ||
| local use_user_dir=false | ||
| if [[ -f "${install_dir}/manifest.json" ]]; then |
There was a problem hiding this comment.
[Critical] This guard treats any existing directory containing a manifest.json as a managed Qwen standalone install and allows the installer to move/delete/replace it. Because QWEN_INSTALL_LIB_DIR is user-controlled and many unrelated project/application directories contain manifest.json, a mistyped install path can overwrite unrelated data despite the non-managed-directory protection. Please parse and validate the manifest contents before allowing replacement, e.g. require name: "@qwen-code/qwen-code", a supported target, and the expected standalone runtime layout.
— gpt-5.5 via Qwen Code /review
| :EnsureManagedInstallDir | ||
| set "MANAGED_DIR=%~1" | ||
| if not exist "!MANAGED_DIR!" exit /b 0 | ||
| if exist "!MANAGED_DIR!\manifest.json" exit /b 0 |
There was a problem hiding this comment.
[Critical] This has the same overwrite hole as the Unix installer: any directory with a manifest.json is considered managed and can be replaced. Since QWEN_INSTALL_ROOT is user-controlled, a wrong path that points at another app or project with a manifest.json will pass this check and then be moved/deleted during staging. Please validate the manifest contents and expected Qwen standalone structure before treating the directory as safe to overwrite.
— gpt-5.5 via Qwen Code /review
| }); | ||
| }); | ||
|
|
||
| describe('Windows installer end-to-end', () => { |
There was a problem hiding this comment.
[Suggestion] The Windows E2E coverage currently only exercises the local-archive happy path, tampered checksum, and unsafe env-derived install root. The .bat installer has Windows-specific logic for detect fallback semantics, managed-directory refusal, missing SHA256SUMS / checksum entries, archive reparse-point rejection, and missing required archive entries, but those branches are not covered here. Please add Windows-only tests mirroring the critical Unix coverage so integrity, overwrite-safety, and fallback behavior cannot regress independently on Windows.
— gpt-5.5 via Qwen Code /review
|
All four items from the May 7 review pass have been addressed:
The earlier review rounds (Apr 30, May 2, May 4) have also been addressed through several hardening commits on this branch. CI is green. Re-requesting review. |
| 'README.md', | ||
| 'LICENSE', | ||
| 'locales', | ||
| 'examples', |
There was a problem hiding this comment.
[Suggestion] esbuild.json 不在 DIST_ALLOWED_ENTRIES 白名单中。
npm run bundle 在 DEV=true 时会生成 dist/esbuild.json(参见 esbuild.config.js:115),copyRuntimeAssets 会将其视为「Unexpected dist asset」而调用 fail()。CI 当前不使用 DEV=true,但任何开发者在本地构建独立归档都会受阻。
| 'examples', | |
| const DIST_ALLOWED_ENTRY_PATTERNS = [ | |
| /^sandbox-macos-(permissive|restrictive)-(open|closed|proxied)\.sb$/, | |
| /^esbuild\.json$/, | |
| ]; |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| local NPM_GLOBAL_DIR | ||
| NPM_GLOBAL_DIR=$(npm config get prefix 2>/dev/null) || true | ||
| if [[ ! -e "${install_dir}" ]]; then | ||
| return 0 |
There was a problem hiding this comment.
[Suggestion] manifest.json 守卫仅检查文件是否存在,未验证内容。
ensure_managed_install_dir 仅检查 [[ -f "${install_dir}/manifest.json" ]],不验证其中是否包含 "name": "@qwen-code/qwen-code"。任何包含 manifest.json 的目录(PWA、Electron 应用、Chrome 扩展等)都会被当作可覆盖的 Qwen Code 安装目录。
| return 0 | |
| if grep -q '"@qwen-code/qwen-code"' "${install_dir}/manifest.json" 2>/dev/null; then | |
| return 0 | |
| fi |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| :EnsureManagedInstallDir | ||
| set "MANAGED_DIR=%~1" | ||
| if not exist "!MANAGED_DIR!" exit /b 0 | ||
| if exist "!MANAGED_DIR!\manifest.json" exit /b 0 |
There was a problem hiding this comment.
[Suggestion] .bat 中同样存在 manifest.json 守卫仅检查存在的问题(与 shell 脚本相同)。:EnsureManagedInstallDir 仅检查 if exist "!MANAGED_DIR!\manifest.json",不验证文件内容。
| if exist "!MANAGED_DIR!\manifest.json" exit /b 0 | |
| powershell -NoProfile -Command "if (Test-Path '!MANAGED_DIR!\manifest.json') { $c = Get-Content '!MANAGED_DIR!\manifest.json' -Raw; if ($c -match '@qwen-code/qwen-code') { exit 0 } }" | |
| if %ERRORLEVEL% EQU 0 exit /b 0 |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| const runtimeDir = fs.mkdtempSync( | ||
| path.join(runtimeParent, 'qwen-node-runtime-'), | ||
| ); | ||
| const nodeDistUrl = `https://nodejs.org/dist/v${nodeVersion}`; |
There was a problem hiding this comment.
[Suggestion] --node-version 未去除前导 v,导致双 v URL。
第 75 行:const nodeDistUrl = \https://nodejs.org/dist/v${nodeVersion}\`;` — 已硬编码前缀 v。用户输入 --node-version v20.19.0(与 node -v 输出格式一致)将产生 https://nodejs.org/dist/vv20.19.0/... 的无效 URL。
| const nodeDistUrl = `https://nodejs.org/dist/v${nodeVersion}`; | |
| const nodeVersion = (args.nodeVersion || process.versions.node).replace(/^v/, ''); |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| echo. | ||
| echo =========================================== | ||
| set "TEMP_DIR=%TEMP%\qwen-code-install-%RANDOM%%RANDOM%" | ||
| mkdir "!TEMP_DIR!" >nul 2>&1 |
There was a problem hiding this comment.
[Suggestion] :InstallStandalone 中 mkdir 的退出码被静默忽略。
第 485/499/511/542/543/545 行共 6 处 mkdir 调用未检查 %ERRORLEVEL%。目录创建失败(磁盘满、权限不足)会被静默忽略,导致后续步骤产生误导性错误(如「Failed to download」而实际是目录创建失败)。Shell 脚本通过 set -e 正确处理了此问题。
mkdir "!TEMP_DIR!" >nul 2>&1
if %ERRORLEVEL% NEQ 0 (
echo ERROR: Failed to create temp directory.
exit /b 1
)各 mkdir 调用处均需添加相同的错误检查。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
|
|
||
| :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 |
There was a problem hiding this comment.
[Suggestion] 版本验证正则 v*(零个或多个)应为 v?(零个或一个)。
第 307 行:findstr /R /C:"^v*[0-9]..." — v* 可匹配多个前导 v(如 vv1.2.3)。Shell 脚本正确使用 ^v?。多 v 版本号会通过验证并产生无效的下载链接。与 shell 脚本行为不一致。
| echo(!VERSION!| findstr /R /C:"^v*[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*[A-Za-z0-9.-]*$" >nul | |
| 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% NEQ 0 ( | |
| echo ERROR: Invalid version format. | |
| exit /b 1 | |
| ) |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
|
|
||
| ### Quick Install (Recommended) | ||
|
|
||
| The installer uses a standalone Qwen Code archive when one is available for |
Summary
Runtime Direction
This PR intentionally uses standalone archives that bundle the official Node.js runtime. The goal is to remove the user's local Node.js/npm requirement while keeping runtime behavior close to the existing npm package.
PR #302, which explores Bun-compiled single-file executables, is still relevant as a future distribution direction. It is not used as the default path in this PR because it needs a separate compatibility pass for runtime assets, optional native dependencies, Node API behavior, release artifacts, checksums, and installer integration. If that path matures later, the installer/release infrastructure added here should still be reusable with a different payload.
Validation
Commands run locally:
Observed result:
npm run test:scriptspassed locally: 2 test files, 30 total tests, 28 passed and 2 Windows-only tests skipped on macOS.git diff --checkexited 0.bash -n scripts/installation/install-qwen-with-source.shexited 0.node scripts/build-standalone-release.js --helploaded successfully and printed usage.CI status after the latest push:
npm run test:ci, which includesnpm run test:scripts..batstandalone install smoke tests.Reviewer Focus
detect,standalone, andnpm.Scope / Risk
Linked Issues / Bugs
Related #3728
Related #3738