Files
ChuBlog/scripts/install.ps1
T

432 lines
18 KiB
PowerShell

[CmdletBinding()]
param(
[string]$Component = '',
[string]$InstallDir = '',
[string]$DataDir = '',
[string]$RootUser = 'root',
[string]$Password = '',
[string]$Version = 'latest',
[string]$Repo = 'TheBadRoger/ChuSQL',
[switch]$ListVersions,
[switch]$Interactive,
[switch]$KeepPackage,
[switch]$NoStart
)
$ErrorActionPreference = 'Stop'
# Windows 安装脚本:装 cli/web、写全局配置、改用户 PATH、
# 跑 csql-bootstrap 建系统目录、最后拉起服务。
$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
}
# 打印步骤标题。
function Step([string]$msg) { Write-Host ''; Write-Host "==> $msg" -ForegroundColor Cyan }
# 打印一行输出。
function Say([string]$msg) { Write-Host $msg }
# 打印缩进提示行。
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'
}
}
# 读口令:终端里不回显;-Interactive 时从 stdin 逐行读。
function Ask-Secret([string]$prompt) {
if ($askFromStdin) {
Write-Host $prompt -NoNewline
$line = [Console]::In.ReadLine()
if ($null -eq $line) { return '' }
return $line
}
$secure = Read-Host $prompt -AsSecureString
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
try { return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) }
finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) }
}
$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'
}
}
# 管理员口令是装机必答项:交互时当场设,非交互时给 -Password 或
# CHUSQL_ADMIN_PASSWORD。口令只经 stdin 传给 csql-bootstrap,不落 settings.toml。
$adminPassword = "$env:CHUSQL_ADMIN_PASSWORD"
if ($Password) { $adminPassword = $Password }
if ($canPrompt) {
Write-Host ''
Write-Host ('The administrator account is "{0}".' -f $RootUser)
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
}
}
if ($adminPassword.Length -eq 0) {
Fail 'no administrator password: pass -Password or set CHUSQL_ADMIN_PASSWORD'
}
$wanted = @()
if ($wantCli) { $wanted += 'cli' }
if ($wantWeb) { $wanted += 'web' }
$componentLabel = ($wanted -join '+')
if (-not $componentLabel) { $componentLabel = 'none (storage only)' }
$pack = Split-Path -Parent $MyInvocation.MyCommand.Path
$downloaded = $false
$tempDir = ''
if (-not $InstallDir) { $InstallDir = Join-Path $env:LOCALAPPDATA 'ChuSQL' }
if (-not $DataDir) { $DataDir = Join-Path (Join-Path $env:LOCALAPPDATA 'ChuSQL') 'data' }
$LogDir = Join-Path (Join-Path $env:LOCALAPPDATA 'ChuSQL') 'logs'
$InstallDir = [System.IO.Path]::GetFullPath($InstallDir)
$DataDir = [System.IO.Path]::GetFullPath($DataDir)
# ---- 取包:脚本旁边有包就装本地的,否则去发行站下载 ----
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
}
# ---- 校验包内容 ----
$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' }
if (-not (Test-Path (Join-Path $pack 'bin\csql-bootstrap.exe'))) { Fail 'package is incomplete: bin\csql-bootstrap.exe not found' }
if ($wantWeb) {
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' }
}
if ($wantCli) {
if (-not (Test-Path (Join-Path $pack 'bin\csql.exe'))) { Fail 'package is incomplete: bin\csql.exe not found' }
}
$template = Join-Path $pack 'resources\settings.toml.windows'
if (-not (Test-Path $template)) { Fail 'package is incomplete: resources\settings.toml.windows not found' }
Say "ChuSQL installer"
Say (" version {0}" -f $Version)
Say (" components {0}" -f $componentLabel)
if ($downloaded) { Say (" release {0}" -f $Version) }
Say (" install to {0}" -f $InstallDir)
Say (" data dir {0}" -f $DataDir)
Say (" root user {0} (password asked above, kept out of settings.toml)" -f $RootUser)
# ---- 释放文件 ----
# 包是合在一起的(web 和 cli 都在),这里按这次的选择逐个释放:没选的组件不落地
Step 'Installing files'
New-Item -ItemType Directory -Force -Path (Join-Path $InstallDir 'bin'), $LogDir, $DataDir | Out-Null
Copy-Item $storageLib (Join-Path $InstallDir 'bin') -Force
Copy-Item (Join-Path $pack 'bin\chusql-server.exe') (Join-Path $InstallDir 'bin') -Force
Copy-Item (Join-Path $pack 'bin\csql-bootstrap.exe') (Join-Path $InstallDir 'bin') -Force
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
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 settings.toml'
$configDir = if ($env:APPDATA) { Join-Path $env:APPDATA 'ChuSQL' } else { Join-Path $InstallDir 'config' }
$configFile = Join-Path $configDir 'settings.toml'
$toml = Get-Content -Path $template -Raw -Encoding UTF8
$toml = $toml.Replace('@DATA_DIR@', ($DataDir -replace '\\', '/')).Replace('@LOG_DIR@', ($LogDir -replace '\\', '/'))
New-Item -ItemType Directory -Force -Path $configDir | Out-Null
Set-Content -Path $configFile -Value $toml -Encoding UTF8
Say (" config file {0}" -f $configFile)
# ---- 引导系统目录 ----
# 引导身份目录、root 与预装类型,口令通过标准输入传递
Step 'Initializing the administrator identity and preinstalled types'
$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}); identities or preinstalled types are not ready" -f $LASTEXITCODE)
}
# ---- 命令入口 ----
Step 'Creating commands'
if ($wantWeb) {
Set-Content -Path (Join-Path $InstallDir 'csql-web.cmd') -Encoding ASCII -Value @'
@echo off
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0csql-web.ps1" %*
'@
}
if ($wantCli) {
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'
if ($downloaded) {
Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue
Say 'package removed'
} else {
$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)
}
}
} elseif ($downloaded) {
Say ("kept the downloaded package: {0}" -f $tempDir)
}
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 $LogDir 'server.err.log'
$serverProc = Start-Process -FilePath $serverPath `
-ArgumentList @('--config', $configFile, '--user', $RootUser) `
-WorkingDirectory $InstallDir -WindowStyle Hidden -PassThru `
-RedirectStandardOutput (Join-Path $LogDir '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)
}
Step 'Done'
Say (" settings.toml {0}" -f $configFile)
Say (" data {0}" -f $DataDir)
$startWith = @()
if ($wantWeb) { $startWith += 'csql-web' }
if ($wantCli) { $startWith += 'csql' }
Say (" start with {0}" -f ($startWith -join ', '))
Say (" tcp server {0} (listens on [server] listen_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)
}