[eric] windows shipping: electron-builder win+nsis + Azure Trusted Signing hook, build-app-win.ps1 / build-python-env-win.ps1 mirroring mac, release-windows.yml tag-triggered CI, icon.ico + .env.windows.example, .claude/ + .env.* gitignored. Runtime fixes: PYTHONUTF8=1 (cp1252 UnicodeDecode), python.exe / Lib\site-packages paths, PYTHONPATH path.delimiter, taskkill /T /F process tree, OAuth UA platform branch, sign-vmp postinstall via node wrapper. Backend: _resolve_command PATHEXT probing (fixes uvx not found), UV_PYTHON Windows path. Cross-env added to 9router+electron for NODE_ENV prefix on cmd. Onboarding: poll subscriptions/status while connect step is open, render Connected for active providers (was stuck on "Connect" after 30s OAuth poll timeout).

This commit is contained in:
Eric
2026-04-21 18:15:49 -07:00
parent 87e42d0a3a
commit 9ff07443db
17 changed files with 772 additions and 47 deletions
+261
View File
@@ -0,0 +1,261 @@
# 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
+100
View File
@@ -0,0 +1,100 @@
# Build an embedded Windows Python environment for the Electron app.
#
# Downloads a standalone CPython build for Windows from python-build-standalone,
# installs backend deps, and leaves it under electron\python-env\.
# Bundled into the .exe installer by electron-builder via extraResources.
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $PSCommandPath
$ProjectRoot = Split-Path -Parent $ScriptDir
$ElectronDir = Join-Path $ProjectRoot 'electron'
$PythonEnvDir = Join-Path $ElectronDir 'python-env'
$PythonVersion = '3.13'
$PythonFullVersion = '3.13.2'
$ReleaseTag = '20250212'
$PlatformTag = 'x86_64-pc-windows-msvc-shared'
$Tarball = "cpython-$PythonFullVersion+$ReleaseTag-$PlatformTag-install_only_stripped.tar.gz"
$DownloadUrl = "https://github.com/indygreg/python-build-standalone/releases/download/$ReleaseTag/$Tarball"
Write-Host "=== Building Windows Python Environment ==="
Write-Host "Architecture: x64 ($PlatformTag)"
Write-Host "Python: $PythonFullVersion"
if (Test-Path $PythonEnvDir) {
Write-Host "Removing old python-env..."
Remove-Item -Recurse -Force $PythonEnvDir
}
$TempDir = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "openswarm-pyenv-$([guid]::NewGuid())") -Force
try {
$ArchivePath = Join-Path $TempDir.FullName 'python.tar.gz'
Write-Host "Downloading standalone Python..."
Write-Host "URL: $DownloadUrl"
Invoke-WebRequest -Uri $DownloadUrl -OutFile $ArchivePath -UseBasicParsing
Write-Host "Extracting..."
# tar is built-in on Windows 10+ (bsdtar) and handles .tar.gz natively.
& tar -xzf $ArchivePath -C $TempDir.FullName
if ($LASTEXITCODE -ne 0) { throw "tar extract failed" }
$Extracted = Join-Path $TempDir.FullName 'python'
if (-not (Test-Path $Extracted)) {
Get-ChildItem $TempDir.FullName | Format-Table | Out-String | Write-Host
throw "Expected extracted directory at $Extracted"
}
Move-Item -Path $Extracted -Destination $PythonEnvDir
Write-Host "Python installed to $PythonEnvDir"
} finally {
if (Test-Path $TempDir.FullName) {
Remove-Item -Recurse -Force $TempDir.FullName
}
}
$PythonBin = Join-Path $PythonEnvDir 'python.exe'
if (-not (Test-Path $PythonBin)) { throw "python.exe not found at $PythonBin" }
Write-Host "Python binary: $PythonBin"
& $PythonBin --version
# Ensure pip is present
& $PythonBin -m pip --version 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "Installing pip..."
& $PythonBin -m ensurepip --upgrade
if ($LASTEXITCODE -ne 0) { throw "ensurepip failed" }
}
Write-Host "Installing backend dependencies..."
& $PythonBin -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed" }
& $PythonBin -m pip install -r (Join-Path $ProjectRoot 'backend\requirements.txt')
if ($LASTEXITCODE -ne 0) { throw "pip install requirements failed" }
Write-Host "Installing debugger module..."
& $PythonBin -m pip install (Join-Path $ProjectRoot 'debugger')
if ($LASTEXITCODE -ne 0) { throw "pip install debugger failed" }
Write-Host "Verifying claude-agent-sdk..."
& $PythonBin -c "import claude_agent_sdk; print('claude-agent-sdk installed')"
if ($LASTEXITCODE -ne 0) { throw "claude-agent-sdk verification failed" }
# Cleanup
Write-Host "Cleaning up..."
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Directory `
| Where-Object { $_.Name -in @('__pycache__','tests','test') } `
| Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Get-ChildItem -Path $PythonEnvDir -Recurse -Force -Filter '*.pyc' `
| Remove-Item -Force -ErrorAction SilentlyContinue
$Size = (Get-ChildItem -Path $PythonEnvDir -Recurse -File `
| Measure-Object -Property Length -Sum).Sum
$SizeMB = [math]::Round($Size / 1MB, 1)
Write-Host ""
Write-Host "=== Python Environment Ready ==="
Write-Host "Location: $PythonEnvDir"
Write-Host ("Size: {0} MB" -f $SizeMB)
Write-Host ""