[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 " } 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 ' 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) }