@prometheus-ai/natives 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -0
- package/native/embedded-addon.js +31 -0
- package/native/index.d.ts +1405 -0
- package/native/index.js +124 -0
- package/native/loader-state.d.ts +65 -0
- package/native/loader-state.js +598 -0
- package/package.json +75 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import * as childProcess from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as zlib from "node:zlib";
|
|
7
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
8
|
+
import { embeddedAddon } from "./embedded-addon.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Native addon loader for `@prometheus-ai/natives`.
|
|
12
|
+
*
|
|
13
|
+
* Owns every step between "Node imports `native/index.js`" and "the right
|
|
14
|
+
* `prometheus_natives.<platform>-<arch>*.node` is required, validated, and returned":
|
|
15
|
+
* platform/variant detection, candidate-path resolution, on-disk staging from
|
|
16
|
+
* `node_modules` (Windows update safety), embedded-addon extraction (Bun
|
|
17
|
+
* standalone binaries), version-sentinel validation, and the aggregated error
|
|
18
|
+
* surface for diagnostic-friendly failures.
|
|
19
|
+
*
|
|
20
|
+
* `native/index.js` is reduced to one `loadNative()` call plus the generated
|
|
21
|
+
* surface-area exports between `MARKER_START`/`MARKER_END` (rewritten by
|
|
22
|
+
* `scripts/gen-enums.ts`); everything else lives here so the pure helpers stay
|
|
23
|
+
* unit-testable without triggering the side-effectful module-load path.
|
|
24
|
+
*
|
|
25
|
+
* Background (issue #823): `bun build --compile --define PROMETHEUS_COMPILED=true`
|
|
26
|
+
* substitutes the bare identifier `PROMETHEUS_COMPILED`, NOT `process.env.PROMETHEUS_COMPILED`,
|
|
27
|
+
* so a runtime read of the env var returns `undefined`. Older CommonJS loader
|
|
28
|
+
* code also saw the original build-host absolute path in `__filename`; ESM
|
|
29
|
+
* `import.meta.url` is rewritten to the bunfs URL. The embedded-addon
|
|
30
|
+
* presence (true iff the build pipeline ran `embed:native`, false in the
|
|
31
|
+
* post-build `--reset` stub) is the authoritative compiled-mode signal.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const SUPPORTED_PLATFORMS = ["linux-x64", "linux-arm64", "darwin-x64", "darwin-arm64", "win32-x64"];
|
|
35
|
+
|
|
36
|
+
function getNativesDir() {
|
|
37
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
38
|
+
if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "prometheus"))) {
|
|
39
|
+
return path.join(xdgDataHome, "prometheus", "natives");
|
|
40
|
+
}
|
|
41
|
+
return path.join(os.homedir(), ".prometheus", "natives");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveLeafPackageDir(platformTag) {
|
|
45
|
+
try {
|
|
46
|
+
const require_ = createRequire(import.meta.url);
|
|
47
|
+
return path.dirname(require_.resolve(`@prometheus-ai/natives-${platformTag}/package.json`));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =========================================================================
|
|
54
|
+
// Pure helpers — re-exported for unit tests in `packages/natives/test/`.
|
|
55
|
+
// =========================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {{
|
|
59
|
+
* embeddedAddon: { platformTag: string; version: string; files: unknown[] } | null | undefined;
|
|
60
|
+
* env: Record<string, string | undefined>;
|
|
61
|
+
* importMetaUrl: string | null | undefined;
|
|
62
|
+
* }} input
|
|
63
|
+
* @returns {boolean}
|
|
64
|
+
*/
|
|
65
|
+
export function detectCompiledBinary({ embeddedAddon, env, importMetaUrl }) {
|
|
66
|
+
if (embeddedAddon) return true;
|
|
67
|
+
if (env && env.PROMETHEUS_COMPILED) return true;
|
|
68
|
+
if (typeof importMetaUrl === "string") {
|
|
69
|
+
if (importMetaUrl.includes("$bunfs")) return true;
|
|
70
|
+
if (importMetaUrl.includes("~BUN")) return true;
|
|
71
|
+
if (importMetaUrl.includes("%7EBUN")) return true;
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {{ tag: string; arch: string; variant: "modern" | "baseline" | null | undefined }} input
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
export function getAddonFilenames({ tag, arch, variant }) {
|
|
81
|
+
const defaultFilename = `prometheus_natives.${tag}.node`;
|
|
82
|
+
if (arch !== "x64" || !variant) return [defaultFilename];
|
|
83
|
+
const baselineFilename = `prometheus_natives.${tag}-baseline.node`;
|
|
84
|
+
const modernFilename = `prometheus_natives.${tag}-modern.node`;
|
|
85
|
+
if (variant === "modern") {
|
|
86
|
+
return [modernFilename, baselineFilename, defaultFilename];
|
|
87
|
+
}
|
|
88
|
+
return [baselineFilename, defaultFilename];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decide whether the loader should mirror the package's `native/<filename>.node`
|
|
93
|
+
* into the per-version cache directory (`~/.prometheus/natives/<version>/`) before loading.
|
|
94
|
+
*
|
|
95
|
+
* Windows-only safety net for `bun install -g` updates: when a previous `prometheus`
|
|
96
|
+
* process is running, bun cannot overwrite the locked `.node` inside
|
|
97
|
+
* `node_modules/@prometheus-ai/natives/native/`, leaving an old binary next to a
|
|
98
|
+
* newer `index.js` and producing `<sym> is not a function` crashes on the next
|
|
99
|
+
* launch. Staging into the version-pinned cache:
|
|
100
|
+
* 1. Gives every package version its own filesystem path, so concurrent prometheus
|
|
101
|
+
* processes never collide on the same file.
|
|
102
|
+
* 2. Makes the running process keep its handle on the cache copy, freeing bun
|
|
103
|
+
* to overwrite the `node_modules` copy on subsequent updates.
|
|
104
|
+
* Disabled on non-Windows (no file-lock problem), in workspace dev (`nativeDir`
|
|
105
|
+
* is not inside a `node_modules` segment), and for compiled binaries (handled
|
|
106
|
+
* by `maybeExtractEmbeddedAddon`).
|
|
107
|
+
*
|
|
108
|
+
* @param {{ platform: NodeJS.Platform | string; isCompiledBinary: boolean; nativeDir: string }} input
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
export function shouldStageNodeModulesAddon({ platform, isCompiledBinary, nativeDir }) {
|
|
112
|
+
if (platform !== "win32") return false;
|
|
113
|
+
if (isCompiledBinary) return false;
|
|
114
|
+
// Check both separators independently of the host's `path.sep`: this helper
|
|
115
|
+
// is shared by the loader (running on Windows with `\`) and the test suite
|
|
116
|
+
// (typically running on POSIX hosts when CI executes the regression test).
|
|
117
|
+
return nativeDir.includes("\\node_modules\\") || nativeDir.includes("/node_modules/");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{
|
|
122
|
+
* addonFilenames: string[];
|
|
123
|
+
* isCompiledBinary: boolean;
|
|
124
|
+
* stageFromNodeModules?: boolean;
|
|
125
|
+
* nativeDir: string;
|
|
126
|
+
* leafPackageDir?: string | null;
|
|
127
|
+
* execDir: string;
|
|
128
|
+
* versionedDir: string;
|
|
129
|
+
* userDataDir: string;
|
|
130
|
+
* }} input
|
|
131
|
+
* @returns {string[]}
|
|
132
|
+
*/
|
|
133
|
+
export function resolveLoaderCandidates({
|
|
134
|
+
addonFilenames,
|
|
135
|
+
isCompiledBinary,
|
|
136
|
+
stageFromNodeModules = false,
|
|
137
|
+
nativeDir,
|
|
138
|
+
leafPackageDir = null,
|
|
139
|
+
execDir,
|
|
140
|
+
versionedDir,
|
|
141
|
+
userDataDir,
|
|
142
|
+
}) {
|
|
143
|
+
const baseReleaseCandidates = addonFilenames.flatMap(filename => [
|
|
144
|
+
path.join(nativeDir, filename),
|
|
145
|
+
path.join(execDir, filename),
|
|
146
|
+
]);
|
|
147
|
+
const leafCandidates = leafPackageDir ? addonFilenames.map(filename => path.join(leafPackageDir, filename)) : [];
|
|
148
|
+
const compiledCandidates = addonFilenames.flatMap(filename => [
|
|
149
|
+
path.join(versionedDir, filename),
|
|
150
|
+
path.join(userDataDir, filename),
|
|
151
|
+
]);
|
|
152
|
+
const stagedCandidates = stageFromNodeModules ? addonFilenames.map(filename => path.join(versionedDir, filename)) : [];
|
|
153
|
+
let releaseCandidates;
|
|
154
|
+
if (isCompiledBinary) {
|
|
155
|
+
releaseCandidates = [...compiledCandidates, ...baseReleaseCandidates];
|
|
156
|
+
} else if (stageFromNodeModules) {
|
|
157
|
+
releaseCandidates = [...stagedCandidates, ...leafCandidates, ...baseReleaseCandidates];
|
|
158
|
+
} else {
|
|
159
|
+
releaseCandidates = [...leafCandidates, ...baseReleaseCandidates];
|
|
160
|
+
}
|
|
161
|
+
return [...new Set(releaseCandidates)];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// Side-effectful loader. Everything below runs only when `loadNative()` is
|
|
166
|
+
// called from `native/index.js` — tests that only import the pure helpers
|
|
167
|
+
// above pay nothing for variant detection, subprocess spawns, or fs probes.
|
|
168
|
+
// =========================================================================
|
|
169
|
+
|
|
170
|
+
function runCommand(command, args) {
|
|
171
|
+
try {
|
|
172
|
+
const result = childProcess.spawnSync(command, args, { encoding: "utf-8" });
|
|
173
|
+
if (result.error) return null;
|
|
174
|
+
if (result.status !== 0) return null;
|
|
175
|
+
return (result.stdout || "").trim();
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function getVariantOverride() {
|
|
182
|
+
const value = process.env.PROMETHEUS_NATIVE_VARIANT;
|
|
183
|
+
if (!value) return null;
|
|
184
|
+
if (value === "modern" || value === "baseline") return value;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectAvx2Support() {
|
|
189
|
+
if (process.arch !== "x64") {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (process.platform === "linux") {
|
|
194
|
+
try {
|
|
195
|
+
const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
|
|
196
|
+
return /\bavx2\b/i.test(cpuInfo);
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (process.platform === "darwin") {
|
|
203
|
+
const leaf7 = runCommand("sysctl", ["-n", "machdep.cpu.leaf7_features"]);
|
|
204
|
+
if (leaf7 && /\bAVX2\b/i.test(leaf7)) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
const features = runCommand("sysctl", ["-n", "machdep.cpu.features"]);
|
|
208
|
+
return Boolean(features && /\bAVX2\b/i.test(features));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (process.platform === "win32") {
|
|
212
|
+
const output = runCommand("powershell.exe", [
|
|
213
|
+
"-NoProfile",
|
|
214
|
+
"-NonInteractive",
|
|
215
|
+
"-Command",
|
|
216
|
+
"[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
|
|
217
|
+
]);
|
|
218
|
+
return output && output.toLowerCase() === "true";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function resolveCpuVariant(override) {
|
|
225
|
+
if (process.arch !== "x64") return null;
|
|
226
|
+
if (override) return override;
|
|
227
|
+
return detectAvx2Support() ? "modern" : "baseline";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function selectEmbeddedAddonFile(selectedVariant) {
|
|
231
|
+
if (!embeddedAddon) return null;
|
|
232
|
+
const defaultFile = embeddedAddon.files.find(file => file.variant === "default") || null;
|
|
233
|
+
if (process.arch !== "x64") return defaultFile || embeddedAddon.files[0] || null;
|
|
234
|
+
if (selectedVariant === "modern") {
|
|
235
|
+
return (
|
|
236
|
+
embeddedAddon.files.find(file => file.variant === "modern") ||
|
|
237
|
+
embeddedAddon.files.find(file => file.variant === "baseline") ||
|
|
238
|
+
null
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return embeddedAddon.files.find(file => file.variant === "baseline") || null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function readTarString(buffer, offset, length) {
|
|
245
|
+
const end = Math.min(offset + length, buffer.length);
|
|
246
|
+
let stringEnd = offset;
|
|
247
|
+
while (stringEnd < end && buffer[stringEnd] !== 0) stringEnd++;
|
|
248
|
+
return buffer.toString("utf8", offset, stringEnd);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readTarOctal(buffer, offset, length) {
|
|
252
|
+
const value = readTarString(buffer, offset, length).trim();
|
|
253
|
+
if (!value) return 0;
|
|
254
|
+
const parsed = Number.parseInt(value, 8);
|
|
255
|
+
if (!Number.isFinite(parsed)) {
|
|
256
|
+
throw new Error(`Invalid tar octal value: ${value}`);
|
|
257
|
+
}
|
|
258
|
+
return parsed;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function isZeroTarBlock(buffer, offset) {
|
|
262
|
+
for (let index = 0; index < 512; index++) {
|
|
263
|
+
if (buffer[offset + index] !== 0) return false;
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getTarEntryName(header) {
|
|
269
|
+
const name = readTarString(header, 0, 100);
|
|
270
|
+
const prefix = readTarString(header, 345, 155);
|
|
271
|
+
return prefix ? `${prefix}/${name}` : name;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isSafeEmbeddedAddonFilename(filename) {
|
|
275
|
+
return filename.length > 0 && path.basename(filename) === filename && !filename.includes("/") && !filename.includes("\\");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isEmbeddedAddonFileCurrent(targetPath, file) {
|
|
279
|
+
try {
|
|
280
|
+
const stat = fs.statSync(targetPath);
|
|
281
|
+
if (!stat.isFile()) return false;
|
|
282
|
+
return typeof file.size !== "number" || stat.size === file.size;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (err && err.code === "ENOENT") return false;
|
|
285
|
+
throw err;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function writeEmbeddedAddonFile(targetPath, content) {
|
|
290
|
+
const tempPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
|
|
291
|
+
try {
|
|
292
|
+
fs.writeFileSync(tempPath, content, { mode: 0o755 });
|
|
293
|
+
fs.renameSync(tempPath, targetPath);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
try {
|
|
296
|
+
fs.unlinkSync(tempPath);
|
|
297
|
+
} catch {
|
|
298
|
+
// Best-effort cleanup only.
|
|
299
|
+
}
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
|
|
305
|
+
const pending = new Map();
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
if (!isSafeEmbeddedAddonFilename(file.filename)) {
|
|
308
|
+
throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
|
|
309
|
+
}
|
|
310
|
+
const targetPath = path.join(targetDir, file.filename);
|
|
311
|
+
if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
|
|
312
|
+
pending.set(file.filename, file);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (pending.size === 0) return [];
|
|
316
|
+
|
|
317
|
+
const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
|
|
318
|
+
const writtenPaths = [];
|
|
319
|
+
let offset = 0;
|
|
320
|
+
|
|
321
|
+
while (offset + 512 <= archive.length) {
|
|
322
|
+
if (isZeroTarBlock(archive, offset)) break;
|
|
323
|
+
const header = archive.subarray(offset, offset + 512);
|
|
324
|
+
const filename = getTarEntryName(header);
|
|
325
|
+
const size = readTarOctal(header, 124, 12);
|
|
326
|
+
const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);
|
|
327
|
+
offset += 512;
|
|
328
|
+
|
|
329
|
+
if (offset + size > archive.length) {
|
|
330
|
+
throw new Error(`Truncated embedded addon archive entry: ${filename}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!isSafeEmbeddedAddonFilename(filename)) {
|
|
334
|
+
throw new Error(`Unsafe embedded addon archive entry: ${filename}`);
|
|
335
|
+
}
|
|
336
|
+
if (typeflag !== "0") {
|
|
337
|
+
throw new Error(`Unsupported embedded addon archive entry type ${typeflag}: ${filename}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const file = pending.get(filename);
|
|
341
|
+
if (file) {
|
|
342
|
+
if (typeof file.size === "number" && file.size !== size) {
|
|
343
|
+
throw new Error(`Embedded addon size mismatch for ${filename}: expected ${file.size}, got ${size}`);
|
|
344
|
+
}
|
|
345
|
+
const targetPath = path.join(targetDir, filename);
|
|
346
|
+
writeEmbeddedAddonFile(targetPath, archive.subarray(offset, offset + size));
|
|
347
|
+
pending.delete(filename);
|
|
348
|
+
writtenPaths.push(targetPath);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
offset += Math.ceil(size / 512) * 512;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (pending.size > 0) {
|
|
355
|
+
throw new Error(`Embedded addon archive missing: ${[...pending.keys()].join(", ")}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return writtenPaths;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function maybeExtractEmbeddedAddon(ctx, errors) {
|
|
362
|
+
if (!ctx.isCompiledBinary || !embeddedAddon) return null;
|
|
363
|
+
if (embeddedAddon.platformTag !== ctx.platformTag || embeddedAddon.version !== ctx.packageVersion) return null;
|
|
364
|
+
|
|
365
|
+
const selectedEmbeddedFile = selectEmbeddedAddonFile(ctx.selectedVariant);
|
|
366
|
+
if (!selectedEmbeddedFile) return null;
|
|
367
|
+
const targetPath = path.join(ctx.versionedDir, selectedEmbeddedFile.filename);
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
fs.mkdirSync(ctx.versionedDir, { recursive: true });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
373
|
+
errors.push(`embedded addon dir: ${message}`);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (embeddedAddon.archive) {
|
|
378
|
+
try {
|
|
379
|
+
extractEmbeddedAddonArchive({
|
|
380
|
+
archivePath: embeddedAddon.archive.filePath,
|
|
381
|
+
files: embeddedAddon.files,
|
|
382
|
+
targetDir: ctx.versionedDir,
|
|
383
|
+
});
|
|
384
|
+
if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
|
|
385
|
+
return targetPath;
|
|
386
|
+
}
|
|
387
|
+
errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): missing ${selectedEmbeddedFile.filename}`);
|
|
388
|
+
return null;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
391
|
+
errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): ${message}`);
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
|
|
397
|
+
return targetPath;
|
|
398
|
+
}
|
|
399
|
+
if (!selectedEmbeddedFile.filePath) {
|
|
400
|
+
errors.push(`embedded addon metadata missing file path for ${selectedEmbeddedFile.filename}`);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
|
|
406
|
+
fs.writeFileSync(targetPath, buffer);
|
|
407
|
+
return targetPath;
|
|
408
|
+
} catch (err) {
|
|
409
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
410
|
+
errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Mirror `leafPackageDir ?? nativeDir` addon binaries to
|
|
417
|
+
* `versionedDir/<filename>.node` on Windows installs so the running process
|
|
418
|
+
* cache path, never on the `node_modules` copy that bun must overwrite on
|
|
419
|
+
* update. No-op on non-Windows, in workspace dev, and for compiled binaries —
|
|
420
|
+
* see `shouldStageNodeModulesAddon` for the gating rules.
|
|
421
|
+
*/
|
|
422
|
+
function maybeStageNodeModulesAddon(ctx, errors) {
|
|
423
|
+
if (!ctx.stageFromNodeModules) return null;
|
|
424
|
+
|
|
425
|
+
let stagedPath = null;
|
|
426
|
+
for (const filename of ctx.addonFilenames) {
|
|
427
|
+
const sourcePath = path.join(ctx.leafPackageDir ?? ctx.nativeDir, filename);
|
|
428
|
+
const targetPath = path.join(ctx.versionedDir, filename);
|
|
429
|
+
|
|
430
|
+
if (fs.existsSync(targetPath)) {
|
|
431
|
+
stagedPath = stagedPath || targetPath;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
fs.mkdirSync(ctx.versionedDir, { recursive: true });
|
|
438
|
+
} catch (err) {
|
|
439
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
440
|
+
errors.push(`staged addon dir: ${message}`);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
// `copyFileSync` is atomic on Windows (CopyFileW) and avoids holding
|
|
446
|
+
// two large buffers in JS for the read/write dance.
|
|
447
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
448
|
+
stagedPath = stagedPath || targetPath;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
451
|
+
errors.push(`staged addon copy (${filename}): ${message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return stagedPath;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function validateLoadedBindings(ctx, bindings, candidate) {
|
|
458
|
+
// In workspace dev (running out of `packages/natives/native/` rather than a
|
|
459
|
+
// `node_modules` install or a compiled bundle) the local `.node` only gains
|
|
460
|
+
// the renamed sentinel after `bun --cwd=packages/natives run build`. Skip
|
|
461
|
+
// validation there so a stale post-pull dev tree boots while the rebuild
|
|
462
|
+
// completes; install and compiled-binary paths still validate.
|
|
463
|
+
if (ctx.isWorkspaceLoad) return;
|
|
464
|
+
if (typeof bindings[ctx.versionSentinelExport] === "function") return;
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Loaded ${candidate} but it does not expose the @prometheus-ai/natives@${ctx.packageVersion} ` +
|
|
467
|
+
`version sentinel \`${ctx.versionSentinelExport}\`. The .node file on disk is from a different ` +
|
|
468
|
+
"release than this loader — reinstall to re-sync.",
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function buildHelpMessage(ctx) {
|
|
473
|
+
if (ctx.isCompiledBinary) {
|
|
474
|
+
const expectedPaths = ctx.addonFilenames.map(filename => ` ${path.join(ctx.versionedDir, filename)}`).join("\n");
|
|
475
|
+
const downloadHints = ctx.addonFilenames
|
|
476
|
+
.map(filename => {
|
|
477
|
+
const downloadUrl = `https://github.com/uttamtrivedi/Prometheus/releases/latest/download/${filename}`;
|
|
478
|
+
const targetPath = path.join(ctx.versionedDir, filename);
|
|
479
|
+
return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
|
|
480
|
+
})
|
|
481
|
+
.join("\n");
|
|
482
|
+
return (
|
|
483
|
+
`The compiled binary should extract one of:\n${expectedPaths}\n\n` +
|
|
484
|
+
`If missing, delete ${ctx.versionedDir} and re-run, or download manually:\n${downloadHints}`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
return (
|
|
488
|
+
"If installed via npm/bun, try reinstalling: bun install @prometheus-ai/natives\n" +
|
|
489
|
+
"If developing locally, build with: bun --cwd=packages/natives run build\n" +
|
|
490
|
+
"Optional x64 variants: TARGET_VARIANT=baseline|modern bun --cwd=packages/natives run build"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Initialize the loader context: resolves every path, variant, and policy
|
|
496
|
+
* decision once so the inner load loop stays a pure require/validate pipeline.
|
|
497
|
+
* Called from `loadNative()` rather than at module scope so importing pure
|
|
498
|
+
* helpers from this file doesn't trigger AVX2 detection or filesystem probes.
|
|
499
|
+
*/
|
|
500
|
+
function initLoaderContext() {
|
|
501
|
+
const platformTag = `${process.platform}-${process.arch}`;
|
|
502
|
+
const packageVersion = packageJson.version;
|
|
503
|
+
const nativeDir = path.join(import.meta.dir, "..", "native");
|
|
504
|
+
const execDir = path.dirname(process.execPath);
|
|
505
|
+
const versionedDir = path.join(getNativesDir(), packageVersion);
|
|
506
|
+
const userDataDir =
|
|
507
|
+
process.platform === "win32"
|
|
508
|
+
? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "prometheus")
|
|
509
|
+
: path.join(os.homedir(), ".local", "bin");
|
|
510
|
+
|
|
511
|
+
const isCompiledBinary = detectCompiledBinary({
|
|
512
|
+
embeddedAddon,
|
|
513
|
+
env: process.env,
|
|
514
|
+
importMetaUrl: import.meta.url,
|
|
515
|
+
});
|
|
516
|
+
const leafPackageDir = isCompiledBinary ? null : resolveLeafPackageDir(platformTag);
|
|
517
|
+
const stageFromNodeModules = shouldStageNodeModulesAddon({
|
|
518
|
+
platform: process.platform,
|
|
519
|
+
isCompiledBinary,
|
|
520
|
+
nativeDir,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const selectedVariant = resolveCpuVariant(getVariantOverride());
|
|
524
|
+
const addonFilenames = getAddonFilenames({ tag: platformTag, arch: process.arch, variant: selectedVariant });
|
|
525
|
+
const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
|
|
526
|
+
|
|
527
|
+
const candidates = resolveLoaderCandidates({
|
|
528
|
+
addonFilenames,
|
|
529
|
+
isCompiledBinary,
|
|
530
|
+
stageFromNodeModules,
|
|
531
|
+
nativeDir,
|
|
532
|
+
leafPackageDir,
|
|
533
|
+
execDir,
|
|
534
|
+
versionedDir,
|
|
535
|
+
userDataDir,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Version sentinel emitted by the Rust addon under a `js_name` that encodes
|
|
539
|
+
// the package version (`__prometheusNativesV{major}_{minor}_{patch}`).
|
|
540
|
+
// `scripts/release.ts` bumps the name in `crates/prometheus-natives/src/lib.rs` in
|
|
541
|
+
// lock-step with the version, so a `.node` from a different release
|
|
542
|
+
// physically cannot expose the symbol this loader is looking for. That
|
|
543
|
+
// turns the silent `<sym> is not a function` crash from a Windows
|
|
544
|
+
// locked-file update into an actionable load-time error.
|
|
545
|
+
const versionSentinelExport = `__prometheusNativesV${packageVersion.replace(/[^A-Za-z0-9]/g, "_")}`;
|
|
546
|
+
const isWorkspaceLoad =
|
|
547
|
+
!isCompiledBinary && !nativeDir.includes("\\node_modules\\") && !nativeDir.includes("/node_modules/");
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
platformTag,
|
|
551
|
+
packageVersion,
|
|
552
|
+
nativeDir,
|
|
553
|
+
leafPackageDir,
|
|
554
|
+
versionedDir,
|
|
555
|
+
isCompiledBinary,
|
|
556
|
+
stageFromNodeModules,
|
|
557
|
+
selectedVariant,
|
|
558
|
+
addonFilenames,
|
|
559
|
+
addonLabel,
|
|
560
|
+
candidates,
|
|
561
|
+
versionSentinelExport,
|
|
562
|
+
isWorkspaceLoad,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function loadNative() {
|
|
567
|
+
const ctx = initLoaderContext();
|
|
568
|
+
const require_ = createRequire(import.meta.url);
|
|
569
|
+
|
|
570
|
+
const errors = [];
|
|
571
|
+
const embeddedCandidate = maybeExtractEmbeddedAddon(ctx, errors);
|
|
572
|
+
const stagedCandidate = embeddedCandidate ? null : maybeStageNodeModulesAddon(ctx, errors);
|
|
573
|
+
const prepended = [embeddedCandidate, stagedCandidate].filter(c => typeof c === "string");
|
|
574
|
+
const runtimeCandidates = prepended.length > 0 ? [...prepended, ...ctx.candidates] : ctx.candidates;
|
|
575
|
+
|
|
576
|
+
for (const candidate of runtimeCandidates) {
|
|
577
|
+
try {
|
|
578
|
+
const bindings = require_(candidate);
|
|
579
|
+
validateLoadedBindings(ctx, bindings, candidate);
|
|
580
|
+
return bindings;
|
|
581
|
+
} catch (err) {
|
|
582
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
583
|
+
errors.push(`${candidate}: ${message}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!SUPPORTED_PLATFORMS.includes(ctx.platformTag)) {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`Unsupported platform: ${ctx.platformTag}\n` +
|
|
590
|
+
`Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
|
|
591
|
+
"If you need support for this platform, please open an issue.",
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const details = errors.map(error => `- ${error}`).join("\n");
|
|
595
|
+
throw new Error(
|
|
596
|
+
`Failed to load prometheus_natives native addon for ${ctx.addonLabel}.\n\nTried:\n${details}\n\n${buildHelpMessage(ctx)}`,
|
|
597
|
+
);
|
|
598
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prometheus-ai/natives",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"homepage": "https://prometheus.trivlab.com",
|
|
7
|
+
"author": "Uttam Trivedi",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/uttamtrivedi/Prometheus.git",
|
|
12
|
+
"directory": "packages/natives"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/uttamtrivedi/Prometheus/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"napi",
|
|
19
|
+
"rust",
|
|
20
|
+
"native",
|
|
21
|
+
"grep",
|
|
22
|
+
"text-processing",
|
|
23
|
+
"clipboard",
|
|
24
|
+
"image",
|
|
25
|
+
"pty",
|
|
26
|
+
"shell",
|
|
27
|
+
"syntax-highlighting"
|
|
28
|
+
],
|
|
29
|
+
"main": "./native/index.js",
|
|
30
|
+
"types": "./native/index.d.ts",
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "bun scripts/build-native.ts",
|
|
33
|
+
"check": "biome check . && bun run check:types",
|
|
34
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
35
|
+
"lint": "biome lint .",
|
|
36
|
+
"test": "bun test --parallel",
|
|
37
|
+
"fix": "biome check --write --unsafe .",
|
|
38
|
+
"fmt": "biome format --write .",
|
|
39
|
+
"embed:native": "bun scripts/embed-native.ts",
|
|
40
|
+
"gen:npm": "bun scripts/gen-npm-packages.ts",
|
|
41
|
+
"bench": "bun bench/grep.ts"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@napi-rs/cli": "3.7.0",
|
|
45
|
+
"@types/bun": "^1.3.14"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.14"
|
|
49
|
+
},
|
|
50
|
+
"napi": {
|
|
51
|
+
"binaryName": "prometheus_natives",
|
|
52
|
+
"triples": {}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"native/index.js",
|
|
56
|
+
"native/index.d.ts",
|
|
57
|
+
"native/loader-state.js",
|
|
58
|
+
"native/loader-state.d.ts",
|
|
59
|
+
"native/embedded-addon.js",
|
|
60
|
+
"README.md"
|
|
61
|
+
],
|
|
62
|
+
"exports": {
|
|
63
|
+
".": {
|
|
64
|
+
"types": "./native/index.d.ts",
|
|
65
|
+
"import": "./native/index.js"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"optionalDependencies": {
|
|
69
|
+
"@prometheus-ai/natives-linux-x64": "0.5.0",
|
|
70
|
+
"@prometheus-ai/natives-linux-arm64": "0.5.0",
|
|
71
|
+
"@prometheus-ai/natives-darwin-x64": "0.5.0",
|
|
72
|
+
"@prometheus-ai/natives-darwin-arm64": "0.5.0",
|
|
73
|
+
"@prometheus-ai/natives-win32-x64": "0.5.0"
|
|
74
|
+
}
|
|
75
|
+
}
|