Files
openswarm/scripts/build-app-win.ps1
T

262 lines
11 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Master build script for the OpenSwarm desktop app on Windows.
#
# Usage:
# pwsh scripts\build-app-win.ps1 Local dev build (unsigned)
# pwsh scripts\build-app-win.ps1 -Sign Signed build (no publish)
# pwsh scripts\build-app-win.ps1 -Publish Production build (sign + publish to GitHub Releases)
#
# Reads .env.windows (gitignored) for Azure Trusted Signing + GH_TOKEN if -Sign or -Publish.
[CmdletBinding()]
param(
[switch]$Sign,
[switch]$Publish
)
$ErrorActionPreference = 'Stop'
if ($Publish) { $Sign = $true }
$ScriptDir = Split-Path -Parent $PSCommandPath
$ProjectRoot = Split-Path -Parent $ScriptDir
# --- Load .env.windows if present ---
$EnvFile = Join-Path $ProjectRoot '.env.windows'
if (Test-Path $EnvFile) {
Get-Content $EnvFile | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith('#') -and $line.Contains('=')) {
$idx = $line.IndexOf('=')
$name = $line.Substring(0, $idx).Trim()
$value = $line.Substring($idx + 1).Trim()
if ($value.StartsWith('"') -and $value.EndsWith('"')) {
$value = $value.Substring(1, $value.Length - 2)
}
Set-Item -Path "Env:$name" -Value $value
}
}
}
Write-Host "========================================"
Write-Host " OpenSwarm Desktop App Builder (Windows)"
if ($Publish) { Write-Host " Mode: PRODUCTION (sign + publish to GitHub Releases)" }
elseif ($Sign) { Write-Host " Mode: SIGNED (sign, no publish)" }
else { Write-Host " Mode: LOCAL (unsigned)" }
Write-Host "========================================"
Write-Host ""
# --- Required env validation ---
if ($Sign) {
$required = @(
'AZURE_TENANT_ID','AZURE_CLIENT_ID','AZURE_CLIENT_SECRET',
'AZURE_SIGNING_ENDPOINT','AZURE_SIGNING_ACCOUNT','AZURE_SIGNING_CERT_PROFILE'
)
if ($Publish) { $required += 'GH_TOKEN' }
$missing = $required | Where-Object { -not [Environment]::GetEnvironmentVariable($_) }
if ($missing.Count -gt 0) {
Write-Host "ERROR: Missing required environment variables:" -ForegroundColor Red
$missing | ForEach-Object { Write-Host " - $_" }
Write-Host "Copy .env.windows.example to .env.windows and fill in values."
exit 1
}
}
# --- Step 0: Bundled uv/uvx (Windows zip) ---
$UvBinDir = Join-Path $ProjectRoot 'backend\uv-bin'
if (-not (Test-Path (Join-Path $UvBinDir 'uvx.exe'))) {
Write-Host "[0] Downloading uv/uvx for Windows..."
New-Item -ItemType Directory -Force -Path $UvBinDir | Out-Null
$UvUrl = 'https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-pc-windows-msvc.zip'
$TmpZip = Join-Path $env:TEMP "uv-win-$([guid]::NewGuid()).zip"
$TmpExtract = Join-Path $env:TEMP "uv-win-extract-$([guid]::NewGuid())"
try {
Invoke-WebRequest -Uri $UvUrl -OutFile $TmpZip -UseBasicParsing
Expand-Archive -Path $TmpZip -DestinationPath $TmpExtract -Force
Get-ChildItem -Path $TmpExtract -Recurse -Filter 'uv.exe' | Select-Object -First 1 | ForEach-Object { Copy-Item $_.FullName (Join-Path $UvBinDir 'uv.exe') -Force }
Get-ChildItem -Path $TmpExtract -Recurse -Filter 'uvx.exe' | Select-Object -First 1 | ForEach-Object { Copy-Item $_.FullName (Join-Path $UvBinDir 'uvx.exe') -Force }
Write-Host "uv/uvx downloaded and bundled."
} finally {
Remove-Item -Force $TmpZip -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $TmpExtract -ErrorAction SilentlyContinue
}
} else {
Write-Host "[0] uv/uvx binaries already present."
}
Write-Host ""
# --- Step 0b: Bundle reddit-mcp-buddy via esbuild ---
$McpBundleDir = Join-Path $ProjectRoot 'backend\mcp-bundles'
New-Item -ItemType Directory -Force -Path $McpBundleDir | Out-Null
$RedditBundle = Join-Path $McpBundleDir 'reddit-mcp-buddy.js'
if (-not (Test-Path $RedditBundle)) {
Write-Host "[0b] Bundling reddit-mcp-buddy..."
$TmpDir = Join-Path $env:TEMP "openswarm-mcp-$([guid]::NewGuid())"
New-Item -ItemType Directory -Force -Path $TmpDir | Out-Null
Push-Location $TmpDir
try {
& npm install reddit-mcp-buddy --silent 2>$null
& npx esbuild "node_modules/reddit-mcp-buddy/dist/index.js" --bundle --platform=node --format=cjs "--outfile=$RedditBundle"
if ($LASTEXITCODE -ne 0) { throw "esbuild failed" }
Write-Host "reddit-mcp-buddy bundled."
} finally {
Pop-Location
Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue
}
} else {
Write-Host "[0b] reddit-mcp-buddy bundle already present."
}
# --- Step 0c: npm MCP servers that can't be bundled ---
$NpmServersDir = Join-Path $ProjectRoot 'backend\npm-servers'
New-Item -ItemType Directory -Force -Path $NpmServersDir | Out-Null
function Install-NpmServer($SubDir, $PackageName) {
$TargetDir = Join-Path $NpmServersDir $SubDir
$NodeModules = Join-Path $TargetDir 'node_modules'
if (Test-Path $NodeModules) {
Write-Host "[0c] $PackageName already present."
return
}
Write-Host "[0c] Installing $PackageName..."
New-Item -ItemType Directory -Force -Path $TargetDir | Out-Null
Push-Location $TargetDir
try {
& npm init -y *>$null
& npm install $PackageName --silent 2>$null
if ($LASTEXITCODE -ne 0) { throw "$PackageName install failed" }
Write-Host "$PackageName installed."
} finally {
Pop-Location
}
}
Install-NpmServer 'softeria-ms-365-mcp-server' '@softeria/ms-365-mcp-server'
Install-NpmServer 'notionhq-notion-mcp-server' '@notionhq/notion-mcp-server'
Write-Host ""
# --- Step 1: Frontend build ---
Write-Host "[1/5] Building frontend..."
Push-Location (Join-Path $ProjectRoot 'frontend')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (frontend) failed" }
& npm run build
if ($LASTEXITCODE -ne 0) { throw "frontend build failed" }
} finally { Pop-Location }
if (-not (Test-Path (Join-Path $ProjectRoot 'frontend\dist\index.html'))) {
throw "Frontend build failed - dist\index.html not found"
}
Write-Host "Frontend build complete."
Write-Host ""
# --- Step 2: Python env ---
$PythonEnv = Join-Path $ProjectRoot 'electron\python-env'
$PythonExe = Join-Path $PythonEnv 'python.exe'
if ((Test-Path $PythonExe) -and -not $env:OPENSWARM_REBUILD_PYTHON) {
Write-Host "[2/5] Python environment already present at $PythonEnv (set `$env:OPENSWARM_REBUILD_PYTHON='1' to force rebuild)."
} else {
Write-Host "[2/5] Building Python environment..."
& (Join-Path $ScriptDir 'build-python-env-win.ps1')
if ($LASTEXITCODE -ne 0) { throw "Python env build failed" }
}
if (-not (Test-Path (Join-Path $ProjectRoot 'electron\python-env'))) {
throw "Python environment not found at electron\python-env\"
}
Write-Host "Python environment ready."
Write-Host ""
# --- Step 3: 9Router build ---
Write-Host "[3/5] Building 9Router..."
Push-Location (Join-Path $ProjectRoot '9router')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (9router) failed" }
& npm run build
if ($LASTEXITCODE -ne 0) { throw "9router build failed" }
} finally { Pop-Location }
$Standalone = Join-Path $ProjectRoot '9router\.next\standalone'
if (-not (Test-Path $Standalone)) { throw "9Router build failed - .next\standalone not found" }
$NextStatic = Join-Path $ProjectRoot '9router\.next\static'
if (Test-Path $NextStatic) {
$StandaloneStatic = Join-Path $Standalone '.next\static'
New-Item -ItemType Directory -Force -Path (Split-Path $StandaloneStatic -Parent) | Out-Null
Copy-Item -Recurse -Force $NextStatic $StandaloneStatic
}
$NextPublic = Join-Path $ProjectRoot '9router\public'
if (Test-Path $NextPublic) {
Copy-Item -Recurse -Force $NextPublic (Join-Path $Standalone 'public')
}
Write-Host "9Router build complete."
Write-Host ""
# --- Step 4: Snapshot source dirs into electron\build-staging\ ---
Write-Host "[4/5] Snapshotting source directories..."
$Staging = Join-Path $ProjectRoot 'electron\build-staging'
if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
New-Item -ItemType Directory -Force -Path $Staging | Out-Null
function Copy-Excluded($Source, $Dest, $Exclude) {
# robocopy: built-in, fast, handles long paths.
$args = @($Source, $Dest, '/E', '/NJH', '/NJS', '/NDL', '/NFL', '/NP', '/MT:8')
foreach ($d in $Exclude.Dirs) { $args += '/XD'; $args += $d }
foreach ($f in $Exclude.Files) { $args += '/XF'; $args += $f }
& robocopy @args | Out-Null
# robocopy exit codes 07 are success
if ($LASTEXITCODE -ge 8) { throw "robocopy failed ($Source -> $Dest, exit $LASTEXITCODE)" }
$global:LASTEXITCODE = 0
}
Copy-Excluded `
(Join-Path $ProjectRoot 'backend') (Join-Path $Staging 'backend') `
@{ Dirs = @('__pycache__','.venv','tools'); Files = @('*.pyc') }
New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'backend\data\tools') | Out-Null
Copy-Excluded `
(Join-Path $ProjectRoot 'debugger') (Join-Path $Staging 'debugger') `
@{ Dirs = @('__pycache__','.venv','node_modules'); Files = @('*.pyc') }
Copy-Item -Recurse -Force (Join-Path $ProjectRoot 'frontend\dist\*') (New-Item -ItemType Directory -Force -Path (Join-Path $Staging 'frontend')).FullName
# 9Router standalone -> staging\9router
Copy-Excluded $Standalone (Join-Path $Staging '9router') @{ Dirs = @(); Files = @() }
if (Test-Path $NextStatic) {
$TargetStatic = Join-Path $Staging '9router\.next\static'
New-Item -ItemType Directory -Force -Path (Split-Path $TargetStatic -Parent) | Out-Null
Copy-Item -Recurse -Force $NextStatic $TargetStatic
}
Write-Host ""
Write-Host "========================================" -BackgroundColor Green -ForegroundColor White
Write-Host " SOURCE SNAPSHOT COMPLETE " -BackgroundColor Green -ForegroundColor White
Write-Host " Safe to modify your codebase now. " -BackgroundColor Green -ForegroundColor White
Write-Host "========================================" -BackgroundColor Green -ForegroundColor White
Write-Host ""
# --- Step 5: Package with electron-builder ---
Write-Host "[5/5] Packaging with electron-builder..."
Push-Location (Join-Path $ProjectRoot 'electron')
try {
& npm install
if ($LASTEXITCODE -ne 0) { throw "npm install (electron) failed" }
if (-not $Sign) {
$env:CSC_IDENTITY_AUTO_DISCOVERY = 'false'
}
if ($Publish) {
& npx electron-builder --win --x64 --publish always
} else {
& npx electron-builder --win --x64 --publish never
}
if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" }
} finally { Pop-Location }
Remove-Item -Recurse -Force $Staging -ErrorAction SilentlyContinue
Write-Host ""
Write-Host "========================================"
Write-Host " Build Complete!"
Write-Host "========================================"
Write-Host ""
Write-Host "Output files:"
Get-ChildItem -Path (Join-Path $ProjectRoot 'electron\dist') -Filter '*.exe' -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime
Get-ChildItem -Path (Join-Path $ProjectRoot 'electron\dist') -Filter '*.zip' -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime