build.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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 { createSolidTransformPlugin } 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. await import("./generate.ts")
  12. import { Script } from "@opencode-ai/script"
  13. import pkg from "../package.json"
  14. // Load migrations from migration directories
  15. const migrationDirs = (
  16. await fs.promises.readdir(path.join(dir, "migration"), {
  17. withFileTypes: true,
  18. })
  19. )
  20. .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
  21. .map((entry) => entry.name)
  22. .sort()
  23. const migrations = await Promise.all(
  24. migrationDirs.map(async (name) => {
  25. const file = path.join(dir, "migration", name, "migration.sql")
  26. const sql = await Bun.file(file).text()
  27. const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
  28. const timestamp = match
  29. ? Date.UTC(
  30. Number(match[1]),
  31. Number(match[2]) - 1,
  32. Number(match[3]),
  33. Number(match[4]),
  34. Number(match[5]),
  35. Number(match[6]),
  36. )
  37. : 0
  38. return { sql, timestamp, name }
  39. }),
  40. )
  41. console.log(`Loaded ${migrations.length} migrations`)
  42. const singleFlag = process.argv.includes("--single")
  43. const baselineFlag = process.argv.includes("--baseline")
  44. const skipInstall = process.argv.includes("--skip-install")
  45. const sourcemapsFlag = process.argv.includes("--sourcemaps")
  46. const plugin = createSolidTransformPlugin()
  47. const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
  48. const createEmbeddedWebUIBundle = async () => {
  49. console.log(`Building Web UI to embed in the binary`)
  50. const appDir = path.join(import.meta.dirname, "../../app")
  51. const dist = path.join(appDir, "dist")
  52. await $`bun run --cwd ${appDir} build`
  53. const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
  54. .map((file) => file.replaceAll("\\", "/"))
  55. .filter((file) => !file.endsWith(".map"))
  56. .sort()
  57. const imports = files.map((file, i) => {
  58. const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/")
  59. return `import file_${i} from ${JSON.stringify(spec.startsWith(".") ? spec : `./${spec}`)} with { type: "file" };`
  60. })
  61. const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`)
  62. return [
  63. `// Import all files as file_$i with type: "file"`,
  64. ...imports,
  65. `// Export with original mappings`,
  66. `export default {`,
  67. ...entries,
  68. `}`,
  69. ].join("\n")
  70. }
  71. const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle()
  72. const allTargets: {
  73. os: string
  74. arch: "arm64" | "x64"
  75. abi?: "musl"
  76. avx2?: false
  77. }[] = [
  78. {
  79. os: "linux",
  80. arch: "arm64",
  81. },
  82. {
  83. os: "linux",
  84. arch: "x64",
  85. },
  86. {
  87. os: "linux",
  88. arch: "x64",
  89. avx2: false,
  90. },
  91. {
  92. os: "linux",
  93. arch: "arm64",
  94. abi: "musl",
  95. },
  96. {
  97. os: "linux",
  98. arch: "x64",
  99. abi: "musl",
  100. },
  101. {
  102. os: "linux",
  103. arch: "x64",
  104. abi: "musl",
  105. avx2: false,
  106. },
  107. {
  108. os: "darwin",
  109. arch: "arm64",
  110. },
  111. {
  112. os: "darwin",
  113. arch: "x64",
  114. },
  115. {
  116. os: "darwin",
  117. arch: "x64",
  118. avx2: false,
  119. },
  120. {
  121. os: "win32",
  122. arch: "arm64",
  123. },
  124. {
  125. os: "win32",
  126. arch: "x64",
  127. },
  128. {
  129. os: "win32",
  130. arch: "x64",
  131. avx2: false,
  132. },
  133. ]
  134. const targets = singleFlag
  135. ? allTargets.filter((item) => {
  136. if (item.os !== process.platform || item.arch !== process.arch) {
  137. return false
  138. }
  139. // When building for the current platform, prefer a single native binary by default.
  140. // Baseline binaries require additional Bun artifacts and can be flaky to download.
  141. if (item.avx2 === false) {
  142. return baselineFlag
  143. }
  144. // also skip abi-specific builds for the same reason
  145. if (item.abi !== undefined) {
  146. return false
  147. }
  148. return true
  149. })
  150. : allTargets
  151. await $`rm -rf dist`
  152. const binaries: Record<string, string> = {}
  153. if (!skipInstall) {
  154. await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
  155. await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
  156. }
  157. for (const item of targets) {
  158. const name = [
  159. pkg.name,
  160. // changing to win32 flags npm for some reason
  161. item.os === "win32" ? "windows" : item.os,
  162. item.arch,
  163. item.avx2 === false ? "baseline" : undefined,
  164. item.abi === undefined ? undefined : item.abi,
  165. ]
  166. .filter(Boolean)
  167. .join("-")
  168. console.log(`building ${name}`)
  169. await $`mkdir -p dist/${name}/bin`
  170. const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
  171. const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
  172. const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
  173. const workerPath = "./src/cli/cmd/tui/worker.ts"
  174. // Use platform-specific bunfs root path based on target OS
  175. const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
  176. const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
  177. await Bun.build({
  178. conditions: ["browser"],
  179. tsconfig: "./tsconfig.json",
  180. plugins: [plugin],
  181. external: ["node-gyp"],
  182. format: "esm",
  183. minify: true,
  184. sourcemap: sourcemapsFlag ? "linked" : "none",
  185. splitting: true,
  186. compile: {
  187. autoloadBunfig: false,
  188. autoloadDotenv: false,
  189. autoloadTsconfig: true,
  190. autoloadPackageJson: true,
  191. target: name.replace(pkg.name, "bun") as any,
  192. outfile: `dist/${name}/bin/opencode`,
  193. execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
  194. windows: {},
  195. },
  196. files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
  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. preferUnplugged: true,
  226. os: [item.os],
  227. cpu: [item.arch],
  228. },
  229. null,
  230. 2,
  231. ),
  232. )
  233. binaries[name] = Script.version
  234. }
  235. if (Script.release) {
  236. for (const key of Object.keys(binaries)) {
  237. if (key.includes("linux")) {
  238. await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
  239. } else {
  240. await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
  241. }
  242. }
  243. await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
  244. }
  245. export { binaries }