#!/usr/bin/env bun import path from "path" import fs from "fs" import { $ } from "bun" import { fileURLToPath } from "url" import { createEncryptPromptsPlugin, generateKeyFragments } from "./encrypt-prompts-plugin" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const dir = path.resolve(__dirname, "..") process.chdir(dir) import pkg from "../package.json" import { Script } from "@synsci/script" // Fetch and generate models.dev snapshot. Runtime refreshes stale snapshots // when current frontier IDs are missing, so avoid inventing model aliases here. const modelsRaw = process.env.MODELS_DEV_API_JSON ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() : await fetch(`https://models.dev/api.json`).then((x) => x.text()) const modelsJson = JSON.parse(modelsRaw) await Bun.write( path.join(dir, "src/provider/models-snapshot.ts"), `// Auto-generated by build.ts - do not edit\nexport const snapshot = ${JSON.stringify(modelsJson)} as const\n`, ) console.log("Generated models-snapshot.ts") const singleFlag = process.argv.includes("--single") const baselineFlag = process.argv.includes("--baseline") const skipInstall = process.argv.includes("--skip-install") 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: "x64", }, { os: "win32", arch: "x64", avx2: false, }, ] const targets = singleFlag ? allTargets.filter((item) => { if (item.os !== process.platform || item.arch !== process.arch) { return false } // When building for the current platform, prefer a single native binary by default. // Baseline binaries require additional Bun artifacts and can be flaky to download. if (item.avx2 === false) { return baselineFlag } // also skip abi-specific builds for the same reason if (item.abi !== undefined) { return false } return true }) : allTargets await $`rm -rf dist` // Build the openscience web UI (frontend/workspace) and regenerate the embedded asset // manifest so the catch-all route in server.ts can serve it locally instead // of proxying to Vercel. The manifest imports every dist file with // `with { type: "file" }` so Bun's --compile bundles them into the binary. // // `frontend/workspace/package.json` references workspace packages via the `catalog:` // protocol, which only resolves after `bun install` at the monorepo root. On a // fresh clone vite's resolver fails with cryptic "Cannot find package" errors // — run the workspace install if `node_modules` is missing. const repoRoot = path.resolve(dir, "../..") const webAppDir = path.resolve(repoRoot, "frontend/workspace") if (!fs.existsSync(path.join(repoRoot, "node_modules")) || !fs.existsSync(path.join(webAppDir, "node_modules"))) { console.log("workspace node_modules missing — running bun install at repo root") await $`bun install`.cwd(repoRoot) } console.log("building frontend/workspace (openscience web)") await $`bun run build`.cwd(webAppDir) await $`bun run script/generate-web-assets.ts` // Generate encryption key for prompt files (unique per build) const keyFragments = generateKeyFragments() const { plugin: encryptPlugin, defines: keyDefines } = createEncryptPromptsPlugin(keyFragments) const binaries: Record = {} if (!skipInstall) { await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` } for (const item of targets) { const name = [ pkg.name, // changing to win32 flags npm for some reason item.os === "win32" ? "windows" : item.os, item.arch, item.avx2 === false ? "baseline" : undefined, item.abi === undefined ? undefined : item.abi, ] .filter(Boolean) .join("-") console.log(`building ${name}`) await $`mkdir -p dist/${name}/bin` await Bun.build({ conditions: ["browser"], tsconfig: "./tsconfig.json", plugins: [encryptPlugin], sourcemap: "external", compile: { autoloadBunfig: false, autoloadDotenv: false, //@ts-ignore (bun types aren't up to date) autoloadTsconfig: true, autoloadPackageJson: true, target: name.replace(pkg.name, "bun") as any, outfile: `dist/${name}/bin/openscience`, execArgv: [`--user-agent=openscience/${Script.version}`, "--use-system-ca", "--"], windows: {}, }, entrypoints: ["./src/index.ts"], define: { ...keyDefines, OPENSCIENCE_VERSION: `'${Script.version}'`, OPENSCIENCE_CHANNEL: `'${Script.channel}'`, OPENSCIENCE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "", }, }) // Skills are served via API — no longer copied into dist await Bun.file(`dist/${name}/package.json`).write( JSON.stringify( { name, version: Script.version, os: [item.os], cpu: [item.arch], // Without libc, linux-x64 hosts install all four x64 variants // (~750 MB) and glibc/musl selection falls to the bin wrapper. ...(item.os === "linux" ? { libc: [item.abi === "musl" ? "musl" : "glibc"] } : {}), // npm provenance refuses packages whose repository.url doesn't match // the repo the workflow ran from (case-sensitive) repository: { type: "git", url: "git+https://github.com/synthetic-sciences/openscience.git", }, }, null, 2, ), ) binaries[name] = Script.version } if (Script.release) { const checksums: string[] = [] for (const key of Object.keys(binaries)) { const archiveName = key.replace(`${pkg.name}-`, "openscience-") + (key.includes("linux") ? ".tar.gz" : ".zip") if (key.includes("linux")) { await $`tar -czf ${path.resolve(dir, `dist/${archiveName}`)} *`.cwd(`dist/${key}/bin`) } else { await $`zip -r ${path.resolve(dir, `dist/${archiveName}`)} *`.cwd(`dist/${key}/bin`) } const bytes = await Bun.file(path.resolve(dir, `dist/${archiveName}`)).arrayBuffer() const hash = new Bun.CryptoHasher("sha256").update(bytes).digest("hex") checksums.push(`${hash} ${archiveName}`) } // sha256sum-compatible manifest; the install script verifies against it await Bun.write(path.resolve(dir, "dist/checksums.txt"), checksums.join("\n") + "\n") await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz ./dist/checksums.txt --clobber` } export { binaries }