mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* fix(installer): auto-detect SYSTEM account and default PATH scope to machine When the Windows standalone installer runs as the SYSTEM account (SID S-1-5-18), which is common for ECS Workbench SSM, WinRM, and scheduled tasks, the default --path-scope is now 'machine' instead of 'user'. Previously, persisting PATH to the SYSTEM user's HKCU hive had no effect on new sessions spawned by the SSM agent service, because the agent inherits its environment at service start time and never re-reads HKCU. Writing to the Machine PATH (HKLM) ensures new sessions pick up the qwen shim immediately. Also adds: - --repair-path flag to fix PATH for an existing install without reinstalling - --path-scope user|machine CLI option and QWEN_INSTALL_PATH_SCOPE env var - Uninstaller now cleans both User and Machine PATH entries - CI validation step for hosted installation assets Resolves #4901 * test(installer): add Windows self-hosted qwen smoke * fix(installer): address Windows PATH review feedback * refactor(installer): remove redundant YAML validation, deduplicate test fixtures - Remove windows-self-hosted-qwen-smoke.yml (operational tool, not part of fix) - Revert sync-release-to-oss.yml validate step (redundant with JS behavior guards) - Extract duplicated test fixture strings into shared constants - Pass normalized path to Remove-PathEntry to avoid double Get-NormalizedPath - Fix stale comment referencing "user PATH" when scope is now configurable * chore: drop unrelated dev.test.js changes from installer PR
1353 lines
51 KiB
Batchfile
1353 lines
51 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
|
|
|
|
call :ValidateRawEnvironmentOptions
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
set "SOURCE=unknown"
|
|
set "METHOD="
|
|
if defined QWEN_INSTALL_METHOD set "METHOD=!QWEN_INSTALL_METHOD!"
|
|
set "MIRROR=auto"
|
|
if defined QWEN_INSTALL_MIRROR set "MIRROR=!QWEN_INSTALL_MIRROR!"
|
|
set "NO_MODIFY_PATH=0"
|
|
if defined QWEN_NO_MODIFY_PATH set "NO_MODIFY_PATH=!QWEN_NO_MODIFY_PATH!"
|
|
set "REPAIR_PATH=0"
|
|
if defined QWEN_INSTALL_REPAIR_PATH set "REPAIR_PATH=!QWEN_INSTALL_REPAIR_PATH!"
|
|
set "PATH_SCOPE=user"
|
|
REM Auto-detect SYSTEM account and default to machine scope so that new sessions
|
|
REM spawned by service processes (SSM agent, WinRM, scheduled tasks) inherit PATH.
|
|
if not defined QWEN_INSTALL_PATH_SCOPE (
|
|
whoami /user 2>nul | findstr /i "S-1-5-18" >nul 2>&1
|
|
if !ERRORLEVEL! EQU 0 set "PATH_SCOPE=machine"
|
|
)
|
|
if defined QWEN_INSTALL_PATH_SCOPE set "PATH_SCOPE=!QWEN_INSTALL_PATH_SCOPE!"
|
|
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
|
|
set "ARG_RAW=%~1"
|
|
set "ARG_KEY=%~1"
|
|
set "ARG_VALUE="
|
|
set "ARG_HAS_INLINE_VALUE=0"
|
|
for /f "tokens=1,* delims==" %%A in ("%~1") do (
|
|
set "ARG_KEY=%%~A"
|
|
set "ARG_VALUE=%%~B"
|
|
)
|
|
if not "!ARG_KEY!"=="!ARG_RAW!" set "ARG_HAS_INLINE_VALUE=1"
|
|
if /i "!ARG_KEY!"=="--source" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --source requires a value
|
|
exit /b 1
|
|
)
|
|
set "SOURCE=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
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 "!ARG_KEY!"=="--method" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --method requires a value
|
|
exit /b 1
|
|
)
|
|
set "METHOD=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --method requires a value
|
|
exit /b 1
|
|
)
|
|
set "METHOD=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--mirror" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --mirror requires a value
|
|
exit /b 1
|
|
)
|
|
set "MIRROR=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --mirror requires a value
|
|
exit /b 1
|
|
)
|
|
set "MIRROR=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--base-url" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --base-url requires a value
|
|
exit /b 1
|
|
)
|
|
set "BASE_URL=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --base-url requires a value
|
|
exit /b 1
|
|
)
|
|
set "BASE_URL=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--archive" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --archive requires a value
|
|
exit /b 1
|
|
)
|
|
set "ARCHIVE_PATH=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --archive requires a value
|
|
exit /b 1
|
|
)
|
|
set "ARCHIVE_PATH=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--version" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --version requires a value
|
|
exit /b 1
|
|
)
|
|
set "VERSION=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --version requires a value
|
|
exit /b 1
|
|
)
|
|
set "VERSION=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--registry" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --registry requires a value
|
|
exit /b 1
|
|
)
|
|
set "NPM_REGISTRY=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --registry requires a value
|
|
exit /b 1
|
|
)
|
|
set "NPM_REGISTRY=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--no-modify-path" (
|
|
set "NO_MODIFY_PATH=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="--repair-path" (
|
|
set "REPAIR_PATH=1"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "!ARG_KEY!"=="--path-scope" (
|
|
if "!ARG_HAS_INLINE_VALUE!"=="1" (
|
|
if "!ARG_VALUE!"=="" (
|
|
echo ERROR: --path-scope requires a value
|
|
exit /b 1
|
|
)
|
|
set "PATH_SCOPE=!ARG_VALUE!"
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if "%~2"=="" (
|
|
echo ERROR: --path-scope requires a value
|
|
exit /b 1
|
|
)
|
|
set "PATH_SCOPE=%~2"
|
|
shift
|
|
shift
|
|
goto parse_args
|
|
)
|
|
if /i "%~1"=="-h" goto usage
|
|
if /i "%~1"=="--help" goto usage
|
|
|
|
echo ERROR: Unknown option.
|
|
echo.
|
|
goto usage_error
|
|
|
|
:end_parse
|
|
|
|
call :ValidateOptions
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
if /i "!REPAIR_PATH!"=="1" (
|
|
call :RepairPath
|
|
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
|
|
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
|
|
exit /b 0
|
|
)
|
|
|
|
call :PrintHeader
|
|
|
|
REM Discover all qwen executables on disk BEFORE we install. We can't
|
|
REM reliably simulate the user's PATH ordering, so enumerate well-known
|
|
REM per-tool bin directories plus everything `where qwen` returns.
|
|
call :CreateTempFile "qwen-pre-install"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "PRE_INSTALL_QWENS_FILE=!TEMP_FILE!"
|
|
rem Avoid `call echo` here: `call` triggers an extra parse pass on the
|
|
rem expanded path, so a directory containing &/|/<,>/etc. would be re-evaluated
|
|
rem as command separators. Plain `echo` writes the literal value.
|
|
for /f "delims=" %%i in ('where qwen 2^>nul') do echo %%i>>"!PRE_INSTALL_QWENS_FILE!"
|
|
for %%c in (
|
|
"!USERPROFILE!\.opencode\bin\qwen.cmd"
|
|
"!APPDATA!\npm\qwen.cmd"
|
|
"!USERPROFILE!\.bun\bin\qwen.cmd"
|
|
"!LOCALAPPDATA!\bun\bin\qwen.cmd"
|
|
"!LOCALAPPDATA!\qwen-code\bin\qwen.cmd"
|
|
) do if exist %%c echo %%~c>>"!PRE_INSTALL_QWENS_FILE!"
|
|
for /f "delims=" %%i in ('npm prefix -g 2^>nul') do (
|
|
if exist "%%i\qwen.cmd" echo %%i\qwen.cmd>>"!PRE_INSTALL_QWENS_FILE!"
|
|
)
|
|
set "PRE_INSTALL_QWENS_LIST="
|
|
if exist "!PRE_INSTALL_QWENS_FILE!" (
|
|
for /f "delims=" %%i in ('sort "!PRE_INSTALL_QWENS_FILE!" 2^>nul ^| findstr /v "^$"') do (
|
|
if "!PRE_INSTALL_QWENS_LIST!"=="" (
|
|
set "PRE_INSTALL_QWENS_LIST=%%i"
|
|
) else (
|
|
echo !PRE_INSTALL_QWENS_LIST! | findstr /i /c:"%%i" >nul 2>&1
|
|
if errorlevel 1 set "PRE_INSTALL_QWENS_LIST=!PRE_INSTALL_QWENS_LIST!|%%i"
|
|
)
|
|
)
|
|
del /f /q "!PRE_INSTALL_QWENS_FILE!" >nul 2>&1
|
|
)
|
|
|
|
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!" "!INSTALL_DIR!" "standalone"
|
|
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
|
|
exit /b 0
|
|
)
|
|
|
|
if /i "!METHOD!"=="npm" (
|
|
call :InstallNpm
|
|
if !ERRORLEVEL! NEQ 0 exit /b !ERRORLEVEL!
|
|
call :PrintFinalInstructions "" "" "npm"
|
|
endlocal
|
|
exit /b 0
|
|
)
|
|
|
|
call :InstallStandalone
|
|
set "STANDALONE_STATUS=!ERRORLEVEL!"
|
|
if !STANDALONE_STATUS! EQU 0 (
|
|
call :PrintFinalInstructions "!INSTALL_BIN_DIR!" "!INSTALL_DIR!" "standalone"
|
|
endlocal & set "PATH=%INSTALL_BIN_DIR%;%PATH%"
|
|
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 22+ and rerun --method npm.
|
|
exit /b !ERRORLEVEL!
|
|
)
|
|
call :PrintFinalInstructions "" "" "npm"
|
|
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-standalone.bat [OPTIONS]
|
|
echo.
|
|
echo Options:
|
|
echo --method METHOD Install method: detect, standalone, or npm (default: detect)
|
|
echo --mirror MIRROR Mirror: auto, github, or aliyun (default: auto)
|
|
echo --base-url URL Override standalone archive base URL
|
|
echo --archive PATH Install from a local standalone archive
|
|
echo --version VERSION Release version (default: latest)
|
|
echo --registry URL npm registry (default: https://registry.npmmirror.com)
|
|
echo --no-modify-path Do not modify PATH
|
|
echo --repair-path Repair PATH for an existing standalone install
|
|
echo --path-scope SCOPE PATH scope: user or machine (default: user, auto machine for SYSTEM)
|
|
echo -s, --source SOURCE Record installation source
|
|
echo -h, --help Show this help message
|
|
exit /b 0
|
|
|
|
:PrintHeader
|
|
set "DISPLAY_VERSION=!VERSION!"
|
|
if /i not "!DISPLAY_VERSION!"=="latest" (
|
|
if /i "!DISPLAY_VERSION:~0,1!"=="v" set "DISPLAY_VERSION=!DISPLAY_VERSION:~1!"
|
|
)
|
|
echo.
|
|
echo Installing Qwen Code version: !DISPLAY_VERSION!
|
|
echo.
|
|
exit /b 0
|
|
|
|
:PrintLogo
|
|
rem QWEN CODE logo with color (Windows Terminal VT100 support)
|
|
echo.
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$e=[char]27; Write-Host \" $e[38;2;71;150;228mQ W E N $e[38;2;132;122;206mC O D E$e[0m\""
|
|
echo.
|
|
exit /b 0
|
|
|
|
:PrintProgressComplete
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$esc = [char]27; $bar = [string]::new([char]0x25A0, 50); Write-Host \"$esc[38;5;214m$bar 100%%$esc[0m\""
|
|
exit /b 0
|
|
|
|
:ValidateRawEnvironmentOptions
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124); $rawNames = @('QWEN_INSTALL_METHOD','QWEN_INSTALL_MIRROR','QWEN_NO_MODIFY_PATH','QWEN_INSTALL_REPAIR_PATH','QWEN_INSTALL_PATH_SCOPE','QWEN_INSTALL_BASE_URL','QWEN_INSTALL_ARCHIVE','QWEN_INSTALL_VERSION','QWEN_NPM_REGISTRY','QWEN_INSTALL_ROOT','QWEN_INSTALL_LIB_DIR','QWEN_INSTALL_BIN_DIR','QWEN_INSTALL_GITHUB_REPO','QWEN_INSTALL_CURL_EXE'); foreach ($name in $rawNames) { $value = [Environment]::GetEnvironmentVariable($name); if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 } }; exit 0"
|
|
if %ERRORLEVEL% EQU 0 exit /b 0
|
|
echo ERROR: installer options contain unsafe command characters.
|
|
exit /b 1
|
|
|
|
: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!"
|
|
set "QWEN_VALIDATE_PATH_SCOPE=!PATH_SCOPE!"
|
|
call :CreateTempFile "qwen-validate-options" ".ps1"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "QWEN_VALIDATE_OPTIONS_SCRIPT=!TEMP_FILE!"
|
|
> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $unsafe = [char[]](10,13,33,34,37,38,60,62,94,96,124)
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $names = @('METHOD','MIRROR','BASE_URL','ARCHIVE_PATH','VERSION','NPM_REGISTRY','INSTALL_BASE','INSTALL_DIR','INSTALL_BIN_DIR','SOURCE','PATH_SCOPE')
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo foreach ($name in $names) {
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo $value = [Environment]::GetEnvironmentVariable('QWEN_VALIDATE_' + $name)
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo if ($null -ne $value -and $value.IndexOfAny($unsafe) -ge 0) { exit 1 }
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo }
|
|
>> "!QWEN_VALIDATE_OPTIONS_SCRIPT!" echo exit 0
|
|
powershell -NoProfile -ExecutionPolicy Bypass -File "!QWEN_VALIDATE_OPTIONS_SCRIPT!"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
del /F /Q "!QWEN_VALIDATE_OPTIONS_SCRIPT!" >nul 2>&1
|
|
set "QWEN_VALIDATE_OPTIONS_SCRIPT="
|
|
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="
|
|
set "QWEN_VALIDATE_PATH_SCOPE="
|
|
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
|
|
if /i "!MIRROR!"=="auto" goto validate_mirror_ok
|
|
echo ERROR: --mirror must be auto, github, or aliyun.
|
|
exit /b 1
|
|
|
|
:validate_mirror_ok
|
|
if "!REPAIR_PATH!"=="0" goto validate_repair_path_ok
|
|
if "!REPAIR_PATH!"=="1" goto validate_repair_path_ok
|
|
echo ERROR: QWEN_INSTALL_REPAIR_PATH must be 0 or 1.
|
|
exit /b 1
|
|
|
|
:validate_repair_path_ok
|
|
if /i "!PATH_SCOPE!"=="user" goto validate_path_scope_ok
|
|
if /i "!PATH_SCOPE!"=="machine" goto validate_path_scope_ok
|
|
echo ERROR: --path-scope must be user or machine.
|
|
exit /b 1
|
|
|
|
:validate_path_scope_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 :ValidateGithubRepo
|
|
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
|
|
set "QWEN_VERSION_VALUE=!VERSION!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$value = $env:QWEN_VERSION_VALUE; if ($value -match '^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$') { exit 0 }; exit 1"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_VERSION_VALUE="
|
|
if %PS_STATUS% EQU 0 exit /b 0
|
|
echo ERROR: --version must be 'latest' or a semver string.
|
|
exit /b 1
|
|
|
|
:ValidateGithubRepo
|
|
if not defined QWEN_INSTALL_GITHUB_REPO exit /b 0
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$value = $env:QWEN_INSTALL_GITHUB_REPO; if ($value -match '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$') { exit 0 }; exit 1"
|
|
if %ERRORLEVEL% EQU 0 exit /b 0
|
|
|
|
echo ERROR: QWEN_INSTALL_GITHUB_REPO must be in owner/repo format.
|
|
exit /b 1
|
|
|
|
:ValidateSource
|
|
if "!SOURCE!"=="unknown" exit /b 0
|
|
echo(!SOURCE!| findstr /R /C:"^[A-Za-z][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="
|
|
rem Keep :DetectTarget in sync with RELEASE_TARGETS in scripts/build-standalone-release.js.
|
|
rem RELEASE_TARGETS currently has no win-arm64 entry, so ARM64 falls through
|
|
rem to the unsupported-architecture branch and the caller can fall back to npm.
|
|
if /i "!PROCESSOR_ARCHITECTURE!"=="AMD64" set "TARGET=win-x64"
|
|
if /i "!PROCESSOR_ARCHITECTURE!"=="X64" set "TARGET=win-x64"
|
|
if /i "!PROCESSOR_ARCHITEW6432!"=="AMD64" set "TARGET=win-x64"
|
|
if /i "!PROCESSOR_ARCHITEW6432!"=="X64" 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
|
|
|
|
:GithubBaseUrlForVersion
|
|
rem args: %~1=version_path → sets QWEN_GH_BASE_URL
|
|
set "QWEN_GH_REPO=QwenLM/qwen-code"
|
|
if defined QWEN_INSTALL_GITHUB_REPO set "QWEN_GH_REPO=!QWEN_INSTALL_GITHUB_REPO!"
|
|
if /i "%~1"=="latest" (
|
|
set "QWEN_GH_BASE_URL=https://github.com/!QWEN_GH_REPO!/releases/latest/download"
|
|
) else (
|
|
set "QWEN_GH_BASE_URL=https://github.com/!QWEN_GH_REPO!/releases/download/%~1"
|
|
)
|
|
set "QWEN_GH_REPO="
|
|
exit /b 0
|
|
|
|
:AliyunBaseUrlForVersion
|
|
rem args: %~1=version_path → sets QWEN_OSS_BASE_URL
|
|
set "QWEN_OSS_BASE_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/%~1"
|
|
exit /b 0
|
|
|
|
:AliyunLatestVersionUrl
|
|
set "QWEN_OSS_LATEST_VERSION_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/latest/VERSION"
|
|
exit /b 0
|
|
|
|
:RaceMirrorHead
|
|
rem args: %~1=timeout_seconds %~2=gh_url %~3=oss_url
|
|
rem Sets QWEN_RACE_RESULT to "aliyun", "github", or "timeout". Sequential
|
|
rem (OSS first, GH fallback) keeps the PowerShell snippet small; a true
|
|
rem parallel race adds a lot of escaping for marginal speedup since OSS HEAD
|
|
rem is sub-second when reachable. Caller decides what to do with "timeout"
|
|
rem (currently: log it and fall back to github).
|
|
set "QWEN_RACE_TIMEOUT=%~1"
|
|
set "QWEN_RACE_GH_URL=%~2"
|
|
set "QWEN_RACE_OSS_URL=%~3"
|
|
set "QWEN_RACE_RESULT=timeout"
|
|
for /f "delims=" %%r in ('powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='SilentlyContinue'; $t=[int]$env:QWEN_RACE_TIMEOUT; try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }; function Probe($url) { try { $r = [Net.WebRequest]::Create($url); $r.Method = 'HEAD'; $r.Timeout = $t * 1000; if ($r -is [Net.HttpWebRequest]) { $r.AllowAutoRedirect = $true }; $resp = $r.GetResponse(); $resp.Close(); return $true } catch { return $false } }; if (Probe $env:QWEN_RACE_OSS_URL) { Write-Output 'aliyun'; exit 0 } elseif (Probe $env:QWEN_RACE_GH_URL) { Write-Output 'github'; exit 0 } else { Write-Output 'timeout'; exit 0 }"') do set "QWEN_RACE_RESULT=%%r"
|
|
set "QWEN_RACE_TIMEOUT="
|
|
set "QWEN_RACE_GH_URL="
|
|
set "QWEN_RACE_OSS_URL="
|
|
exit /b 0
|
|
|
|
:StandaloneBaseUrl
|
|
set "STANDALONE_VERSION_PATH="
|
|
if not "!BASE_URL!"=="" (
|
|
set "STANDALONE_BASE_URL=!BASE_URL!"
|
|
exit /b 0
|
|
)
|
|
|
|
call :ReleaseVersionPath
|
|
set "STANDALONE_VERSION_PATH=!VERSION_PATH!"
|
|
|
|
if /i "!MIRROR!"=="auto" (
|
|
call :GithubBaseUrlForVersion "!VERSION_PATH!"
|
|
if /i "!VERSION_PATH!"=="latest" (
|
|
call :AliyunLatestVersionUrl
|
|
set "QWEN_OSS_PROBE_URL=!QWEN_OSS_LATEST_VERSION_URL!"
|
|
) else (
|
|
call :AliyunBaseUrlForVersion "!VERSION_PATH!"
|
|
set "QWEN_OSS_PROBE_URL=!QWEN_OSS_BASE_URL!/SHA256SUMS"
|
|
)
|
|
call :RaceMirrorHead 2 "!QWEN_GH_BASE_URL!/SHA256SUMS" "!QWEN_OSS_PROBE_URL!"
|
|
if /i "!QWEN_RACE_RESULT!"=="timeout" (
|
|
REM Mirror auto-selection timed out; defaulting to github.
|
|
set "MIRROR=github"
|
|
) else (
|
|
set "MIRROR=!QWEN_RACE_RESULT!"
|
|
REM Mirror auto-selected: !QWEN_RACE_RESULT!
|
|
)
|
|
set "QWEN_GH_BASE_URL="
|
|
set "QWEN_OSS_BASE_URL="
|
|
set "QWEN_OSS_LATEST_VERSION_URL="
|
|
set "QWEN_OSS_PROBE_URL="
|
|
set "QWEN_RACE_RESULT="
|
|
)
|
|
|
|
if /i "!MIRROR!"=="aliyun" (
|
|
call :ResolveAliyunVersionPath "!VERSION_PATH!"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "STANDALONE_VERSION_PATH=!RESOLVED_VERSION_PATH!"
|
|
call :AliyunBaseUrlForVersion "!RESOLVED_VERSION_PATH!"
|
|
set "STANDALONE_BASE_URL=!QWEN_OSS_BASE_URL!"
|
|
set "QWEN_OSS_BASE_URL="
|
|
set "RESOLVED_VERSION_PATH="
|
|
exit /b 0
|
|
)
|
|
|
|
call :GithubBaseUrlForVersion "!VERSION_PATH!"
|
|
set "STANDALONE_BASE_URL=!QWEN_GH_BASE_URL!"
|
|
set "QWEN_GH_BASE_URL="
|
|
exit /b 0
|
|
|
|
:UseGithubFallbackBaseUrl
|
|
set "STANDALONE_BASE_URL=!GITHUB_FALLBACK_BASE_URL!"
|
|
set "ARCHIVE_URL=!STANDALONE_BASE_URL!/!ARCHIVE_NAME!"
|
|
set "CHECKSUM_SOURCE=!STANDALONE_BASE_URL!/SHA256SUMS"
|
|
set "GITHUB_FALLBACK_BASE_URL="
|
|
set "MIRROR=github"
|
|
exit /b 0
|
|
|
|
:MaybeUpdatePath
|
|
rem args: %~1=install_bin_dir
|
|
rem Prepend the install dir to the selected PATH scope via
|
|
rem [Environment]::SetEnvironmentVariable. Idempotent: skips if the dir is
|
|
rem already on PATH. Uses PowerShell rather than `setx` because setx
|
|
rem truncates PATH at 1024 chars, which can silently mangle long PATHs.
|
|
set "QWEN_NEW_BIN=%~1"
|
|
if "!QWEN_NEW_BIN!"=="" exit /b 0
|
|
set "QWEN_PATH_SCOPE=!PATH_SCOPE!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; try { $bin = $env:QWEN_NEW_BIN; $scope = $env:QWEN_PATH_SCOPE; $target = if ($scope -ieq 'machine') { 'Machine' } else { 'User' }; $pathValue = [Environment]::GetEnvironmentVariable('Path', $target); if ([string]::IsNullOrEmpty($pathValue)) { $pathValue = '' }; $entries = @($pathValue -split ';' | Where-Object { $_ -ne '' }); $remaining = @($entries | Where-Object { -not [string]::Equals($_, $bin, [StringComparison]::OrdinalIgnoreCase) }); if ($entries.Count -gt 0 -and [string]::Equals($entries[0], $bin, [StringComparison]::OrdinalIgnoreCase) -and $remaining.Count -eq ($entries.Count - 1)) { exit 0 }; $newPath = (@($bin) + $remaining) -join ';'; [Environment]::SetEnvironmentVariable('Path', $newPath, $target); exit 0 } catch { exit 1 }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_NEW_BIN="
|
|
set "QWEN_PATH_SCOPE="
|
|
exit /b %PS_STATUS%
|
|
|
|
:UrlExists
|
|
set "QWEN_CHECK_URL=%~1"
|
|
rem Prefer Tls12+Tls13; fall back to Tls12 alone on older .NET Framework where the Tls13 enum is missing.
|
|
rem AllowAutoRedirect=true is required for GitHub release asset URLs which return HTTP 302.
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }; function Test-QwenUrl($method, $range) { try { $request = [Net.WebRequest]::Create($env:QWEN_CHECK_URL); $request.Timeout = 10000; $request.Method = $method; if ($range) { $request.Headers.Add('Range', 'bytes=0-0') }; if ($request -is [Net.HttpWebRequest]) { $request.ReadWriteTimeout = 30000; $request.AllowAutoRedirect = $true }; $response = $request.GetResponse(); $response.Close(); return $true } catch { return $false } }; if (Test-QwenUrl 'HEAD' $false) { exit 0 }; if (Test-QwenUrl 'GET' $true) { exit 0 }; 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"
|
|
rem Prefer curl.exe -# for a hash-mark progress bar (Windows 10+ includes it);
|
|
rem fall back to Invoke-WebRequest (which shows its own progress bar).
|
|
rem Progress output is suppressed (-s overrides -#) because PrintProgressComplete provides the visual.
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $curl = $env:QWEN_INSTALL_CURL_EXE; if ([string]::IsNullOrEmpty($curl)) { $cmd = Get-Command curl.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1; if ($null -ne $cmd) { $curl = $cmd.Source } }; if (-not [string]::IsNullOrEmpty($curl)) { & $curl --connect-timeout 15 --max-time 300 --retry 2 -#fSLo $env:QWEN_DOWNLOAD_DEST $env:QWEN_DOWNLOAD_URL -s --show-error; if ($LASTEXITCODE -ne 0) { throw ('curl.exe download failed (exit code ' + $LASTEXITCODE + ')') }; exit 0 }; try { $ProgressPreference = 'SilentlyContinue'; try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }; Invoke-WebRequest -Uri $env:QWEN_DOWNLOAD_URL -OutFile $env:QWEN_DOWNLOAD_DEST -UseBasicParsing -MaximumRedirection 10 -TimeoutSec 300; exit 0 } catch { [Console]::Error.WriteLine('Download error: ' + $_.Exception.Message); exit 1 }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_DOWNLOAD_URL="
|
|
set "QWEN_DOWNLOAD_DEST="
|
|
exit /b %PS_STATUS%
|
|
|
|
:DownloadFileQuiet
|
|
set "QWEN_DOWNLOAD_URL=%~1"
|
|
set "QWEN_DOWNLOAD_DEST=%~2"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $curl = $env:QWEN_INSTALL_CURL_EXE; if ([string]::IsNullOrEmpty($curl)) { $cmd = Get-Command curl.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1; if ($null -ne $cmd) { $curl = $cmd.Source } }; if (-not [string]::IsNullOrEmpty($curl)) { & $curl --connect-timeout 15 --max-time 300 --retry 2 -fsSLo $env:QWEN_DOWNLOAD_DEST $env:QWEN_DOWNLOAD_URL; if ($LASTEXITCODE -ne 0) { throw ('curl.exe download failed (exit code ' + $LASTEXITCODE + ')') }; exit 0 }; try { $ProgressPreference = 'SilentlyContinue'; try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13 } catch { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 }; Invoke-WebRequest -Uri $env:QWEN_DOWNLOAD_URL -OutFile $env:QWEN_DOWNLOAD_DEST -UseBasicParsing -MaximumRedirection 10 -TimeoutSec 300; exit 0 } catch { [Console]::Error.WriteLine('Download error: ' + $_.Exception.Message); exit 1 }"
|
|
set "PS_STATUS=%ERRORLEVEL%"
|
|
set "QWEN_DOWNLOAD_URL="
|
|
set "QWEN_DOWNLOAD_DEST="
|
|
exit /b %PS_STATUS%
|
|
|
|
:ResolveAliyunVersionPath
|
|
set "RESOLVED_VERSION_PATH="
|
|
if /i not "%~1"=="latest" (
|
|
set "RESOLVED_VERSION_PATH=%~1"
|
|
exit /b 0
|
|
)
|
|
|
|
call :AliyunLatestVersionUrl
|
|
call :CreateTempFile "qwen-code-latest-version"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "TEMP_VERSION_FILE=!TEMP_FILE!"
|
|
|
|
call :DownloadFileQuiet "!QWEN_OSS_LATEST_VERSION_URL!" "!TEMP_VERSION_FILE!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_VERSION_FILE!" del /F /Q "!TEMP_VERSION_FILE!" >nul 2>&1
|
|
set "TEMP_VERSION_FILE="
|
|
set "QWEN_OSS_LATEST_VERSION_URL="
|
|
echo WARNING: Failed to resolve Aliyun latest VERSION pointer.
|
|
exit /b 1
|
|
)
|
|
|
|
set "NORMALIZED_VERSION_FILE=!TEMP_VERSION_FILE!.normalized"
|
|
set "QWEN_VERSION_POINTER_FILE=!TEMP_VERSION_FILE!"
|
|
set "QWEN_NORMALIZED_VERSION_FILE=!NORMALIZED_VERSION_FILE!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$value = [IO.File]::ReadAllText($env:QWEN_VERSION_POINTER_FILE).Trim(); $value = $value.Trim([char]0xfeff); if ($value -match '^v?[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$') { if (-not $value.StartsWith('v')) { $value = 'v' + $value }; [IO.File]::WriteAllText($env:QWEN_NORMALIZED_VERSION_FILE, $value, [Text.UTF8Encoding]::new($false)); exit 0 }; exit 1"
|
|
if !ERRORLEVEL! EQU 0 (
|
|
for /f "usebackq delims=" %%V in ("!NORMALIZED_VERSION_FILE!") do if not defined RESOLVED_VERSION_PATH set "RESOLVED_VERSION_PATH=%%V"
|
|
)
|
|
set "QWEN_VERSION_POINTER_FILE="
|
|
set "QWEN_NORMALIZED_VERSION_FILE="
|
|
if exist "!NORMALIZED_VERSION_FILE!" del /F /Q "!NORMALIZED_VERSION_FILE!" >nul 2>&1
|
|
set "NORMALIZED_VERSION_FILE="
|
|
if exist "!TEMP_VERSION_FILE!" del /F /Q "!TEMP_VERSION_FILE!" >nul 2>&1
|
|
set "TEMP_VERSION_FILE="
|
|
set "QWEN_OSS_LATEST_VERSION_URL="
|
|
|
|
if "!RESOLVED_VERSION_PATH!"=="" (
|
|
echo ERROR: Aliyun latest VERSION pointer is not a valid semver value.
|
|
exit /b 1
|
|
)
|
|
|
|
REM Resolved Aliyun latest to !RESOLVED_VERSION_PATH!
|
|
exit /b 0
|
|
|
|
:VerifyChecksum
|
|
set "ARCHIVE_FILE=%~1"
|
|
set "CHECKSUM_SOURCE=%~2"
|
|
set "ARCHIVE_NAME=%~3"
|
|
set "CHECKSUM_FILE=!CHECKSUM_SOURCE!"
|
|
set "TEMP_CHECKSUM="
|
|
if "!CHECKSUM_FILE!"=="" (
|
|
for %%I in ("!ARCHIVE_FILE!") do set "CHECKSUM_FILE=%%~dpISHA256SUMS"
|
|
) else (
|
|
if /i "!CHECKSUM_FILE:~0,8!"=="https://" (
|
|
call :CreateTempFile "qwen-code-checksums"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "TEMP_CHECKSUM=!TEMP_FILE!"
|
|
call :DownloadFileQuiet "!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!" (
|
|
echo ERROR: SHA256SUMS not found at !CHECKSUM_FILE!; cannot verify archive.
|
|
exit /b 1
|
|
)
|
|
|
|
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
|
|
echo ERROR: Checksum entry for !ARCHIVE_NAME! not found.
|
|
exit /b 1
|
|
)
|
|
|
|
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!"=="" (
|
|
echo ERROR: Could not calculate SHA-256 checksum for archive.
|
|
exit /b 1
|
|
)
|
|
|
|
if /i not "!EXPECTED_HASH!"=="!ACTUAL_HASH!" (
|
|
echo ERROR: Checksum mismatch for !ARCHIVE_NAME!: expected !EXPECTED_HASH!, got !ACTUAL_HASH!.
|
|
exit /b 1
|
|
)
|
|
|
|
REM 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-!TARGET!.zip"
|
|
set "REQUESTED_MIRROR=!MIRROR!"
|
|
set "REQUESTED_VERSION_PATH="
|
|
set "GITHUB_FALLBACK_BASE_URL="
|
|
if "!BASE_URL!"=="" if /i "!REQUESTED_MIRROR!"=="auto" (
|
|
call :ReleaseVersionPath
|
|
set "REQUESTED_VERSION_PATH=!VERSION_PATH!"
|
|
call :GithubBaseUrlForVersion "!VERSION_PATH!"
|
|
set "GITHUB_FALLBACK_BASE_URL=!QWEN_GH_BASE_URL!"
|
|
set "QWEN_GH_BASE_URL="
|
|
)
|
|
|
|
call :StandaloneBaseUrl
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
set "USE_GITHUB_FALLBACK=0"
|
|
if not "!GITHUB_FALLBACK_BASE_URL!"=="" if /i "!MIRROR!"=="aliyun" set "USE_GITHUB_FALLBACK=1"
|
|
if "!USE_GITHUB_FALLBACK!"=="1" (
|
|
echo WARNING: Aliyun standalone release metadata unavailable; retrying GitHub mirror.
|
|
call :UseGithubFallbackBaseUrl
|
|
) else (
|
|
if /i "!METHOD!"=="detect" exit /b 2
|
|
exit /b 1
|
|
)
|
|
)
|
|
if not "!GITHUB_FALLBACK_BASE_URL!"=="" if /i "!REQUESTED_VERSION_PATH!"=="latest" if /i "!MIRROR!"=="aliyun" if not "!STANDALONE_VERSION_PATH!"=="" (
|
|
call :GithubBaseUrlForVersion "!STANDALONE_VERSION_PATH!"
|
|
set "GITHUB_FALLBACK_BASE_URL=!QWEN_GH_BASE_URL!"
|
|
set "QWEN_GH_BASE_URL="
|
|
)
|
|
if /i "!STANDALONE_BASE_URL!"=="!GITHUB_FALLBACK_BASE_URL!" set "GITHUB_FALLBACK_BASE_URL="
|
|
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 (
|
|
set "USE_GITHUB_FALLBACK=0"
|
|
if not "!GITHUB_FALLBACK_BASE_URL!"=="" set "USE_GITHUB_FALLBACK=1"
|
|
if "!USE_GITHUB_FALLBACK!"=="1" (
|
|
set "GITHUB_ARCHIVE_URL=!GITHUB_FALLBACK_BASE_URL!/!ARCHIVE_NAME!"
|
|
call :UrlExists "!GITHUB_ARCHIVE_URL!"
|
|
if !ERRORLEVEL! EQU 0 (
|
|
echo WARNING: Aliyun standalone archive not found; retrying GitHub mirror.
|
|
call :UseGithubFallbackBaseUrl
|
|
) else (
|
|
set "GITHUB_ARCHIVE_URL="
|
|
echo WARNING: Standalone archive not found: !ARCHIVE_NAME!
|
|
exit /b 2
|
|
)
|
|
set "GITHUB_ARCHIVE_URL="
|
|
) else (
|
|
echo WARNING: Standalone archive not found: !ARCHIVE_NAME!
|
|
exit /b 2
|
|
)
|
|
)
|
|
)
|
|
|
|
call :CreateTempDir
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
set "ARCHIVE_FILE=!TEMP_DIR!\!ARCHIVE_NAME!"
|
|
|
|
echo Downloading !ARCHIVE_NAME!
|
|
call :DownloadFile "!ARCHIVE_URL!" "!ARCHIVE_FILE!"
|
|
set "DOWNLOAD_STATUS=!ERRORLEVEL!"
|
|
if not "!DOWNLOAD_STATUS!"=="0" if not "!GITHUB_FALLBACK_BASE_URL!"=="" (
|
|
if exist "!ARCHIVE_FILE!" del /F /Q "!ARCHIVE_FILE!" >nul 2>&1
|
|
echo WARNING: Aliyun standalone archive download failed; retrying GitHub mirror.
|
|
call :UseGithubFallbackBaseUrl
|
|
call :DownloadFile "!ARCHIVE_URL!" "!ARCHIVE_FILE!"
|
|
set "DOWNLOAD_STATUS=!ERRORLEVEL!"
|
|
)
|
|
if not "!DOWNLOAD_STATUS!"=="0" (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo WARNING: Failed to download standalone archive.
|
|
if /i "!METHOD!"=="detect" exit /b 2
|
|
exit /b 1
|
|
)
|
|
call :PrintProgressComplete
|
|
)
|
|
|
|
if "!TEMP_DIR!"=="" (
|
|
call :CreateTempDir
|
|
if !ERRORLEVEL! NEQ 0 exit /b 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"
|
|
call :EnsureDir "!EXTRACT_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 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 "$ProgressPreference = 'SilentlyContinue'; 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
|
|
)
|
|
|
|
call :EnsureDir "!INSTALL_BASE!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
call :EnsureDir "!INSTALL_BIN_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
for %%I in ("!INSTALL_DIR!") do set "INSTALL_PARENT=%%~dpI"
|
|
call :EnsureDir "!INSTALL_PARENT!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
exit /b 1
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
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 (
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
echo ERROR: Failed to remove stale staging directory: !NEW_INSTALL_DIR!.
|
|
exit /b 1
|
|
)
|
|
)
|
|
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 install backup: !OLD_INSTALL_DIR!.
|
|
exit /b 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 (
|
|
call :RestoreOldInstall
|
|
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
|
|
)
|
|
|
|
rem SAFETY: this writer expands !INSTALL_DIR! / !INSTALL_BIN_DIR! into a generated
|
|
rem .cmd file. :ValidateOptions must continue to reject delayed-expansion sentinels
|
|
rem (`!`) and other shell-metacharacters in those values; if that validator is ever
|
|
rem loosened, the wrapper write below becomes a command injection sink.
|
|
(
|
|
echo @echo off
|
|
echo call "!INSTALL_DIR!\bin\qwen.cmd" %%*
|
|
) > "!INSTALL_BIN_DIR!\qwen.cmd.new"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
call :RemoveInstalledDirWithWarning
|
|
call :RestoreOldInstall
|
|
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
|
|
call :RemoveInstalledDirWithWarning
|
|
call :RestoreOldInstall
|
|
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
|
|
if !ERRORLEVEL! NEQ 0 echo WARNING: Failed to remove old install backup: !OLD_INSTALL_DIR!
|
|
)
|
|
|
|
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
|
|
call :CreateSourceJson
|
|
if exist "!TEMP_DIR!" rmdir /S /Q "!TEMP_DIR!" >nul 2>&1
|
|
|
|
REM Standalone archive installed to !INSTALL_DIR!
|
|
exit /b 0
|
|
|
|
:RepairPath
|
|
set "INSTALLED_BIN=!INSTALL_BIN_DIR!\qwen.cmd"
|
|
if not exist "!INSTALLED_BIN!" (
|
|
echo ERROR: Qwen Code standalone wrapper was not found:
|
|
echo !INSTALLED_BIN!
|
|
echo ERROR: Set QWEN_INSTALL_ROOT or QWEN_INSTALL_BIN_DIR to the existing install, or install Qwen Code first.
|
|
exit /b 1
|
|
)
|
|
|
|
set "PATH=!INSTALL_BIN_DIR!;!PATH!"
|
|
if /i not "!NO_MODIFY_PATH!"=="1" (
|
|
call :MaybeUpdatePath "!INSTALL_BIN_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo ERROR: Failed to update !PATH_SCOPE! PATH. Run as administrator for --path-scope machine, or add this directory manually:
|
|
echo !INSTALL_BIN_DIR!
|
|
exit /b 1
|
|
)
|
|
)
|
|
|
|
set "INSTALLED_VERSION=unknown"
|
|
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
|
|
|
|
echo.
|
|
echo Qwen Code !INSTALLED_VERSION! is already installed.
|
|
echo PATH repaired for !PATH_SCOPE! scope:
|
|
echo !INSTALL_BIN_DIR!
|
|
echo.
|
|
exit /b 0
|
|
|
|
:CreateTempDir
|
|
set "TEMP_DIR="
|
|
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $dir = Join-Path $env:TEMP ('qwen-code-install-' + [IO.Path]::GetRandomFileName()); New-Item -ItemType Directory -Path $dir -ErrorAction Stop | Out-Null; [Console]::Write($dir)"`) do set "TEMP_DIR=%%I"
|
|
if "!TEMP_DIR!"=="" (
|
|
echo ERROR: Failed to create a temporary directory.
|
|
exit /b 1
|
|
)
|
|
exit /b 0
|
|
|
|
:CreateTempFile
|
|
set "TEMP_FILE="
|
|
set "QWEN_TEMP_FILE_PREFIX=%~1"
|
|
set "QWEN_TEMP_FILE_EXTENSION=%~2"
|
|
for /f "usebackq delims=" %%I in (`powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $file = Join-Path $env:TEMP ($env:QWEN_TEMP_FILE_PREFIX + '-' + [IO.Path]::GetRandomFileName() + $env:QWEN_TEMP_FILE_EXTENSION); New-Item -ItemType File -Path $file -ErrorAction Stop | Out-Null; [Console]::Write($file)"`) do set "TEMP_FILE=%%I"
|
|
set "QWEN_TEMP_FILE_PREFIX="
|
|
set "QWEN_TEMP_FILE_EXTENSION="
|
|
if "!TEMP_FILE!"=="" (
|
|
echo ERROR: Failed to create a temporary file.
|
|
exit /b 1
|
|
)
|
|
exit /b 0
|
|
|
|
:EnsureDir
|
|
set "REQUIRED_DIR=%~1"
|
|
set "QWEN_REQUIRED_DIR=!REQUIRED_DIR!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $path = $env:QWEN_REQUIRED_DIR; if (Test-Path -LiteralPath $path -PathType Container) { exit 0 }; if (Test-Path -LiteralPath $path) { exit 2 }; New-Item -ItemType Directory -Path $path -Force | Out-Null; exit 0"
|
|
set "PS_STATUS=!ERRORLEVEL!"
|
|
set "QWEN_REQUIRED_DIR="
|
|
if !PS_STATUS! EQU 0 exit /b 0
|
|
if !PS_STATUS! EQU 2 (
|
|
echo ERROR: Path exists but is not a directory: !REQUIRED_DIR!
|
|
exit /b 1
|
|
)
|
|
echo ERROR: Failed to create directory: !REQUIRED_DIR!
|
|
exit /b 1
|
|
|
|
:ValidateArchiveContents
|
|
set "QWEN_ARCHIVE_FILE=%~1"
|
|
REM Normalize backslashes to forward slashes before checking. Some Windows
|
|
REM zip producers (including PowerShell's Compress-Archive) emit entries
|
|
REM with backslash separators even though the ZIP spec requires '/'. We
|
|
REM accept either separator and reject only entries that, after
|
|
REM normalization, are empty, absolute, drive-rooted, or contain a '..'
|
|
REM segment.
|
|
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); if ($archive.Entries.Count -eq 0) { [Console]::Error.WriteLine('Archive is empty: ' + $env:QWEN_ARCHIVE_FILE); exit 3 }; 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
|
|
)
|
|
if %PS_STATUS% EQU 3 (
|
|
echo ERROR: Archive is empty: %~1
|
|
exit /b 1
|
|
)
|
|
echo ERROR: Archive validation failed before extraction.
|
|
exit /b %PS_STATUS%
|
|
|
|
:RemoveInstalledDirWithWarning
|
|
if not exist "!INSTALL_DIR!" exit /b 0
|
|
rmdir /S /Q "!INSTALL_DIR!" >nul 2>&1
|
|
if !ERRORLEVEL! NEQ 0 echo WARNING: Failed to remove failed install directory: !INSTALL_DIR!
|
|
exit /b 0
|
|
|
|
:RestoreOldInstall
|
|
if not exist "!OLD_INSTALL_DIR!" exit /b 0
|
|
move /Y "!OLD_INSTALL_DIR!" "!INSTALL_DIR!" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo WARNING: Failed to restore previous install from !OLD_INSTALL_DIR! to !INSTALL_DIR!.
|
|
exit /b 1
|
|
)
|
|
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 }"
|
|
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"
|
|
set "QWEN_MANAGED_DIR=!MANAGED_DIR!"
|
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference = 'Stop'; $dir = $env:QWEN_MANAGED_DIR; if (!(Test-Path -LiteralPath $dir)) { exit 0 }; if (!(Test-Path -LiteralPath $dir -PathType Container)) { exit 1 }; $manifest = Join-Path $dir 'manifest.json'; if (!(Test-Path -LiteralPath $manifest -PathType Leaf)) { exit 1 }; try { $data = Get-Content -LiteralPath $manifest -Raw | ConvertFrom-Json } catch { exit 1 }; if ($data.name -ne '@qwen-code/qwen-code') { exit 1 }; if ([string]$data.target -notmatch '^win-(x64|arm64)$') { exit 1 }; if (!(Test-Path -LiteralPath (Join-Path $dir 'bin\qwen.cmd') -PathType Leaf)) { exit 1 }; if (!(Test-Path -LiteralPath (Join-Path $dir 'node\node.exe') -PathType Leaf)) { exit 1 }; exit 0"
|
|
set "PS_STATUS=!ERRORLEVEL!"
|
|
set "QWEN_MANAGED_DIR="
|
|
if !PS_STATUS! EQU 0 exit /b 0
|
|
|
|
rem Directory exists but is not a qwen-code standalone install.
|
|
rem Back it up so the user doesn't lose data, then proceed.
|
|
for /f "delims=" %%t in ('powershell -NoProfile -Command "Get-Date -Format yyyyMMddTHHmmss"') do set "BACKUP_TIMESTAMP=%%t"
|
|
set "BACKUP_DIR=!MANAGED_DIR!.backup.!BACKUP_TIMESTAMP!"
|
|
if "!BACKUP_TIMESTAMP!"=="" set "BACKUP_DIR=!MANAGED_DIR!.backup"
|
|
rem Silently back up existing directory
|
|
move /Y "!MANAGED_DIR!" "!BACKUP_DIR!" >nul
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo ERROR: Failed to back up !MANAGED_DIR!. Move or remove it manually, then rerun the installer.
|
|
exit /b 1
|
|
)
|
|
exit /b 0
|
|
|
|
:RequireNode
|
|
where node >nul 2>&1
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: Node.js was not found.
|
|
echo.
|
|
echo Node.js 22 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 22 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 22 or newer is required before installing Qwen Code with npm.
|
|
exit /b 1
|
|
)
|
|
|
|
if %NODE_MAJOR_NUM% LSS 22 (
|
|
echo ERROR: Node.js %NODE_VERSION% is installed, but Node.js 22 or newer is required.
|
|
echo Please install Node.js from https://nodejs.org/ and rerun this installer.
|
|
exit /b 1
|
|
)
|
|
|
|
REM Node.js %NODE_VERSION% detected.
|
|
exit /b 0
|
|
|
|
:RequireNpm
|
|
where npm >nul 2>&1
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: npm was not found. Install Node.js with npm from https://nodejs.org/
|
|
exit /b 1
|
|
)
|
|
|
|
for /f "delims=" %%i in ('npm -v 2^>nul') do set "NPM_VERSION=%%i"
|
|
REM npm %NPM_VERSION% detected.
|
|
exit /b 0
|
|
|
|
:NpmPackageSpec
|
|
set "NPM_PACKAGE_SPEC=@qwen-code/qwen-code@latest"
|
|
if /i "!VERSION!"=="latest" exit /b 0
|
|
set "NPM_VERSION_SPEC=!VERSION!"
|
|
if /i "!NPM_VERSION_SPEC:~0,1!"=="v" set "NPM_VERSION_SPEC=!NPM_VERSION_SPEC:~1!"
|
|
set "NPM_PACKAGE_SPEC=@qwen-code/qwen-code@!NPM_VERSION_SPEC!"
|
|
exit /b 0
|
|
|
|
:InstallNpm
|
|
call :RequireNode
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :RequireNpm
|
|
if %ERRORLEVEL% NEQ 0 exit /b 1
|
|
|
|
call :NpmPackageSpec
|
|
|
|
call npm install -g !NPM_PACKAGE_SPEC! --registry "!NPM_REGISTRY!"
|
|
if %ERRORLEVEL% NEQ 0 (
|
|
echo ERROR: Failed to install. Try: npm install -g !NPM_PACKAGE_SPEC! --registry !NPM_REGISTRY!
|
|
exit /b 1
|
|
)
|
|
call :CreateSourceJson
|
|
exit /b 0
|
|
|
|
:CreateSourceJson
|
|
if "!SOURCE!"=="unknown" exit /b 0
|
|
|
|
set "QWEN_DIR=!USERPROFILE!\.qwen"
|
|
call :EnsureDir "!QWEN_DIR!"
|
|
if !ERRORLEVEL! NEQ 0 exit /b 1
|
|
|
|
(
|
|
echo {
|
|
echo "source": "!SOURCE!"
|
|
echo }
|
|
) > "!QWEN_DIR!\source.json"
|
|
|
|
exit /b 0
|
|
|
|
:PrintFinalInstructions
|
|
set "EXTRA_BIN=%~1"
|
|
set "SUMMARY_INSTALL_DIR=%~2"
|
|
set "SUMMARY_INSTALL_METHOD=%~3"
|
|
set "STANDALONE_UNINSTALL_URL=https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/uninstall-qwen-standalone.ps1"
|
|
set "PATH_UPDATE_APPLIED=0"
|
|
if "!SUMMARY_INSTALL_METHOD!"=="" set "SUMMARY_INSTALL_METHOD=standalone"
|
|
|
|
set "INSTALLED_BIN="
|
|
if not "!EXTRA_BIN!"=="" (
|
|
set "INSTALLED_BIN=!EXTRA_BIN!\qwen.cmd"
|
|
set "PATH=!EXTRA_BIN!;!PATH!"
|
|
)
|
|
|
|
echo.
|
|
|
|
set "INSTALLED_VERSION=unknown"
|
|
if not "!INSTALLED_BIN!"=="" if exist "!INSTALLED_BIN!" (
|
|
for /f "delims=" %%i in ('"!INSTALLED_BIN!" --version 2^>nul') do set "INSTALLED_VERSION=%%i"
|
|
)
|
|
|
|
rem Persist the install bin to PATH unless --no-modify-path is set.
|
|
if not "!EXTRA_BIN!"=="" if /i not "!NO_MODIFY_PATH!"=="1" (
|
|
call :MaybeUpdatePath "!EXTRA_BIN!"
|
|
if !ERRORLEVEL! NEQ 0 (
|
|
echo WARNING: Failed to update !PATH_SCOPE! PATH. Add the directory manually:
|
|
echo !EXTRA_BIN!
|
|
) else (
|
|
set "PATH_UPDATE_APPLIED=1"
|
|
)
|
|
)
|
|
|
|
echo.
|
|
echo Qwen Code !INSTALLED_VERSION! installed successfully, to start:
|
|
echo.
|
|
echo cd ^<project^>
|
|
echo qwen
|
|
echo.
|
|
echo For more information visit https://github.com/QwenLM/qwen-code
|
|
|
|
if /i "!QWEN_INSTALLER_PARENT_POWERSHELL!"=="1" (
|
|
REM Final PATH refresh is handled by the PowerShell entrypoint.
|
|
exit /b 0
|
|
)
|
|
exit /b 0
|