#!/usr/bin/env bun import { $ } from "bun" import { mkdir, rm } from "fs/promises" import path from "path" import { Script } from "@opencode-ai/script" import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin" import type { BunPlugin } from "bun" import pkg from "../package.json" import { buildAppArchive } from "./app-assets" import { verifyArtifact, verifySimulationGraph } from "./verify-artifact" const dir = path.resolve(import.meta.dirname, "..") const binary = "opencode2" const outdir = path.resolve( dir, process.argv.find((arg) => arg.startsWith("--outdir="))?.slice("--outdir=".length) ?? "dist", ) if (outdir === dir) throw new Error("--outdir must not be the package directory") process.chdir(dir) await rm(outdir, { recursive: true, force: true }) const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const requestedTarget = process.argv.find((arg) => arg.startsWith("--target="))?.slice("--target=".length) const skipInstall = process.argv.includes("--skip-install") const skipWebUi = process.argv.includes("--skip-web-ui") const solidPlugin = createSolidTransformPlugin() const releaseAssets = new Map>>() const allTargets: { os: string arch: "arm64" | "x64" abi?: "musl" avx2?: false }[] = [ { os: "linux", arch: "arm64" }, { os: "linux", arch: "x64" }, { os: "linux", arch: "x64", avx2: false }, { os: "linux", arch: "arm64", abi: "musl" }, { os: "linux", arch: "x64", abi: "musl" }, { os: "linux", arch: "x64", abi: "musl", avx2: false }, { os: "darwin", arch: "arm64" }, { os: "darwin", arch: "x64" }, { os: "darwin", arch: "x64", avx2: false }, { os: "win32", arch: "arm64" }, { os: "win32", arch: "x64" }, { os: "win32", arch: "x64", avx2: false }, ] const targets = requestedTarget !== undefined ? allTargets.filter((item) => targetName(item) === requestedTarget) : singleFlag ? allTargets.filter((item) => { if (item.os !== process.platform || item.arch !== process.arch) return false if (item.avx2 === false) return baselineFlag return item.abi === undefined }) : allTargets if (!targets.length) throw new Error(`Unknown build target: ${requestedTarget}`) if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}` const appArchive = await buildAppArchive(Script.channel, { skipBuild: skipWebUi }) const appAssetsPlugin: BunPlugin = { name: "opencode-app-assets", setup(build) { build.onResolve({ filter: /^virtual:opencode-app-assets$/ }, () => ({ path: "opencode-app-assets", namespace: "opencode", })) build.onLoad({ filter: /^opencode-app-assets$/, namespace: "opencode" }, () => ({ loader: "js", contents: `export default ${JSON.stringify(appArchive)}`, })) }, } for (const item of targets) { const simulationInputs = new Set() const simulationGraphPlugin: BunPlugin = { name: "opencode-simulation-graph", setup(build) { build.onLoad( { filter: /packages[/\\]simulation[/\\]src[/\\](frontend[/\\](simulation|server)|control-server)\.ts$/ }, (args) => void simulationInputs.add(args.path), ) }, } const parcelWatcherPackage = `@parcel/watcher-${item.os}-${item.arch}${item.os === "linux" ? `-${item.abi ?? "glibc"}` : ""}` const parcelWatcherPlugin: BunPlugin = { name: "parcel-watcher-binding", setup(build) { build.onLoad({ filter: /filesystem\/watcher-binding\.ts$/ }, () => ({ contents: `export default () => require(${JSON.stringify(parcelWatcherPackage)})`, loader: "js", })) }, } const target = targetName(item) const name = target.replace(binary, "cli") const executablePath = await compileExecutable(item) console.log(`building ${name}`) const result = await Bun.build({ entrypoints: ["./src/index.ts"], tsconfig: "./tsconfig.json", plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin, simulationGraphPlugin], external: ["node-gyp"], format: "esm", minify: true, sourcemap: "inline", splitting: true, compile: { autoloadBunfig: false, autoloadDotenv: false, autoloadTsconfig: true, autoloadPackageJson: true, target: target.replace(binary, "bun") as Bun.Build.CompileTarget, ...(executablePath ? { executablePath } : {}), outfile: path.join(outdir, name, "bin", binary), execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--no-warnings", "--"], windows: {}, }, define: { OPENCODE_VERSION: `'${Script.version}'`, OPENCODE_CLI_NAME: `'${binary}'`, OPENCODE_CHANNEL: `'${Script.channel}'`, OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined", // FFF_LIBC selects the fff native lib variant: "musl" or "gnu". FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined", ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}), }, }) if (!result.success) { for (const log of result.logs) console.error(log) process.exit(1) } verifySimulationGraph(simulationInputs) await Bun.write( path.join(outdir, name, "package.json"), JSON.stringify( { name: `@opencode-ai/${name}`, version: Script.version, license: "MIT", repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" }, os: [item.os], cpu: [item.arch], }, null, 2, ), ) await verifyArtifact(path.join(outdir, name)) } async function compileExecutable(item: (typeof allTargets)[number]) { const release = process.env.BUN_COMPILE_RELEASE if (!release) return const platform = item.os === "win32" ? "windows" : item.os const name = [ "bun", platform, item.arch === "arm64" ? "aarch64" : item.arch, item.abi, item.avx2 === false ? "baseline" : undefined, ] .filter(Boolean) .join("-") const cache = path.join(outdir, ".bun", release) const executable = path.join(cache, name, item.os === "win32" ? "bun.exe" : "bun") if (await Bun.file(executable).exists()) return executable await mkdir(cache, { recursive: true }) const archive = path.join(cache, `${name}.zip`) const assets = await compileReleaseAssets(release) const url = assets.get(`${name}.zip`) if (!url) throw new Error(`Bun release ${release} does not include ${name}.zip`) const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN const response = await fetch(url, { headers: { Accept: "application/octet-stream", ...(token ? { Authorization: `Bearer ${token}` } : {}) }, }) if (!response.ok) throw new Error(`Failed to download ${name} from Bun release ${release}: ${response.status}`) await Bun.write(archive, response) await $`unzip -oq ${archive} -d ${cache}` await rm(archive) return executable } function compileReleaseAssets(release: string) { const existing = releaseAssets.get(release) if (existing) return existing const pending = fetch(`https://api.github.com/repos/oven-sh/bun/releases/tags/${release}?cache=${Date.now()}`) .then(async (response) => { if (!response.ok) throw new Error(`Failed to resolve Bun release ${release}: ${response.status}`) const data: unknown = await response.json() if (typeof data !== "object" || data === null || !("assets" in data) || !Array.isArray(data.assets)) { throw new Error(`Bun release ${release} returned invalid metadata`) } return new Map( data.assets .filter( (asset): asset is { name: string; url: string } => typeof asset === "object" && asset !== null && "name" in asset && typeof asset.name === "string" && "url" in asset && typeof asset.url === "string", ) .map((asset) => [asset.name, asset.url]), ) }) .catch((error) => { releaseAssets.delete(release) throw error }) releaseAssets.set(release, pending) return pending } function targetName(item: (typeof allTargets)[number]) { return [ binary, item.os === "win32" ? "windows" : item.os, item.arch, item.avx2 === false ? "baseline" : undefined, item.abi, ] .filter(Boolean) .join("-") }