| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import path from "path"
- import { Context, Effect, Layer, Stream } from "effect"
- import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
- import { ChildProcess } from "effect/unstable/process"
- import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"
- import { CrossSpawnSpawner } from "../cross-spawn-spawner"
- import { makeGlobalNode } from "../effect/app-node"
- import { httpClient } from "../effect/app-node-platform"
- import { FSUtil } from "../fs-util"
- import { Global } from "../global"
- import { which } from "../util/which"
- export namespace RipgrepBinary {
- const VERSION = "15.1.0"
- const PLATFORM = {
- "arm64-darwin": { platform: "aarch64-apple-darwin", extension: "tar.gz" },
- "arm64-linux": { platform: "aarch64-unknown-linux-gnu", extension: "tar.gz" },
- "x64-darwin": { platform: "x86_64-apple-darwin", extension: "tar.gz" },
- "x64-linux": { platform: "x86_64-unknown-linux-musl", extension: "tar.gz" },
- "arm64-win32": { platform: "aarch64-pc-windows-msvc", extension: "zip" },
- "ia32-win32": { platform: "i686-pc-windows-msvc", extension: "zip" },
- "x64-win32": { platform: "x86_64-pc-windows-msvc", extension: "zip" },
- } as const
- interface Interface {
- readonly filepath: Effect.Effect<string, Error>
- }
- export class Service extends Context.Service<Service, Interface>()("@opencode/RipgrepBinary") {}
- export const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const fs = yield* FSUtil.Service
- const http = HttpClient.filterStatusOk(yield* HttpClient.HttpClient)
- const spawner = yield* ChildProcessSpawner
- const run = Effect.fnUntraced(function* (command: string, args: string[]) {
- const handle = yield* spawner.spawn(ChildProcess.make(command, args, { extendEnv: true, stdin: "ignore" }))
- const [stdout, stderr, code] = yield* Effect.all(
- [
- Stream.mkString(Stream.decodeText(handle.stdout)),
- Stream.mkString(Stream.decodeText(handle.stderr)),
- handle.exitCode,
- ],
- { concurrency: "unbounded" },
- )
- return { stdout, stderr, code }
- }, Effect.scoped)
- const extract = Effect.fnUntraced(function* (
- archive: string,
- config: (typeof PLATFORM)[keyof typeof PLATFORM],
- target: string,
- ) {
- const dir = yield* fs.makeTempDirectoryScoped({ directory: Global.Path.bin, prefix: "ripgrep-" })
- if (config.extension === "zip") {
- const shell = (yield* Effect.sync(() => which("powershell.exe") ?? which("pwsh.exe"))) ?? "powershell.exe"
- const result = yield* run(shell, [
- "-NoProfile",
- "-NonInteractive",
- "-Command",
- `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -LiteralPath '${archive.replaceAll("'", "''")}' -DestinationPath '${dir.replaceAll("'", "''")}' -Force`,
- ])
- if (result.code !== 0)
- throw new Error(
- result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
- )
- }
- if (config.extension === "tar.gz") {
- const result = yield* run("tar", ["-xzf", archive, "-C", dir])
- if (result.code !== 0)
- throw new Error(
- result.stderr.trim() || result.stdout.trim() || `ripgrep extraction failed with code ${result.code}`,
- )
- }
- const extracted = path.join(
- dir,
- `ripgrep-${VERSION}-${config.platform}`,
- process.platform === "win32" ? "rg.exe" : "rg",
- )
- if (!(yield* fs.isFile(extracted))) throw new Error(`ripgrep archive did not contain executable: ${extracted}`)
- yield* fs.copyFile(extracted, target)
- if (process.platform !== "win32") yield* fs.chmod(target, 0o755)
- }, Effect.scoped)
- return Service.of({
- filepath: yield* Effect.cached(
- Effect.gen(function* () {
- const system = yield* Effect.sync(() => which(process.platform === "win32" ? "rg.exe" : "rg"))
- if (system && (yield* fs.isFile(system).pipe(Effect.orDie))) return system
- const target = path.join(Global.Path.bin, `rg${process.platform === "win32" ? ".exe" : ""}`)
- if (yield* fs.isFile(target).pipe(Effect.orDie)) return target
- const platformKey = `${process.arch}-${process.platform}` as keyof typeof PLATFORM
- const config = PLATFORM[platformKey]
- if (!config) throw new Error(`unsupported platform for ripgrep: ${platformKey}`)
- const filename = `ripgrep-${VERSION}-${config.platform}.${config.extension}`
- const url = `https://github.com/BurntSushi/ripgrep/releases/download/${VERSION}/${filename}`
- const archive = path.join(Global.Path.bin, filename)
- yield* Effect.logInfo("downloading ripgrep", { url })
- yield* fs.ensureDir(Global.Path.bin).pipe(Effect.orDie)
- const bytes = yield* HttpClientRequest.get(url).pipe(
- http.execute,
- Effect.flatMap((response) => response.arrayBuffer),
- Effect.mapError((cause) => (cause instanceof Error ? cause : new Error(String(cause)))),
- )
- if (bytes.byteLength === 0) throw new Error(`failed to download ripgrep from ${url}`)
- yield* fs.writeWithDirs(archive, new Uint8Array(bytes))
- yield* extract(archive, config, target)
- yield* fs.remove(archive, { force: true }).pipe(Effect.ignore)
- return target
- }),
- ),
- })
- }),
- )
- export const defaultLayer = layer.pipe(
- Layer.provide(FetchHttpClient.layer),
- Layer.provide(FSUtil.defaultLayer),
- Layer.provide(CrossSpawnSpawner.defaultLayer),
- )
- export const node = makeGlobalNode({
- service: Service,
- layer: layer,
- deps: [FSUtil.node, httpClient, CrossSpawnSpawner.node],
- })
- }
|