fix(installer): address standalone review feedback

This commit is contained in:
yiliang114 2026-05-15 15:14:34 +08:00
parent 40ac124927
commit bae933332e
6 changed files with 169 additions and 27 deletions

View file

@ -241,7 +241,7 @@ if /i "!METHOD!"=="standalone" (
call :InstallStandalone
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
endlocal & set "PATH=!INSTALL_BIN_DIR!;%PATH%"
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
exit /b 0
)
@ -257,7 +257,7 @@ call :InstallStandalone
set "STANDALONE_STATUS=!ERRORLEVEL!"
if !STANDALONE_STATUS! EQU 0 (
call :PrintFinalInstructions "!INSTALL_BIN_DIR!"
endlocal & set "PATH=!INSTALL_BIN_DIR!;%PATH%"
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
exit /b 0
)
@ -906,6 +906,12 @@ if !ERRORLEVEL! NEQ 0 (
exit /b 1
)
call :RestoreStaleInstallBackup
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 !ERRORLEVEL! NEQ 0 (
@ -918,7 +924,7 @@ if exist "!OLD_INSTALL_DIR!" (
rmdir /S /Q "!OLD_INSTALL_DIR!" >nul 2>&1
if !ERRORLEVEL! NEQ 0 (
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
echo ERROR: Failed to remove stale backup directory: !OLD_INSTALL_DIR!.
echo ERROR: Failed to remove stale install backup: !OLD_INSTALL_DIR!.
exit /b 1
)
)
@ -1057,6 +1063,18 @@ if !ERRORLEVEL! NEQ 0 (
)
exit /b 0
:RestoreStaleInstallBackup
if exist "!INSTALL_DIR!" exit /b 0
if not exist "!OLD_INSTALL_DIR!" exit /b 0
echo WARNING: Found previous install backup without an active install: !OLD_INSTALL_DIR!
echo WARNING: Restoring backup to !INSTALL_DIR! before continuing.
move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
if !ERRORLEVEL! NEQ 0 (
echo ERROR: Failed to restore previous install from !OLD_INSTALL_DIR!.
exit /b 1
)
exit /b 0
: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 }"

View file

