binary.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. import path from "path"
  2. import { Context, Effect, Layer, Stream } from "effect"
  3. import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
  4. import { ChildProcess } from "effect/unstable/process"
  5. import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
  6. import { CrossSpawnSpawner } from "../cross-spawn-spawner"
  7. import { makeGlobalNode } from "../effect/app-node"
  8. import { httpClient } from "../effect/app-node-platform"
  9. import { FSUtil } from "../fs-util"
  10. import { Global } from "../global"
  11. import { which } from "../util/which"
  12. export namespace RipgrepBinary {
  13. const VERSION = "15.1.0"
  14. const PLATFORM = {
  15. "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
  16. "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
  17. "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
  18. "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
  19. "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" },
  20. "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" },
  21. "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
  22. } as const
  23. interface Interface {
  24. readonly filepath: Effect.Effect<string, Error>
  25. }
  26. export class Service extends Context.Service<Service, Interface>()("@opencode/RipgrepBinary") {}
  27. export const layer = Layer.effect(
  28. Service,
  29. Effect.gen(function* () {
  30. const fs = yield* FSUtil.Service
  31. const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
  32. const spawner = yield* ChildProcessSpawner
  33. const run = Effect.fnUntraced(function* (command: string, args: string[]) {
  34. const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" }))
  35. const [stdout, stderr, code] = yield* Effect.all(
  36. [
  37. Stream.mkString(Stream.decodeText(handle.stdout)),
  38. Stream.mkString(Stream.decodeText(handle.stderr)),
  39. handle.exitCode,
  40. ],
  41. { concurrency: "unbounded" },
  42. )
  43. return { stdout, stderr, code }
  44. }, Effect.scoped)
  45. const extract = Effect.fnUntraced(function* (
  46. archive: string,
  47. config: (typeof PLATFORM)[keyof typeof PLATFORM],
  48. target: string,
  49. ) {
  50. const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
  51. if (config.extension === "zip") {
  52. const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe"
  53. const result = yield* run(shell, [
  54. "-NoProfile",
  55. "-NonInteractive",
  56. "-Command",
  57. `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`,
  58. ])
  59. if (result.code !== 0)
  60. throw new Error(
  61. result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
  62. )
  63. }
  64. if (config.extension === "tar.gz") {
  65. const result = yield* run("tar", ["-xzf", archive, "-C", dir])
  66. if (result.code !== 0)
  67. throw new Error(
  68. result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
  69. )
  70. }
  71. const extracted = path.join(
  72. dir,
  73. `ripgrep-${VERSION}-${config.platform}`,
  74. process.platform === "win32" ? "rg.exe" : "rg",
  75. )
  76. if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`)
  77. yield* fs.copyFile(extracted, target)
  78. if (process.platform !== "win32") yield* fs.chmod(target, 0o755)
  79. }, Effect.scoped)
  80. return Service.of({
  81. filepath: yield* Effect.cached(
  82. Effect.gen(function* () {
  83. const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg"))
  84. if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system
  85. const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`)
  86. if (yield* fs.isFile(target).pipe(Effect.orDie)) return target
  87. const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
  88. const config = PLATFORM[platformKey]
  89. if (!config) throw new Error(`unsupported platform for ripgrep: ${platformKey}`)
  90. const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`
  91. const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`
  92. const archive = path.join(Global.Path.bin, filename)
  93. yield* Effect.logInfo("downloading ripgrep", { url })
  94. yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
  95. const bytes = yield* HttpClientRequest.get(url).pipe(
  96. http.execute,
  97. Effect.flatMap((response) => response.arrayBuffer),
  98. Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
  99. )
  100. if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)
  101. yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
  102. yield* extract(archive, config, target)
  103. yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
  104. return target
  105. }),
  106. ),
  107. })
  108. }),
  109. )
  110. export const defaultLayer = layer.pipe(
  111. Layer.provide(FetchHttpClient.layer),
  112. Layer.provide(FSUtil.defaultLayer),
  113. Layer.provide(CrossSpawnSpawner.defaultLayer),
  114. )
  115. export const node = makeGlobalNode({
  116. service: Service,
  117. layer: layer,
  118. deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node],
  119. })
  120. }