cc-haha/desktop/scripts/prepare-ripgrep.ts
2026-07-14 22:11:22 +08:00

188 lines
6.4 KiB
TypeScript

import { createHash } from 'node:crypto'
import {
chmod,
copyFile,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
export const RIPGREP_VERSION = '15.1.0'
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url))
type RipgrepAsset = {
assetTriple: string
archiveName: string
sha256: string
executableName: 'rg' | 'rg.exe'
}
const RIPGREP_ASSETS: Record<string, RipgrepAsset> = {
'aarch64-apple-darwin': {
assetTriple: 'aarch64-apple-darwin',
archiveName: `ripgrep-${RIPGREP_VERSION}-aarch64-apple-darwin.tar.gz`,
sha256: '378e973289176ca0c6054054ee7f631a065874a352bf43f0fa60ef079b6ba715',
executableName: 'rg',
},
'x86_64-apple-darwin': {
assetTriple: 'x86_64-apple-darwin',
archiveName: `ripgrep-${RIPGREP_VERSION}-x86_64-apple-darwin.tar.gz`,
sha256: '64811cb24e77cac3057d6c40b63ac9becf9082eedd54ca411b475b755d334882',
executableName: 'rg',
},
'aarch64-pc-windows-msvc': {
assetTriple: 'aarch64-pc-windows-msvc',
archiveName: `ripgrep-${RIPGREP_VERSION}-aarch64-pc-windows-msvc.zip`,
sha256: '00d931fb5237c9696ca49308818edb76d8eb6fc132761cb2a1bd616b2df02f8e',
executableName: 'rg.exe',
},
'x86_64-pc-windows-msvc': {
assetTriple: 'x86_64-pc-windows-msvc',
archiveName: `ripgrep-${RIPGREP_VERSION}-x86_64-pc-windows-msvc.zip`,
sha256: '124510b94b6baa3380d051fdf4650eaa80a302c876d611e9dba0b2e18d87493a',
executableName: 'rg.exe',
},
'aarch64-unknown-linux-gnu': {
assetTriple: 'aarch64-unknown-linux-gnu',
archiveName: `ripgrep-${RIPGREP_VERSION}-aarch64-unknown-linux-gnu.tar.gz`,
sha256: '2b661c6ef508e902f388e9098d9c4c5aca72c87b55922d94abdba830b4dc885e',
executableName: 'rg',
},
// The official musl build is static PIE and is the most portable x64 Linux asset.
'x86_64-unknown-linux-gnu': {
assetTriple: 'x86_64-unknown-linux-musl',
archiveName: `ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz`,
sha256: '1c9297be4a084eea7ecaedf93eb03d058d6faae29bbc57ecdaf5063921491599',
executableName: 'rg',
},
}
export function getRipgrepAsset(targetTriple: string): RipgrepAsset {
const asset = RIPGREP_ASSETS[targetTriple]
if (!asset) {
throw new Error(`[prepare-ripgrep] Unsupported target triple: ${targetTriple}`)
}
return asset
}
export function getBundledRipgrepName(targetTriple: string): string {
return targetTriple.includes('windows') ? 'rg.exe' : 'rg'
}
export function getRipgrepDownloadUrl(targetTriple: string): string {
const asset = getRipgrepAsset(targetTriple)
return `https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/${asset.archiveName}`
}
export async function prepareRipgrep({
targetTriple,
archivePath = process.env.CC_HAHA_RIPGREP_ARCHIVE,
}: {
targetTriple: string
archivePath?: string
}): Promise<string> {
const asset = getRipgrepAsset(targetTriple)
const desktopRoot = path.resolve(scriptDirectory, '..')
const binariesDir = path.join(desktopRoot, 'src-tauri', 'binaries')
const temporaryDir = await mkdtemp(path.join(tmpdir(), 'cc-haha-ripgrep-'))
try {
const downloadedArchive = path.join(temporaryDir, asset.archiveName)
if (archivePath) {
await copyFile(path.resolve(archivePath), downloadedArchive)
} else {
const response = await fetch(getRipgrepDownloadUrl(targetTriple), {
redirect: 'follow',
})
if (!response.ok) {
throw new Error(
`[prepare-ripgrep] Download failed (${response.status} ${response.statusText})`,
)
}
await writeFile(downloadedArchive, Buffer.from(await response.arrayBuffer()))
}
const archiveHash = createHash('sha256')
.update(await readFile(downloadedArchive))
.digest('hex')
if (archiveHash !== asset.sha256) {
throw new Error(
`[prepare-ripgrep] SHA256 mismatch for ${asset.archiveName}: expected ${asset.sha256}, got ${archiveHash}`,
)
}
const extractDir = path.join(temporaryDir, 'extracted')
await mkdir(extractDir, { recursive: true })
const extract = Bun.spawn(['tar', '-xf', downloadedArchive, '-C', extractDir], {
stdout: 'inherit',
stderr: 'inherit',
})
const extractExit = await extract.exited
if (extractExit !== 0) {
throw new Error(`[prepare-ripgrep] Failed to extract ${asset.archiveName} (exit ${extractExit})`)
}
const archiveRoot = path.join(
extractDir,
`ripgrep-${RIPGREP_VERSION}-${asset.assetTriple}`,
)
const extractedExecutable = path.join(archiveRoot, asset.executableName)
const destination = path.join(binariesDir, getBundledRipgrepName(targetTriple))
await mkdir(binariesDir, { recursive: true })
for (const entry of await readdir(binariesDir)) {
if (
(entry === 'rg' || entry === 'rg.exe' || entry.startsWith('rg-')) &&
entry !== path.basename(destination)
) {
await rm(path.join(binariesDir, entry), { force: true })
}
}
await copyFile(extractedExecutable, destination)
await chmod(destination, 0o755)
await writeFile(
path.join(binariesDir, 'ripgrep-manifest.json'),
`${JSON.stringify({
version: RIPGREP_VERSION,
targetTriple,
archiveName: asset.archiveName,
sha256: asset.sha256,
}, null, 2)}\n`,
)
const licensesDir = path.join(binariesDir, 'ripgrep-licenses')
await mkdir(licensesDir, { recursive: true })
await Promise.all([
copyFile(path.join(archiveRoot, 'COPYING'), path.join(licensesDir, 'COPYING')),
copyFile(path.join(archiveRoot, 'LICENSE-MIT'), path.join(licensesDir, 'LICENSE-MIT')),
copyFile(path.join(archiveRoot, 'UNLICENSE'), path.join(licensesDir, 'UNLICENSE')),
])
console.log(`[prepare-ripgrep] ${destination}`)
return destination
} finally {
await rm(temporaryDir, { recursive: true, force: true })
}
}
function parseTargetTriple(argv: string[]): string | null {
const index = argv.indexOf('--target-triple')
if (index >= 0) return argv[index + 1] ?? null
return process.env.SIDECAR_TARGET_TRIPLE ?? null
}
if (import.meta.main) {
const targetTriple = parseTargetTriple(process.argv.slice(2))
if (!targetTriple) {
throw new Error('[prepare-ripgrep] Pass --target-triple or set SIDECAR_TARGET_TRIPLE')
}
await prepareRipgrep({ targetTriple })
}