2026-10-01 20:48:38 +08:00
[ CmdletBinding ()]
param (
2026-10-02 14:53:02 +08:00
[ string ] $Component = '' ,
2026-10-01 20:48:38 +08:00
[ string ] $InstallDir = '' ,
[ string ] $DataDir = '' ,
[ string ] $RootUser = 'root' ,
2026-10-03 02:17:46 +08:00
[ string ] $Password = '' ,
2026-10-02 14:53:02 +08:00
[ string ] $Version = 'latest' ,
[ string ] $Repo = 'TheBadRoger/ChuSQL' ,
[ switch ] $ListVersions ,
[ switch ] $Interactive ,
2026-10-03 02:17:46 +08:00
[ switch ] $KeepPackage ,
[ switch ] $NoStart
2026-10-01 20:48:38 +08:00
)
$ErrorActionPreference = 'Stop'
2026-10-03 02:17:46 +08:00
# Windows 安装脚本:装 cli/web、写全局配置、改用户 PATH、
# 跑 csql-bootstrap 建系统目录、最后拉起服务。
2026-10-02 14:53:02 +08:00
$script:tempDir = ''
# 报错退出,并清掉半截临时目录。
function Fail([string]$msg ) {
Write-Host "!! $msg " -ForegroundColor Red
if ( $script:tempDir -and ( Test-Path $script:tempDir )) {
Remove-Item -Recurse -Force $script:tempDir -ErrorAction SilentlyContinue
}
exit 1
}
# 打印步骤标题。
2026-10-01 20:48:38 +08:00
function Step([string]$msg ) { Write-Host '' ; Write-Host "==> $msg " -ForegroundColor Cyan }
2026-10-02 14:53:02 +08:00
# 打印一行输出。
2026-10-01 20:48:38 +08:00
function Say([string]$msg ) { Write-Host $msg }
2026-10-02 14:53:02 +08:00
# 打印缩进提示行。
function Note([string]$msg ) { Write-Host " note: $msg " }
# ---- 发行站、平台标签 ----
$githubBase = if ( $env:CHUSQL_GITHUB_BASE ) { $env:CHUSQL_GITHUB_BASE . TrimEnd ( '/' ) } else { 'https://github.com' }
$apiBase = if ( $githubBase -match '^https?://github\.com$' ) { 'https://api.github.com' } else { " $githubBase /api/v3" }
# 查版本走 API,未认证只有 60 次/小时;给了 token 就带上(CI 里 GITHUB_TOKEN 一般就有)
$githubToken = if ( $env:CHUSQL_GITHUB_TOKEN ) { $env:CHUSQL_GITHUB_TOKEN } elseif ( $env:GITHUB_TOKEN ) { $env:GITHUB_TOKEN } else { '' }
$apiHeaders = @ { 'User-Agent' = 'chusql-install' ; 'Accept' = 'application/vnd.github+json' }
if ( $githubToken ) { $apiHeaders [ 'Authorization' ] = "Bearer $githubToken " }
$arch = if ( $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' ) { 'arm64' } else { 'x86_64' }
$labels = @ ( "windows- $arch " , 'windows' )
# 取一份 release 的资源文件名列表。
function Get-AssetNames($release ) { @ ( $release . assets | ForEach-Object { $_ . name }) }
# 这份 release 里有没有本平台的包。
function Test-HasPackage($release ) {
$names = Get-AssetNames $release
foreach ( $label in $labels ) {
if ( $names -contains "chusql- $label .zip" ) { return $true }
}
return ( $names -contains 'chusql.zip' )
}
# 按正式版与带包两档条件挑第一个 tag。
function Select-Tag([bool]$stableOnly , [ bool ] $needPackage , $releases ) {
foreach ( $release in $releases ) {
if ( $stableOnly -and $release . prerelease ) { continue }
if ( $needPackage -and -not ( Test-HasPackage $release )) { continue }
return $release . tag_name
}
return ''
}
# 取 URL 的文本内容(字节数组先转字符串)。
function Get-ResponseText([string]$url ) {
$response = Invoke-WebRequest -Uri $url -Headers $apiHeaders -UseBasicParsing
if ( $response . Content -is [ byte[] ]) { return [ System.Text.Encoding ]:: UTF8 . GetString ( $response . Content ) }
return [ string ] $response . Content
}
# 拉取 release 列表并逐个输出。
function Get-Releases {
try { $text = Get-ResponseText " $apiBase /repos/ $Repo /releases?per_page=100" } catch { return }
try { $parsed = ConvertFrom-Json -InputObject $text } catch { return }
if ( $null -eq $parsed ) { return }
foreach ( $release in @ ( $parsed )) {
if ( $null -ne $release ) { Write-Output $release }
}
}
# ---- 列版本 ----
if ( $ListVersions ) {
Say ( "ChuSQL releases in {0}" -f $Repo )
$releases = @ ( Get-Releases )
if ( $releases . Count -eq 0 ) {
Fail "cannot list the releases of $Repo from $apiBase `n (offline, rate limited, or the repository has no release yet) `n open $githubBase / $Repo /releases and pass a tag: -Version <tag>"
}
foreach ( $release in $releases ) {
$mark = ''
if ( $release . prerelease ) { $mark += ' (prerelease)' }
if ( -not ( Test-HasPackage $release )) { $mark += " (no package for windows- $arch )" }
Say ( " {0}{1}" -f $release . tag_name , $mark )
}
Say ''
Say 'install one with: .\install.ps1 -Version <tag>'
exit 0
}
if ( $Component -notin @ ( 'web' , 'cli' , 'both' , '' )) { Fail "pass -Component web, cli or both (got: $Component )" }
# ---- 交互:控制台里直接问答;-Interactive 时没有终端也从 stdin 逐行读(自动化用)----
$askFromStdin = $false
$canPrompt = $false
if ( $Interactive ) { $askFromStdin = $true ; $canPrompt = $true }
elseif ( -not [ Console ]:: IsInputRedirected ) { $canPrompt = $true }
# 读一行输入(可选从 stdin 读)。
function Ask-Line([string]$prompt ) {
if ( $askFromStdin ) {
Write-Host $prompt -NoNewline
$line = [ Console ]:: In . ReadLine ()
if ( $null -eq $line ) { $line = '' }
return $line
}
return ( Read-Host $prompt )
}
# 问 y/n,回车用缺省值,返回布尔。
function Ask-YesNo([string]$prompt , [ bool ] $defaultYes ) {
while ( $true ) {
$answer = ( Ask-Line $prompt ). Trim ()
if ( $answer -eq '' ) { return $defaultYes }
if ( $answer -match '^(y|yes)$' ) { return $true }
if ( $answer -match '^(n|no)$' ) { return $false }
Write-Host ' please answer y or n'
}
}
2026-10-03 02:17:46 +08:00
# 读口令:终端里不回显;-Interactive 时从 stdin 逐行读。
2026-10-02 14:53:02 +08:00
function Ask-Secret([string]$prompt ) {
2026-10-03 02:17:46 +08:00
if ( $askFromStdin ) {
Write-Host $prompt -NoNewline
$line = [ Console ]:: In . ReadLine ()
if ( $null -eq $line ) { return '' }
return $line
}
2026-10-02 14:53:02 +08:00
$secure = Read-Host $prompt -AsSecureString
2026-10-03 02:17:46 +08:00
$ptr = [ System.Runtime.InteropServices.Marshal ]:: SecureStringToBSTR ( $secure )
try { return [ System.Runtime.InteropServices.Marshal ]:: PtrToStringBSTR ( $ptr ) }
finally { [ System.Runtime.InteropServices.Marshal ]:: ZeroFreeBSTR ( $ptr ) }
2026-10-02 14:53:02 +08:00
}
$wantWeb = $false
$wantCli = $false
if ( $Component -eq 'web' ) { $wantWeb = $true }
if ( $Component -eq 'cli' ) { $wantCli = $true }
if ( $Component -eq 'both' ) { $wantWeb = $true ; $wantCli = $true }
if ( -not $Component ) {
if ( -not $canPrompt ) {
Fail '-Component is required (web, cli or both) when there is no terminal to ask on'
}
Write-Host ''
Write-Host 'ChuSQL installer: which parts do you want?'
$wantCli = Ask-YesNo 'Install the command line client (csql)? [Y/n]' $true
$wantWeb = Ask-YesNo 'Install the web front end (browser UI)? [y/N]' $false
if ( -not $wantWeb -and -not $wantCli ) {
Say ' neither selected: only the storage library and the server get installed'
}
}
2026-10-03 02:17:46 +08:00
# 管理员口令是装机必答项:交互时当场设,非交互时给 -Password 或
# CHUSQL_ADMIN_PASSWORD。口令只经 stdin 传给 csql-bootstrap,不落 chusql.toml。
$adminPassword = " $env:CHUSQL_ADMIN_PASSWORD "
if ( $Password ) { $adminPassword = $Password }
if ( $canPrompt ) {
2026-10-02 14:53:02 +08:00
Write-Host ''
Write-Host ( 'The administrator account is "{0}".' -f $RootUser )
2026-10-03 02:17:46 +08:00
while ( $true ) {
$first = Ask-Secret ' Password: '
if ( $first . Length -eq 0 ) {
Write-Host ' the password cannot be empty'
continue
}
$second = Ask-Secret ' Repeat it: '
if ( $first -ne $second ) {
Write-Host ' the two entries differ, try again'
continue
}
$adminPassword = $first
break
2026-10-02 14:53:02 +08:00
}
2026-10-03 02:17:46 +08:00
}
if ( $adminPassword . Length -eq 0 ) {
Fail 'no administrator password: pass -Password or set CHUSQL_ADMIN_PASSWORD'
2026-10-02 14:53:02 +08:00
}
$wanted = @ ()
if ( $wantCli ) { $wanted += 'cli' }
if ( $wantWeb ) { $wanted += 'web' }
$componentLabel = ( $wanted -join '+' )
if ( -not $componentLabel ) { $componentLabel = 'none (storage only)' }
2026-10-01 20:48:38 +08:00
$pack = Split-Path -Parent $MyInvocation . MyCommand . Path
2026-10-02 14:53:02 +08:00
$downloaded = $false
$tempDir = ''
2026-10-01 20:48:38 +08:00
if ( -not $InstallDir ) { $InstallDir = Join-Path $env:LOCALAPPDATA 'ChuSQL' }
if ( -not $DataDir ) { $DataDir = Join-Path $InstallDir 'data' }
$InstallDir = [ System.IO.Path ]:: GetFullPath ( $InstallDir )
$DataDir = [ System.IO.Path ]:: GetFullPath ( $DataDir )
2026-10-02 14:53:02 +08:00
# ---- 取包:脚本旁边有包就装本地的,否则去发行站下载 ----
if ( -not ( Test-Path ( Join-Path $pack 'bin\chusql_core_storage.dll' ))) {
Step 'Looking for the package'
if ( $Version -eq 'latest' ) {
$releases = @ ( Get-Releases )
if ( $releases . Count -eq 0 ) {
Note "version latest (cannot reach $apiBase ; using the latest release redirect)"
} else {
$picked = Select-Tag $true $true $releases
if ( -not $picked ) { $picked = Select-Tag $false $true $releases }
if ( -not $picked ) { $picked = Select-Tag $true $false $releases }
if ( -not $picked ) { $picked = Select-Tag $false $false $releases }
if ( $picked ) { $Version = $picked ; Note "version $Version " }
else { Note 'no release lists a matching package; falling back to the latest release redirect' }
}
}
$tempDir = Join-Path ([ System.IO.Path ]:: GetTempPath ()) ( "chusql-install-" + [ Guid ]:: NewGuid (). ToString ( 'N' ))
$script:tempDir = $tempDir
New-Item -ItemType Directory -Force -Path $tempDir | Out-Null
# 一个平台一个包:web 和 cli 都在里面,装哪几个由上面的选择决定
Step "Looking for a chusql package for windows- $arch "
$zip = Join-Path $tempDir 'chusql.zip'
$asset = ''
$assetUrl = ''
$candidates = @ ()
foreach ( $label in $labels ) { $candidates += "chusql- $label .zip" }
$candidates += 'chusql.zip'
foreach ( $candidate in $candidates ) {
$tryUrl = if ( $Version -eq 'latest' ) { " $githubBase / $Repo /releases/latest/download/ $candidate " }
else { " $githubBase / $Repo /releases/download/ $Version / $candidate " }
try {
Invoke-WebRequest -Uri $tryUrl -Headers $apiHeaders -OutFile $zip -UseBasicParsing
$asset = $candidate
$assetUrl = $tryUrl
break
} catch {
continue
}
}
if ( -not $asset ) {
Fail "no package for windows- $arch in $Repo ( $Version ); try -ListVersions, or install from an archive you downloaded yourself"
}
Note "asset $asset "
$published = ''
try { $published = Get-ResponseText " $assetUrl .sha256" } catch { $published = '' }
if ( $published ) {
$expected = ( $published . Trim () -split '\s+' )[ 0 ]. ToLower ()
$actual = ( Get-FileHash -Path $zip -Algorithm SHA256 ). Hash . ToLower ()
if ( $actual -ne $expected ) { Fail "checksum mismatch: expected $expected , got $actual " }
Note "checksum $expected (ok)"
} else {
Note 'checksum no .sha256 published for this asset (skipped)'
}
Step 'Unpacking the package'
$expanded = Join-Path $tempDir 'package'
New-Item -ItemType Directory -Force -Path $expanded | Out-Null
Expand-Archive -Path $zip -DestinationPath $expanded -Force
if ( -not ( Test-Path ( Join-Path $expanded 'bin\chusql_core_storage.dll' ))) {
# 归档里多包了一层目录也能装
$inner = Get-ChildItem -Path $expanded -Directory | Select-Object -First 1
if ( $inner -and ( Test-Path ( Join-Path $inner . FullName 'bin\chusql_core_storage.dll' ))) { $expanded = $inner . FullName }
}
$pack = $expanded
$downloaded = $true
}
2026-10-01 20:48:38 +08:00
# ---- 校验包内容 ----
2026-10-02 14:53:02 +08:00
$storageLib = Join-Path $pack 'bin\chusql_core_storage.dll'
if ( -not ( Test-Path $storageLib )) { Fail "package is incomplete: bin\chusql_core_storage.dll not found in $pack " }
if ( -not ( Test-Path ( Join-Path $pack 'bin\chusql-server.exe' ))) { Fail 'package is incomplete: bin\chusql-server.exe not found' }
2026-10-03 02:17:46 +08:00
if ( -not ( Test-Path ( Join-Path $pack 'bin\csql-bootstrap.exe' ))) { Fail 'package is incomplete: bin\csql-bootstrap.exe not found' }
2026-10-02 14:53:02 +08:00
if ( $wantWeb ) {
2026-10-01 20:48:38 +08:00
if ( -not ( Test-Path ( Join-Path $pack 'bin\chusql-web.exe' ))) { Fail 'package is incomplete: bin\chusql-web.exe not found' }
if ( -not ( Test-Path ( Join-Path $pack 'static' ))) { Fail 'package is incomplete: static\ not found' }
2026-10-02 14:53:02 +08:00
}
if ( $wantCli ) {
2026-10-01 20:48:38 +08:00
if ( -not ( Test-Path ( Join-Path $pack 'bin\csql.exe' ))) { Fail 'package is incomplete: bin\csql.exe not found' }
}
$template = Join-Path $pack 'scripts\chusql.toml'
if ( -not ( Test-Path $template )) { Fail 'package is incomplete: scripts\chusql.toml not found' }
Say "ChuSQL installer"
2026-10-02 14:53:02 +08:00
Say ( " version {0}" -f $Version )
Say ( " components {0}" -f $componentLabel )
if ( $downloaded ) { Say ( " release {0}" -f $Version ) }
2026-10-01 20:48:38 +08:00
Say ( " install to {0}" -f $InstallDir )
Say ( " data dir {0}" -f $DataDir )
2026-10-03 02:17:46 +08:00
Say ( " root user {0} (password asked above, kept out of chusql.toml)" -f $RootUser )
2026-10-01 20:48:38 +08:00
# ---- 释放文件 ----
2026-10-02 14:53:02 +08:00
# 包是合在一起的(web 和 cli 都在),这里按这次的选择逐个释放:没选的组件不落地
2026-10-01 20:48:38 +08:00
Step 'Installing files'
New-Item -ItemType Directory -Force -Path ( Join-Path $InstallDir 'bin' ), ( Join-Path $InstallDir 'logs' ), $DataDir | Out-Null
2026-10-02 14:53:02 +08:00
Copy-Item $storageLib ( Join-Path $InstallDir 'bin' ) -Force
Copy-Item ( Join-Path $pack 'bin\chusql-server.exe' ) ( Join-Path $InstallDir 'bin' ) -Force
2026-10-03 02:17:46 +08:00
Copy-Item ( Join-Path $pack 'bin\csql-bootstrap.exe' ) ( Join-Path $InstallDir 'bin' ) -Force
2026-10-02 14:53:02 +08:00
Get-ChildItem ( Join-Path $pack 'bin' ) -Filter '*.dll' -File -ErrorAction SilentlyContinue |
ForEach-Object { Copy-Item $_ . FullName ( Join-Path $InstallDir 'bin' ) -Force }
if ( $wantCli ) {
Copy-Item ( Join-Path $pack 'bin\csql.exe' ) ( Join-Path $InstallDir 'bin' ) -Force
}
if ( $wantWeb ) {
Copy-Item ( Join-Path $pack 'bin\chusql-web.exe' ) ( Join-Path $InstallDir 'bin' ) -Force
2026-10-01 20:48:38 +08:00
New-Item -ItemType Directory -Force -Path ( Join-Path $InstallDir 'static' ) | Out-Null
Copy-Item ( Join-Path $pack 'static\*' ) ( Join-Path $InstallDir 'static' ) -Recurse -Force
$launcher = Join-Path $pack 'csql-web.ps1'
if ( Test-Path $launcher ) {
Copy-Item $launcher $InstallDir -Force
} else {
Say ' note: csql-web.ps1 is not bundled yet; until it is, start bin\chusql-web.exe yourself'
}
}
# ---- 写全局配置 ----
Step 'Writing chusql.toml'
$configDir = if ( $env:APPDATA ) { Join-Path $env:APPDATA 'ChuSQL' } else { Join-Path $InstallDir 'config' }
$configFile = Join-Path $configDir 'chusql.toml'
$toml = Get-Content -Path $template -Raw -Encoding UTF8
$toml = $toml -replace '(?m)^(\s*user\s*=\s*).*$' , " `$ 1 `" $RootUser `" "
$toml = $toml -replace '(?m)^(\s*data_dir\s*=\s*).*$' , " `$ 1 `" $( $DataDir -replace '\\' , '/' ) `" "
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
Set-Content -Path $configFile -Value $toml -Encoding UTF8
Say ( " config file {0}" -f $configFile )
2026-10-03 02:17:46 +08:00
# ---- 引导系统目录 ----
# csql-bootstrap 建 system 数据库和 __system_users 表,口令只走 stdin,不进 chusql.toml
Step 'Creating the system catalog'
$bootstrap = Join-Path $InstallDir 'bin\csql-bootstrap.exe'
$bootstrapArgs = @ ( '--config' , $configFile , '--user' , $RootUser )
$bootstrapArgs += '--password-stdin'
$bootstrapOutput = $adminPassword | & $bootstrap @bootstrapArgs 2 >& 1
$bootstrapOutput | ForEach-Object { Say ( " {0}" -f $_ ) }
if ( $LASTEXITCODE -ne 0 ) {
Fail ( "the bootstrap program failed (exit {0}); the system catalog is not ready" -f $LASTEXITCODE )
}
2026-10-01 20:48:38 +08:00
# ---- 命令入口 ----
Step 'Creating commands'
2026-10-02 14:53:02 +08:00
if ( $wantWeb ) {
2026-10-01 20:48:38 +08:00
Set-Content -Path ( Join-Path $InstallDir 'csql-web.cmd' ) -Encoding ASCII -Value @'
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0csql-web.ps1" %*
'@
2026-10-02 14:53:02 +08:00
}
if ( $wantCli ) {
2026-10-01 20:48:38 +08:00
Set-Content -Path ( Join-Path $InstallDir 'csql.cmd' ) -Encoding ASCII -Value @'
@echo off
"%~dp0bin\csql.exe" %*
'@
}
# ---- 环境变量 ----
Step 'Updating PATH'
$userPath = [ Environment ]:: GetEnvironmentVariable ( 'Path' , 'User' )
if ( -not $userPath ) { $userPath = '' }
$entries = $userPath -split ';' | Where-Object { $_ }
if ( $entries -notcontains $InstallDir ) {
$updated = ( @ ( $entries ) + $InstallDir ) -join ';'
[ Environment ]:: SetEnvironmentVariable ( 'Path' , $updated , 'User' )
Say ( "added {0} to the user PATH (open a new terminal to pick it up)" -f $InstallDir )
} else {
Say 'PATH already has the install directory'
}
# ---- 删掉安装包 ----
if ( -not $KeepPackage ) {
Step 'Removing the package'
2026-10-02 14:53:02 +08:00
if ( $downloaded ) {
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
2026-10-01 20:48:38 +08:00
Say 'package removed'
} else {
2026-10-02 14:53:02 +08:00
$marker = Join-Path $pack 'bin\chusql_core_storage.dll'
$safe = ( Test-Path $marker ) -and ( $pack -ne $InstallDir ) -and ( -not $InstallDir . StartsWith ( $pack , [ StringComparison ]:: OrdinalIgnoreCase ))
if ( $safe ) {
Remove-Item -Recurse -Force $pack
Say 'package removed'
} else {
Say ( "kept the package (not safe to delete automatically): {0}" -f $pack )
}
2026-10-01 20:48:38 +08:00
}
2026-10-02 14:53:02 +08:00
} elseif ( $downloaded ) {
Say ( "kept the downloaded package: {0}" -f $tempDir )
2026-10-01 20:48:38 +08:00
}
2026-10-03 02:17:46 +08:00
Step 'Starting the server'
$serverPath = Join-Path $InstallDir 'bin\chusql-server.exe'
$serverPid = 0
if ( $NoStart ) {
Say ' -NoStart: the server was not started'
} else {
$serverLog = Join-Path $InstallDir 'logs\server.err.log'
$serverProc = Start-Process -FilePath $serverPath `
-ArgumentList @ ( '--config' , $configFile ) `
-WorkingDirectory $InstallDir -WindowStyle Hidden -PassThru `
-RedirectStandardOutput ( Join-Path $InstallDir 'logs\server.log' ) `
-RedirectStandardError $serverLog
Start-Sleep -Milliseconds 1200
if ( $serverProc . HasExited ) {
Fail ( "the server stopped right away (exit {0}); look at {1}" -f $serverProc . ExitCode , $serverLog )
}
$serverPid = $serverProc . Id
Say ( " server running, pid {0}" -f $serverPid )
Say ( " log {0}" -f $serverLog )
}
2026-10-01 20:48:38 +08:00
Step 'Done'
Say ( " chusql.toml {0}" -f $configFile )
Say ( " data {0}" -f $DataDir )
2026-10-02 14:53:02 +08:00
$startWith = @ ()
if ( $wantWeb ) { $startWith += 'csql-web' }
if ( $wantCli ) { $startWith += 'csql' }
Say ( " start with {0}" -f ( $startWith -join ', ' ))
2026-10-03 02:17:46 +08:00
Say ( " tcp server {0} (listens on [server] host/port, defaults 127.0.0.1:7777)" -f $serverPath )
if ( $serverPid -gt 0 ) {
Say ( " running pid {0} (stop it with: Stop-Process -Id {0})" -f $serverPid )
} else {
Say ( " start it {0} --config `" {1} `" " -f $serverPath , $configFile )
}