2
0

build.ts 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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.ts"),
  20. `// Auto-generated by build.ts - do not edit\nexport const snapshot = ${modelsData} as const\n`,
  21. )
  22. console.log("Generated models-snapshot.ts")
  23. // Load migrations from migration directories
  24. const migrationDirs = (
  25. await fs.promises.readdir(path.join(dir, "migration"), {
  26. withFileTypes: true,
  27. })
  28. )
  29. .filter((entry) => entry.isDirectory() && /^\d{4}\d{2}\d{2}\d{2}\d{2}\d{2}/.test(entry.name))
  30. .map((entry) => entry.name)
  31. .sort()
  32. const migrations = await Promise.all(
  33. migrationDirs.map(async (name) => {
  34. const file = path.join(dir, "migration", name, "migration.sql")
  35. const sql = await Bun.file(file).text()
  36. const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name)
  37. const timestamp = match
  38. ? Date.UTC(
  39. Number(match[1]),
  40. Number(match[2]) - 1,
  41. Number(match[3]),
  42. Number(match[4]),
  43. Number(match[5]),
  44. Number(match[6]),
  45. )
  46. : 0
  47. return { sql, timestamp, name }
  48. }),
  49. )
  50. console.log(`Loaded ${migrations.length} migrations`)
  51. const singleFlag = process.argv.includes("--single")
  52. const baselineFlag = process.argv.includes("--baseline")
  53. const skipInstall = process.argv.includes("--skip-install")
  54. const skipUpload = process.argv.includes("--skip-release-upload") || process.env.OPENCODE_SKIP_RELEASE_UPLOAD === "1"
  55. const sign = process.env.APPLE_SIGNING_IDENTITY
  56. const entitlements = process.env.OPENCODE_CODESIGN_ENTITLEMENTS || path.join(dir, "script/entitlements.plist")
  57. const raw = process.env.OPENCODE_BUILD_OS?.trim()
  58. const allTargets: {
  59. os: string
  60. arch: "arm64" | "x64"
  61. abi?: "musl"
  62. avx2?: false
  63. }[] = [
  64. {
  65. os: "linux",
  66. arch: "arm64",
  67. },
  68. {
  69. os: "linux",
  70. arch: "x64",
  71. },
  72. {
  73. os: "linux",
  74. arch: "x64",
  75. avx2: false,
  76. },
  77. {
  78. os: "linux",
  79. arch: "arm64",
  80. abi: "musl",
  81. },
  82. {
  83. os: "linux",
  84. arch: "x64",
  85. abi: "musl",
  86. },
  87. {
  88. os: "linux",
  89. arch: "x64",
  90. abi: "musl",
  91. avx2: false,
  92. },
  93. {
  94. os: "darwin",
  95. arch: "arm64",
  96. },
  97. {
  98. os: "darwin",
  99. arch: "x64",
  100. },
  101. {
  102. os: "darwin",
  103. arch: "x64",
  104. avx2: false,
  105. },
  106. {
  107. os: "win32",
  108. arch: "arm64",
  109. },
  110. {
  111. os: "win32",
  112. arch: "x64",
  113. },
  114. {
  115. os: "win32",
  116. arch: "x64",
  117. avx2: false,
  118. },
  119. ]
  120. const targets = singleFlag
  121. ? allTargets.filter((item) => {
  122. if (item.os !== process.platform || item.arch !== process.arch) {
  123. return false
  124. }
  125. // When building for the current platform, prefer a single native binary by default.
  126. // Baseline binaries require additional Bun artifacts and can be flaky to download.
  127. if (item.avx2 === false) {
  128. return baselineFlag
  129. }
  130. // also skip abi-specific builds for the same reason
  131. if (item.abi !== undefined) {
  132. return false
  133. }
  134. return true
  135. })
  136. : allTargets
  137. const os = raw
  138. ? Array.from(
  139. new Set(
  140. raw
  141. .split(",")
  142. .map((x) => x.trim())
  143. .filter(Boolean),
  144. ),
  145. )
  146. : undefined
  147. if (os) {
  148. const set = new Set(allTargets.map((item) => item.os))
  149. const bad = os.filter((item) => !set.has(item))
  150. if (bad.length > 0) {
  151. throw new Error(`Invalid OPENCODE_BUILD_OS value: ${bad.join(", ")}`)
  152. }
  153. }
  154. const list = os ? targets.filter((item) => os.includes(item.os)) : targets
  155. if (list.length === 0) {
  156. throw new Error("No build targets selected")
  157. }
  158. await $`rm -rf dist`
  159. const binaries: Record<string, string> = {}
  160. if (!skipInstall) {
  161. await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
  162. await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}`
  163. }
  164. for (const item of list) {
  165. const name = [
  166. pkg.name,
  167. // changing to win32 flags npm for some reason
  168. item.os === "win32" ? "windows" : item.os,
  169. item.arch,
  170. item.avx2 === false ? "baseline" : undefined,
  171. item.abi === undefined ? undefined : item.abi,
  172. ]
  173. .filter(Boolean)
  174. .join("-")
  175. console.log(`building ${name}`)
  176. await $`mkdir -p dist/${name}/bin`
  177. const localPath = path.resolve(dir, "node_modules/@opentui/core/parser.worker.js")
  178. const rootPath = path.resolve(dir, "../../node_modules/@opentui/core/parser.worker.js")
  179. const parserWorker = fs.realpathSync(fs.existsSync(localPath) ? localPath : rootPath)
  180. const workerPath = "./src/cli/cmd/tui/worker.ts"
  181. // Use platform-specific bunfs root path based on target OS
  182. const bunfsRoot = item.os === "win32" ? "B:/~BUN/root/" : "/$bunfs/root/"
  183. const workerRelativePath = path.relative(dir, parserWorker).replaceAll("\\", "/")
  184. await Bun.build({
  185. conditions: ["browser"],
  186. tsconfig: "./tsconfig.json",
  187. plugins: [solidPlugin],
  188. compile: {
  189. autoloadBunfig: false,
  190. autoloadDotenv: false,
  191. autoloadTsconfig: true,
  192. autoloadPackageJson: true,
  193. target: name.replace(pkg.name, "bun") as any,
  194. outfile: `dist/${name}/bin/opencode`,
  195. execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
  196. windows: {},
  197. },
  198. entrypoints: ["./src/index.ts", parserWorker, workerPath],
  199. define: {
  200. OPENCODE_VERSION: `'${Script.version}'`,
  201. OPENCODE_MIGRATIONS: JSON.stringify(migrations),
  202. OTUI_TREE_SITTER_WORKER_PATH: bunfsRoot + workerRelativePath,
  203. OPENCODE_WORKER_PATH: workerPath,
  204. OPENCODE_CHANNEL: `'${Script.channel}'`,
  205. OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "",
  206. },
  207. })
  208. if (item.os === "darwin") {
  209. if (Script.release && process.platform !== "darwin") {
  210. throw new Error("darwin release binaries must be built on macOS runners")
  211. }
  212. if (Script.release && !sign) {
  213. throw new Error("APPLE_SIGNING_IDENTITY is required for darwin release binaries")
  214. }
  215. if (process.platform === "darwin" && sign) {
  216. if (!fs.existsSync(entitlements)) {
  217. throw new Error(`Codesign entitlements file not found: ${entitlements}`)
  218. }
  219. const file = `dist/${name}/bin/opencode`
  220. console.log(`codesigning ${name}`)
  221. await $`codesign --entitlements ${entitlements} -vvvv --deep --sign ${sign} ${file} --force`
  222. await $`codesign -vvv --verify ${file}`
  223. }
  224. }
  225. // Smoke test: only run if binary is for current platform
  226. if (item.os === process.platform && item.arch === process.arch && !item.abi) {
  227. const binaryPath = `dist/${name}/bin/opencode`
  228. console.log(`Running smoke test: ${binaryPath} --version`)
  229. try {
  230. const versionOutput = await $`${binaryPath} --version`.text()
  231. console.log(`Smoke test passed: ${versionOutput.trim()}`)
  232. } catch (e) {
  233. console.error(`Smoke test failed for ${name}:`, e)
  234. process.exit(1)
  235. }
  236. }
  237. await $`rm -rf ./dist/${name}/bin/tui`
  238. await Bun.file(`dist/${name}/package.json`).write(
  239. JSON.stringify(
  240. {
  241. name,
  242. version: Script.version,
  243. os: [item.os],
  244. cpu: [item.arch],
  245. },
  246. null,
  247. 2,
  248. ),
  249. )
  250. binaries[name] = Script.version
  251. }
  252. if (Script.release) {
  253. for (const key of Object.keys(binaries)) {
  254. if (key.includes("linux")) {
  255. await $`tar -czf ../../${key}.tar.gz *`.cwd(`dist/${key}/bin`)
  256. } else {
  257. await $`zip -r ../../${key}.zip *`.cwd(`dist/${key}/bin`)
  258. }
  259. }
  260. if (!skipUpload) {
  261. await $`gh release upload v${Script.version} ./dist/*.zip ./dist/*.tar.gz --clobber --repo ${process.env.GH_REPO}`
  262. }
  263. }
  264. export { binaries }