249 lines
9.1 KiB
PowerShell
249 lines
9.1 KiB
PowerShell
# Build QwenPaw backend with PyInstaller for Tauri sidecar (Windows)
|
|
# Creates an onedir backend bundle with embedded Python runtime
|
|
#
|
|
# Usage:
|
|
# powershell ./scripts/pack-tauri/build_pyinstaller.ps1
|
|
#
|
|
# Prerequisites:
|
|
# - Python 3.10+ with virtual environment
|
|
# - PyInstaller 6.0+ (will be installed if not present)
|
|
|
|
param()
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$REPO_ROOT = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
|
Set-Location $REPO_ROOT
|
|
|
|
$DIST = if ($env:DIST) { $env:DIST } else { "dist" }
|
|
if (-not [System.IO.Path]::IsPathRooted($DIST)) {
|
|
$DIST = Join-Path $REPO_ROOT $DIST
|
|
}
|
|
$VERSION_FILE = "src\pineagents\__version__.py"
|
|
|
|
# Extract version
|
|
if (Test-Path $VERSION_FILE) {
|
|
$content = Get-Content $VERSION_FILE -Raw
|
|
if ($content -match '__version__\s*=\s*"([^"]+)"') {
|
|
$VERSION = $Matches[1]
|
|
} else {
|
|
throw "Failed to extract version from $VERSION_FILE"
|
|
}
|
|
} else {
|
|
throw "Version file not found: $VERSION_FILE"
|
|
}
|
|
|
|
Write-Host "=========================================" -ForegroundColor Cyan
|
|
Write-Host "QwenPaw PyInstaller Build - Windows" -ForegroundColor Cyan
|
|
Write-Host "=========================================" -ForegroundColor Cyan
|
|
Write-Host "Version: $VERSION"
|
|
Write-Host "Repository: $REPO_ROOT"
|
|
Write-Host ""
|
|
|
|
# Check prerequisites
|
|
Write-Host "== Checking prerequisites ==" -ForegroundColor Yellow
|
|
|
|
$UV_BIN = (Get-Command uv -ErrorAction SilentlyContinue).Source
|
|
$PYTHON_BIN = Join-Path $REPO_ROOT ".venv\Scripts\python.exe"
|
|
if (-not (Test-Path $PYTHON_BIN)) {
|
|
if ($UV_BIN) {
|
|
Write-Host ".venv not found, creating virtual environment with uv" -ForegroundColor Yellow
|
|
& $UV_BIN venv "$REPO_ROOT\.venv"
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Failed to create virtual environment with uv"
|
|
}
|
|
} else {
|
|
Write-Host ".venv not found, using system Python" -ForegroundColor Yellow
|
|
$PYTHON_BIN = (Get-Command python -ErrorAction SilentlyContinue).Source
|
|
}
|
|
if (-not $PYTHON_BIN -or -not (Test-Path $PYTHON_BIN)) {
|
|
Write-Host "ERROR: Python not found in .venv or PATH" -ForegroundColor Red
|
|
Write-Host "Please create virtual environment first: python -m venv .venv"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
$pythonVersion = & $PYTHON_BIN --version
|
|
Write-Host "Python: $pythonVersion" -ForegroundColor Green
|
|
|
|
function Test-PythonImport {
|
|
param([string]$Statement)
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
try {
|
|
$ErrorActionPreference = "Continue"
|
|
& $PYTHON_BIN -c $Statement *> $null
|
|
return $LASTEXITCODE -eq 0
|
|
} finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
}
|
|
|
|
function Assert-LastExit {
|
|
param([string]$Message)
|
|
if ($LASTEXITCODE -ne 0) { throw $Message }
|
|
}
|
|
|
|
function Install-PythonPackages {
|
|
param([string[]]$Packages)
|
|
if ($UV_BIN) {
|
|
& $UV_BIN pip install --python $PYTHON_BIN @Packages
|
|
} else {
|
|
& $PYTHON_BIN -m pip install @Packages
|
|
}
|
|
Assert-LastExit "Failed to install Python packages: $($Packages -join ', ')"
|
|
}
|
|
|
|
function Uninstall-PythonPackage {
|
|
param([string]$Package)
|
|
$previousErrorActionPreference = $ErrorActionPreference
|
|
try {
|
|
$ErrorActionPreference = "Continue"
|
|
if ($UV_BIN) {
|
|
& $UV_BIN pip uninstall --python $PYTHON_BIN -y $Package *> $null
|
|
} else {
|
|
& $PYTHON_BIN -m pip uninstall -y $Package *> $null
|
|
}
|
|
} finally {
|
|
$ErrorActionPreference = $previousErrorActionPreference
|
|
}
|
|
}
|
|
|
|
# Install PyInstaller if not present
|
|
Write-Host "== Installing PyInstaller ==" -ForegroundColor Yellow
|
|
if (Test-PythonImport "import PyInstaller") {
|
|
Write-Host "PyInstaller already installed" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "Installing PyInstaller..."
|
|
Install-PythonPackages -Packages @("pyinstaller>=6.0.0")
|
|
Write-Host "PyInstaller installed" -ForegroundColor Green
|
|
}
|
|
|
|
# Install python-dotenv if not present (required by PyInstaller collect_submodules)
|
|
if (Test-PythonImport "import dotenv") {
|
|
Write-Host "python-dotenv already installed" -ForegroundColor Green
|
|
} else {
|
|
Write-Host "Installing python-dotenv..."
|
|
Install-PythonPackages -Packages @("python-dotenv")
|
|
Write-Host "python-dotenv installed" -ForegroundColor Green
|
|
}
|
|
|
|
Write-Host ""
|
|
|
|
# Install project dependencies (ensures ALL runtime deps are importable)
|
|
Write-Host "== Installing project dependencies ==" -ForegroundColor Yellow
|
|
Install-PythonPackages -Packages @("-e", ".[full]")
|
|
Write-Host "Project dependencies installed with full extras" -ForegroundColor Green
|
|
|
|
# Fix agent-client-protocol namespace collision
|
|
# PyPI has an empty 'acp' stub that shadows the real package
|
|
if (-not (Test-PythonImport "from acp import Agent")) {
|
|
Write-Host "Fixing agent-client-protocol namespace..."
|
|
Uninstall-PythonPackage "acp"
|
|
Install-PythonPackages -Packages @("agent-client-protocol>=0.9.0,<0.11.0")
|
|
Write-Host "agent-client-protocol installed" -ForegroundColor Green
|
|
}
|
|
|
|
# Run PyInstaller
|
|
Write-Host "== Running PyInstaller ==" -ForegroundColor Yellow
|
|
Write-Host "Building onedir backend bundle..."
|
|
|
|
$SPEC_FILE = Join-Path $REPO_ROOT "scripts\pack-tauri\pineagents.spec"
|
|
if (-not (Test-Path $SPEC_FILE)) {
|
|
Write-Host "ERROR: Spec file not found at $SPEC_FILE" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
& $PYTHON_BIN -m PyInstaller $SPEC_FILE `
|
|
--distpath "${DIST}\pyinstaller" `
|
|
--workpath "${DIST}\pyinstaller-build" `
|
|
--clean `
|
|
--noconfirm
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "PyInstaller build failed"
|
|
}
|
|
|
|
Write-Host "PyInstaller build complete" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
# Verify output
|
|
$BACKEND_DIR = Join-Path $DIST "pyinstaller\qwenpaw-backend"
|
|
$BACKEND_EXE = Join-Path $BACKEND_DIR "qwenpaw-backend.exe"
|
|
$CLI_EXE = Join-Path $BACKEND_DIR "qwenpaw.exe"
|
|
if (-not (Test-Path $BACKEND_DIR)) {
|
|
Write-Host "ERROR: Backend bundle directory not found at $BACKEND_DIR" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
if (-not (Test-Path $BACKEND_EXE)) {
|
|
Write-Host "ERROR: Backend executable not found at $BACKEND_EXE" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
if (-not (Test-Path $CLI_EXE)) {
|
|
Write-Host "ERROR: CLI executable not found at $CLI_EXE" -ForegroundColor Red
|
|
exit 1
|
|
}
|
|
|
|
Write-Host "Backend bundle created: $BACKEND_DIR" -ForegroundColor Green
|
|
|
|
# Get size
|
|
$bundleSize = (Get-ChildItem $BACKEND_DIR -Recurse -File | Measure-Object -Property Length -Sum).Sum / 1MB
|
|
Write-Host "Bundle size: $([math]::Round($bundleSize, 2)) MB"
|
|
Write-Host ""
|
|
|
|
# Copy to Tauri resources directory
|
|
Write-Host "== Copying to Tauri binaries directory ==" -ForegroundColor Yellow
|
|
$BINARIES_DIR = Join-Path $REPO_ROOT "console\src-tauri\binaries"
|
|
New-Item -ItemType Directory -Force -Path $BINARIES_DIR | Out-Null
|
|
|
|
$DEST = Join-Path $BINARIES_DIR "qwenpaw-backend"
|
|
New-Item -ItemType Directory -Force -Path $DEST | Out-Null
|
|
Get-ChildItem -LiteralPath $DEST -Force | Remove-Item -Recurse -Force
|
|
Copy-Item -Recurse -Force (Join-Path $BACKEND_DIR "*") $DEST
|
|
Write-Host "Copied to: $DEST" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
# Stage a standalone CPython (same X.Y/arch as this build's interpreter) so the
|
|
# frozen backend can install third-party plugin dependencies at runtime.
|
|
Write-Host "== Staging bundled Python runtime ==" -ForegroundColor Yellow
|
|
& $PYTHON_BIN (Join-Path $REPO_ROOT "scripts\pack-tauri\stage_python_runtime.py") `
|
|
--dest (Join-Path $BINARIES_DIR "python-runtime")
|
|
Assert-LastExit "Failed to stage bundled Python runtime"
|
|
Write-Host ""
|
|
|
|
Write-Host "== Staging bundled Node runtime ==" -ForegroundColor Yellow
|
|
& $PYTHON_BIN (Join-Path $REPO_ROOT "scripts\pack-tauri\stage_node_runtime.py") `
|
|
--dest (Join-Path $BINARIES_DIR "node-runtime")
|
|
Assert-LastExit "Failed to stage bundled Node runtime"
|
|
Write-Host "== Building Computer Use helper ==" -ForegroundColor Yellow
|
|
$CARGO_BIN = (Get-Command cargo -ErrorAction SilentlyContinue).Source
|
|
if (-not $CARGO_BIN) {
|
|
throw "cargo not found; Rust toolchain is required to build qwenpaw-computer-use-helper"
|
|
}
|
|
$TAURI_DIR = Join-Path $REPO_ROOT "console\src-tauri"
|
|
Push-Location $TAURI_DIR
|
|
try {
|
|
& $CARGO_BIN build --release --bin qwenpaw-computer-use-helper
|
|
Assert-LastExit "Failed to build qwenpaw-computer-use-helper"
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
$TARGET_DIR = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $TAURI_DIR "target" }
|
|
if (-not [System.IO.Path]::IsPathRooted($TARGET_DIR)) {
|
|
$TARGET_DIR = Join-Path $TAURI_DIR $TARGET_DIR
|
|
}
|
|
$COMPUTER_USE_HELPER_EXE = Join-Path $TARGET_DIR "release\qwenpaw-computer-use-helper.exe"
|
|
if (-not (Test-Path $COMPUTER_USE_HELPER_EXE)) {
|
|
throw "Computer Use helper executable not found at $COMPUTER_USE_HELPER_EXE"
|
|
}
|
|
$COMPUTER_USE_HELPER_DEST = Join-Path $DEST "qwenpaw-computer-use-helper.exe"
|
|
Copy-Item -Force $COMPUTER_USE_HELPER_EXE $COMPUTER_USE_HELPER_DEST
|
|
Write-Host "Computer Use helper staged: $COMPUTER_USE_HELPER_DEST" -ForegroundColor Green
|
|
Write-Host ""
|
|
|
|
Write-Host "=========================================" -ForegroundColor Cyan
|
|
Write-Host "PyInstaller Build Complete!" -ForegroundColor Green
|
|
Write-Host "=========================================" -ForegroundColor Cyan
|
|
Write-Host "Output:"
|
|
Write-Host " Bundle: $BACKEND_DIR"
|
|
Write-Host " Tauri resource: $DEST"
|
|
Write-Host ""
|