Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $bellRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) $repositoryRoot = Split-Path $bellRoot -Parent $serverRoot = Join-Path $bellRoot 'server' $uiRoot = Join-Path $bellRoot 'ui' $distRoot = Join-Path $bellRoot 'dist' $target = Join-Path $distRoot 'bell-windows-amd64' $archive = Join-Path $distRoot 'bell-windows-amd64.zip' $staging = Join-Path $distRoot ('.bell-windows-amd64.staging-' + $PID) function Assert-ChildPath([string]$Parent,[string]$Child) { $parentPath = [IO.Path]::GetFullPath($Parent).TrimEnd('\') + '\' $childPath = [IO.Path]::GetFullPath($Child) if (-not $childPath.StartsWith($parentPath,[StringComparison]::OrdinalIgnoreCase)) { throw "Unsafe build path outside $Parent`: $Child" } } function Get-FileSha256([string]$Path) { $sha = [Security.Cryptography.SHA256]::Create(); $stream = [IO.File]::OpenRead($Path) try { return ([BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-','') } finally { $stream.Dispose(); $sha.Dispose() } } Assert-ChildPath $bellRoot $distRoot; Assert-ChildPath $distRoot $target; Assert-ChildPath $distRoot $archive; Assert-ChildPath $distRoot $staging $savedToolchain = $env:GOTOOLCHAIN $env:GOTOOLCHAIN = 'go1.26.5' try { Push-Location $serverRoot try { $goVersion = (& go env GOVERSION).Trim() } finally { Pop-Location } $nodeVersion = (& node --version).Trim().TrimStart('v') $pnpmVersion = (& corepack pnpm@9.15.1 --version).Trim() if ($goVersion -ne 'go1.26.5') { throw "Go 1.26.5 is required; found $goVersion." } if ($nodeVersion -ne '22.22.1') { throw "Node 22.22.1 is required; found $nodeVersion." } if ($pnpmVersion -ne '9.15.1') { throw "pnpm 9.15.1 is required; found $pnpmVersion." } New-Item -ItemType Directory -Force -Path $distRoot | Out-Null if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } New-Item -ItemType Directory -Path $staging | Out-Null Push-Location $uiRoot try { & corepack pnpm@9.15.1 install --frozen-lockfile if ($LASTEXITCODE -ne 0) { throw 'pnpm install failed.' } & corepack pnpm@9.15.1 run build:prod if ($LASTEXITCODE -ne 0) { throw 'Bell UI production build failed.' } # The frozen Vue CLI differential build references a module runtime # that ScriptExt removes from disk. The complete legacy bundle is # present, so make that reproducible bundle the package entry point. $builtIndex = Join-Path $uiRoot 'dist\index.html' $html = Get-Content -LiteralPath $builtIndex -Raw -Encoding UTF8 $html = [regex]::Replace($html, ']+type="module"[^>]*>', '') $html = $html.Replace(' nomodule', '') [IO.File]::WriteAllText($builtIndex, $html, (New-Object Text.UTF8Encoding($false))) } finally { Pop-Location } $oldGOOS,$oldGOARCH,$oldCGO = $env:GOOS,$env:GOARCH,$env:CGO_ENABLED try { $env:GOOS='windows'; $env:GOARCH='amd64'; $env:CGO_ENABLED='0' Push-Location $serverRoot try { & go build -trimpath -ldflags '-s -w' -o (Join-Path $staging 'bell.exe') .; if ($LASTEXITCODE -ne 0) { throw 'Bell server Windows build failed.' } } finally { Pop-Location } } finally { $env:GOOS,$env:GOARCH,$env:CGO_ENABLED=$oldGOOS,$oldGOARCH,$oldCGO } Copy-Item -LiteralPath (Join-Path $uiRoot 'dist') -Destination (Join-Path $staging 'web') -Recurse New-Item -ItemType Directory -Path (Join-Path $staging 'scripts\runtime'),(Join-Path $staging 'config'),(Join-Path $staging 'LICENSES') | Out-Null Copy-Item -Path (Join-Path $bellRoot 'scripts\runtime\*.ps1') -Destination (Join-Path $staging 'scripts\runtime') foreach ($name in @('start-bell','stop-bell','check-bell')) { Copy-Item -LiteralPath (Join-Path $bellRoot "scripts\runtime\$name.bat") -Destination (Join-Path $staging "$name.bat") } Copy-Item -LiteralPath (Join-Path $bellRoot 'config\bell.env.example') -Destination (Join-Path $staging 'config\bell.env.example') Copy-Item -LiteralPath (Join-Path $bellRoot 'config\bell.env.example') -Destination (Join-Path $staging 'config\bell.env') Copy-Item -LiteralPath (Join-Path $serverRoot 'config\settings.yml') -Destination (Join-Path $staging 'config\settings.yml') Copy-Item -LiteralPath (Join-Path $serverRoot 'config\db.sql') -Destination (Join-Path $staging 'config\db.sql') Copy-Item -LiteralPath (Join-Path $serverRoot 'config\pg.sql') -Destination (Join-Path $staging 'config\pg.sql') Copy-Item -LiteralPath (Join-Path $bellRoot 'README-WINDOWS.md') -Destination (Join-Path $staging 'README-WINDOWS.md') Copy-Item -LiteralPath (Join-Path $bellRoot 'LICENSES') -Destination $staging -Recurse -Force Copy-Item -LiteralPath (Join-Path $serverRoot 'LICENSE.md') -Destination (Join-Path $staging 'LICENSES\Bell-server-LICENSE.md') Copy-Item -LiteralPath (Join-Path $uiRoot 'LICENSE') -Destination (Join-Path $staging 'LICENSES\Bell-ui-LICENSE') $commit = (& git -C $repositoryRoot rev-parse HEAD).Trim() [IO.File]::WriteAllLines((Join-Path $staging 'VERSION.txt'),@("source_commit=$commit",'go=1.26.5','node=22.22.1','pnpm=9.15.1'),(New-Object Text.UTF8Encoding($false))) & (Join-Path $PSScriptRoot 'test-package.ps1') -PackageRoot $staging if ($LASTEXITCODE -ne 0) { throw 'Bell package audit failed.' } $manifest = foreach ($file in Get-ChildItem -LiteralPath $staging -Recurse -File | Sort-Object FullName) { "$(Get-FileSha256 $file.FullName) $($file.FullName.Substring($staging.Length+1).Replace('\','/'))" } [IO.File]::WriteAllLines((Join-Path $staging 'MANIFEST.sha256'),$manifest,(New-Object Text.UTF8Encoding($false))) if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force } Move-Item -LiteralPath $staging -Destination $target if (Test-Path -LiteralPath $archive) { Remove-Item -LiteralPath $archive -Force } Compress-Archive -LiteralPath $target -DestinationPath $archive -CompressionLevel Optimal Write-Host "Bell Windows package: $target" Write-Host "Bell Windows archive: $archive" } finally { $env:GOTOOLCHAIN = $savedToolchain if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } }