Files
2026-08-16 18:24:52 +07:00

426 lines
18 KiB
PowerShell

# release/build-release.ps1 — Build a complete portable release of HOT-Step CPP
#
# Usage:
# .\release\build-release.ps1 [-Version "1.5.0"] [-SkipEngine] [-SkipUI] [-Variant cuda]
#
# Produces:
# release/out/HOT-Step-CPP-v{version}-win-x64-{variant}.zip
#
# Requirements:
# - Node.js 22 LTS (for building with correct native module ABI)
# - Visual Studio 2022 Build Tools with C++ workload
# - CUDA Toolkit 12.x (for CUDA variant)
# - Vulkan SDK (for Vulkan variant)
param(
[string]$Version = "0.0.0",
[switch]$SkipEngine,
[switch]$SkipUI,
[string]$Variant = "cuda", # cuda, vulkan, cpu
[string]$NodeVersion = "22.16.0" # Node.js LTS version to bundle
)
$ErrorActionPreference = "Stop"
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$ReleaseDir = Join-Path $ProjectRoot "release"
$StagingDir = Join-Path $ReleaseDir "staging"
$OutputDir = Join-Path $ReleaseDir "out"
# Portable Node.js download cache
$NodeCacheDir = Join-Path $ReleaseDir ".node-cache"
$NodeZipName = "node-v${NodeVersion}-win-x64.zip"
$NodeUrl = "https://nodejs.org/dist/v${NodeVersion}/${NodeZipName}"
$NodeExe = Join-Path (Join-Path $NodeCacheDir "node-v${NodeVersion}-win-x64") "node.exe"
Write-Host "`n════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host " HOT-Step CPP Release Builder" -ForegroundColor Cyan
Write-Host " Version: $Version | Variant: $Variant" -ForegroundColor Cyan
Write-Host "════════════════════════════════════════════════════════`n" -ForegroundColor Cyan
# ── Phase 0: Clean staging ────────────────────────────────────────────
Write-Host "[Phase 0] Cleaning staging directory..." -ForegroundColor Yellow
if (Test-Path $StagingDir) { Remove-Item -Recurse -Force $StagingDir }
New-Item -ItemType Directory -Force $StagingDir | Out-Null
New-Item -ItemType Directory -Force $OutputDir | Out-Null
# ── Phase 1: Download portable Node.js ────────────────────────────────
Write-Host "`n[Phase 1] Portable Node.js $NodeVersion..." -ForegroundColor Yellow
if (-not (Test-Path $NodeExe)) {
Write-Host " Downloading from nodejs.org..."
New-Item -ItemType Directory -Force $NodeCacheDir | Out-Null
$zipPath = Join-Path $NodeCacheDir $NodeZipName
if (-not (Test-Path $zipPath)) {
Invoke-WebRequest -Uri $NodeUrl -OutFile $zipPath -UseBasicParsing
}
Write-Host " Extracting..."
Expand-Archive -Path $zipPath -DestinationPath $NodeCacheDir -Force
Remove-Item $zipPath -ErrorAction SilentlyContinue
if (-not (Test-Path $NodeExe)) {
throw "Node.js extraction failed - $NodeExe not found"
}
}
$nodeVer = & $NodeExe --version
Write-Host " Using: $NodeExe ($nodeVer)" -ForegroundColor Green
# ── Phase 2: Build C++ Engine ─────────────────────────────────────────
if (-not $SkipEngine) {
Write-Host "`n[Phase 2] Building C++ engine ($Variant)..." -ForegroundColor Yellow
$engineDir = Join-Path $ProjectRoot "engine"
# Use buildall.cmd for multi-arch release builds
$buildScript = Join-Path $engineDir "buildall.cmd"
# For release builds, we want static MSVC runtime and multi-arch CUDA
# buildall.cmd already does multi-arch + Vulkan.
# We modify the cmake flags by setting them before calling the script.
Write-Host " Running buildall.cmd with static runtime..."
# Smart CMake cache handling: only clear if build flags have changed.
# Deleting CMakeCache.txt forces a full reconfigure -> full rebuild of ALL
# targets including 100+ CUDA vendor kernels (~1 hour). If the cache already
# has the correct release flags, we skip deletion for a fast incremental build.
$buildDir = Join-Path $engineDir "build"
$cacheFile = Join-Path $buildDir "CMakeCache.txt"
$needsCacheClear = $false
if (Test-Path $cacheFile) {
$cacheContent = Get-Content $cacheFile -Raw
$requiredFlags = @{
"HOT_STEP_STATIC_RUNTIME:BOOL=ON" = "Static runtime (/MT)"
"GGML_CUDA:BOOL=ON" = "CUDA backend"
"GGML_VULKAN:BOOL=ON" = "Vulkan backend"
"GGML_CPU_ALL_VARIANTS:BOOL=ON" = "CPU all variants"
}
foreach ($flag in $requiredFlags.Keys) {
if ($cacheContent -notmatch [regex]::Escape($flag)) {
Write-Host " Cache mismatch: $($requiredFlags[$flag]) ($flag)" -ForegroundColor Yellow
$needsCacheClear = $true
}
}
if ($needsCacheClear) {
Write-Host " Clearing CMake cache (flags changed since last build)..." -ForegroundColor Yellow
Remove-Item $cacheFile -Force
} else {
Write-Host " CMake cache valid -- incremental build (CUDA vendor files will NOT rebuild)" -ForegroundColor Green
}
} else {
Write-Host " No CMake cache -- full build required" -ForegroundColor Yellow
}
# Set cmake flags for the build
# buildall.cmd runs cmake with its own flags, so we override via env
$env:RELEASE_CMAKE_EXTRA = "-DHOT_STEP_STATIC_RUNTIME=ON"
Push-Location $engineDir
try {
# buildall.cmd handles vcvars, ORT download, and multi-config build
cmd /c "buildall.cmd"
if ($LASTEXITCODE -ne 0) { throw "Engine build failed (exit code $LASTEXITCODE)" }
} finally {
Pop-Location
}
# Verify output
$aceServerExe = Join-Path (Join-Path $buildDir "Release") "ace-server.exe"
if (-not (Test-Path $aceServerExe)) {
throw "Build succeeded but ace-server.exe not found at $aceServerExe"
}
Write-Host " Engine build complete" -ForegroundColor Green
} else {
Write-Host "`n[Phase 2] SKIPPED (engine build)" -ForegroundColor DarkGray
}
# ── Phase 3: Bundle Server ────────────────────────────────────────────
Write-Host "`n[Phase 3] Bundling server..." -ForegroundColor Yellow
# Install server deps with system npm (dev machine only)
Write-Host " Installing server dependencies..."
Push-Location (Join-Path $ProjectRoot "server")
try {
# Use cmd /c to prevent PS treating npm stderr warnings as errors
cmd /c "npm install --ignore-scripts 2>&1" | Out-Null
# Verify portable Node works
& $NodeExe --version | Out-Null
Write-Host " Dependencies installed" -ForegroundColor Green
} finally {
Pop-Location
}
# Rebuild better-sqlite3 native addon for the portable Node.js ABI
# Dev machine may run Node 24 (ABI 137) but portable bundle ships Node 22 (ABI 127)
# IMPORTANT: We backup and restore the original .node file so the dev environment
# is not contaminated by the release build.
Write-Host " Rebuilding better-sqlite3 for Node $NodeVersion..."
$bsqlPkg = Join-Path (Join-Path $ProjectRoot "server") "node_modules\better-sqlite3"
$nativeAddon = Join-Path $bsqlPkg "build\Release\better_sqlite3.node"
$nativeBackup = Join-Path $bsqlPkg "build\Release\better_sqlite3.node.dev-backup"
# Backup the dev machine's native addon
if (Test-Path $nativeAddon) {
Copy-Item $nativeAddon $nativeBackup -Force
Write-Host " Backed up dev addon"
}
Push-Location $bsqlPkg
try {
cmd /c "npx prebuild-install --runtime node --target $NodeVersion --arch x64 --platform win32 2>&1"
if ($LASTEXITCODE -ne 0) {
Write-Warning " prebuild-install failed, trying npm rebuild..."
Pop-Location
Push-Location (Join-Path $ProjectRoot "server")
cmd /c "npm rebuild better-sqlite3 2>&1" | Out-Null
}
Write-Host " Native addon rebuilt for Node $NodeVersion" -ForegroundColor Green
} finally {
Pop-Location
}
# Copy the Node 22 addon to staging before restoring dev version
$stagingBsqlBuild = Join-Path $StagingDir "server\node_modules\better-sqlite3\build\Release"
New-Item -ItemType Directory -Force $stagingBsqlBuild | Out-Null
Copy-Item $nativeAddon (Join-Path $stagingBsqlBuild "better_sqlite3.node") -Force -ErrorAction SilentlyContinue
# Restore the dev machine's native addon
if (Test-Path $nativeBackup) {
Copy-Item $nativeBackup $nativeAddon -Force
Remove-Item $nativeBackup -Force
Write-Host " Restored dev addon"
}
# Install esbuild in release dir (dev dependency for bundling)
Write-Host " Installing esbuild..."
Push-Location $ReleaseDir
try {
if (-not (Test-Path (Join-Path $ReleaseDir "node_modules\esbuild"))) {
npm install esbuild --save-dev 2>&1 | Out-Null
}
} finally {
Pop-Location
}
# Run esbuild (cmd /c prevents PS from treating esbuild stderr warnings as fatal)
Write-Host " Running esbuild..."
cmd /c "node `"$(Join-Path $ReleaseDir 'esbuild.config.mjs')`" 2>&1"
if ($LASTEXITCODE -ne 0) { throw "esbuild bundle failed" }
Write-Host " Server bundle complete" -ForegroundColor Green
# ── Phase 4: Build UI ─────────────────────────────────────────────────
if (-not $SkipUI) {
Write-Host "`n[Phase 4] Building UI..." -ForegroundColor Yellow
Push-Location (Join-Path $ProjectRoot "ui")
try {
cmd /c "npm install 2>&1" | Out-Null
cmd /c "npm run build 2>&1"
if ($LASTEXITCODE -ne 0) { throw "UI build failed" }
} finally {
Pop-Location
}
Write-Host " UI build complete" -ForegroundColor Green
} else {
Write-Host "`n[Phase 4] SKIPPED (UI build)" -ForegroundColor DarkGray
}
# ── Phase 5: Assemble Release ─────────────────────────────────────────
Write-Host "`n[Phase 5] Assembling release..." -ForegroundColor Yellow
$dist = $StagingDir
# Runtime: portable Node.js
Write-Host " Copying runtime..."
New-Item -ItemType Directory -Force (Join-Path $dist "runtime") | Out-Null
Copy-Item $NodeExe (Join-Path $dist "runtime\node.exe")
# Engine: binaries + DLLs
Write-Host " Copying engine binaries..."
$engineOut = Join-Path $dist "engine"
New-Item -ItemType Directory -Force $engineOut | Out-Null
$buildRelease = Join-Path (Join-Path $ProjectRoot "engine") "build\Release"
$engineFiles = @(
"ace-server.exe", "mastering.exe", "mp3-codec.exe",
"neural-codec.exe", "vst-host.exe", "quantize.exe",
"ggml.dll", "ggml-base.dll", "ggml-cpu.dll"
)
# Add variant-specific DLLs
switch ($Variant) {
"cuda" { $engineFiles += "ggml-cuda.dll" }
"vulkan" { $engineFiles += "ggml-vulkan.dll" }
# cpu: no GPU backend DLL needed
}
foreach ($file in $engineFiles) {
$src = Join-Path $buildRelease $file
if (Test-Path $src) {
Copy-Item $src (Join-Path $engineOut $file)
} else {
Write-Warning " Missing engine file: $file"
}
}
# Also copy any ggml-cpu-* variant DLLs (from GGML_CPU_ALL_VARIANTS)
Get-ChildItem (Join-Path $buildRelease "ggml-cpu-*.dll") -ErrorAction SilentlyContinue | ForEach-Object {
Copy-Item $_.FullName (Join-Path $engineOut $_.Name)
}
# Lua plugins — dual directory system:
# engine/plugins/ = native/built-in plugins (solvers, schedulers, guidance)
# plugins/ = community/user plugins (same structure, overrides native)
# ace-server scans both via engine_dir and project_dir resolution from binary path
Write-Host " Copying Lua plugins..."
$enginePluginsSrc = Join-Path (Join-Path $ProjectRoot "engine") "plugins"
if (Test-Path $enginePluginsSrc) {
$enginePluginsDst = Join-Path $engineOut "plugins"
Copy-Item -Recurse $enginePluginsSrc $enginePluginsDst
$pluginCount = (Get-ChildItem -Recurse $enginePluginsDst -Filter "*.lua" | Measure-Object).Count
Write-Host " $pluginCount native plugins (engine/plugins/)" -ForegroundColor Green
} else {
Write-Warning " engine/plugins/ not found -- engine will have no built-in plugins!"
}
$communityPluginsSrc = Join-Path $ProjectRoot "plugins"
if (Test-Path $communityPluginsSrc) {
Copy-Item -Recurse $communityPluginsSrc (Join-Path $dist "plugins")
Write-Host " Community plugins dir copied (plugins/)" -ForegroundColor Green
}
# MSVC C++ Runtime - bundle so the app works on clean machines without VC++ Redistributable
$msvcRedist = Get-ChildItem "C:\Program Files\Microsoft Visual Studio" -Recurse -Filter "vcruntime140.dll" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match 'x64' -and $_.FullName -notmatch 'debug' -and $_.FullName -match 'Redist' } |
Select-Object -First 1
if ($msvcRedist) {
$msvcDir = $msvcRedist.DirectoryName
foreach ($dll in @("vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll")) {
$src = Join-Path $msvcDir $dll
if (Test-Path $src) {
Copy-Item $src (Join-Path $engineOut $dll)
}
}
Write-Host " Bundled MSVC runtime DLLs"
} else {
Write-Warning " MSVC Redist not found - vcruntime DLLs not bundled"
}
# Server: bundled JS + native deps
Write-Host " Copying server..."
$serverOut = Join-Path $dist "server"
# server.mjs was already placed by esbuild into staging/server/
# Copy better-sqlite3 minimal package
$bsqlSrc = Join-Path (Join-Path $ProjectRoot "server") "node_modules\better-sqlite3"
$bsqlDst = Join-Path $serverOut "node_modules\better-sqlite3"
New-Item -ItemType Directory -Force $bsqlDst | Out-Null
# Copy the JS package + native binding
Copy-Item (Join-Path $bsqlSrc "package.json") (Join-Path $bsqlDst "package.json")
New-Item -ItemType Directory -Force (Join-Path $bsqlDst "lib") | Out-Null
Copy-Item -Recurse (Join-Path $bsqlSrc "lib\*") (Join-Path $bsqlDst "lib")
# NOTE: Do NOT copy build/Release/better_sqlite3.node from source tree here!
# The correct Node 22 addon was already staged at Phase 3 (line ~166).
# Copying from $bsqlSrc would overwrite it with the restored dev addon (Node 24 ABI).
# See: https://github.com/scragnog/HOT-Step-CPP/issues/18
# Copy better-sqlite3 runtime dependencies: bindings + file-uri-to-path
$nmSrc = Join-Path (Join-Path $ProjectRoot "server") "node_modules"
foreach ($dep in @("bindings", "file-uri-to-path")) {
$depSrc = Join-Path $nmSrc $dep
$depDst = Join-Path $serverOut "node_modules\$dep"
if (Test-Path $depSrc) {
Copy-Item -Recurse $depSrc $depDst
Write-Host " Copied $dep"
} else {
Write-Warning " Missing dependency: $dep"
}
}
# Copy ffmpeg.exe
$ffmpegSrc = Join-Path (Join-Path (Join-Path $ProjectRoot "server") "node_modules\ffmpeg-static") "ffmpeg.exe"
if (Test-Path $ffmpegSrc) {
Copy-Item $ffmpegSrc (Join-Path $serverOut "ffmpeg.exe")
} else {
Write-Warning " ffmpeg.exe not found - audio conversion will be limited"
}
# Copy data files (model-registry.json, assistant-knowledge.md)
$dataOut = Join-Path $serverOut "data"
New-Item -ItemType Directory -Force $dataOut | Out-Null
Copy-Item (Join-Path (Join-Path (Join-Path $ProjectRoot "server") "src\data") "model-registry.json") (Join-Path $dataOut "model-registry.json")
Copy-Item (Join-Path (Join-Path (Join-Path $ProjectRoot "server") "src\data") "assistant-knowledge.md") (Join-Path $dataOut "assistant-knowledge.md")
# UI: pre-built dist
Write-Host " Copying UI..."
$uiSrc = Join-Path (Join-Path $ProjectRoot "ui") "dist"
$uiDst = Join-Path (Join-Path $dist "ui") "dist"
if (Test-Path $uiSrc) {
New-Item -ItemType Directory -Force (Join-Path $dist "ui") | Out-Null
Copy-Item -Recurse $uiSrc $uiDst
} else {
Write-Warning " UI dist not found - skipping"
}
# Essentia
$essentiaSrc = Join-Path $ProjectRoot "Essentia"
if (Test-Path $essentiaSrc) {
Write-Host " Copying Essentia..."
Copy-Item -Recurse $essentiaSrc (Join-Path $dist "Essentia")
}
# Noise samples
$noiseSrc = Join-Path $ProjectRoot "noise_samples"
if (Test-Path $noiseSrc) {
Copy-Item -Recurse $noiseSrc (Join-Path $dist "noise_samples")
}
# Empty directories for user content
New-Item -ItemType Directory -Force (Join-Path $dist "models") | Out-Null
New-Item -ItemType Directory -Force (Join-Path $dist "adapters") | Out-Null
# Config and docs
Copy-Item (Join-Path $ProjectRoot ".env.example") (Join-Path $dist ".env.example")
Copy-Item (Join-Path $ReleaseDir "HOT-Step.bat") (Join-Path $dist "HOT-Step.bat")
Copy-Item (Join-Path $ReleaseDir "README.txt") (Join-Path $dist "README.txt")
Write-Host " Assembly complete" -ForegroundColor Green
# ── Phase 6: Package ──────────────────────────────────────────────────
Write-Host "`n[Phase 6] Packaging..." -ForegroundColor Yellow
$zipName = "HOT-Step-CPP-v${Version}-win-x64-${Variant}.zip"
$zipPath = Join-Path $OutputDir $zipName
if (Test-Path $zipPath) { Remove-Item $zipPath }
# Compress — use .NET for better compression than Compress-Archive
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::CreateFromDirectory($dist, $zipPath, [System.IO.Compression.CompressionLevel]::Optimal, $false)
$zipSize = (Get-Item $zipPath).Length
$zipSizeMB = [math]::Round($zipSize / 1MB, 1)
# Generate SHA256
$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash
$hashFile = Join-Path $OutputDir "${zipName}.sha256"
"$hash $zipName" | Set-Content $hashFile -NoNewline
Write-Host " $zipName ($zipSizeMB MB)" -ForegroundColor Green
Write-Host " SHA256: $hash" -ForegroundColor DarkGray
Write-Host "`n════════════════════════════════════════════════════════" -ForegroundColor Cyan
Write-Host " Release build complete!" -ForegroundColor Green
Write-Host " Output: $zipPath" -ForegroundColor Cyan
Write-Host "════════════════════════════════════════════════════════`n" -ForegroundColor Cyan