build.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. const generated = await import("./generate.ts")
  12. import { Script } from "@kirincode-ai/script"
  13. import pkg from "../package.json"
  14. const singleFlag = process.argv.includes("--single")
  15. const baselineFlag = process.argv.includes("--baseline")
  16. const skipInstall = process.argv.includes("--skip-install")
  17. const sourcemapsFlag = process.argv.includes("--sourcemaps")
  18. const plugin = createSolidTransformPlugin()
  19. const skipEmbedWebUi = process.argv.includes("--skip-embed-web-ui")
  20. const createEmbeddedWebUIBundle = async () => {
  21. console.log(`Building Web UI to embed in the binary`)
  22. const appDir = path.join(import.meta.dirname, "../../app")
  23. const dist = path.join(appDir, "dist")
  24. await $`KIRINCODE_CHANNEL=${Script.channel} bun run --cwd ${appDir} build`
  25. const files = (await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: dist })))
  26. .map((file) => file.replaceAll("\\", "/"))
  27. .filter((file) => !file.endsWith(".map"))
  28. .sort()
  29. const imports = files.map((file, i) => {
  30. const spec = path.relative(dir, path.join(dist, file)).replaceAll("\\", "/")
  31. return `import file_${i} from ${JSON.stringify(spec.startsWith(".") ? spec : `./${spec}`)} with { type: "file" };`
  32. })
  33. const entries = files.map((file, i) => ` ${JSON.stringify(file)}: file_${i},`)
  34. return [
  35. `// Import all files as file_$i with type: "file"`,
  36. ...imports,
  37. `// Export with original mappings`,
  38. `export default {`,
  39. ...entries,
  40. `}`,
  41. ].join("\n")
  42. }
  43. const embeddedFileMap = skipEmbedWebUi ? null : await createEmbeddedWebUIBundle()
  44. const allTargets: {
  45. os: string
  46. arch: "arm64" | "x64"
  47. abi?: "musl"
  48. avx2?: false
  49. }[] = [
  50. {
  51. os: "linux",
  52. arch: "arm64",
  53. },
  54. {
  55. os: "linux",
  56. arch: "x64",
  57. },
  58. {
  59. os: "linux",
  60. arch: "x64",
  61. avx2: false,
  62. },
  63. {
  64. os: "linux",
  65. arch: "arm64",
  66. abi: "musl",
  67. },
  68. {
  69. os: "linux",
  70. arch: "x64",
  71. abi: "musl",
  72. },
  73. {
  74. os: "linux",
  75. arch: "x64",
  76. abi: "musl",
  77. avx2: false,
  78. },
  79. {
  80. os: "darwin",
  81. arch: "arm64",
  82. },
  83. {
  84. os: "darwin",
  85. arch: "x64",
  86. },
  87. {
  88. os: "darwin",
  89. arch: "x64",
  90. avx2: false,
  91. },
  92. {
  93. os: "win32",
  94. arch: "arm64",
  95. },
  96. {
  97. os: "win32",
  98. arch: "x64",
  99. },
  100. {
  101. os: "win32",
  102. arch: "x64",
  103. avx2: false,
  104. },
  105. ]
  106. const targets = singleFlag
  107. ? allTargets.filter((item) => {
  108. if (item.os !== process.platform || item.arch !== process.arch) {
  109. return false
  110. }
  111. // When building for the current platform, prefer a single native binary by default.
  112. // Baseline binaries require additional Bun artifacts and can be flaky to download.
  113. if (item.avx2 === false) {
  114. return baselineFlag
  115. }
  116. // also skip abi-specific builds for the same reason
  117. if (item.abi !== undefined) {
  118. return false
  119. }
  120. return true
  121. })
  122. : allTargets
  123. await $`rm -rf dist`
  124. const binaries: Record<string, string> = {}
  125. if (!skipInstall) {
  126. await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
  127. await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
  128. await $`bun install --os="*" --cpu="*" @ff-labs/fff-bun@${pkg.dependencies["@ff-labs/fff-bun"]}`
  129. }
  130. for (const item of targets) {
  131. const name = [
  132. pkg.name,
  133. // changing to win32 flags npm for some reason
  134. item.os === "win32" ? "windows" : item.os,
  135. item.arch,
  136. item.avx2 === false ? "baseline" : undefined,
  137. item.abi === undefined ? undefined : item.abi,
  138. ]
  139. .filter(Boolean)
  140. .join("-")
  141. console.log(`building ${name}`)
  142. await $`mkdir -p dist/${name}/bin`
  143. const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
  144. const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
  145. const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
  146. const workerPath = "./src/cli/tui/worker.ts"
  147. // Use platform-specific bunfs root path based on target OS
  148. const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
  149. const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
  150. await Bun.build({
  151. conditions: ["bun", "node"],
  152. tsconfig: "./tsconfig.json",
  153. plugins: [plugin],
  154. external: ["node-gyp"],
  155. format: "esm",
  156. minify: true,
  157. sourcemap: sourcemapsFlag ? "linked" : "none",
  158. splitting: true,
  159. compile: {
  160. autoloadBunfig: false,
  161. autoloadDotenv: false,
  162. autoloadTsconfig: true,
  163. autoloadPackageJson: true,
  164. target: name.replace(pkg.name, "bun") as any,
  165. outfile: `dist/${name}/bin/opencode`,
  166. execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
  167. windows: {},
  168. },
  169. files: embeddedFileMap ? { "opencode-web-ui.gen.ts": embeddedFileMap } : {},
  170. entrypoints: ["./src/index.ts", parserWorker, workerPath, ...(embeddedFileMap ? ["opencode-web-ui.gen.ts"] : [])],
  171. define: {
  172. FFF_LIBC: JSON.stringify(item.abi === "musl" ? "musl" : "gnu"),
  173. KIRINCODE_VERSION: `'${Script.version}'`,
  174. KIRINCODE_MODELS_DEV: generated.modelsData,
  175. OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
  176. KIRINCODE_WORKER_PATH: workerPath,
  177. KIRINCODE_CHANNEL: `'${Script.channel}'`,
  178. KIRINCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
  179. ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
  180. },
  181. })
  182. // Smoke test: only run if binary is for current platform
  183. if (item.os === process.platform && item.arch === process.arch && !item.abi) {
  184. const binaryPath = `dist/${name}/bin/opencode`
  185. console.log(`Running smoke test: ${binaryPath} --version`)
  186. try {
  187. const versionOutput = await $`${binaryPath} --version`.text()
  188. console.log(`Smoke test passed: ${versionOutput.trim()}`)
  189. } catch (e) {
  190. console.error(`Smoke test failed for ${name}:`, e)
  191. process.exit(1)
  192. }
  193. }
  194. await $`rm -rf ./dist/${name}/bin/tui`
  195. await Bun.file(`dist/${name}/package.json`).write(
  196. JSON.stringify(
  197. {
  198. name,
  199. version: Script.version,
  200. preferUnplugged: true,
  201. os: [item.os],
  202. cpu: [item.arch],
  203. ...(item.abi ? { libc: [item.abi] } : {}),
  204. },
  205. null,
  206. 2,
  207. ),
  208. )
  209. binaries[name] = Script.version
  210. }
  211. if (Script.release) {
  212. for (const key of Object.keys(binaries)) {
  213. if (key.includes("linux")) {
  214. await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
  215. } else {
  216. await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
  217. }
  218. }
  219. await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
  220. }
  221. export { binaries }