Skip to content

Commit 6ecdb87

Browse files
committed
-
1 parent 1bf584d commit 6ecdb87

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

scripts/prepare-llama-binaries.mjs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
#!/usr/bin/env node
2+
3+
import fs from 'node:fs/promises';
4+
import path from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
import { spawn } from 'node:child_process';
7+
8+
const __filename = fileURLToPath(import.meta.url);
9+
const __dirname = path.dirname(__filename);
10+
const repoRoot = path.resolve(__dirname, '..');
11+
const outputDir = path.join(repoRoot, 'example', 'bin');
12+
13+
const backend = process.env.LLAMA_BACKEND;
14+
if (!backend) {
15+
console.error('[llama-prep] LLAMA_BACKEND is required (e.g. win-cpu-x64, macos-arm64).');
16+
process.exit(1);
17+
}
18+
19+
const dryRun = process.argv.includes('--dry-run');
20+
const repo = process.env.LLAMA_CPP_REPO || 'ggml-org/llama.cpp';
21+
const tag = process.env.LLAMA_CPP_TAG?.trim() || null;
22+
23+
const apiBase = `https://api.github.com/repos/${repo}/releases`;
24+
const releaseUrl = tag ? `${apiBase}/tags/${encodeURIComponent(tag)}` : `${apiBase}/latest`;
25+
26+
function getAssetPatterns(target) {
27+
switch (target) {
28+
case 'win-cpu-x64':
29+
return [/^llama-.*-bin-win-cpu-x64\.zip$/i, /^llama-.*-bin-win-common_cpus-x64\.zip$/i];
30+
case 'win-arm64':
31+
return [/^llama-.*-bin-win-cpu-arm64\.zip$/i, /^llama-.*-bin-win-arm64\.zip$/i];
32+
case 'ubuntu-x64':
33+
return [
34+
/^llama-.*-bin-ubuntu-x64\.tar\.gz$/i,
35+
/^llama-.*-bin-linux-x64\.tar\.gz$/i,
36+
/^llama-.*-bin-linux-common_cpus-x64\.tar\.gz$/i,
37+
];
38+
case 'macos-arm64':
39+
return [/^llama-.*-bin-macos-arm64\.tar\.gz$/i, /^llama-.*-bin-darwin-arm64\.tar\.gz$/i];
40+
case 'macos-x64':
41+
return [/^llama-.*-bin-macos-x64\.tar\.gz$/i, /^llama-.*-bin-darwin-x64\.tar\.gz$/i];
42+
default:
43+
throw new Error(`Unsupported LLAMA_BACKEND "${target}"`);
44+
}
45+
}
46+
47+
function runCommand(command, args, cwd) {
48+
return new Promise((resolve, reject) => {
49+
const child = spawn(command, args, { cwd, stdio: 'inherit' });
50+
child.on('error', reject);
51+
child.on('exit', (code) => {
52+
if (code === 0) {
53+
resolve();
54+
} else {
55+
reject(new Error(`${command} exited with code ${code}`));
56+
}
57+
});
58+
});
59+
}
60+
61+
function runCommandCapture(command, args, cwd) {
62+
return new Promise((resolve, reject) => {
63+
const child = spawn(command, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
64+
let stdout = '';
65+
let stderr = '';
66+
child.stdout.on('data', (chunk) => {
67+
stdout += chunk.toString();
68+
});
69+
child.stderr.on('data', (chunk) => {
70+
stderr += chunk.toString();
71+
});
72+
child.on('error', reject);
73+
child.on('exit', (code) => {
74+
if (code === 0) {
75+
resolve({ stdout, stderr });
76+
} else {
77+
reject(new Error(`${command} exited with code ${code}: ${stderr}`));
78+
}
79+
});
80+
});
81+
}
82+
83+
async function fetchRelease() {
84+
const args = [
85+
'-fsSL',
86+
'-A',
87+
'oxide-lab-ci-llama-prep',
88+
'-H',
89+
'Accept: application/vnd.github+json',
90+
];
91+
if (process.env.GITHUB_TOKEN) {
92+
args.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`);
93+
}
94+
args.push(releaseUrl);
95+
const { stdout } = await runCommandCapture('curl', args, repoRoot);
96+
return JSON.parse(stdout);
97+
}
98+
99+
async function downloadToFile(url, destination) {
100+
await fs.mkdir(path.dirname(destination), { recursive: true });
101+
const args = [
102+
'-fL',
103+
'--retry',
104+
'3',
105+
'--retry-delay',
106+
'2',
107+
'--retry-connrefused',
108+
'-A',
109+
'oxide-lab-ci-llama-prep',
110+
'-H',
111+
'Accept: application/octet-stream',
112+
];
113+
if (process.env.GITHUB_TOKEN) {
114+
args.push('-H', `Authorization: Bearer ${process.env.GITHUB_TOKEN}`);
115+
}
116+
args.push('-o', destination, url);
117+
await runCommand('curl', args, repoRoot);
118+
}
119+
120+
async function fileExists(filePath) {
121+
try {
122+
await fs.access(filePath);
123+
return true;
124+
} catch {
125+
return false;
126+
}
127+
}
128+
129+
async function findLlamaServer(rootDir) {
130+
const queue = [rootDir];
131+
while (queue.length > 0) {
132+
const current = queue.pop();
133+
const entries = await fs.readdir(current, { withFileTypes: true });
134+
for (const entry of entries) {
135+
const fullPath = path.join(current, entry.name);
136+
if (entry.isDirectory()) {
137+
queue.push(fullPath);
138+
continue;
139+
}
140+
const lower = entry.name.toLowerCase();
141+
if (lower === 'llama-server' || lower === 'llama-server.exe') {
142+
return fullPath;
143+
}
144+
}
145+
}
146+
return null;
147+
}
148+
149+
async function main() {
150+
console.log(`[llama-prep] backend=${backend}`);
151+
console.log(`[llama-prep] release=${tag ?? 'latest'}`);
152+
153+
const release = await fetchRelease();
154+
const patterns = getAssetPatterns(backend);
155+
156+
const assets = Array.isArray(release.assets) ? release.assets : [];
157+
const selected = patterns
158+
.map((pattern) => assets.find((asset) => pattern.test(asset.name)))
159+
.find(Boolean);
160+
161+
if (!selected) {
162+
const available = assets.map((a) => a.name).join('\n');
163+
throw new Error(
164+
`[llama-prep] No matching asset for backend "${backend}" in release "${release.tag_name}".\nAvailable assets:\n${available}`,
165+
);
166+
}
167+
168+
console.log(`[llama-prep] release_tag=${release.tag_name}`);
169+
console.log(`[llama-prep] asset=${selected.name}`);
170+
171+
if (dryRun) {
172+
return;
173+
}
174+
175+
await fs.mkdir(outputDir, { recursive: true });
176+
const archivePath = path.join(outputDir, selected.name);
177+
178+
if (!(await fileExists(archivePath))) {
179+
console.log(`[llama-prep] downloading ${selected.browser_download_url}`);
180+
await downloadToFile(selected.browser_download_url, archivePath);
181+
} else {
182+
console.log(`[llama-prep] using cached archive ${archivePath}`);
183+
}
184+
185+
console.log(`[llama-prep] extracting ${selected.name}`);
186+
await runCommand('tar', ['-xf', archivePath, '-C', outputDir], repoRoot);
187+
188+
const serverPath = await findLlamaServer(outputDir);
189+
if (!serverPath) {
190+
throw new Error('[llama-prep] Extraction finished but llama-server binary was not found');
191+
}
192+
console.log(`[llama-prep] ready: ${serverPath}`);
193+
}
194+
195+
main().catch((error) => {
196+
console.error(error instanceof Error ? error.message : error);
197+
process.exit(1);
198+
});

0 commit comments

Comments
 (0)