// Build one release artifact for one target, on the matching host: // bun scripts/release.ts [--target mac-arm64|win-x64|linux-x64] // Default target = the host. electrobun cannot cross-compile (Hutch builds for // its host OS and arch only; packaging needs host tools: codesign/hdiutil on // mac, rcedit/PowerShell on Windows), so win-x64 must run on a Windows machine. // There is no mac-x64: Electrobun 2.x publishes no macOS x64 core at all. // Artifacts land in release//miniSCP-Build-. import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; import path from "node:path"; const ROOT = path.join(import.meta.dir, ".."); const TARGETS = ["mac-arm64", "win-x64", "linux-x64"] as const; type Target = (typeof TARGETS)[number]; function hostTarget(): Target { if (process.platform === "win32") return "win-x64"; if (process.platform === "linux") return "linux-x64"; return "mac-arm64"; } function fail(message: string): never { console.error(`\n[release] ERROR: ${message}`); process.exit(1); } function run(cmd: string[], opts: { cwd?: string } = {}): void { console.log(`[release] $ ${cmd.join(" ")}`); const proc = Bun.spawnSync(cmd, { cwd: opts.cwd ?? ROOT, stdout: "inherit", stderr: "inherit", }); if (!proc.success) fail(`command failed: ${cmd.join(" ")}`); } // ---- target selection + host guards ---- const argIndex = process.argv.indexOf("--target"); const arg = argIndex >= 0 ? process.argv[argIndex + 1] : hostTarget(); if (arg === "mac-x64") fail("mac-x64 was dropped — Electrobun 2.x publishes no macOS x64 core."); if (!TARGETS.includes(arg as Target)) fail(`unknown target "${arg}" (${TARGETS.join(", ")})`); const requested = arg as Target; if (requested === "win-x64" && process.platform !== "win32") { fail("win-x64 must be built on a Windows machine/runner — electrobun cannot cross-compile."); } if (requested === "linux-x64" && process.platform !== "linux") { fail("linux-x64 must be built on a Linux machine/runner — electrobun cannot cross-compile."); } if (requested === "mac-arm64" && (process.platform !== "darwin" || process.arch !== "arm64")) { fail("mac-arm64 must be built on an Apple Silicon Mac (Electrobun 2.x has no macOS x64 build)."); } // ---- prepare: version, devkit, icons, UI bundle ---- run(["bun", "scripts/gen-version.ts"]); run(["bunx", "electrobun", "prepare"]); run(["bun", "scripts/gen-icons.ts"]); run(["bun", "run", "--cwd", "ui", "build"]); const { BUILD } = await import("../src/shared/version.gen"); const { BRANDING } = await import("../src/shared/branding"); const buildName = `Build${BUILD.number}`; // ---- electrobun build ---- // Start from an empty artifacts/: site/deploy.sh publishes it verbatim (the // updater contract), so an earlier build's files must not ride along. const artifactsDir = path.join(ROOT, "artifacts"); rmSync(artifactsDir, { recursive: true, force: true }); run(["bunx", "electrobun", "build", "--env=stable"]); // ---- collect artifacts ---- const [osName, archName] = requested === "win-x64" ? ["win", "x64"] : requested === "linux-x64" ? ["linux", "x64"] : ["macos", "arm64"]; // 2.x installers carry no channel prefix (macos-arm64-miniSCP.dmg); only the // update feed (stable---*) keeps it, for v1-compatible clients. const prefix = `${osName}-${archName}-`; const outDir = path.join(ROOT, "release", requested); mkdirSync(outDir, { recursive: true }); const produced = readdirSync(artifactsDir).filter( (f) => f.startsWith(prefix) && f.includes(BRANDING.appName), ); if (!produced.length) fail(`no ${prefix}* installer found in artifacts/ — did the build run?`); const out: string[] = []; for (const file of produced) { const ext = [".dmg", "-Setup.zip", "-Setup.tar.gz"].find((e) => file.endsWith(e)); if (!ext) continue; const name = `${BRANDING.appName}-${buildName}-${requested}${ext}`; cpSync(path.join(artifactsDir, file), path.join(outDir, name)); out.push(name); } // Windows: also zip the assembled app folder — the portable build (unzip // anywhere and run) next to the Setup installer. Best-effort: the folder // layout is confirmed by the first real 2.x Windows build. if (requested === "win-x64") { const appDir = path.join(ROOT, "build", "stable-win-x64", BRANDING.appName); if (existsSync(appDir)) { const portable = path.join(outDir, `${BRANDING.appName}-${buildName}-win-x64-portable.zip`); run(["powershell", "-Command", `Compress-Archive -Force -Path '${appDir}\\*' -DestinationPath '${portable}'`]); out.push(path.basename(portable)); } else { console.warn(`[release] WARN: portable skipped — app folder not found at ${appDir}`); } } if (!out.length) fail("artifacts were found but none matched a known package extension."); console.log(`\n[release] ${BRANDING.appName} ${BRANDING.buildLabel} (${BUILD.commit}) — ${requested}`); for (const name of out) console.log(`[release] release/${requested}/${name}`);