| @@ -1,622 +0,0 @@ |
| 1 | | −#!/usr/bin/env bun |
| 2 | | − |
| 3 | | −import fs from "node:fs/promises"; |
| 4 | | −import os from "node:os"; |
| 5 | | −import path from "node:path"; |
| 6 | | −import { createHash, generateKeyPairSync } from "node:crypto"; |
| 7 | | −import net from "node:net"; |
| 8 | | − |
| 9 | | −import { managedRuntimeVersions } from "../../packages/core/src/managed/runtime-info"; |
| 10 | | − |
| 11 | | −type ReleaseTarget = { |
| 12 | | − platform: "linux" | "darwin"; |
| 13 | | − arch: "x64" | "arm64"; |
| 14 | | − bunTarget: string; |
| 15 | | −}; |
| 16 | | − |
| 17 | | −const targets: ReleaseTarget[] = [ |
| 18 | | − { platform: "linux", arch: "x64", bunTarget: "bun-linux-x64" }, |
| 19 | | − { platform: "linux", arch: "arm64", bunTarget: "bun-linux-arm64" }, |
| 20 | | − { platform: "darwin", arch: "x64", bunTarget: "bun-darwin-x64" }, |
| 21 | | − { platform: "darwin", arch: "arm64", bunTarget: "bun-darwin-arm64" }, |
| 22 | | −]; |
| 23 | | − |
| 24 | | −const convexBackendRepo = "get-convex/convex-backend"; |
| 25 | | −const defaultManagedSitePort = 5411; |
| 26 | | − |
| 27 | | −function hostPlatformArch(): { platform: "linux" | "darwin"; arch: "x64" | "arm64" } { |
| 28 | | − if (process.platform !== "linux" && process.platform !== "darwin") { |
| 29 | | − throw new Error(`Unsupported host platform for release build: ${process.platform}`); |
| 30 | | − } |
| 31 | | − if (process.arch !== "x64" && process.arch !== "arm64") { |
| 32 | | − throw new Error(`Unsupported host architecture for release build: ${process.arch}`); |
| 33 | | − } |
| 34 | | − return { platform: process.platform, arch: process.arch }; |
| 35 | | −} |
| 36 | | − |
| 37 | | −function archiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string { |
| 38 | | − return `executor-${platform}-${arch}.tar.gz`; |
| 39 | | −} |
| 40 | | − |
| 41 | | −function webArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string { |
| 42 | | − return `executor-web-${platform}-${arch}.tar.gz`; |
| 43 | | −} |
| 44 | | − |
| 45 | | −function runtimeArchiveName(platform: ReleaseTarget["platform"], arch: ReleaseTarget["arch"]): string { |
| 46 | | − return `executor-runtime-${platform}-${arch}.tar.gz`; |
| 47 | | −} |
| 48 | | − |
| 49 | | −function backendAssetName(target: ReleaseTarget): string { |
| 50 | | − if (target.platform === "linux" && target.arch === "x64") { |
| 51 | | − return "convex-local-backend-x86_64-unknown-linux-gnu.zip"; |
| 52 | | − } |
| 53 | | − if (target.platform === "linux" && target.arch === "arm64") { |
| 54 | | − return "convex-local-backend-aarch64-unknown-linux-gnu.zip"; |
| 55 | | − } |
| 56 | | − if (target.platform === "darwin" && target.arch === "x64") { |
| 57 | | − return "convex-local-backend-x86_64-apple-darwin.zip"; |
| 58 | | − } |
| 59 | | − return "convex-local-backend-aarch64-apple-darwin.zip"; |
| 60 | | −} |
| 61 | | − |
| 62 | | −function nodeArchiveName(target: ReleaseTarget): string { |
| 63 | | − return `node-v${managedRuntimeVersions.nodeVersion}-${target.platform}-${target.arch}.tar.gz`; |
| 64 | | −} |
| 65 | | − |
| 66 | | −function backendBinaryName(target: ReleaseTarget): string { |
| 67 | | − return target.platform === "linux" || target.platform === "darwin" |
| 68 | | − ? "convex-local-backend" |
| 69 | | − : "convex-local-backend.exe"; |
| 70 | | −} |
| 71 | | − |
| 72 | | −async function sha256(filePath: string): Promise<string> { |
| 73 | | − const bytes = await Bun.file(filePath).arrayBuffer(); |
| 74 | | − return createHash("sha256").update(Buffer.from(bytes)).digest("hex"); |
| 75 | | −} |
| 76 | | − |
| 77 | | −async function runCommand(command: string[], options: { cwd?: string; env?: Record<string, string | undefined> } = {}): Promise<void> { |
| 78 | | − const proc = Bun.spawn(command, { |
| 79 | | − cwd: options.cwd, |
| 80 | | − env: options.env, |
| 81 | | − stdin: "inherit", |
| 82 | | − stdout: "inherit", |
| 83 | | − stderr: "inherit", |
| 84 | | − }); |
| 85 | | − const code = await proc.exited; |
| 86 | | − if (code !== 0) { |
| 87 | | − throw new Error(`Command failed (${code}): ${command.join(" ")}`); |
| 88 | | − } |
| 89 | | −} |
| 90 | | − |
| 91 | | −async function runArchiveCommand(command: string[], cwd?: string): Promise<void> { |
| 92 | | − await runCommand(command, { cwd }); |
| 93 | | −} |
| 94 | | − |
| 95 | | −async function pathExists(filePath: string): Promise<boolean> { |
| 96 | | − try { |
| 97 | | − await fs.access(filePath); |
| 98 | | − return true; |
| 99 | | − } catch { |
| 100 | | − return false; |
| 101 | | − } |
| 102 | | −} |
| 103 | | − |
| 104 | | −async function downloadFile(url: string, destination: string): Promise<void> { |
| 105 | | − await runCommand(["curl", "-fsSL", url, "-o", destination]); |
| 106 | | −} |
| 107 | | − |
| 108 | | −async function extractZipArchive(archivePath: string, destinationDir: string): Promise<void> { |
| 109 | | − try { |
| 110 | | − await runCommand(["unzip", "-o", archivePath, "-d", destinationDir]); |
| 111 | | − return; |
| 112 | | − } catch { |
| 113 | | − const pythonScript = [ |
| 114 | | − "import sys, zipfile", |
| 115 | | − "zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])", |
| 116 | | − ].join(";"); |
| 117 | | − await runCommand(["python3", "-c", pythonScript, archivePath, destinationDir]); |
| 118 | | − } |
| 119 | | −} |
| 120 | | − |
| 121 | | −async function waitForHttp(url: string, timeoutMs: number): Promise<void> { |
| 122 | | − const startedAt = Date.now(); |
| 123 | | − while (Date.now() - startedAt < timeoutMs) { |
| 124 | | − try { |
| 125 | | − const response = await fetch(url); |
| 126 | | − if (response.ok) { |
| 127 | | − return; |
| 128 | | − } |
| 129 | | − } catch { |
| 130 | | − // still starting |
| 131 | | − } |
| 132 | | − await Bun.sleep(500); |
| 133 | | − } |
| 134 | | − throw new Error(`Timed out waiting for ${url}`); |
| 135 | | −} |
| 136 | | − |
| 137 | | −async function canBindPort(port: number): Promise<boolean> { |
| 138 | | − return await new Promise((resolve) => { |
| 139 | | − const server = net.createServer(); |
| 140 | | − server.once("error", () => { |
| 141 | | − resolve(false); |
| 142 | | − }); |
| 143 | | − server.listen(port, "127.0.0.1", () => { |
| 144 | | − server.close(() => resolve(true)); |
| 145 | | − }); |
| 146 | | − }); |
| 147 | | −} |
| 148 | | − |
| 149 | | −async function randomPortBase(): Promise<number> { |
| 150 | | − for (let attempt = 0; attempt < 100; attempt += 1) { |
| 151 | | − const base = 20000 + Math.floor(Math.random() * 20000); |
| 152 | | − if (await canBindPort(base) && await canBindPort(base + 1) && await canBindPort(base + 2)) { |
| 153 | | − return base; |
| 154 | | − } |
| 155 | | − } |
| 156 | | − |
| 157 | | − throw new Error("Unable to allocate free ports for runtime seeding"); |
| 158 | | −} |
| 159 | | − |
| 160 | | −type AnonymousAuthSeed = { |
| 161 | | − privateKeyPem: string; |
| 162 | | − publicKeyPem: string; |
| 163 | | − apiKeySecret: string; |
| 164 | | −}; |
| 165 | | − |
| 166 | | −type RuntimeConfig = { |
| 167 | | − instanceName: string; |
| 168 | | − instanceSecret: string; |
| 169 | | − hostInterface: string; |
| 170 | | − backendPort: number; |
| 171 | | − siteProxyPort: number; |
| 172 | | −}; |
| 173 | | − |
| 174 | | −type TanstackStartOutput = { |
| 175 | | − outputRoot: string; |
| 176 | | − serverEntry: string; |
| 177 | | − stagedServerEntry: string; |
| 178 | | −}; |
| 179 | | − |
| 180 | | −function normalizePemForEnv(value: string): string { |
| 181 | | − return value.replace(/\r\n/g, "\n").replace(/\n/g, "\\n").trim(); |
| 182 | | −} |
| 183 | | − |
| 184 | | −function createAnonymousAuthSeed(): AnonymousAuthSeed { |
| 185 | | − const keyPair = generateKeyPairSync("ec", { |
| 186 | | − namedCurve: "P-256", |
| 187 | | − privateKeyEncoding: { |
| 188 | | − type: "pkcs8", |
| 189 | | − format: "pem", |
| 190 | | − }, |
| 191 | | − publicKeyEncoding: { |
| 192 | | − type: "spki", |
| 193 | | − format: "pem", |
| 194 | | − }, |
| 195 | | − }); |
| 196 | | − |
| 197 | | − const privateKeyPem = normalizePemForEnv(keyPair.privateKey); |
| 198 | | − return { |
| 199 | | − privateKeyPem, |
| 200 | | − publicKeyPem: normalizePemForEnv(keyPair.publicKey), |
| 201 | | − apiKeySecret: privateKeyPem, |
| 202 | | − }; |
| 203 | | −} |
| 204 | | − |
| 205 | | −async function resolveTanstackStartOutput(webAppDir: string): Promise<TanstackStartOutput> { |
| 206 | | − const candidates: Array<{ outputRoot: string; serverEntry: string; stagedServerEntry: string }> = [ |
| 207 | | − { |
| 208 | | − outputRoot: path.join(webAppDir, ".output"), |
| 209 | | − serverEntry: path.join(".output", "server", "index.mjs"), |
| 210 | | − stagedServerEntry: path.join(".output", "server", "index.mjs"), |
| 211 | | − }, |
| 212 | | − { |
| 213 | | − outputRoot: path.join(webAppDir, "dist"), |
| 214 | | − serverEntry: path.join("dist", "server", "server.mjs"), |
| 215 | | − stagedServerEntry: path.join("dist", "server", "server.mjs"), |
| 216 | | − }, |
| 217 | | − ]; |
| 218 | | − |
| 219 | | − for (const candidate of candidates) { |
| 220 | | − const absoluteServerEntry = path.join(webAppDir, candidate.serverEntry); |
| 221 | | − if (await pathExists(absoluteServerEntry)) { |
| 222 | | − return { |
| 223 | | − outputRoot: candidate.outputRoot, |
| 224 | | − serverEntry: candidate.serverEntry, |
| 225 | | − stagedServerEntry: candidate.stagedServerEntry, |
| 226 | | − }; |
| 227 | | − } |
| 228 | | − } |
| 229 | | − |
| 230 | | − const checkedPaths = candidates |
| 231 | | − .map((candidate) => path.join(webAppDir, candidate.serverEntry)) |
| 232 | | − .join(", "); |
| 233 | | − |
| 234 | | − throw new Error(`Could not find TanStack Start server output. Checked: ${checkedPaths}`); |
| 235 | | −} |
| 236 | | − |
| 237 | | −async function readRuntimeConfig(runtimeDir: string): Promise<RuntimeConfig> { |
| 238 | | − const configPath = path.join(runtimeDir, "convex-backend.json"); |
| 239 | | − const raw = await fs.readFile(configPath, "utf8"); |
| 240 | | − const parsed = JSON.parse(raw) as Partial<RuntimeConfig>; |
| 241 | | − |
| 242 | | − if ( |
| 243 | | − typeof parsed.instanceName !== "string" |
| 244 | | − || typeof parsed.instanceSecret !== "string" |
| 245 | | − || typeof parsed.hostInterface !== "string" |
| 246 | | − || typeof parsed.backendPort !== "number" |
| 247 | | − || typeof parsed.siteProxyPort !== "number" |
| 248 | | − ) { |
| 249 | | − throw new Error(`Invalid runtime config at ${configPath}`); |
| 250 | | − } |
| 251 | | − |
| 252 | | − return { |
| 253 | | − instanceName: parsed.instanceName, |
| 254 | | − instanceSecret: parsed.instanceSecret, |
| 255 | | − hostInterface: parsed.hostInterface, |
| 256 | | − backendPort: parsed.backendPort, |
| 257 | | − siteProxyPort: parsed.siteProxyPort, |
| 258 | | − }; |
| 259 | | −} |
| 260 | | − |
| 261 | | −async function generateSelfHostedAdminKey(config: RuntimeConfig): Promise<string> { |
| 262 | | − const response = await fetch("https://api.convex.dev/api/local_deployment/generate_admin_key", { |
| 263 | | − method: "POST", |
| 264 | | − headers: { |
| 265 | | − "Content-Type": "application/json", |
| 266 | | − "Convex-Client": managedRuntimeVersions.convexClientHeader, |
| 267 | | − }, |
| 268 | | − body: JSON.stringify({ |
| 269 | | − instanceName: config.instanceName, |
| 270 | | − instanceSecret: config.instanceSecret, |
| 271 | | − }), |
| 272 | | − }); |
| 273 | | − |
| 274 | | − if (!response.ok) { |
| 275 | | − const text = await response.text(); |
| 276 | | − throw new Error(`Failed generating self-hosted admin key: ${text || response.statusText}`); |
| 277 | | − } |
| 278 | | − |
| 279 | | − const parsed = await response.json() as { adminKey?: string }; |
| 280 | | − if (!parsed.adminKey || parsed.adminKey.trim().length === 0) { |
| 281 | | − throw new Error("Self-hosted admin key API did not return adminKey"); |
| 282 | | − } |
| 283 | | − |
| 284 | | − return parsed.adminKey; |
| 285 | | −} |
| 286 | | − |
| 287 | | −async function assertSeededClientConfig(config: RuntimeConfig, expectedAnonymousIssuer: string): Promise<void> { |
| 288 | | − const response = await fetch(`http://${config.hostInterface}:${config.backendPort}/api/query`, { |
| 289 | | − method: "POST", |
| 290 | | − headers: { |
| 291 | | − "content-type": "application/json", |
| 292 | | − }, |
| 293 | | − body: JSON.stringify({ |
| 294 | | − path: "app:getClientConfig", |
| 295 | | − args: {}, |
| 296 | | − }), |
| 297 | | − }); |
| 298 | | − |
| 299 | | − if (!response.ok) { |
| 300 | | − const text = await response.text(); |
| 301 | | − throw new Error(`Failed querying app:getClientConfig during runtime seeding: ${text || response.statusText}`); |
| 302 | | − } |
| 303 | | − |
| 304 | | − const parsed = await response.json() as { |
| 305 | | − status?: string; |
| 306 | | − value?: { |
| 307 | | − anonymousAuthIssuer?: unknown; |
| 308 | | − }; |
| 309 | | − }; |
| 310 | | − |
| 311 | | − if (parsed.status !== "success") { |
| 312 | | − throw new Error(`Runtime seeding query returned unexpected status: ${JSON.stringify(parsed)}`); |
| 313 | | − } |
| 314 | | − |
| 315 | | − if (parsed.value?.anonymousAuthIssuer !== expectedAnonymousIssuer) { |
| 316 | | − throw new Error( |
| 317 | | − `Runtime seeding issuer mismatch. Expected ${expectedAnonymousIssuer}, got ${String(parsed.value?.anonymousAuthIssuer)}`, |
| 318 | | − ); |
| 319 | | − } |
| 320 | | −} |
| 321 | | − |
| 322 | | −async function seedManagedRuntimeState( |
| 323 | | − rootDir: string, |
| 324 | | − runtimeDir: string, |
| 325 | | − config: RuntimeConfig, |
| 326 | | − anonymousAuth: AnonymousAuthSeed, |
| 327 | | −): Promise<void> { |
| 328 | | − const expectedAnonymousIssuer = `http://${config.hostInterface}:${defaultManagedSitePort}`; |
| 329 | | − const adminKey = await generateSelfHostedAdminKey(config); |
| 330 | | − const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "executor-seed-env-")); |
| 331 | | − const envFilePath = path.join(tempDir, "selfhost.env"); |
| 332 | | − const envContents = [ |
| 333 | | − `CONVEX_SELF_HOSTED_URL=http://${config.hostInterface}:${config.backendPort}`, |
| 334 | | − `CONVEX_SELF_HOSTED_ADMIN_KEY=${adminKey}`, |
| 335 | | − "WORKOS_CLIENT_ID=disabled", |
| 336 | | − "DANGEROUSLY_ALLOW_LOCAL_VM=1", |
| 337 | | − ].join("\n"); |
| 338 | | − |
| 339 | | − await fs.writeFile(envFilePath, `${envContents}\n`, "utf8"); |
| 340 | | − |
| 341 | | − const envEntries: Array<{ name: string; value: string }> = [ |
| 342 | | − { name: "WORKOS_CLIENT_ID", value: "disabled" }, |
| 343 | | − { name: "DANGEROUSLY_ALLOW_LOCAL_VM", value: "1" }, |
| 344 | | − { name: "ANONYMOUS_AUTH_ISSUER", value: expectedAnonymousIssuer }, |
| 345 | | − { name: "ANONYMOUS_AUTH_PRIVATE_KEY_PEM", value: anonymousAuth.privateKeyPem }, |
| 346 | | − { name: "ANONYMOUS_AUTH_PUBLIC_KEY_PEM", value: anonymousAuth.publicKeyPem }, |
| 347 | | − { name: "MCP_API_KEY_SECRET", value: anonymousAuth.apiKeySecret }, |
| 348 | | − ]; |
| 349 | | − |
| 350 | | − try { |
| 351 | | − for (const entry of envEntries) { |
| 352 | | − await runCommand(["bunx", "convex", "env", "set", `${entry.name}=${entry.value}`, "--env-file", envFilePath], { |
| 353 | | − cwd: rootDir, |
| 354 | | − }); |
| 355 | | − } |
| 356 | | − |
| 357 | | − await runCommand(["bunx", "convex", "deploy", "--yes", "--typecheck", "disable", "--codegen", "disable", "--env-file", envFilePath], { |
| 358 | | − cwd: rootDir, |
| 359 | | − }); |
| 360 | | − |
| 361 | | − await assertSeededClientConfig(config, expectedAnonymousIssuer); |
| 362 | | − } finally { |
| 363 | | − await fs.rm(tempDir, { recursive: true, force: true }); |
| 364 | | − } |
| 365 | | − |
| 366 | | − await fs.writeFile( |
| 367 | | − path.join(runtimeDir, "managed-anonymous-auth.json"), |
| 368 | | − `${JSON.stringify({ |
| 369 | | − ANONYMOUS_AUTH_PRIVATE_KEY_PEM: anonymousAuth.privateKeyPem, |
| 370 | | − ANONYMOUS_AUTH_PUBLIC_KEY_PEM: anonymousAuth.publicKeyPem, |
| 371 | | − MCP_API_KEY_SECRET: anonymousAuth.apiKeySecret, |
| 372 | | − }, null, 2)}\n`, |
| 373 | | − { encoding: "utf8", mode: 0o600 }, |
| 374 | | − ); |
| 375 | | −} |
| 376 | | − |
| 377 | | −async function seedRuntimeState(rootDir: string): Promise<{ tempRoot: string; runtimeDir: string }> { |
| 378 | | − const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "executor-runtime-seed-")); |
| 379 | | − const runtimeDir = path.join(tempRoot, "runtime"); |
| 380 | | − const basePort = await randomPortBase(); |
| 381 | | − const anonymousAuth = createAnonymousAuthSeed(); |
| 382 | | − const host = hostPlatformArch(); |
| 383 | | − const hostTarget: ReleaseTarget = { |
| 384 | | − platform: host.platform, |
| 385 | | − arch: host.arch, |
| 386 | | − bunTarget: host.platform === "linux" |
| 387 | | − ? (host.arch === "x64" ? "bun-linux-x64" : "bun-linux-arm64") |
| 388 | | − : (host.arch === "x64" ? "bun-darwin-x64" : "bun-darwin-arm64"), |
| 389 | | − }; |
| 390 | | − |
| 391 | | − await fs.mkdir(runtimeDir, { recursive: true }); |
| 392 | | − const hostNodeArchive = path.join(tempRoot, nodeArchiveName(hostTarget)); |
| 393 | | − const hostNodeUrl = `https://nodejs.org/dist/v${managedRuntimeVersions.nodeVersion}/${nodeArchiveName(hostTarget)}`; |
| 394 | | − await downloadFile(hostNodeUrl, hostNodeArchive); |
| 395 | | − await runArchiveCommand(["tar", "-xzf", hostNodeArchive, "-C", runtimeDir]); |
| 396 | | − |
| 397 | | − const env = { |
| 398 | | − ...process.env, |
| 399 | | − EXECUTOR_SKIP_RUNTIME_IMAGE: "1", |
| 400 | | − EXECUTOR_RUNTIME_DIR: runtimeDir, |
| 401 | | − EXECUTOR_PROJECT_DIR: rootDir, |
| 402 | | − EXECUTOR_BACKEND_PORT: String(basePort), |
| 403 | | − EXECUTOR_BACKEND_SITE_PORT: String(basePort + 1), |
| 404 | | − EXECUTOR_WEB_PORT: String(basePort + 2), |
| 405 | | − ANONYMOUS_AUTH_PRIVATE_KEY_PEM: anonymousAuth.privateKeyPem, |
| 406 | | − ANONYMOUS_AUTH_PUBLIC_KEY_PEM: anonymousAuth.publicKeyPem, |
| 407 | | − MCP_API_KEY_SECRET: anonymousAuth.apiKeySecret, |
| 408 | | − }; |
| 409 | | − |
| 410 | | − const backendProc = Bun.spawn(["bun", "run", "executor.ts", "up", "--disable-beacon"], { |
| 411 | | − cwd: rootDir, |
| 412 | | − env, |
| 413 | | − stdin: "ignore", |
| 414 | | − stdout: "ignore", |
| 415 | | − stderr: "ignore", |
| 416 | | − }); |
| 417 | | − |
| 418 | | − try { |
| 419 | | − await waitForHttp(`http://127.0.0.1:${basePort}/version`, 120_000); |
| 420 | | − const config = await readRuntimeConfig(runtimeDir); |
| 421 | | − await seedManagedRuntimeState(rootDir, runtimeDir, config, anonymousAuth); |
| 422 | | − } catch (error) { |
| 423 | | − backendProc.kill(); |
| 424 | | − await backendProc.exited; |
| 425 | | − throw error; |
| 426 | | − } |
| 427 | | − |
| 428 | | − backendProc.kill(); |
| 429 | | − await backendProc.exited; |
| 430 | | − |
| 431 | | − const convexDataDir = path.join(runtimeDir, "convex-data"); |
| 432 | | − if (!(await pathExists(convexDataDir))) { |
| 433 | | − throw new Error("Runtime seeding failed: convex-data directory missing"); |
| 434 | | − } |
| 435 | | − |
| 436 | | − return { tempRoot, runtimeDir }; |
| 437 | | −} |
| 438 | | − |
| 439 | | −async function buildWebArtifact(rootDir: string, releaseDir: string, checksums: string[]): Promise<void> { |
| 440 | | − const host = hostPlatformArch(); |
| 441 | | − const webAppDir = path.join(rootDir, "apps", "web"); |
| 442 | | − const webArtifactName = webArchiveName(host.platform, host.arch); |
| 443 | | − const archivePath = path.join(releaseDir, webArtifactName); |
| 444 | | − const webBuildEnv = { |
| 445 | | − ...process.env, |
| 446 | | − EXECUTOR_WEB_CONVEX_URL: "http://127.0.0.1:5410", |
| 447 | | − EXECUTOR_WEB_CONVEX_SITE_URL: "http://127.0.0.1:5411", |
| 448 | | − VITE_CONVEX_URL: "http://127.0.0.1:5410", |
| 449 | | − VITE_CONVEX_SITE_URL: "http://127.0.0.1:5411", |
| 450 | | − NODE_ENV: "production", |
| 451 | | − VITE_USER_NODE_ENV: "production", |
| 452 | | − }; |
| 453 | | − |
| 454 | | − await runCommand(["bunx", "vite", "build", "--mode", "production"], { |
| 455 | | − cwd: webAppDir, |
| 456 | | − env: webBuildEnv, |
| 457 | | − }); |
| 458 | | − |
| 459 | | − const tanstackStart = await resolveTanstackStartOutput(webAppDir); |
| 460 | | − |
| 461 | | − const outputRoot = tanstackStart.outputRoot; |
| 462 | | − const outputServerEntry = path.join(webAppDir, tanstackStart.serverEntry); |
| 463 | | − const stageRoot = path.join(releaseDir, `executor-web-${host.platform}-${host.arch}`); |
| 464 | | − const stagedAppRoot = path.join(stageRoot, "executor", "apps", "web"); |
| 465 | | − const stagedServerEntry = tanstackStart.stagedServerEntry; |
| 466 | | − const stagedOutputRoot = path.basename(outputRoot); |
| 467 | | − const stagedServerEntryWithLeadingSlash = `/${stagedServerEntry.split(path.sep).join("/")}`; |
| 468 | | − |
| 469 | | − if (!(await pathExists(outputServerEntry))) { |
| 470 | | − throw new Error(`Missing TanStack Start output at ${outputServerEntry}. Ensure vite build completed.`); |
| 471 | | − } |
| 472 | | − |
| 473 | | − await fs.rm(stageRoot, { recursive: true, force: true }); |
| 474 | | − await fs.mkdir(stageRoot, { recursive: true }); |
| 475 | | − await fs.mkdir(stagedAppRoot, { recursive: true }); |
| 476 | | − |
| 477 | | − await fs.cp(outputRoot, path.join(stagedAppRoot, stagedOutputRoot), { recursive: true }); |
| 478 | | − |
| 479 | | − const stagedServerEntryAbsolute = path.join(stagedAppRoot, stagedServerEntry); |
| 480 | | − if (!(await pathExists(stagedServerEntryAbsolute))) { |
| 481 | | − throw new Error(`Expected staged TanStack Start server entry at ${stagedServerEntryAbsolute}. Packaging failed.`); |
| 482 | | − } |
| 483 | | − |
| 484 | | − await Bun.write( |
| 485 | | − path.join(stageRoot, "server.js"), |
| 486 | | − `process.chdir(__dirname + '/executor/apps/web');\nimport(process.cwd() + '${stagedServerEntryWithLeadingSlash}');\n`, |
| 487 | | − ); |
| 488 | | − |
| 489 | | − await runArchiveCommand(["tar", "-czf", archivePath, "-C", stageRoot, "."]); |
| 490 | | − const digest = await sha256(archivePath); |
| 491 | | − checksums.push(`${digest} ${path.basename(archivePath)}`); |
| 492 | | − console.log(`built ${webArtifactName}`); |
| 493 | | − |
| 494 | | − for (const target of targets) { |
| 495 | | − if (target.platform === host.platform && target.arch === host.arch) { |
| 496 | | − continue; |
| 497 | | − } |
| 498 | | − |
| 499 | | − const targetArtifactName = webArchiveName(target.platform, target.arch); |
| 500 | | − const targetArchivePath = path.join(releaseDir, targetArtifactName); |
| 501 | | − await fs.copyFile(archivePath, targetArchivePath); |
| 502 | | − |
| 503 | | − const targetDigest = await sha256(targetArchivePath); |
| 504 | | − checksums.push(`${targetDigest} ${targetArtifactName}`); |
| 505 | | − console.log(`aliased ${targetArtifactName} from ${webArtifactName}`); |
| 506 | | − } |
| 507 | | −} |
| 508 | | − |
| 509 | | −async function buildRuntimeArtifact(rootDir: string, releaseDir: string, checksums: string[]): Promise<void> { |
| 510 | | − const seeded = await seedRuntimeState(rootDir); |
| 511 | | − const convexDataDir = path.join(seeded.runtimeDir, "convex-data"); |
| 512 | | − const managedAnonymousAuthFile = path.join(seeded.runtimeDir, "managed-anonymous-auth.json"); |
| 513 | | − |
| 514 | | − const tempDownloads = await fs.mkdtemp(path.join(os.tmpdir(), "executor-runtime-build-")); |
| 515 | | − |
| 516 | | − try { |
| 517 | | − for (const target of targets) { |
| 518 | | − const stageRoot = path.join(releaseDir, `executor-runtime-${target.platform}-${target.arch}`); |
| 519 | | − const artifactPath = path.join(releaseDir, runtimeArchiveName(target.platform, target.arch)); |
| 520 | | − |
| 521 | | − await fs.rm(stageRoot, { recursive: true, force: true }); |
| 522 | | − await fs.mkdir(stageRoot, { recursive: true }); |
| 523 | | − |
| 524 | | − await fs.cp(convexDataDir, path.join(stageRoot, "convex-data"), { recursive: true }); |
| 525 | | − if (await pathExists(managedAnonymousAuthFile)) { |
| 526 | | − await fs.copyFile(managedAnonymousAuthFile, path.join(stageRoot, "managed-anonymous-auth.json")); |
| 527 | | − } |
| 528 | | − |
| 529 | | − const backendArchive = path.join(tempDownloads, backendAssetName(target)); |
| 530 | | − const backendUrl = `https://github.com/${convexBackendRepo}/releases/latest/download/${backendAssetName(target)}`; |
| 531 | | − await downloadFile(backendUrl, backendArchive); |
| 532 | | − await fs.mkdir(path.join(stageRoot, "convex-backend"), { recursive: true }); |
| 533 | | − await extractZipArchive(backendArchive, path.join(stageRoot, "convex-backend")); |
| 534 | | − |
| 535 | | − const backendBinary = path.join(stageRoot, "convex-backend", backendBinaryName(target)); |
| 536 | | − if (!(await pathExists(backendBinary))) { |
| 537 | | − throw new Error(`Runtime artifact missing backend binary after extraction: ${backendBinary}`); |
| 538 | | − } |
| 539 | | − await fs.chmod(backendBinary, 0o755); |
| 540 | | − |
| 541 | | − const nodeArchive = path.join(tempDownloads, nodeArchiveName(target)); |
| 542 | | − const nodeUrl = `https://nodejs.org/dist/v${managedRuntimeVersions.nodeVersion}/${nodeArchiveName(target)}`; |
| 543 | | − await downloadFile(nodeUrl, nodeArchive); |
| 544 | | − await runArchiveCommand(["tar", "-xzf", nodeArchive, "-C", stageRoot]); |
| 545 | | − |
| 546 | | − const nodeDir = path.join(stageRoot, `node-v${managedRuntimeVersions.nodeVersion}-${target.platform}-${target.arch}`); |
| 547 | | − if (!(await pathExists(path.join(nodeDir, "bin", "node")))) { |
| 548 | | − throw new Error(`Runtime artifact missing extracted node runtime: ${nodeDir}`); |
| 549 | | − } |
| 550 | | − |
| 551 | | − const webArchive = path.join(releaseDir, webArchiveName(target.platform, target.arch)); |
| 552 | | − if (!(await pathExists(webArchive))) { |
| 553 | | − throw new Error(`Missing web artifact for runtime packaging: ${webArchive}`); |
| 554 | | − } |
| 555 | | − const webStage = path.join(tempDownloads, `web-${target.platform}-${target.arch}`); |
| 556 | | − await fs.rm(webStage, { recursive: true, force: true }); |
| 557 | | − await fs.mkdir(webStage, { recursive: true }); |
| 558 | | − await runArchiveCommand(["tar", "-xzf", webArchive, "-C", webStage]); |
| 559 | | − await fs.cp(webStage, path.join(stageRoot, "web"), { recursive: true }); |
| 560 | | − |
| 561 | | − if (!(await pathExists(path.join(stageRoot, "web", "server.js")))) { |
| 562 | | − throw new Error(`Runtime artifact missing web server entry for ${target.platform}-${target.arch}`); |
| 563 | | − } |
| 564 | | − |
| 565 | | − await runArchiveCommand(["tar", "-czf", artifactPath, "-C", stageRoot, "."]); |
| 566 | | − const digest = await sha256(artifactPath); |
| 567 | | − checksums.push(`${digest} ${path.basename(artifactPath)}`); |
| 568 | | − console.log(`built ${path.basename(artifactPath)}`); |
| 569 | | − } |
| 570 | | − } finally { |
| 571 | | − await fs.rm(tempDownloads, { recursive: true, force: true }); |
| 572 | | − await fs.rm(seeded.tempRoot, { recursive: true, force: true }); |
| 573 | | − } |
| 574 | | −} |
| 575 | | − |
| 576 | | −async function main(): Promise<void> { |
| 577 | | − const rootDir = path.resolve(import.meta.dir, "..", ".."); |
| 578 | | − const releaseDir = path.join(rootDir, "dist", "release"); |
| 579 | | − |
| 580 | | − await fs.rm(releaseDir, { recursive: true, force: true }); |
| 581 | | − await fs.mkdir(releaseDir, { recursive: true }); |
| 582 | | − |
| 583 | | − const checksums: string[] = []; |
| 584 | | − |
| 585 | | − for (const target of targets) { |
| 586 | | − const dirName = `executor-${target.platform}-${target.arch}`; |
| 587 | | − const bundleDir = path.join(releaseDir, dirName); |
| 588 | | − const binDir = path.join(bundleDir, "bin"); |
| 589 | | − const binName = "executor"; |
| 590 | | − const binPath = path.join(binDir, binName); |
| 591 | | − |
| 592 | | − await fs.mkdir(binDir, { recursive: true }); |
| 593 | | − |
| 594 | | − const build = await Bun.build({ |
| 595 | | − entrypoints: [path.join(rootDir, "executor.ts")], |
| 596 | | − compile: { |
| 597 | | − target: target.bunTarget as never, |
| 598 | | − outfile: binPath, |
| 599 | | − }, |
| 600 | | − }); |
| 601 | | − |
| 602 | | − if (!build.success) { |
| 603 | | − const logs = build.logs.map((log) => log.message).join("\n"); |
| 604 | | − throw new Error(`Failed to compile target ${target.bunTarget}\n${logs}`); |
| 605 | | − } |
| 606 | | − |
| 607 | | − const archivePath = path.join(releaseDir, archiveName(target.platform, target.arch)); |
| 608 | | − await runArchiveCommand(["tar", "-czf", archivePath, "-C", binDir, binName]); |
| 609 | | − |
| 610 | | − const digest = await sha256(archivePath); |
| 611 | | − checksums.push(`${digest} ${path.basename(archivePath)}`); |
| 612 | | − console.log(`built ${path.basename(archivePath)}`); |
| 613 | | − } |
| 614 | | − |
| 615 | | − await buildWebArtifact(rootDir, releaseDir, checksums); |
| 616 | | − await buildRuntimeArtifact(rootDir, releaseDir, checksums); |
| 617 | | − |
| 618 | | − await Bun.write(path.join(releaseDir, "checksums.txt"), `${checksums.join("\n")}\n`); |
| 619 | | − console.log(`wrote ${path.join("dist", "release", "checksums.txt")}`); |
| 620 | | −} |
| 621 | | − |
| 622 | | −await main(); |