426 lines
18 KiB
PowerShell
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
|