mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
262 lines
11 KiB
PowerShell
262 lines
11 KiB
PowerShell
# 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 0–7 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
|