Skip to content

Commit ee0d930

Browse files
authored
fix(ripgrep): use @vscode/ripgrep package as the builtin source (Gitlawb#911) (Gitlawb#932)
The vendored-binary lookup at vendor/ripgrep/<arch>-<platform>/rg never resolved in this fork — that directory does not ship — so users without a system rg had no working fallback. Switch to the @vscode/ripgrep package so Microsoft maintains the platform/arch matrix and the binary is delivered via npm. - src/utils/ripgrep.ts: replace hand-rolled vendor-path resolution with rgPath from @vscode/ripgrep. Lazy require so a missing package falls through to the system rg branch instead of throwing at import. Drop builtinExists from the config args; builtinCommand is now a string-or-null. The system override (USE_BUILTIN_RIPGREP=0), the Bun-compiled standalone embedded mode, the macOS codesign hook, and all retry/timeout/error logic are preserved untouched. - scripts/build.ts: mark @vscode/ripgrep as external. The package resolves rgPath via __dirname at runtime, so bundling would freeze the build host's absolute path into dist/cli.mjs. - src/utils/ripgrep.test.ts: update for the new config shape and add tests covering USE_BUILTIN_RIPGREP=0, embedded mode, last-resort fallback, and null builtin path. Tested locally on Linux (Bun 1.3.13). macOS (codesign hook) and Windows (rg.exe extension) need contributor verification.
1 parent 0ca4333 commit ee0d930

5 files changed

Lines changed: 106 additions & 29 deletions

File tree

bun.lock

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
"@opentelemetry/sdk-trace-base": "2.6.1",
7575
"@opentelemetry/sdk-trace-node": "2.6.1",
7676
"@opentelemetry/semantic-conventions": "1.40.0",
77+
"@vscode/ripgrep": "^1.17.1",
7778
"ajv": "8.18.0",
7879
"auto-bind": "5.0.1",
7980
"axios": "1.15.0",

scripts/build.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,11 @@ ${exports}
472472
'@aws-sdk/credential-providers',
473473
'@azure/identity',
474474
'google-auth-library',
475+
// @vscode/ripgrep ships a platform-specific binary alongside its
476+
// index.js and resolves the path via __dirname at runtime. Bundling
477+
// would freeze the build host's absolute path into dist/cli.mjs, so we
478+
// keep it external and rely on the npm package being installed.
479+
'@vscode/ripgrep',
475480
],
476481
})
477482

src/utils/ripgrep.test.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,15 @@ import { resolveRipgrepConfig, wrapRipgrepUnavailableError } from './ripgrep.js'
55

66
const MOCK_BUILTIN_PATH = path.normalize(
77
process.platform === 'win32'
8-
? `vendor/ripgrep/${process.arch}-win32/rg.exe`
9-
: `vendor/ripgrep/${process.arch}-${process.platform}/rg`,
8+
? `node_modules/@vscode/ripgrep/bin/rg.exe`
9+
: `node_modules/@vscode/ripgrep/bin/rg`,
1010
)
1111

