postinstall.mjs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env node
  2. import childProcess from "node:child_process"
  3. import fs from "node:fs"
  4. import os from "node:os"
  5. import path from "node:path"
  6. import { createRequire } from "node:module"
  7. import { fileURLToPath } from "node:url"
  8. const directory = path.dirname(fileURLToPath(import.meta.url))
  9. const require = createRequire(import.meta.url)
  10. const packageJson = JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8"))
  11. const command = Object.keys(packageJson.bin ?? {})[0]
  12. if (!command) throw new Error("OpenCode package does not declare a binary")
  13. const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] ?? os.platform()
  14. const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] ?? os.arch()
  15. const sourceBinary = platform === "windows" ? `${command}.exe` : command
  16. const targetBinary = path.resolve(directory, packageJson.bin[command])
  17. const dependencies = packageJson.optionalDependencies ?? {}
  18. const base = Object.keys(dependencies).find((name) => name.endsWith(`-${platform}-${arch}`))
  19. if (!base) throw new Error(`OpenCode does not provide a binary for ${platform}-${arch}`)
  20. function supportsAvx2() {
  21. if (arch !== "x64") return false
  22. if (platform === "linux") {
  23. try {
  24. return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
  25. } catch {
  26. return false
  27. }
  28. }
  29. if (platform === "darwin") {
  30. try {
  31. const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], {
  32. encoding: "utf8",
  33. timeout: 1500,
  34. })
  35. return result.status === 0 && (result.stdout || "").trim() === "1"
  36. } catch {
  37. return false
  38. }
  39. }
  40. if (platform === "windows") {
  41. const script =
  42. '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
  43. for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
  44. try {
  45. const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", script], {
  46. encoding: "utf8",
  47. timeout: 3000,
  48. windowsHide: true,
  49. })
  50. if (result.status !== 0) continue
  51. const output = (result.stdout || "").trim().toLowerCase()
  52. if (output === "true" || output === "1") return true
  53. if (output === "false" || output === "0") return false
  54. } catch {
  55. continue
  56. }
  57. }
  58. }
  59. return false
  60. }
  61. function isMusl() {
  62. if (platform !== "linux") return false
  63. try {
  64. if (fs.existsSync("/etc/alpine-release")) return true
  65. const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
  66. return `${result.stdout || ""}${result.stderr || ""}`.toLowerCase().includes("musl")
  67. } catch {
  68. return false
  69. }
  70. }
  71. function packageNames() {
  72. const baseline = arch === "x64" && !supportsAvx2()
  73. const names =
  74. platform === "linux"
  75. ? isMusl()
  76. ? arch === "x64"
  77. ? baseline
  78. ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
  79. : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
  80. : [`${base}-musl`, base]
  81. : arch === "x64"
  82. ? baseline
  83. ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
  84. : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
  85. : [base, `${base}-musl`]
  86. : arch === "x64"
  87. ? baseline
  88. ? [`${base}-baseline`, base]
  89. : [base, `${base}-baseline`]
  90. : [base]
  91. return names.filter((name) => dependencies[name])
  92. }
  93. function copyBinary(source) {
  94. if (!fs.existsSync(source)) throw new Error(`Binary not found at ${source}`)
  95. fs.mkdirSync(path.dirname(targetBinary), { recursive: true })
  96. if (fs.existsSync(targetBinary)) fs.unlinkSync(targetBinary)
  97. try {
  98. fs.linkSync(source, targetBinary)
  99. } catch {
  100. fs.copyFileSync(source, targetBinary)
  101. }
  102. fs.chmodSync(targetBinary, 0o755)
  103. }
  104. function resolveBinary(name) {
  105. const packagePath = require.resolve(`${name}/package.json`)
  106. return path.join(path.dirname(packagePath), "bin", sourceBinary)
  107. }
  108. function installPackage(name) {
  109. const temp = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-install-"))
  110. try {
  111. const result = childProcess.spawnSync(
  112. "npm",
  113. ["install", "--ignore-scripts", "--no-save", "--loglevel=error", "--prefix", temp, `${name}@${dependencies[name]}`],
  114. { stdio: "inherit", windowsHide: true },
  115. )
  116. if (result.status !== 0) return false
  117. copyBinary(path.join(temp, "node_modules", name, "bin", sourceBinary))
  118. return true
  119. } finally {
  120. fs.rmSync(temp, { recursive: true, force: true })
  121. }
  122. }
  123. function verifyBinary() {
  124. return (
  125. childProcess.spawnSync(targetBinary, ["--version"], {
  126. stdio: "ignore",
  127. windowsHide: true,
  128. }).status === 0
  129. )
  130. }
  131. function main() {
  132. const names = packageNames()
  133. for (const name of names) {
  134. try {
  135. copyBinary(resolveBinary(name))
  136. if (verifyBinary()) return
  137. } catch {
  138. if (installPackage(name) && verifyBinary()) return
  139. }
  140. }
  141. throw new Error(`Failed to install OpenCode. Try manually installing ${names.map((name) => JSON.stringify(name)).join(" or ")}.`)
  142. }
  143. try {
  144. main()
  145. } catch (error) {
  146. console.error(error instanceof Error ? error.message : String(error))
  147. process.exit(1)
  148. }