# package-installer.ps1 -- build the standalone desktop installer (2026-10-28 wave).
#
# Product ruling (user, 2026-10-28): installer only (no green zip); desktop connects
# by hand click; product name "ek-station 桌面"; headless daemon auto-connects flagged
# rows; headless write-once = `ekapp add-account` (password via stdin).
#
# What this does, in order:
# 1. preflight: ISCC present, dumpbin present, scaffolding patch OFF (the installer
# must ship the *published* chain face -- with .cargo\config.toml present the
# build resolves local directories, which is not what an installed user gets);
# 2. release builds:
# ek-ui --release --features ctp-native,bundled-font --bin ekapp
# ek-station --release --features ctp-native --bin ek-station
# 3. staging: exes + the two CTP SDK DLLs (runtime import requirement of ekapp.exe,
# from ek-ctp/native/sdk/win64) + license texts + first-run doc;
# 4. dependency audit: dumpbin /dependents ekapp.exe must list exactly the two CTP
# DLLs (plus system DLLs) -- any path outside the staging dir is a STOP;
# 5. ISCC compiles packaging\ek-station-desktop.iss -> dist\ek-station-app-setup-<v>.exe;
# 6. SHA256 of the setup exe -> dist\SHA256.txt.
#
# Usage:
# powershell -ExecutionPolicy Bypass -File scripts\package-installer.ps1
# powershell -ExecutionPolicy Bypass -File scripts\package-installer.ps1 -SkipBuild
# powershell -ExecutionPolicy Bypass -File scripts\package-installer.ps1 -Version 0.9.0
# NOTE: ASCII-only on purpose (Windows PowerShell 5.1 reads BOM-less .ps1 as ANSI).
[CmdletBinding()]
param(
[switch]$SkipBuild,
[string]$Version
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
$ui = Join-Path $root 'ek-ui'
$st = Join-Path $root 'ek-station'
$ctp = Join-Path $root 'ek-ctp'
$pack = Join-Path $ui 'packaging'
$stage = Join-Path $pack 'staging'
$dist = Join-Path $ui 'dist'
if (-not $Version) {
$toml = Get-Content (Join-Path $ui 'Cargo.toml') -Raw -Encoding UTF8
if ($toml -match '(?m)^version\s*=\s*"([^"]+)"') { $Version = $Matches[1] } else {
Write-Host 'STOP: cannot read ek-ui version' -ForegroundColor Red; exit 2
}
}
Write-Host ("== ek-station desktop packaging, version " + $Version + " ==") -ForegroundColor Cyan
# ---- 1. preflight ----
$iscc = @(
'C:\Program Files (x86)\Inno Setup 6\ISCC.exe',
'C:\Program Files\Inno Setup 6\ISCC.exe'
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) { Write-Host 'STOP: ISCC.exe not found (install Inno Setup 6)' -ForegroundColor Red; exit 2 }
$dumpbin = Get-ChildItem 'C:\Program Files\Microsoft Visual Studio','C:\Program Files (x86)\Microsoft Visual Studio' -Filter dumpbin.exe -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'Hostx64\\x64' } | Select-Object -First 1
if (-not $dumpbin) { Write-Host 'STOP: dumpbin.exe not found (MSVC Build Tools needed for the dependency audit)' -ForegroundColor Red; exit 2 }
$patch = Join-Path $root '.cargo\config.toml'
if (Test-Path $patch) {
Write-Host ('STOP: local scaffolding patch is present: ' + $patch) -ForegroundColor Red
Write-Host ' the installer must ship the published chain face. Move it away first:' -ForegroundColor Red
Write-Host ' Rename-Item .cargo\config.toml config.toml.off' -ForegroundColor Yellow
exit 2
}
Write-Host 'preflight OK (ISCC + dumpbin present, scaffolding patch off)' -ForegroundColor DarkGray
# ---- 2. release builds ----
if (-not $SkipBuild) {
Push-Location $ui
Write-Host 'build: ekapp (release, ctp-native + bundled-font)' -ForegroundColor Cyan
& cargo build --release --offline --features ctp-native,bundled-font --bin ekapp
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host 'STOP: ekapp build failed' -ForegroundColor Red; exit 1 }
Pop-Location
Push-Location $st
Write-Host 'build: ek-station daemon (release, ctp-native)' -ForegroundColor Cyan
& cargo build --release --offline --features ctp-native --bin ek-station
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host 'STOP: ek-station daemon build failed' -ForegroundColor Red; exit 1 }
Pop-Location
}
# ---- 3. staging ----
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
New-Item -ItemType Directory -Path $stage | Out-Null
Copy-Item (Join-Path $ui 'target\release\ekapp.exe') $stage
Copy-Item (Join-Path $st 'target\release\ek-station.exe') $stage
Copy-Item (Join-Path $ctp 'native\sdk\win64\thosttraderapi_se.dll') $stage
Copy-Item (Join-Path $ctp 'native\sdk\win64\thostmduserapi_se.dll') $stage
Copy-Item (Join-Path $ui 'assets\fonts\OFL-NotoSansSC-LICENSE.txt') $stage
Copy-Item (Join-Path $ctp 'NOTICE') (Join-Path $stage 'NOTICE-ek-ctp.txt')
Copy-Item (Join-Path $pack '首次运行说明.md') $stage
# combined license screen: MIT (our crates) + OFL (bundled font) + CTP notice
$mit = Get-Content (Join-Path $ui 'LICENSE.txt') -Raw -Encoding UTF8
$ofl = Get-Content (Join-Path $stage 'OFL-NotoSansSC-LICENSE.txt') -Raw -Encoding UTF8
$ntc = Get-Content (Join-Path $stage 'NOTICE-ek-ctp.txt') -Raw -Encoding UTF8
$combined = "=== ek-ui / ek-station (MIT) ===`r`n`r`n" + $mit + "`r`n`r`n=== bundled font (SIL OFL 1.1) ===`r`n`r`n" + $ofl + "`r`n`r`n=== CTP SDK notice (CTP API binaries) ===`r`n`r`n" + $ntc
[System.IO.File]::WriteAllText((Join-Path $stage 'LICENSE-combined.txt'), $combined, (New-Object System.Text.UTF8Encoding($true)))
Write-Host 'staging ready' -ForegroundColor DarkGray
# ---- 4. dependency audit ----
$depLog = & $dumpbin.FullName /NOLOGO /dependents (Join-Path $stage 'ekapp.exe')
$deps = @($depLog | Where-Object { $_ -match '^\s+\S+\.(dll|DLL)' } | ForEach-Object { $_.Trim() })
Write-Host 'ekapp.exe direct imports:' -ForegroundColor Cyan
$deps | ForEach-Object { Write-Host (' ' + $_) }
$want = @('thosttraderapi_se.dll', 'thostmduserapi_se.dll')
foreach ($w in $want) {
if (-not ($deps | Where-Object { $_ -ieq $w })) {
Write-Host ('STOP: expected import ' + $w + ' missing from ekapp.exe') -ForegroundColor Red
exit 1
}
}
$bads = @($deps | Where-Object { $_ -match '\\dev\\|\\target\\|thost.*\.lib$' })
if ($bads.Count -gt 0) {
Write-Host ('STOP: import escapes the staging face: ' + ($bads -join ', ')) -ForegroundColor Red
exit 1
}
$depLog | Set-Content (Join-Path $dist 'dumpbin-ekapp.txt') -Encoding UTF8 -ErrorAction SilentlyContinue
Write-Host 'dependency audit OK (both CTP DLLs imported; shipped next to the exe)' -ForegroundColor DarkGray
# ---- 5. ISCC ----
if (-not (Test-Path $dist)) { New-Item -ItemType Directory -Path $dist | Out-Null }
$env:EK_APP_VERSION = $Version
& powershell -ExecutionPolicy Bypass -NoProfile -Command "& '$iscc' '$(Join-Path $pack 'ek-station-desktop.iss')"
if ($LASTEXITCODE -ne 0) { Write-Host 'STOP: ISCC failed' -ForegroundColor Red; exit 1 }
# ---- 6. SHA256 ----
$setup = Join-Path $dist ("ek-station-app-setup-" + $Version + ".exe")
if (-not (Test-Path $setup)) { Write-Host ('STOP: setup exe not found: ' + $setup) -ForegroundColor Red; exit 1 }
$h = (Get-FileHash $setup -Algorithm SHA256).Hash.ToLower()
$sha = $h + ' ' + (Split-Path -Leaf $setup) + "`r`n"
Set-Content -Path (Join-Path $dist 'SHA256.txt') -Value $sha -Encoding UTF8
Write-Host ''
Write-Host ('INSTALLER ' + $setup) -ForegroundColor Green
Write-Host ('SHA256 ' + $h) -ForegroundColor Green
exit 0