|
| 1 | +#Requires -RunAsAdministrator |
| 2 | +<# |
| 3 | +.SYNOPSIS |
| 4 | + Installs exo worker node on a second Windows host. |
| 5 | +
|
| 6 | +.DESCRIPTION |
| 7 | + - Clones the ubisoft/exo repo (peter/wip branch) |
| 8 | + - Installs Python dependencies via uv |
| 9 | + - Builds the dashboard |
| 10 | + - Downloads llama.cpp b7836 CUDA binaries |
| 11 | + - Opens firewall ports 52415 (API) and 4001 (libp2p) |
| 12 | + - Creates a startup script |
| 13 | +
|
| 14 | +.PARAMETER InstallDir |
| 15 | + Directory to install exo into. Default: C:\exo |
| 16 | +
|
| 17 | +.PARAMETER NoApi |
| 18 | + Start as worker-only node (no dashboard/API). |
| 19 | +
|
| 20 | +.PARAMETER SkipDashboard |
| 21 | + Skip the npm dashboard build step. |
| 22 | +
|
| 23 | +.PARAMETER CudaVersion |
| 24 | + CUDA version suffix for llama.cpp download (e.g. cu12.4, cu12.2, cu11.7). |
| 25 | + Default: cu12.4 |
| 26 | +
|
| 27 | +.EXAMPLE |
| 28 | + .\install-worker.ps1 |
| 29 | + .\install-worker.ps1 -InstallDir D:\exo -NoApi -CudaVersion cu12.2 |
| 30 | +#> |
| 31 | +param( |
| 32 | + [string]$InstallDir = "C:\exo", |
| 33 | + [switch]$NoApi, |
| 34 | + [switch]$SkipDashboard, |
| 35 | + [string]$CudaVersion = "cu12.4" |
| 36 | +) |
| 37 | + |
| 38 | +Set-StrictMode -Version Latest |
| 39 | +$ErrorActionPreference = "Stop" |
| 40 | + |
| 41 | +$LlamaBuild = "b7836" |
| 42 | +$LlamaZip = "llama-$LlamaBuild-bin-win-cuda-$CudaVersion-x64.zip" |
| 43 | +$LlamaUrl = "https://github.com/ggerganov/llama.cpp/releases/download/$LlamaBuild/$LlamaZip" |
| 44 | +$RepoUrl = "git@github.com:ubisoft/exo.git" |
| 45 | +$Branch = "supportWindows" |
| 46 | +$LibP2PPort = 4001 |
| 47 | +$ApiPort = 52415 |
| 48 | + |
| 49 | +# ── helpers ────────────────────────────────────────────────────────────────── |
| 50 | + |
| 51 | +function Write-Step([string]$msg) { |
| 52 | + Write-Host "`n==> $msg" -ForegroundColor Cyan |
| 53 | +} |
| 54 | + |
| 55 | +function Assert-Command([string]$cmd, [string]$install) { |
| 56 | + if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) { |
| 57 | + Write-Host "[ERROR] '$cmd' not found. $install" -ForegroundColor Red |
| 58 | + exit 1 |
| 59 | + } |
| 60 | +} |
| 61 | + |
| 62 | +# ── prerequisite checks ─────────────────────────────────────────────────────── |
| 63 | + |
| 64 | +Write-Step "Checking prerequisites" |
| 65 | + |
| 66 | +Assert-Command "git" "Install Git from https://git-scm.com" |
| 67 | +Assert-Command "uv" "Install uv: irm https://astral.sh/uv/install.ps1 | iex" |
| 68 | +Assert-Command "python" "Install Python 3.10 from https://python.org" |
| 69 | + |
| 70 | +$pyVer = python --version 2>&1 |
| 71 | +Write-Host " Python : $pyVer" |
| 72 | +Write-Host " uv : $(uv --version)" |
| 73 | +Write-Host " git : $(git --version)" |
| 74 | + |
| 75 | +if (-not (Get-Command "npm" -ErrorAction SilentlyContinue) -and -not $SkipDashboard) { |
| 76 | + Write-Host "[WARN] npm not found — dashboard build will be skipped." -ForegroundColor Yellow |
| 77 | + $SkipDashboard = $true |
| 78 | +} |
| 79 | + |
| 80 | +# ── clone ───────────────────────────────────────────────────────────────────── |
| 81 | + |
| 82 | +Write-Step "Cloning repository into $InstallDir" |
| 83 | + |
| 84 | +if (Test-Path $InstallDir) { |
| 85 | + Write-Host " Directory exists — pulling latest changes" |
| 86 | + git -C $InstallDir fetch ubisoft |
| 87 | + git -C $InstallDir checkout $Branch |
| 88 | + git -C $InstallDir pull ubisoft $Branch |
| 89 | +} else { |
| 90 | + git clone $RepoUrl $InstallDir |
| 91 | + git -C $InstallDir checkout $Branch |
| 92 | +} |
| 93 | + |
| 94 | +Set-Location $InstallDir |
| 95 | + |
| 96 | +# ── python deps ─────────────────────────────────────────────────────────────── |
| 97 | + |
| 98 | +Write-Step "Installing Python dependencies" |
| 99 | +$env:UV_SKIP_WHEEL_FILENAME_CHECK = "1" |
| 100 | +uv sync |
| 101 | + |
| 102 | +# ── dashboard ───────────────────────────────────────────────────────────────── |
| 103 | + |
| 104 | +if (-not $SkipDashboard) { |
| 105 | + Write-Step "Building dashboard" |
| 106 | + Push-Location "$InstallDir\dashboard" |
| 107 | + npm install |
| 108 | + npm run build |
| 109 | + Pop-Location |
| 110 | +} |
| 111 | + |
| 112 | +# ── llama.cpp binaries ──────────────────────────────────────────────────────── |
| 113 | + |
| 114 | +Write-Step "Downloading llama.cpp $LlamaBuild ($CudaVersion)" |
| 115 | + |
| 116 | +$tmpZip = "$env:TEMP\$LlamaZip" |
| 117 | +$tmpDir = "$env:TEMP\llama-extract" |
| 118 | + |
| 119 | +if (-not (Test-Path $tmpZip)) { |
| 120 | + Write-Host " Downloading $LlamaUrl" |
| 121 | + Invoke-WebRequest -Uri $LlamaUrl -OutFile $tmpZip -UseBasicParsing |
| 122 | +} else { |
| 123 | + Write-Host " Archive already cached at $tmpZip" |
| 124 | +} |
| 125 | + |
| 126 | +Write-Host " Extracting..." |
| 127 | +if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force } |
| 128 | +Expand-Archive -Path $tmpZip -DestinationPath $tmpDir |
| 129 | + |
| 130 | +# Copy only the needed files |
| 131 | +$needed = @( |
| 132 | + "llama-server.exe", |
| 133 | + "ggml.dll", "ggml-base.dll", "ggml-cuda.dll", "ggml-rpc.dll", |
| 134 | + "llama.dll", "mtmd.dll", "libomp140.x86_64.dll" |
| 135 | +) |
| 136 | +$cpuDlls = Get-ChildItem $tmpDir -Filter "ggml-cpu-*.dll" -Recurse |
| 137 | + |
| 138 | +foreach ($f in $needed) { |
| 139 | + $src = Get-ChildItem $tmpDir -Filter $f -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 |
| 140 | + if ($src) { |
| 141 | + Copy-Item $src.FullName "$InstallDir\" -Force |
| 142 | + Write-Host " Copied $f" |
| 143 | + } else { |
| 144 | + Write-Host " [WARN] $f not found in archive" -ForegroundColor Yellow |
| 145 | + } |
| 146 | +} |
| 147 | +foreach ($dll in $cpuDlls) { |
| 148 | + Copy-Item $dll.FullName "$InstallDir\" -Force |
| 149 | + Write-Host " Copied $($dll.Name)" |
| 150 | +} |
| 151 | + |
| 152 | +# ── firewall rules ──────────────────────────────────────────────────────────── |
| 153 | + |
| 154 | +Write-Step "Configuring firewall" |
| 155 | + |
| 156 | +$rules = @( |
| 157 | + @{ Name = "exo API"; Port = $ApiPort; Proto = "TCP" }, |
| 158 | + @{ Name = "exo libp2p"; Port = $LibP2PPort; Proto = "TCP" } |
| 159 | +) |
| 160 | +foreach ($r in $rules) { |
| 161 | + $existing = Get-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue |
| 162 | + if ($existing) { |
| 163 | + Write-Host " Rule '$($r.Name)' already exists — skipping" |
| 164 | + } else { |
| 165 | + New-NetFirewallRule -DisplayName $r.Name ` |
| 166 | + -Direction Inbound -Action Allow ` |
| 167 | + -Protocol $r.Proto -LocalPort $r.Port ` |
| 168 | + -Profile Private | Out-Null |
| 169 | + Write-Host " Created rule: $($r.Name) ($($r.Proto)/$($r.Port))" |
| 170 | + } |
| 171 | +} |
| 172 | + |
| 173 | +# ── startup script ──────────────────────────────────────────────────────────── |
| 174 | + |
| 175 | +Write-Step "Writing startup script" |
| 176 | + |
| 177 | +$startScript = @" |
| 178 | +# exo worker startup script — generated by install-worker.ps1 |
| 179 | +Set-Location "$InstallDir" |
| 180 | +
|
| 181 | +`$env:UV_SKIP_WHEEL_FILENAME_CHECK = "1" |
| 182 | +`$env:EXO_LLAMA_SERVER_PATH = "$InstallDir\llama-server.exe" |
| 183 | +`$env:EXO_LIBP2P_LISTEN_PORT = "$LibP2PPort" |
| 184 | +`$env:EXO_LIBP2P_LISTEN_ADDR = "0.0.0.0" |
| 185 | +
|
| 186 | +$(if ($NoApi) { 'uv run exo --no-api' } else { 'uv run exo' }) |
| 187 | +"@ |
| 188 | + |
| 189 | +$startPath = "$InstallDir\start-exo.ps1" |
| 190 | +Set-Content -Path $startPath -Value $startScript -Encoding UTF8 |
| 191 | +Write-Host " Written to $startPath" |
| 192 | + |
| 193 | +# ── done ───────────────────────────────────────────────────────────────────── |
| 194 | + |
| 195 | +Write-Host @" |
| 196 | +
|
| 197 | +========================================================== |
| 198 | + Installation complete! |
| 199 | +
|
| 200 | + Start exo: |
| 201 | + powershell -ExecutionPolicy Bypass -File "$startPath" |
| 202 | +
|
| 203 | + Cluster will form automatically via mDNS once both |
| 204 | + nodes are running on the same LAN. |
| 205 | +========================================================== |
| 206 | +"@ -ForegroundColor Green |
0 commit comments