Skip to content

Commit 252a00d

Browse files
authored
Fix cross-platform ripgrep support for MCPB bundles (#262)
* feat: Add cross-platform ripgrep support for MCPB bundles - Add download-all-ripgrep.cjs script to download binaries for all platforms - Create ripgrep-wrapper.js for runtime platform detection - Update build-mcpb.cjs to download and bundle all ripgrep binaries - Replace @vscode/ripgrep index.js with platform-aware wrapper - Fixes Windows install error: spawn rg.exe ENOENT This ensures the MCPB bundle works on macOS (x64/ARM), Windows (x64/ARM/x86), and Linux (x64/ARM/ARM64/PPC64/S390X) without requiring platform-specific builds. * docs: Add documentation for cross-platform ripgrep support * fix: Correct ripgrep binary path resolution in wrapper The wrapper is installed at lib/index.js, so __dirname points to lib/. Need to go up one directory (../) to reach the bin/ folder. * Few fixes
1 parent 0fb34fa commit 252a00d

File tree

5 files changed

+307
-3
lines changed

5 files changed

+307
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@ mcpb-bundle/
5454
# Test files
5555
test_files/
5656
test_fix.js
57-
test_search.js
57+
test_search.js
58+
.ripgrep-downloads/

package-lock.json

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

scripts/build-mcpb.cjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ const MANIFEST_PATH = path.join(BUNDLE_DIR, 'manifest.json');
2121

2222
console.log('🏗️ Building Desktop Commander MCPB Bundle...');
2323

24+
// Step 0: Download all ripgrep binaries for cross-platform support
25+
console.log('🌍 Downloading ripgrep binaries for all platforms...');
26+
try {
27+
execSync('node scripts/download-all-ripgrep.cjs', { cwd: PROJECT_ROOT, stdio: 'inherit' });
28+
console.log('✅ Ripgrep binaries downloaded');
29+
} catch (error) {
30+
console.error('❌ Failed to download ripgrep binaries:', error.message);
31+
process.exit(1);
32+
}
33+
2434
// Step 1: Build the TypeScript project
2535
console.log('📦 Building TypeScript project...');
2636
try {
@@ -131,6 +141,40 @@ try {
131141
process.exit(1);
132142
}
133143

144+
// Step 6c: Copy platform-specific ripgrep binaries and wrapper
145+
console.log('🔧 Setting up cross-platform ripgrep support...');
146+
try {
147+
const ripgrepBinSrc = path.join(PROJECT_ROOT, 'node_modules/@vscode/ripgrep/bin');
148+
const ripgrepBinDest = path.join(BUNDLE_DIR, 'node_modules/@vscode/ripgrep/bin');
149+
const ripgrepWrapperSrc = path.join(PROJECT_ROOT, 'scripts/ripgrep-wrapper.js');
150+
const ripgrepIndexDest = path.join(BUNDLE_DIR, 'node_modules/@vscode/ripgrep/lib/index.js');
151+
152+
// Ensure bin directory exists
153+
if (!fs.existsSync(ripgrepBinDest)) {
154+
fs.mkdirSync(ripgrepBinDest, { recursive: true });
155+
}
156+
157+
// Copy all platform-specific ripgrep binaries
158+
const binaries = fs.readdirSync(ripgrepBinSrc).filter(f => f.startsWith('rg-'));
159+
binaries.forEach(binary => {
160+
const src = path.join(ripgrepBinSrc, binary);
161+
const dest = path.join(ripgrepBinDest, binary);
162+
fs.copyFileSync(src, dest);
163+
// Preserve executable permissions
164+
if (!binary.endsWith('.exe')) {
165+
fs.chmodSync(dest, 0o755);
166+
}
167+
});
168+
console.log(`✅ Copied ${binaries.length} ripgrep binaries`);
169+
170+
// Replace index.js with our wrapper
171+
fs.copyFileSync(ripgrepWrapperSrc, ripgrepIndexDest);
172+
console.log('✅ Installed ripgrep runtime wrapper');
173+
} catch (error) {
174+
console.error('❌ Failed to setup ripgrep:', error.message);
175+
process.exit(1);
176+
}
177+
134178
// Step 7: Validate manifest
135179
console.log('🔍 Validating manifest...');
136180
try {

scripts/download-all-ripgrep.cjs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Download all ripgrep binaries for cross-platform MCPB bundles
5+
*
6+
* This script downloads ripgrep binaries for all supported platforms
7+
* and places them in node_modules/@vscode/ripgrep/bin/ with platform-specific names.
8+
*/
9+
10+
const https = require('https');
11+
const fs = require('fs');
12+
const path = require('path');
13+
const { promisify } = require('util');
14+
const yauzl = require('yauzl');
15+
const { pipeline } = require('stream');
16+
17+
const streamPipeline = promisify(pipeline);
18+
19+
// Ripgrep versions from @vscode/ripgrep
20+
const VERSION = 'v13.0.0-10';
21+
const MULTI_ARCH_VERSION = 'v13.0.0-4';
22+
23+
// All supported platforms
24+
const PLATFORMS = [
25+
{ name: 'x86_64-apple-darwin', ext: 'tar.gz', version: VERSION },
26+
{ name: 'aarch64-apple-darwin', ext: 'tar.gz', version: VERSION },
27+
{ name: 'x86_64-pc-windows-msvc', ext: 'zip', version: VERSION },
28+
{ name: 'aarch64-pc-windows-msvc', ext: 'zip', version: VERSION },
29+
{ name: 'i686-pc-windows-msvc', ext: 'zip', version: VERSION },
30+
{ name: 'x86_64-unknown-linux-musl', ext: 'tar.gz', version: VERSION },
31+
{ name: 'aarch64-unknown-linux-musl', ext: 'tar.gz', version: VERSION },
32+
{ name: 'i686-unknown-linux-musl', ext: 'tar.gz', version: VERSION },
33+
{ name: 'arm-unknown-linux-gnueabihf', ext: 'tar.gz', version: MULTI_ARCH_VERSION },
34+
{ name: 'powerpc64le-unknown-linux-gnu', ext: 'tar.gz', version: MULTI_ARCH_VERSION },
35+
{ name: 's390x-unknown-linux-gnu', ext: 'tar.gz', version: VERSION }
36+
];
37+
38+
const TEMP_DIR = path.join(__dirname, '../.ripgrep-downloads');
39+
const OUTPUT_DIR = path.join(__dirname, '../node_modules/@vscode/ripgrep/bin');
40+
41+
function ensureDir(dir) {
42+
if (!fs.existsSync(dir)) {
43+
fs.mkdirSync(dir, { recursive: true });
44+
}
45+
}
46+
47+
async function downloadFile(url, dest) {
48+
return new Promise((resolve, reject) => {
49+
console.log(` 📥 ${path.basename(dest)}...`);
50+
51+
https.get(url, {
52+
headers: {
53+
'User-Agent': 'vscode-ripgrep',
54+
'Accept': 'application/octet-stream'
55+
}
56+
}, (response) => {
57+
if (response.statusCode === 302 || response.statusCode === 301) {
58+
// Follow redirect
59+
downloadFile(response.headers.location, dest).then(resolve).catch(reject);
60+
return;
61+
}
62+
63+
if (response.statusCode !== 200) {
64+
reject(new Error(`Failed to download: ${response.statusCode}`));
65+
return;
66+
}
67+
68+
const file = fs.createWriteStream(dest);
69+
response.pipe(file);
70+
71+
file.on('finish', () => {
72+
file.close();
73+
resolve();
74+
});
75+
76+
file.on('error', (err) => {
77+
fs.unlink(dest, () => reject(err));
78+
});
79+
}).on('error', reject);
80+
});
81+
}
82+
83+
async function extractZip(zipPath, outputDir, targetBinary) {
84+
return new Promise((resolve, reject) => {
85+
yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => {
86+
if (err) return reject(err);
87+
88+
zipfile.readEntry();
89+
zipfile.on('entry', (entry) => {
90+
if (entry.fileName.endsWith('rg.exe')) {
91+
zipfile.openReadStream(entry, (err, readStream) => {
92+
if (err) return reject(err);
93+
94+
const outputPath = path.join(outputDir, targetBinary);
95+
const writeStream = fs.createWriteStream(outputPath);
96+
97+
readStream.pipe(writeStream);
98+
writeStream.on('close', () => {
99+
zipfile.close();
100+
resolve();
101+
});
102+
writeStream.on('error', reject);
103+
});
104+
} else {
105+
zipfile.readEntry();
106+
}
107+
});
108+
109+
zipfile.on('end', resolve);
110+
zipfile.on('error', reject);
111+
});
112+
});
113+
}
114+
115+
async function extractTarGz(tarPath, outputDir, targetBinary) {
116+
return new Promise((resolve, reject) => {
117+
const { execSync } = require('child_process');
118+
const tempExtractDir = path.join(TEMP_DIR, 'extract-' + Date.now());
119+
120+
try {
121+
ensureDir(tempExtractDir);
122+
123+
// Extract tar.gz
124+
execSync(`tar -xzf "${tarPath}" -C "${tempExtractDir}"`, { stdio: 'pipe' });
125+
126+
// Find the rg binary
127+
const files = fs.readdirSync(tempExtractDir, { recursive: true });
128+
const rgFile = files.find(f => f.endsWith('/rg') || f === 'rg');
129+
130+
if (rgFile) {
131+
const sourcePath = path.join(tempExtractDir, rgFile);
132+
const destPath = path.join(outputDir, targetBinary);
133+
fs.copyFileSync(sourcePath, destPath);
134+
fs.chmodSync(destPath, 0o755);
135+
}
136+
137+
// Cleanup
138+
fs.rmSync(tempExtractDir, { recursive: true, force: true });
139+
resolve();
140+
} catch (error) {
141+
reject(error);
142+
}
143+
});
144+
}
145+
146+
async function downloadAndExtractPlatform(platform) {
147+
const { name, ext, version } = platform;
148+
const fileName = `ripgrep-${version}-${name}.${ext}`;
149+
const url = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/${version}/${fileName}`;
150+
const tempFile = path.join(TEMP_DIR, fileName);
151+
152+
// Determine output binary name
153+
const isWindows = name.includes('windows');
154+
const binaryName = isWindows ? `rg-${name}.exe` : `rg-${name}`;
155+
const outputPath = path.join(OUTPUT_DIR, binaryName);
156+
157+
// Skip if binary already exists
158+
if (fs.existsSync(outputPath)) {
159+
console.log(` ✓ ${name} (cached)`);
160+
return;
161+
}
162+
163+
try {
164+
// Download if not already cached
165+
if (!fs.existsSync(tempFile)) {
166+
await downloadFile(url, tempFile);
167+
}
168+
169+
// Extract
170+
ensureDir(OUTPUT_DIR);
171+
172+
if (ext === 'zip') {
173+
await extractZip(tempFile, OUTPUT_DIR, binaryName);
174+
} else {
175+
await extractTarGz(tempFile, OUTPUT_DIR, binaryName);
176+
}
177+
178+
console.log(` ✓ ${name}`);
179+
} catch (error) {
180+
console.error(` ✗ Failed: ${name} - ${error.message}`);
181+
throw error;
182+
}
183+
}
184+
185+
async function main() {
186+
console.log('🌍 Downloading ripgrep binaries for all platforms...\n');
187+
188+
ensureDir(TEMP_DIR);
189+
ensureDir(OUTPUT_DIR);
190+
191+
// Download and extract all platforms
192+
for (const platform of PLATFORMS) {
193+
await downloadAndExtractPlatform(platform);
194+
}
195+
196+
console.log('\n✅ All ripgrep binaries downloaded successfully!');
197+
console.log(`📁 Binaries location: ${OUTPUT_DIR}`);
198+
console.log('\nFiles created:');
199+
const files = fs.readdirSync(OUTPUT_DIR).filter(f => f.startsWith('rg-'));
200+
files.forEach(f => console.log(` - ${f}`));
201+
}
202+
203+
// Run if called directly
204+
if (require.main === module) {
205+
main().catch((error) => {
206+
console.error('❌ Error:', error);
207+
process.exit(1);
208+
});
209+
}
210+
211+
module.exports = { downloadAndExtractPlatform, PLATFORMS };

scripts/ripgrep-wrapper.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Runtime platform detection wrapper for @vscode/ripgrep
2+
// This replaces the original index.js to support cross-platform MCPB bundles
3+
4+
const os = require('os');
5+
const path = require('path');
6+
const fs = require('fs');
7+
8+
function getTarget() {
9+
const arch = process.env.npm_config_arch || os.arch();
10+
11+
switch (os.platform()) {
12+
case 'darwin':
13+
return arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin';
14+
case 'win32':
15+
return arch === 'x64' ? 'x86_64-pc-windows-msvc' :
16+
arch === 'arm64' ? 'aarch64-pc-windows-msvc' :
17+
'i686-pc-windows-msvc';
18+
case 'linux':
19+
return arch === 'x64' ? 'x86_64-unknown-linux-musl' :
20+
arch === 'arm' ? 'arm-unknown-linux-gnueabihf' :
21+
arch === 'armv7l' ? 'arm-unknown-linux-gnueabihf' :
22+
arch === 'arm64' ? 'aarch64-unknown-linux-musl' :
23+
arch === 'ppc64' ? 'powerpc64le-unknown-linux-gnu' :
24+
arch === 's390x' ? 's390x-unknown-linux-gnu' :
25+
'i686-unknown-linux-musl';
26+
default:
27+
throw new Error('Unknown platform: ' + os.platform());
28+
}
29+
}
30+
31+
const target = getTarget();
32+
const isWindows = os.platform() === 'win32';
33+
const binaryName = isWindows ? `rg-${target}.exe` : `rg-${target}`;
34+
// __dirname is lib/, so go up one level to reach bin/
35+
const rgPath = path.join(__dirname, '..', 'bin', binaryName);
36+
37+
// Verify binary exists
38+
if (!fs.existsSync(rgPath)) {
39+
// Try fallback to original rg location
40+
const fallbackPath = path.join(__dirname, '..', 'bin', isWindows ? 'rg.exe' : 'rg');
41+
if (fs.existsSync(fallbackPath)) {
42+
module.exports.rgPath = fallbackPath;
43+
} else {
44+
throw new Error(`ripgrep binary not found for platform ${target}: ${rgPath}`);
45+
}
46+
} else {
47+
module.exports.rgPath = rgPath;
48+
}

0 commit comments

Comments
 (0)