12-
test('ripgrepCommand falls back to system rg when builtin binary is missing', () => {
12+
test('falls back to system rg when @vscode/ripgrep cannot be resolved', () => {
1313
const config = resolveRipgrepConfig({
1414
userWantsSystemRipgrep: false,
1515
bundledMode: false,
16-
builtinCommand: MOCK_BUILTIN_PATH,
17-
builtinExists: false,
16+
builtinCommand: null,
1817
systemExecutablePath: '/usr/bin/rg',
1918
processExecPath: '/fake/bun',
2019
})
@@ -26,12 +25,11 @@ test('ripgrepCommand falls back to system rg when builtin binary is missing', ()
2625
})
2726
})
2827

29-
test('ripgrepCommand keeps builtin mode when bundled binary exists', () => {
28+
test('uses builtin @vscode/ripgrep path when the package resolves', () => {
3029
const config = resolveRipgrepConfig({
3130
userWantsSystemRipgrep: false,
3231
bundledMode: false,
3332
builtinCommand: MOCK_BUILTIN_PATH,
34-
builtinExists: true,
3533
systemExecutablePath: '/usr/bin/rg',
3634
processExecPath: '/fake/bun',
3735
})
@@ -43,10 +41,59 @@ test('ripgrepCommand keeps builtin mode when bundled binary exists', () => {
4341
})
4442
})
4543

44+
test('honors USE_BUILTIN_RIPGREP=0 by selecting system rg even when builtin is available', () => {
45+
const config = resolveRipgrepConfig({
46+
userWantsSystemRipgrep: true,
47+
bundledMode: false,
48+
builtinCommand: MOCK_BUILTIN_PATH,
49+
systemExecutablePath: '/usr/bin/rg',
50+
processExecPath: '/fake/bun',
51+
})
52+
53+
expect(config).toMatchObject({
54+
mode: 'system',
55+
command: 'rg',
56+
args: [],
57+
})
58+
})
59+
60+
test('keeps embedded mode for Bun-compiled standalone executables', () => {
61+
const config = resolveRipgrepConfig({
62+
userWantsSystemRipgrep: false,
63+
bundledMode: true,
64+
builtinCommand: null,
65+
systemExecutablePath: '/usr/bin/rg',
66+
processExecPath: '/opt/openclaude/bin/openclaude',
67+
})
68+
69+
expect(config).toMatchObject({
70+
mode: 'embedded',
71+
command: '/opt/openclaude/bin/openclaude',
72+
args: ['--no-config'],
73+
argv0: 'rg',
74+
})
75+
})
76+
77+
test('falls through to system rg as a last resort even when not on PATH', () => {
78+
const config = resolveRipgrepConfig({
79+
userWantsSystemRipgrep: false,
80+
bundledMode: false,
81+
builtinCommand: null,
82+
systemExecutablePath: 'rg',
83+
processExecPath: '/fake/bun',
84+
})
85+
86+
expect(config).toMatchObject({
87+
mode: 'system',
88+
command: 'rg',
89+
args: [],
90+
})
91+
})
92+
4693
test('wrapRipgrepUnavailableError explains missing packaged fallback', () => {
4794
const error = wrapRipgrepUnavailableError(
4895
{ code: 'ENOENT', message: 'spawn rg ENOENT' },
49-
{ mode: 'builtin', command: 'C:\\fake\\vendor\\ripgrep\\rg.exe', args: [] },
96+
{ mode: 'builtin', command: 'C:\\fake\\node_modules\\@vscode\\ripgrep\\bin\\rg.exe', args: [] },
5097
'win32',
5198
)
5299

src/utils/ripgrep.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import memoize from 'lodash-es/memoize.js'
55
import { homedir } from 'os'
66
import * as path from 'path'
77
import { logEvent } from 'src/services/analytics/index.js'
8-
import { fileURLToPath } from 'url'
98
import { isInBundledMode } from './bundledMode.js'
109
import { logForDebugging } from './debug.js'
1110
import { isEnvDefinedFalsy } from './envUtils.js'
@@ -15,13 +14,6 @@ import { logError } from './log.js'
1514
import { getPlatform } from './platform.js'
1615
import { countCharInString } from './stringUtils.js'
1716

18-
const __filename = fileURLToPath(import.meta.url)
19-
// we use node:path.join instead of node:url.resolve because the former doesn't encode spaces
20-
const __dirname = path.join(
21-
__filename,
22-
process.env.NODE_ENV === 'test' ? '../../../' : '../',
23-
)
24-
2517
type RipgrepConfig = {
2618
mode: 'system' | 'builtin' | 'embedded'
2719
command: string
@@ -35,11 +27,31 @@ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
3527
return error instanceof Error
3628
}
3729

30+
/**
31+
* Returns the ripgrep binary path provided by the @vscode/ripgrep package.
32+
* The package downloads a platform/arch-specific binary at npm install time
33+
* (cached under the package's bin/ directory). Returns null when the package
34+
* cannot be resolved — for example when running as a Bun-compiled standalone
35+
* executable that doesn't ship node_modules.
36+
*/
37+
function resolveBuiltinRgPath(): string | null {
38+
try {
39+
// Lazy require so the resolution failure path stays graceful at import
40+
// time. The package only exports `rgPath`, so we do not need the rest.
41+
const mod = require('@vscode/ripgrep') as { rgPath?: string }
42+
if (mod.rgPath && existsSync(mod.rgPath)) {
43+
return mod.rgPath
44+
}
45+
} catch {
46+
// Falls through to null — caller decides the fallback.
47+
}
48+
return null
49+
}
50+
3851
type ResolveRipgrepConfigArgs = {
3952
userWantsSystemRipgrep: boolean
4053
bundledMode: boolean
41-
builtinCommand: string
42-
builtinExists: boolean
54+
builtinCommand: string | null
4355
systemExecutablePath: string
4456
processExecPath?: string
4557
}
@@ -48,7 +60,6 @@ export function resolveRipgrepConfig({
4860
userWantsSystemRipgrep,
4961
bundledMode,
5062
builtinCommand,
51-
builtinExists,
5263
systemExecutablePath,
5364
processExecPath = process.execPath,
5465
}: ResolveRipgrepConfigArgs): RipgrepConfig {
@@ -66,35 +77,31 @@ export function resolveRipgrepConfig({
6677
}
6778
}
6879

69-
if (builtinExists) {
80+
if (builtinCommand) {
7081
return { mode: 'builtin', command: builtinCommand, args: [] }
7182
}
7283

7384
if (systemExecutablePath !== 'rg') {
7485
return { mode: 'system', command: 'rg', args: [] }
7586
}
7687

77-
return { mode: 'builtin', command: builtinCommand, args: [] }
88+
// Last resort — leaves error reporting to the executor when no binary
89+
// can be located. wrapRipgrepUnavailableError() surfaces an install hint.
90+
return { mode: 'system', command: 'rg', args: [] }
7891
}
7992

8093
const getRipgrepConfig = memoize((): RipgrepConfig => {
8194
const userWantsSystemRipgrep = isEnvDefinedFalsy(
8295
process.env.USE_BUILTIN_RIPGREP,
8396
)
8497
const bundledMode = isInBundledMode()
85-
const rgRoot = path.resolve(__dirname, 'vendor', 'ripgrep')
86-
const builtinCommand =
87-
process.platform === 'win32'
88-
? path.resolve(rgRoot, `${process.arch}-win32`, 'rg.exe')
89-
: path.resolve(rgRoot, `${process.arch}-${process.platform}`, 'rg')
90-
const builtinExists = existsSync(builtinCommand)
98+
const builtinCommand = resolveBuiltinRgPath()
9199
const { cmd: systemExecutablePath } = findExecutable('rg', [])
92100

93101
return resolveRipgrepConfig({
94102
userWantsSystemRipgrep,
95103
bundledMode,
96104
builtinCommand,
97-
builtinExists,
98105
systemExecutablePath,
99106
})
100107
})

0 commit comments

Comments
 (0)