From 6a3fd223fcdba4dab352d920478e26227502496f Mon Sep 17 00:00:00 2001 From: arc <@> Date: Sat, 4 Apr 2026 14:49:04 +0800 Subject: [PATCH] =?UTF-8?q?fix=20issues/114:=20bun=20install=E6=8A=A5?= =?UTF-8?q?=E9=94=99download-ripgrep=20MODULE=5FNOT=5FFOUND?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/download-ripgrep.ts | 205 ++++++++++++++++++++++++++++-------- 1 file changed, 159 insertions(+), 46 deletions(-) diff --git a/scripts/download-ripgrep.ts b/scripts/download-ripgrep.ts index fce392f6a..4582f38a4 100644 --- a/scripts/download-ripgrep.ts +++ b/scripts/download-ripgrep.ts @@ -6,9 +6,16 @@ * * Idempotent — skips download if the binary already exists. * Use --force to re-download. + * + * Environment: + * - HTTPS_PROXY / HTTP_PROXY — when set, download uses `undici` + EnvHttpProxyAgent. + * - RIPGREP_DOWNLOAD_BASE — override release URL prefix, e.g. mirror: + * `https://ghproxy.net/https://github.com/microsoft/ripgrep-prebuilt/releases/download/v15.0.1` */ -import { existsSync, mkdirSync, renameSync, rmSync, statSync } from 'fs' +import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync } from 'fs' +import { setDefaultResultOrder } from 'node:dns' +import { tmpdir } from 'os' import { chmodSync } from 'fs' import { spawnSync } from 'child_process' import * as path from 'path' @@ -17,8 +24,16 @@ import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) +// Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths. +try { + setDefaultResultOrder('ipv4first') +} catch { + /* ignore */ +} + const RG_VERSION = '15.0.1' -const BASE_URL = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` +const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` +const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, '') // --- Platform mapping --- @@ -97,10 +112,94 @@ function getBinaryPath(): string { // --- Download & extract --- +function proxyEnvSet(): boolean { + const v = (s: string | undefined) => (s ?? '').trim() + return !!( + v(process.env.HTTPS_PROXY) || + v(process.env.HTTP_PROXY) || + v(process.env.ALL_PROXY) || + v(process.env.https_proxy) || + v(process.env.http_proxy) + ) +} + +async function fetchRelease(url: string): Promise { + if (proxyEnvSet()) { + const { EnvHttpProxyAgent, fetch: undiciFetch } = await import('undici') + return (await undiciFetch(url, { + redirect: 'follow', + dispatcher: new EnvHttpProxyAgent(), + })) as unknown as Response + } + return await fetch(url, { redirect: 'follow' }) +} + +function tryPowerShellDownload(url: string, dest: string): boolean { + const u = url.replace(/'/g, "''") + const d = dest.replace(/'/g, "''") + const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing` + const result = spawnSync( + 'powershell.exe', + ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', cmd], + { stdio: 'pipe', windowsHide: true }, + ) + return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 +} + +function tryCurlDownload(url: string, dest: string): boolean { + const curl = process.platform === 'win32' ? 'curl.exe' : 'curl' + const result = spawnSync(curl, ['-fsSL', '-L', '--fail', '-o', dest, url], { + stdio: 'pipe', + windowsHide: true, + }) + return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 +} + +/** Bun `fetch` on Windows can fail while browser / WinINET still works — use subprocess fallbacks. */ +async function downloadUrlToBuffer(url: string): Promise { + const response = await fetchRelease(url) + if (!response.ok) { + throw new Error(`Download failed: ${response.status} ${response.statusText}`) + } + return Buffer.from(await response.arrayBuffer()) +} + +async function downloadUrlToBufferWithFallback(url: string): Promise { + let firstError: unknown + try { + return await downloadUrlToBuffer(url) + } catch (e) { + firstError = e + } + + const tmpRoot = path.join(tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`) + const tmpFile = path.join(tmpRoot, 'archive') + mkdirSync(tmpRoot, { recursive: true }) + try { + if (process.platform === 'win32' && tryPowerShellDownload(url, tmpFile)) { + return readFileSync(tmpFile) + } + if (tryCurlDownload(url, tmpFile)) { + return readFileSync(tmpFile) + } + } finally { + rmSync(tmpRoot, { recursive: true, force: true }) + } + + throw firstError +} + +function findZipEntryKey(files: Record, want: string): string | undefined { + return Object.keys(files).find(k => { + const norm = k.replace(/\\/g, '/') + return norm === want || norm.endsWith(`/${want}`) + }) +} + async function downloadAndExtract(): Promise { const { target, ext } = getPlatformMapping() const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}` - const downloadUrl = `${BASE_URL}/${assetName}` + const downloadUrl = `${RELEASE_BASE}/${assetName}` const binaryPath = getBinaryPath() const binaryDir = path.dirname(binaryPath) @@ -118,66 +217,80 @@ async function downloadAndExtract(): Promise { console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`) console.log(`[ripgrep] URL: ${downloadUrl}`) - // Prepare temp directory - const tmpDir = path.join(binaryDir, '.tmp-download') - rmSync(tmpDir, { recursive: true, force: true }) - mkdirSync(tmpDir, { recursive: true }) + const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg' + const { writeFileSync } = await import('fs') try { - const archivePath = path.join(tmpDir, assetName) - - // Download - const response = await fetch(downloadUrl, { redirect: 'follow' }) - if (!response.ok) { - throw new Error(`Download failed: ${response.status} ${response.statusText}`) - } - - const buffer = Buffer.from(await response.arrayBuffer()) - const { writeFileSync } = await import('fs') - writeFileSync(archivePath, buffer) + const buffer = await downloadUrlToBufferWithFallback(downloadUrl) console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`) - // Extract mkdirSync(binaryDir, { recursive: true }) if (ext === 'tar.gz') { - const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], { - stdio: 'pipe', - }) - if (result.status !== 0) { - throw new Error(`tar extract failed: ${result.stderr?.toString()}`) + const tmpDir = path.join(binaryDir, '.tmp-download') + rmSync(tmpDir, { recursive: true, force: true }) + mkdirSync(tmpDir, { recursive: true }) + try { + const archivePath = path.join(tmpDir, assetName) + writeFileSync(archivePath, buffer) + const result = spawnSync('tar', ['xzf', archivePath, '-C', tmpDir], { + stdio: 'pipe', + }) + if (result.status !== 0) { + throw new Error(`tar extract failed: ${result.stderr?.toString()}`) + } + const srcBinary = path.join(tmpDir, extractedBinary) + if (!existsSync(srcBinary)) { + throw new Error(`Binary not found at expected path: ${srcBinary}`) + } + renameSync(srcBinary, binaryPath) + } finally { + rmSync(tmpDir, { recursive: true, force: true }) } } else { - // .zip - const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], { - stdio: 'pipe', - }) - if (result.status !== 0) { - throw new Error(`unzip failed: ${result.stderr?.toString()}`) + try { + const { unzipSync } = await import('fflate') + const unzipped = unzipSync(new Uint8Array(buffer)) + const key = findZipEntryKey(unzipped, extractedBinary) + if (!key) { + throw new Error(`Binary ${extractedBinary} not found in zip`) + } + writeFileSync(binaryPath, Buffer.from(unzipped[key])) + } catch { + // No fflate or bad archive — try `unzip` CLI (common on Unix / Git for Windows) + const tmpDir = path.join(binaryDir, '.tmp-download') + rmSync(tmpDir, { recursive: true, force: true }) + mkdirSync(tmpDir, { recursive: true }) + try { + const archivePath = path.join(tmpDir, assetName) + writeFileSync(archivePath, buffer) + const result = spawnSync('unzip', ['-o', archivePath, '-d', tmpDir], { + stdio: 'pipe', + }) + if (result.status !== 0) { + throw new Error(`unzip failed: ${result.stderr?.toString()}`) + } + const srcBinary = path.join(tmpDir, extractedBinary) + if (!existsSync(srcBinary)) { + throw new Error(`Binary not found at expected path: ${srcBinary}`) + } + renameSync(srcBinary, binaryPath) + } finally { + rmSync(tmpDir, { recursive: true, force: true }) + } } } - // Find the rg binary in the extracted directory - // microsoft/ripgrep-prebuilt archives extract flat: ./rg (no subdirectory) - const extractedBinary = process.platform === 'win32' ? 'rg.exe' : 'rg' - const srcBinary = path.join(tmpDir, extractedBinary) - - if (!existsSync(srcBinary)) { - throw new Error(`Binary not found at expected path: ${srcBinary}`) - } - - // Move to final location - renameSync(srcBinary, binaryPath) - - // Make executable (non-Windows) if (process.platform !== 'win32') { chmodSync(binaryPath, 0o755) } console.log(`[ripgrep] Installed to ${binaryPath}`) - } finally { - // Cleanup temp directory - rmSync(tmpDir, { recursive: true, force: true }) + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + const hint = + 'Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header).' + throw new Error(`${msg} ${hint}`) } }