@ -811,6 +811,7 @@ verify_checksum() {
validate_archive_entry_path() {
local entry="$1"
entry="${entry//\\//}"
while [[ "${entry}" == ./* ]]; do
entry="${entry#./}"
@ -826,7 +827,7 @@ validate_archive_entry_path() {
esac
case "${entry}" in
""|/*|..|../*|*/..|*/../*|*\\*)
""|/*|..|../*|*/..|*/../*)
log_error "Archive contains unsafe path: ${entry:-<empty>}"
return 1
;;
@ -923,6 +924,24 @@ ensure_managed_install_dir() {
return 1
}
restore_stale_install_backup() {
local old_install_dir="$1"
local current_install_dir="$2"
if [[ -e "${current_install_dir}" || ! -e "${old_install_dir}" ]]; then
return 0
fi
log_warning "Found previous install backup without an active install: ${old_install_dir}"
log_warning "Restoring backup to ${current_install_dir} before continuing."
if mv "${old_install_dir}" "${current_install_dir}"; then
return 0
fi
log_error "Failed to restore previous install from ${old_install_dir}."
return 1
}
is_qwen_standalone_install_dir() {
local install_dir="$1"
local manifest_path="${install_dir}/manifest.json"
@ -1113,7 +1132,18 @@ install_standalone() {
rm -rf "${temp_dir}"
return 1
fi
rm -rf "${new_install_dir}" "${old_install_dir}" "${wrapper_tmp}"
if ! restore_stale_install_backup "${old_install_dir}" "${INSTALL_LIB_DIR}"; then
rm -rf "${temp_dir}"
return 1
fi
if [[ -e "${old_install_dir}" ]]; then
rm -rf "${old_install_dir}" || {
rm -rf "${temp_dir}"
log_error "Failed to remove stale install backup: ${old_install_dir}"
return 1
}
fi
rm -rf "${new_install_dir}" "${wrapper_tmp}"
mv "${extract_dir}/qwen-code" "${new_install_dir}"
if ! write_unix_wrapper "${wrapper_tmp}" "${INSTALL_LIB_DIR}/bin/qwen"; then

View file

@ -320,9 +320,8 @@ $installDir = Get-QwenInstallDir
$installBinDir = Get-QwenInstallBinDir
$installWasManaged = Test-QwenStandaloneInstallDir -InstallDir $installDir
Remove-CurrentCmdPathShim
if ($installWasManaged) {
Remove-CurrentCmdPathShim
Remove-Item -LiteralPath $installDir -Recurse -Force
Write-Success "Removed $installDir"
} elseif (Test-Path -LiteralPath $installDir) {

View file

@ -193,8 +193,14 @@ remove_shell_path_entry() {
}
awk -v marker="${marker}" '
$0 == marker { skip_next = 1; next }
skip_next == 1 { skip_next = 0; next }
index($0, marker) { check_next = 1; next }
check_next == 1 {
check_next = 0
if ($0 ~ /^[[:space:]]*export PATH=/ ||
$0 ~ /^[[:space:]]*set -gx PATH /) {
next
}
}
{ print }
' "${rc_file}" > "${temp_file}" && mv "${temp_file}" "${rc_file}" || {
rm -f "${temp_file}"

View file

@ -143,6 +143,14 @@ describe('installation scripts', () => {
expect(script).toContain('/latest/VERSION');
expect(script).toContain('resolve_aliyun_version_path()');
expect(script).toContain('retrying GitHub mirror');
expect(script).toContain('entry="${entry//\\\\//}"');
expect(script).toContain('restore_stale_install_backup()');
expect(script).toContain(
'restore_stale_install_backup "${old_install_dir}" "${INSTALL_LIB_DIR}"',
);
expect(script).not.toContain(
'rm -rf "${new_install_dir}" "${old_install_dir}" "${wrapper_tmp}"',
);
expect(script).not.toContain('-print -quit');
});
@ -252,6 +260,15 @@ describe('installation scripts', () => {
expect(script).toContain(':ResolveAliyunVersionPath');
expect(script).toContain(':UseGithubFallbackBaseUrl');
expect(script).toContain('retrying GitHub mirror');
expect(script).toContain('endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"');
expect(script).not.toContain(
'endlocal & set "PATH=!INSTALL_BIN_DIR!;%PATH%"',
);
expect(script).toContain(':RestoreStaleInstallBackup');
expect(script).toContain('call :RestoreStaleInstallBackup');
expect(script).not.toContain(
'ERROR: Failed to remove stale backup directory',
);
expect(script).not.toContain('%RANDOM%');
});
@ -1341,6 +1358,14 @@ describe('standalone release packaging', () => {
expect(workflow.slice(publishLatestStepIndex)).toContain(
'releases/qwen-code/latest/VERSION',
);
const syncStep = workflow.slice(syncStepIndex, verifyStepIndex);
expect(syncStep).toContain('IS_STABLE_RELEASE');
expect(syncStep).toContain(
'if [[ "${IS_STABLE_RELEASE}" == "true" ]]; then',
);
expect(syncStep).toContain(
'Skipping hosted installation asset upload for prerelease',
);
expect(workflow).toContain('installation/install-qwen-standalone.sh');
expect(workflow).toContain('installation/install-qwen-standalone.bat');
expect(workflow).toContain('installation/install-qwen-standalone.ps1');
@ -1356,6 +1381,14 @@ describe('standalone release packaging', () => {
expect(workflow).not.toContain(
'npm run verify:installation-release -- --base-url "${ALIYUN_OSS_PUBLIC_BASE_URL}/releases/qwen-code/latest"',
);
const verifyStep = workflow.slice(verifyStepIndex, publishLatestStepIndex);
expect(verifyStep).toContain('IS_STABLE_RELEASE');
expect(verifyStep).toContain(
'if [[ "${IS_STABLE_RELEASE}" == "true" ]]; then',
);
expect(verifyStep).toContain(
'Skipping hosted installation asset verification for prerelease',
);
expect(workflow).toContain('hosted_tmp_dir="$(mktemp -d)"');
expect(workflow).toContain(
'cmp -s "dist/installation/SHA256SUMS" "${hosted_tmp_dir}/SHA256SUMS"',
@ -1421,6 +1454,12 @@ describe('standalone release packaging', () => {
);
expect(uninstallPowerShellSource).toContain('QWEN_UNINSTALL_PURGE');
expect(uninstallPowerShellSource).toContain('Preserving');
expect(uninstallPowerShellSource).toMatch(
/if \(\$installWasManaged\) \{\n\s+Remove-CurrentCmdPathShim\n\s+Remove-Item/,
);
expect(uninstallPowerShellSource).not.toMatch(
/\$installWasManaged = Test-QwenStandaloneInstallDir[^\n]*\n\nRemove-CurrentCmdPathShim\n\nif \(\$installWasManaged\)/,
);
});
});
@ -1748,6 +1787,46 @@ describe('Linux/macOS installer end-to-end', () => {
}
});
itOnUnix(
'removes only installer-owned shell rc PATH lines during uninstall',
() => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-uninstall-test-'));
try {
const archive = packageFakeStandalone(tmpDir);
const installRoot = path.join(tmpDir, 'install');
const home = path.join(tmpDir, 'home');
runUnixInstaller(archive, installRoot, home);
const rcFile = path.join(home, '.zshrc');
writeFileSync(
rcFile,
[
'before',
'# Added by qwen-code installer (multi-qwen shadow fix) ',
`export PATH='${installRoot}/bin':$PATH`,
'middle',
'# Added by qwen-code installer (multi-qwen shadow fix)',
'echo keep-me',
'after',
].join('\n') + '\n',
);
runUnixUninstaller(installRoot, home);
expect(readScript(rcFile)).toBe(
['before', 'middle', 'echo keep-me', 'after'].join('\n') + '\n',
);
} finally {
rmSync(tmpDir, { recursive: true, force: true });
if (createdDist) {
rmSync('dist', { recursive: true, force: true });
}
}
},
);
itOnUnix('shell-quotes custom install paths in the generated wrapper', () => {
const createdDist = ensureMinimalDist();
const tmpDir = mkdtempSync(path.join(tmpdir(), 'qwen-install-test-'));