build.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. #!/usr/bin/env bun
  2. import { $ } from "bun"
  3. import fs from "fs"
  4. import path from "path"
  5. import { fileURLToPath } from "url"
  6. import solidPlugin from "@opentui/solid/bun-plugin"
  7. const __filename = fileURLToPath(import.meta.url)
  8. const __dirname = path.dirname(__filename)
  9. const dir = path.resolve(__dirname, "..")
  10. process.chdir(dir)
  11. import { Script } from "@opencode-ai/script"
  12. import pkg from "../package.json"
  13. const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev"
  14. // Fetch and generate models.dev snapshot
  15. const modelsData = process.env.MODELS_DEV_API_JSON
  16. ? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
  17. : await fetch(`${modelsUrl}/api.json`).then((x) => x.text())
  18. await Bun.write(
  19. path.join(dir, "src/provider/models-snapshot.js"),
  20. `// @ts-nocheck\n// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData}\n`,
  21. )
  22. await Bun.write(
  23. path.join(dir, "src/provider/models-snapshot.d.ts"),
  24. `// Auto-generated by build.ts - do not edit\nexport declare const snapshot: Record<string, unknown>\n`,
  25. )
  26. console.log("Generated models-snapshot.js")
  27. // Load migrations from migration directories
  28. const migrationDirs = (
  29. await fs.promises.readdir(path.join(dir, "migration"), {
  30. withFileTypes: true,
  31. })
  32. )
  33. .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
  34. .map((entry) => entry.name)
  35. .sort()
  36. const migrations = await Promise.all(
  37. migrationDirs.map(async (name) => {
  38. const file = path.join(dir, "migration", name, "migration.sql")
  39. const sql = await Bun.file(file).text()
  40. const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
  41. const timestamp = match
  42. ? Date.UTC(
  43. Number(match[1]),
  44. Number(match[2]) - 1,
  45. Number(match[3]),
  46. Number(match[4]),
  47. Number(match[5]),
  48. Number(match[6]),
  49. )
  50. : 0
  51. return { sql, timestamp, name }
  52. }),
  53. )
  54. console.log(`Loaded ${migrations.length} migrations`)
  55. const singleFlag = process.argv.includes("--single")
  56. const baselineFlag = process.argv.includes("--baseline")
  57. const skipInstall = process.argv.includes("--skip-install")
  58. const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
  59. const createEmbeddedWebUIBundle = async () => {
  60. console.log(`Building Web UI to embed in the binary`)
  61. const appDir = path.join(import.meta.dirname, "../../app")
  62. await $`bun run --cwd ${appDir} build`
  63. const allFiles = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: path.join(appDir, "dist") }))
  64. const fileMap = `
  65. // Import all files as file_$i with type: "file"
  66. ${allFiles.map((filePath, i) => `import file_${i} from "${path.join(appDir, "dist", filePath)}" with { type: "file" };`).join("\n")}
  67. // Export with original mappings
  68. export default {
  69. ${allFiles.map((filePath, i) => `"${filePath}": file_${i},`).join("\n")}
  70. }
  71. `.trim()
  72. return fileMap
  73. }
  74. const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle()
  75. const allTargets: {
  76. os: string
  77. arch: "arm64" | "x64"
  78. abi?: "musl"
  79. avx2?: false
  80. }[] = [
  81. {
  82. os: "linux",
  83. arch: "arm64",
  84. },
  85. {
  86. os: "linux",
  87. arch: "x64",
  88. },
  89. {
  90. os: "linux",
  91. arch: "x64",
  92. avx2: false,
  93. },
  94. {
  95. os: "linux",
  96. arch: "arm64",
  97. abi: "musl",
  98. },
  99. {
  100. os: "linux",
  101. arch: "x64",
  102. abi: "musl",
  103. },
  104. {
  105. os: "linux",
  106. arch: "x64",
  107. abi: "musl",
  108. avx2: false,
  109. },
  110. {
  111. os: "darwin",
  112. arch: "arm64",
  113. },
  114. {
  115. os: "darwin",
  116. arch: "x64",
  117. },
  118. {
  119. os: "darwin",
  120. arch: "x64",
  121. avx2: false,
  122. },
  123. {
  124. os: "win32",
  125. arch: "arm64",
  126. },
  127. {
  128. os: "win32",
  129. arch: "x64",
  130. },
  131. {
  132. os: "win32",
  133. arch: "x64",
  134. avx2: false,
  135. },
  136. ]
  137. const targets = singleFlag
  138. ? allTargets.filter((item) => {
  139. if (item.os !== process.platform || item.arch !== process.arch) {
  140. return false
  141. }
  142. // When building for the current platform, prefer a single native binary by default.
  143. // Baseline binaries require additional Bun artifacts and can be flaky to download.
  144. if (item.avx2 === false) {
  145. return baselineFlag
  146. }
  147. // also skip abi-specific builds for the same reason
  148. if (item.abi !== undefined) {
  149. return false
  150. }
  151. return true
  152. })
  153. : allTargets
  154. await $`rm -rf dist`
  155. const binaries: Record<string, string> = {}
  156. if (!skipInstall) {
  157. await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
  158. await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
  159. }
  160. for (const item of targets) {
  161. const name = [
  162. pkg.name,
  163. // changing to win32 flags npm for some reason
  164. item.os === "win32" ? "windows" : item.os,
  165. item.arch,
  166. item.avx2 === false ? "baseline" : undefined,
  167. item.abi === undefined ? undefined : item.abi,
  168. ]
  169. .filter(Boolean)
  170. .join("-")
  171. console.log(`building ${name}`)
  172. await $`mkdir -p dist/${name}/bin`
  173. const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
  174. const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
  175. const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
  176. const workerPath = "./src/cli/cmd/tui/worker.ts"
  177. // Use platform-specific bunfs root path based on target OS
  178. const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
  179. const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
  180. await Bun.build({
  181. conditions: ["browser"],
  182. tsconfig: "./tsconfig.json",
  183. plugins: [solidPlugin],
  184. compile: {
  185. autoloadBunfig: false,
  186. autoloadDotenv: false,
  187. autoloadTsconfig: true,
  188. autoloadPackageJson: true,
  189. target: name.replace(pkg.name, "bun") as any,
  190. outfile: `dist/${name}/bin/opencode`,
  191. execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
  192. windows: {},
  193. },
  194. files: {
  195. ...(embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {}),
  196. },
  197. entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
  198. define: {
  199. OPENCODE_VERSION: `'${Script.version}'`,
  200. OPENCODE_MIGRATIONS: JSON.stringify(migrations),
  201. OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
  202. OPENCODE_WORKER_PATH: workerPath,
  203. OPENCODE_CHANNEL: `'${Script.channel}'`,
  204. OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
  205. },
  206. })
  207. // Smoke test: only run if binary is for current platform
  208. if (item.os === process.platform && item.arch === process.arch && !item.abi) {
  209. const binaryPath = `dist/${name}/bin/opencode`
  210. console.log(`Running smoke test: ${binaryPath} --version`)
  211. try {
  212. const versionOutput = await $`${binaryPath} --version`.text()
  213. console.log(`Smoke test passed: ${versionOutput.trim()}`)
  214. } catch (e) {
  215. console.error(`Smoke test failed for ${name}:`, e)
  216. process.exit(1)
  217. }
  218. }
  219. await $`rm -rf ./dist/${name}/bin/tui`
  220. await Bun.file(`dist/${name}/package.json`).write(
  221. JSON.stringify(
  222. {
  223. name,
  224. version: Script.version,
  225. os: [item.os],
  226. cpu: [item.arch],
  227. },
  228. null,
  229. 2,
  230. ),
  231. )
  232. binaries[name] = Script.version
  233. }
  234. if (Script.release) {
  235. for (const key of Object.keys(binaries)) {
  236. if (key.includes("linux")) {
  237. await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
  238. } else {
  239. await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
  240. }
  241. }
  242. await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
  243. }
  244. export { binaries }