postinstall.mjs 5.4 KB

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