param([string]$MediaMTXPath = '') Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $senseRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..')) $serverRoot = Join-Path $senseRoot 'server' $uiRoot = Join-Path $senseRoot 'ui' $distRoot = Join-Path $senseRoot 'dist' $target = Join-Path $distRoot 'sense-windows-amd64' $archive = Join-Path $distRoot 'sense-windows-amd64.zip' $staging = Join-Path $distRoot ('.sense-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-SenseFileSha256([string]$Path) { $sha256 = [Security.Cryptography.SHA256]::Create() $stream = [IO.File]::OpenRead($Path) try { return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace('-', '') } finally { $stream.Dispose() $sha256.Dispose() } } Assert-ChildPath $senseRoot $distRoot Assert-ChildPath $distRoot $target Assert-ChildPath $distRoot $archive Assert-ChildPath $distRoot $staging $goVersion = '' 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; module toolchain reported $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; corepack reported $pnpmVersion." } $hadNodeModules = Test-Path -LiteralPath (Join-Path $uiRoot 'node_modules') $hadUIDist = Test-Path -LiteralPath (Join-Path $uiRoot 'dist') try { 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 'Sense UI production build failed.' } } 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 'sense.exe') . if ($LASTEXITCODE -ne 0) { throw 'Sense 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 'bin') | Out-Null Copy-Item -Path (Join-Path $senseRoot 'scripts\runtime\*.ps1') -Destination (Join-Path $staging 'scripts\runtime') foreach ($name in @('start-sense', 'stop-sense', 'check-sense', 'migrate-sense', 'backup-sense', 'restore-sense', 'initialize-admin')) { Copy-Item -LiteralPath (Join-Path $senseRoot "scripts\runtime\$name.bat") -Destination (Join-Path $staging "$name.bat") } Copy-Item -LiteralPath (Join-Path $senseRoot 'config\sense.env.example') -Destination (Join-Path $staging 'config\sense.env.example') Copy-Item -LiteralPath (Join-Path $senseRoot 'config\sense.env.example') -Destination (Join-Path $staging 'config\sense.env') Copy-Item -LiteralPath (Join-Path $senseRoot 'config\sense.demo.env.example') -Destination (Join-Path $staging 'config\sense.demo.env.example') Copy-Item -LiteralPath (Join-Path $senseRoot 'config\sense.demo.env.example') -Destination (Join-Path $staging 'config\sense.demo.env') Copy-Item -LiteralPath (Join-Path $senseRoot 'config\mediamtx.yml') -Destination (Join-Path $staging 'config\mediamtx.yml') Copy-Item -LiteralPath (Join-Path $senseRoot 'config\mediamtx-shards.example.json') -Destination (Join-Path $staging 'config\mediamtx-shards.example.json') 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 $senseRoot 'README-WINDOWS.md') -Destination (Join-Path $staging 'README-WINDOWS.md') Copy-Item -LiteralPath (Join-Path $senseRoot 'package\README-MEDIAMTX.txt') -Destination (Join-Path $staging 'bin\README-MEDIAMTX.txt') Copy-Item -LiteralPath (Join-Path $senseRoot 'LICENSES') -Destination (Join-Path $staging 'LICENSES') -Recurse if (-not [string]::IsNullOrWhiteSpace($MediaMTXPath)) { $mediaSource = [IO.Path]::GetFullPath($MediaMTXPath) if (-not (Test-Path -LiteralPath $mediaSource -PathType Leaf)) { throw "MediaMTX binary not found: $mediaSource" } Copy-Item -LiteralPath $mediaSource -Destination (Join-Path $staging 'bin\mediamtx.exe') } $commit = (& git -C (Split-Path $senseRoot -Parent) 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 'Sense package audit failed.' } $manifest = foreach ($file in Get-ChildItem -LiteralPath $staging -Recurse -File | Sort-Object FullName) { $relative = $file.FullName.Substring($staging.Length + 1).Replace('\', '/') "$(Get-SenseFileSha256 -Path $file.FullName) $relative" } [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 "Sense Windows package: $target" Write-Host "Sense Windows archive: $archive" } finally { if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force } if (-not $hadUIDist -and (Test-Path -LiteralPath (Join-Path $uiRoot 'dist'))) { Remove-Item -LiteralPath (Join-Path $uiRoot 'dist') -Recurse -Force } if (-not $hadNodeModules -and (Test-Path -LiteralPath (Join-Path $uiRoot 'node_modules'))) { Remove-Item -LiteralPath (Join-Path $uiRoot 'node_modules') -Recurse -Force } }