[CmdletBinding()] param( [switch]$Clean, [switch]$SkipTests ) $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true $projectRoot = Split-Path -Parent $PSScriptRoot $n2nRoot = Join-Path $projectRoot 'third_party\n2n' $cargoManifest = Join-Path $projectRoot 'src-tauri\Cargo.toml' $packageScript = Join-Path $PSScriptRoot 'package-portable.ps1' $n2nBuildScript = Join-Path $PSScriptRoot 'build-n2n.sh' $archivePath = Join-Path $projectRoot 'release\super-n2n-portable.zip' $checksumPath = $archivePath + '.sha256' function Get-RequiredCommand { param( [string[]]$Names, [string]$Purpose ) foreach ($name in $Names) { $command = Get-Command $name -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($command) { return $command.Source } } throw "Missing required tool for $Purpose. Expected one of: $($Names -join ', ')." } function Get-GitBash { param([string]$GitExecutable) $gitRoot = Split-Path -Parent (Split-Path -Parent $GitExecutable) $candidatePaths = @( (Join-Path $gitRoot 'bin\bash.exe') ) $candidatePaths += Get-Command bash.exe -CommandType Application -All -ErrorAction SilentlyContinue | ForEach-Object Source foreach ($bashPath in $candidatePaths | Select-Object -Unique) { if (-not (Test-Path -LiteralPath $bashPath)) { continue } $bashDirectory = Split-Path -Parent $bashPath $gitUnixBinCandidates = @( $bashDirectory, (Join-Path (Split-Path -Parent $bashDirectory) 'usr\bin') ) foreach ($gitUnixBin in $gitUnixBinCandidates) { if ((Test-Path -LiteralPath (Join-Path $gitUnixBin 'sh.exe')) -and (Test-Path -LiteralPath (Join-Path $gitUnixBin 'cygpath.exe'))) { return [pscustomobject]@{ Bash = $bashPath UnixBin = $gitUnixBin } } } } throw 'Git Bash was not found. Install Git for Windows.' } function Invoke-Checked { param( [string]$Executable, [string[]]$Arguments ) & $Executable @Arguments if ($LASTEXITCODE -ne 0) { throw "Command failed with exit code ${LASTEXITCODE}: $Executable $($Arguments -join ' ')" } } if ($env:OS -ne 'Windows_NT') { throw 'This script builds the Windows portable package and must run on Windows.' } $git = Get-RequiredCommand -Names @('git.exe', 'git') -Purpose 'source checkout' $npm = Get-RequiredCommand -Names @('npm.cmd', 'npm') -Purpose 'frontend dependency installation' $cargo = Get-RequiredCommand -Names @('cargo.exe', 'cargo') -Purpose 'Tauri compilation' $gitBash = Get-GitBash -GitExecutable $git $make = Get-RequiredCommand -Names @('mingw32-make.exe', 'mingw32-make') -Purpose 'n2n compilation' $null = Get-RequiredCommand -Names @('gcc.exe', 'gcc') -Purpose 'n2n compilation' $null = Get-RequiredCommand -Names @('windres.exe', 'windres') -Purpose 'n2n Windows resource compilation' # Make GNU utilities (rm, cp, gzip) available to n2n's Makefile on Windows. $env:Path = "$($gitBash.UnixBin);$env:Path" $env:SHELL = Join-Path $gitBash.UnixBin 'sh.exe' Push-Location $projectRoot try { Invoke-Checked $git @('submodule', 'sync') Invoke-Checked $git @('submodule', 'update', '--init') if (-not (Test-Path -LiteralPath $n2nRoot)) { throw "The n2n submodule was not initialized: $n2nRoot" } $cygpath = Join-Path $gitBash.UnixBin 'cygpath.exe' $n2nRootUnix = (& $cygpath -u $n2nRoot).Trim() $makeUnix = (& $cygpath -u $make).Trim() $n2nBuildScriptUnix = (& $cygpath -u $n2nBuildScript).Trim() Invoke-Checked $gitBash.Bash @($n2nBuildScriptUnix, $n2nRootUnix, $makeUnix, $(if ($Clean) { '1' } else { '0' })) foreach ($binary in @('edge.exe', 'supernode.exe')) { if (-not (Test-Path -LiteralPath (Join-Path $n2nRoot $binary))) { throw "n2n build did not produce $binary." } } Invoke-Checked $npm @('ci') if (-not $SkipTests) { Invoke-Checked $cargo @('test', '--manifest-path', $cargoManifest) } & $packageScript -Clean:$Clean if ($LASTEXITCODE -ne 0) { throw "Portable package script failed with exit code $LASTEXITCODE." } if (-not (Test-Path -LiteralPath $archivePath)) { throw "Portable archive was not created: $archivePath" } $stream = [System.IO.File]::OpenRead($archivePath) try { $sha256 = [System.Security.Cryptography.SHA256]::Create() try { $hashBytes = $sha256.ComputeHash($stream) $hash = -join ($hashBytes | ForEach-Object { $_.ToString('x2') }) } finally { $sha256.Dispose() } } finally { $stream.Dispose() } "$hash *$(Split-Path -Leaf $archivePath)" | Set-Content -LiteralPath $checksumPath -Encoding ascii Write-Output "Build completed: $archivePath" Write-Output "SHA-256 checksum: $checksumPath" } finally { Pop-Location }