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

268 lines
12 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: Fetch Router from npm ---
# The 9router Next.js server is published as an npm package with a pre-built
# standalone output. Stage it directly from npm instead of vendoring + rebuilding.
Write-Host "[3/5] Fetching Router from npm..."
$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
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $ProjectRoot 'scripts\fetch-router.ps1') -Dest (Join-Path $Staging 'router')
if ($LASTEXITCODE -ne 0) { throw "fetch-router.ps1 failed" }
if (-not (Test-Path (Join-Path $Staging 'router\server.js'))) {
throw "Router fetch failed - server.js not found in staging"
}
Write-Host "Router staged."
Write-Host ""
# --- Step 4: Snapshot source dirs into electron\build-staging\ ---
# (Router was already staged in step 3; do not wipe or re-copy it here.)
Write-Host "[4/5] Snapshotting source directories..."
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','tests'); 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
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) {
# Safety check: warn if the matching Mac release isn't on GitHub yet.
# Mac and Windows publishes don't conflict (different asset names,
# different latest*.yml manifests), but a Windows-only release means
# Mac users will skip this version entirely. Better to know now than
# explain it after the fact. Non-fatal; sleeps 8s to let the user
# Ctrl+C if it surprises them.
try {
$pkgJson = Get-Content -Raw (Join-Path $ProjectRoot 'electron\package.json') | ConvertFrom-Json
$version = $pkgJson.version
$macYmlUrl = "https://github.com/openswarm-ai/openswarm/releases/download/v$version/latest-mac.yml"
$null = Invoke-WebRequest -Uri $macYmlUrl -Method Head -UseBasicParsing -ErrorAction Stop -TimeoutSec 10
Write-Host " > Mac release v$version detected on GitHub (latest-mac.yml present). OK to proceed."
} catch {
Write-Host ""
Write-Host "WARNING: Mac release v$version is NOT yet published on GitHub." -ForegroundColor Yellow
Write-Host " -> Uploading Windows assets to a release with no Mac assets means" -ForegroundColor Yellow
Write-Host " Mac users will skip v$version entirely (electron-updater on Mac" -ForegroundColor Yellow
Write-Host " will see no latest-mac.yml). Recommended order:" -ForegroundColor Yellow
Write-Host " 1. bash publish.sh (on the Mac)" -ForegroundColor Yellow
Write-Host " 2. pwsh publish-win.ps1 (here)" -ForegroundColor Yellow
Write-Host " -> Continuing in 8s. Press Ctrl+C to abort." -ForegroundColor Yellow
Start-Sleep -Seconds 8
}
& 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