@linxiraos/pi-natives 1.0.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.
@@ -0,0 +1,869 @@
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 `@linxiraos/pi-natives`.
12
+ *
13
+ * Owns every step between "Node imports `native/index.js`" and "the right
14
+ * `pi_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 PI_COMPILED=true`
26
+ * substitutes the bare identifier `PI_COMPILED`, NOT `process.env.PI_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
+ /**
37
+ * Streaming startup marker, enabled by `PI_DEBUG_STARTUP`. Local copy of the
38
+ * pi-utils helper (this loader cannot depend on pi-utils). Synchronous on
39
+ * purpose: extraction/dlopen hangs must still leave the `:start` marker.
40
+ * @param {string} text
41
+ */
42
+ function startupMarker(text) {
43
+ if (!process.env.PI_DEBUG_STARTUP) return;
44
+ try {
45
+ fs.writeSync(2, `[startup] ${text}\n`);
46
+ } catch {
47
+ // stderr unavailable; markers are best-effort
48
+ }
49
+ }
50
+
51
+ function getNativesDir() {
52
+ const xdgDataHome = process.env.XDG_DATA_HOME;
53
+ if (xdgDataHome && fs.existsSync(path.join(xdgDataHome, "omp"))) {
54
+ return path.join(xdgDataHome, "omp", "natives");
55
+ }
56
+ return path.join(os.homedir(), ".omp", "natives");
57
+ }
58
+
59
+ function resolveLeafPackageDir(platformTag) {
60
+ try {
61
+ const require_ = createRequire(import.meta.url);
62
+ return path.dirname(require_.resolve(`@linxiraos/pi-natives-${platformTag}/package.json`));
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ // =========================================================================
69
+ // Pure helpers — re-exported for unit tests in `packages/natives/test/`.
70
+ // =========================================================================
71
+
72
+ /**
73
+ * @param {{
74
+ * embeddedAddon: { platformTag: string; version: string; files: unknown[] } | null | undefined;
75
+ * env: Record<string, string | undefined>;
76
+ * importMetaUrl: string | null | undefined;
77
+ * }} input
78
+ * @returns {boolean}
79
+ */
80
+ export function detectCompiledBinary({ embeddedAddon, env, importMetaUrl }) {
81
+ if (embeddedAddon) return true;
82
+ if (env && env.PI_COMPILED) return true;
83
+ if (typeof importMetaUrl === "string") {
84
+ if (importMetaUrl.includes("$bunfs")) return true;
85
+ if (importMetaUrl.includes("~BUN")) return true;
86
+ if (importMetaUrl.includes("%7EBUN")) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * @param {{ tag: string; arch: string; variant: "modern" | "baseline" | null | undefined }} input
93
+ * @returns {string[]}
94
+ */
95
+ export function getAddonFilenames({ tag, arch, variant }) {
96
+ const defaultFilename = `pi_natives.${tag}.node`;
97
+ if (arch !== "x64" || !variant) return [defaultFilename];
98
+ const baselineFilename = `pi_natives.${tag}-baseline.node`;
99
+ const modernFilename = `pi_natives.${tag}-modern.node`;
100
+ if (variant === "modern") {
101
+ return [modernFilename, baselineFilename, defaultFilename];
102
+ }
103
+ return [baselineFilename, defaultFilename];
104
+ }
105
+
106
+ /**
107
+ * Decide whether the loader should mirror the package's `native/<filename>.node`
108
+ * into the per-version cache directory (`~/.omp/natives/<version>/`) before loading.
109
+ *
110
+ * Windows-only safety net for `bun install -g` updates: when a previous `omp`
111
+ * process is running, bun cannot overwrite the locked `.node` inside
112
+ * `node_modules/@linxiraos/pi-natives/native/`, leaving an old binary next to a
113
+ * newer `index.js` and producing `<sym> is not a function` crashes on the next
114
+ * launch. Staging into the version-pinned cache:
115
+ * 1. Gives every package version its own filesystem path, so concurrent omp
116
+ * processes never collide on the same file.
117
+ * 2. Makes the running process keep its handle on the cache copy, freeing bun
118
+ * to overwrite the `node_modules` copy on subsequent updates.
119
+ * Disabled on non-Windows (no file-lock problem), in workspace dev (`nativeDir`
120
+ * is not inside a `node_modules` segment), and for compiled binaries (handled
121
+ * by `maybeExtractEmbeddedAddon`).
122
+ *
123
+ * @param {{ platform: NodeJS.Platform | string; isCompiledBinary: boolean; nativeDir: string }} input
124
+ * @returns {boolean}
125
+ */
126
+ export function shouldStageNodeModulesAddon({ platform, isCompiledBinary, nativeDir }) {
127
+ if (platform !== "win32") return false;
128
+ if (isCompiledBinary) return false;
129
+ // Check both separators independently of the host's `path.sep`: this helper
130
+ // is shared by the loader (running on Windows with `\`) and the test suite
131
+ // (typically running on POSIX hosts when CI executes the regression test).
132
+ const normalizedNativeDir = nativeDir.toLowerCase();
133
+ return normalizedNativeDir.includes("\\node_modules\\") || normalizedNativeDir.includes("/node_modules/");
134
+ }
135
+
136
+ /**
137
+ * @param {{
138
+ * addonFilenames: string[];
139
+ * isCompiledBinary: boolean;
140
+ * stageFromNodeModules?: boolean;
141
+ * nativeDir: string;
142
+ * leafPackageDir?: string | null;
143
+ * execDir: string;
144
+ * versionedDir: string;
145
+ * userDataDir: string;
146
+ * }} input
147
+ * @returns {string[]}
148
+ */
149
+ export function resolveLoaderCandidates({
150
+ addonFilenames,
151
+ isCompiledBinary,
152
+ stageFromNodeModules = false,
153
+ nativeDir,
154
+ leafPackageDir = null,
155
+ execDir,
156
+ versionedDir,
157
+ userDataDir,
158
+ }) {
159
+ const baseReleaseCandidates = addonFilenames.flatMap(filename => [
160
+ path.join(nativeDir, filename),
161
+ path.join(execDir, filename),
162
+ ]);
163
+ const leafCandidates = leafPackageDir ? addonFilenames.map(filename => path.join(leafPackageDir, filename)) : [];
164
+ const compiledCandidates = addonFilenames.flatMap(filename => [
165
+ path.join(versionedDir, filename),
166
+ path.join(userDataDir, filename),
167
+ ]);
168
+ const stagedCandidates = stageFromNodeModules ? addonFilenames.map(filename => path.join(versionedDir, filename)) : [];
169
+ let releaseCandidates;
170
+ if (isCompiledBinary) {
171
+ releaseCandidates = [...compiledCandidates, ...baseReleaseCandidates];
172
+ } else if (stageFromNodeModules) {
173
+ releaseCandidates = [...stagedCandidates, ...leafCandidates, ...baseReleaseCandidates];
174
+ } else {
175
+ releaseCandidates = [...leafCandidates, ...baseReleaseCandidates];
176
+ }
177
+ return [...new Set(releaseCandidates)];
178
+ }
179
+
180
+ // =========================================================================
181
+
182
+ function parseReleaseVersion(version) {
183
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
184
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
185
+ }
186
+
187
+ function isOlderReleaseVersion(candidate, current) {
188
+ const candidateParts = parseReleaseVersion(candidate);
189
+ const currentParts = parseReleaseVersion(current);
190
+ if (!candidateParts || !currentParts) return false;
191
+ for (let index = 0; index < candidateParts.length; index++) {
192
+ if (candidateParts[index] !== currentParts[index]) {
193
+ return candidateParts[index] < currentParts[index];
194
+ }
195
+ }
196
+ return false;
197
+ }
198
+
199
+ // A concurrently starting older OMP binary creates or refreshes this directory
200
+ // before extracting its addon. Keep fresh directories long enough for that
201
+ // startup to finish; a later launch can reclaim them once they are genuinely
202
+ // stale.
203
+ const NATIVE_CACHE_CLEANUP_GRACE_MS = 10 * 60_000;
204
+
205
+ /**
206
+ * Create a version cache directory and refresh its activity timestamp before
207
+ * extraction or staging begins. Recursive mkdir does not update the mtime of
208
+ * an existing directory, so the explicit touch is what protects interrupted
209
+ * or partially populated caches from concurrent cleanup.
210
+ *
211
+ * @param {string} versionedDir
212
+ */
213
+ export function prepareNativeVersionDir(versionedDir) {
214
+ fs.mkdirSync(versionedDir, { recursive: true });
215
+ const now = new Date();
216
+ fs.utimesSync(versionedDir, now, now);
217
+ }
218
+
219
+ /**
220
+ * Remove version-pinned native cache directories older than the loaded package.
221
+ * Best-effort by design: permission errors and concurrent processes must not
222
+ * abort startup after the native addon has already loaded successfully.
223
+ *
224
+ * @param {{ nativesDir: string; currentVersion: string }} input
225
+ * @returns {string[]}
226
+ */
227
+ export function cleanupStaleNativeVersions({ nativesDir, currentVersion }) {
228
+ const removed = [];
229
+ let entries;
230
+ try {
231
+ entries = fs.readdirSync(nativesDir, { withFileTypes: true });
232
+ } catch {
233
+ return removed;
234
+ }
235
+
236
+ for (const entry of entries) {
237
+ if (!entry.isDirectory() || !isOlderReleaseVersion(entry.name, currentVersion)) continue;
238
+ const targetPath = path.join(nativesDir, entry.name);
239
+ try {
240
+ const stat = fs.statSync(targetPath);
241
+ if (Date.now() - stat.mtimeMs < NATIVE_CACHE_CLEANUP_GRACE_MS) continue;
242
+ fs.rmSync(targetPath, { recursive: true, force: true });
243
+ removed.push(targetPath);
244
+ } catch {
245
+ // Stale caches are opportunistic cleanup only.
246
+ }
247
+ }
248
+ return removed;
249
+ }
250
+
251
+ // Side-effectful loader. Everything below runs only when `loadNative()` is
252
+ // called from `native/index.js` — tests that only import the pure helpers
253
+ // above pay nothing for variant detection, subprocess spawns, or fs probes.
254
+ // =========================================================================
255
+
256
+ /**
257
+ * Hidden env key for the resolved x64 variant. Once any context (main thread,
258
+ * worker, subprocess) finishes variant detection, the result is written here
259
+ * so every Bun worker and child process spawned afterwards inherits the same
260
+ * verdict and skips re-detection. See `selectCpuVariant` for the lookup order.
261
+ */
262
+ const VARIANT_CACHE_ENV_KEY = "__PI_NATIVE_VARIANT_CACHE";
263
+
264
+ /**
265
+ * Spawn `command` with `args` and capture stdout. Prefers `Bun.spawnSync`
266
+ * because Bun's `child_process.spawnSync` shim has been observed to return
267
+ * non-zero / null in worker threads on macOS even when the same binary works
268
+ * fine from the parent — the failure mode behind issue #3238, where the worker
269
+ * silently falls back to the "baseline" variant. Falls back to the Node shim
270
+ * for non-Bun embeds.
271
+ */
272
+ function runCommand(command, args) {
273
+ if (typeof Bun !== "undefined" && typeof Bun.spawnSync === "function") {
274
+ try {
275
+ const result = Bun.spawnSync([command, ...args], { stdout: "pipe", stderr: "pipe" });
276
+ if (result.exitCode === 0) {
277
+ return result.stdout.toString("utf-8").trim();
278
+ }
279
+ } catch {
280
+ // fall through to childProcess
281
+ }
282
+ }
283
+ try {
284
+ const result = childProcess.spawnSync(command, args, { encoding: "utf-8" });
285
+ if (result.error) return null;
286
+ if (result.status !== 0) return null;
287
+ return (result.stdout || "").trim();
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ function getVariantOverride() {
294
+ const value = process.env.PI_NATIVE_VARIANT;
295
+ if (!value) return null;
296
+ if (value === "modern" || value === "baseline") return value;
297
+ return null;
298
+ }
299
+
300
+ function detectAvx2Support() {
301
+ if (process.arch !== "x64") {
302
+ return false;
303
+ }
304
+
305
+ if (process.platform === "linux") {
306
+ try {
307
+ const cpuInfo = fs.readFileSync("/proc/cpuinfo", "utf8");
308
+ return /\bavx2\b/i.test(cpuInfo);
309
+ } catch {
310
+ return false;
311
+ }
312
+ }
313
+
314
+ if (process.platform === "darwin") {
315
+ // Try the absolute path before bare `sysctl`: PATH may not include
316
+ // `/usr/sbin` in worker/embedded spawn contexts (issue #3238).
317
+ for (const sysctlBin of ["/usr/sbin/sysctl", "sysctl"]) {
318
+ const leaf7 = runCommand(sysctlBin, ["-n", "machdep.cpu.leaf7_features"]);
319
+ if (leaf7 && /\bAVX2\b/i.test(leaf7)) return true;
320
+ const features = runCommand(sysctlBin, ["-n", "machdep.cpu.features"]);
321
+ if (features && /\bAVX2\b/i.test(features)) return true;
322
+ }
323
+ return false;
324
+ }
325
+
326
+ if (process.platform === "win32") {
327
+ // Under Bun, ask the kernel: PF_AVX2_INSTRUCTIONS_AVAILABLE == 40. Exact,
328
+ // and ~0.5 ms against ~270 ms for the PowerShell spawn it replaces on the
329
+ // startup path.
330
+ if (typeof Bun !== "undefined") {
331
+ try {
332
+ const { dlopen, FFIType } = createRequire(import.meta.url)("bun:ffi");
333
+ const kernel32 = dlopen("kernel32.dll", {
334
+ IsProcessorFeaturePresent: { args: [FFIType.u32], returns: FFIType.i32 },
335
+ });
336
+ try {
337
+ return kernel32.symbols.IsProcessorFeaturePresent(40) !== 0;
338
+ } finally {
339
+ kernel32.close();
340
+ }
341
+ } catch {
342
+ // No FFI (embedder policy, unusual host): fall through to the shell probe.
343
+ }
344
+ }
345
+ // Node embeds have no `bun:ffi`. `[System.Runtime.Intrinsics.X86.Avx2]`
346
+ // exists only on .NET Core, so `pwsh` (PowerShell 7) answers correctly
347
+ // while a stock `powershell.exe` (Windows PowerShell 5.1, .NET Framework)
348
+ // raises TypeNotFound and pins such hosts to the baseline addon.
349
+ for (const shell of ["pwsh.exe", "powershell.exe"]) {
350
+ const output = runCommand(shell, [
351
+ "-NoProfile",
352
+ "-NonInteractive",
353
+ "-Command",
354
+ "[System.Runtime.Intrinsics.X86.Avx2]::IsSupported",
355
+ ]);
356
+ if (output && output.toLowerCase() === "true") return true;
357
+ if (output && output.toLowerCase() === "false") return false;
358
+ }
359
+ return false;
360
+ }
361
+
362
+ return false;
363
+ }
364
+
365
+ /**
366
+ * Pure variant-selection helper, exposed for unit tests. Resolution order:
367
+ *
368
+ * 1. `override` (user-facing `PI_NATIVE_VARIANT` env var). Always wins.
369
+ * 2. The private `__PI_NATIVE_VARIANT_CACHE` env var, populated by the first
370
+ * context that detected at runtime. Lets child workers / subprocesses
371
+ * inherit the main thread's verdict instead of re-spawning `sysctl` etc.
372
+ * from a worker context where the spawn may fail (issue #3238).
373
+ * 3. `detectAvx2()` — the slow path, called at most once per process.
374
+ *
375
+ * Non-x64 architectures return `{ variant: null }` and never set the cache.
376
+ * When detection runs, the result is surfaced as `cacheEnvKey`/`cacheEnvValue`
377
+ * so the caller can write `process.env` (the pure helper itself stays
378
+ * side-effect-free, which keeps it easy to test).
379
+ *
380
+ * @param {{
381
+ * arch: string;
382
+ * override: "modern" | "baseline" | null | undefined;
383
+ * env: Record<string, string | undefined>;
384
+ * detectAvx2: () => boolean;
385
+ * }} input
386
+ * @returns {{
387
+ * variant: "modern" | "baseline" | null;
388
+ * source: "non-x64" | "override" | "cache" | "detect";
389
+ * cacheEnvKey?: string;
390
+ * cacheEnvValue?: string;
391
+ * }}
392
+ */
393
+ export function selectCpuVariant({ arch, override, env, detectAvx2 }) {
394
+ if (arch !== "x64") return { variant: null, source: "non-x64" };
395
+ if (override === "modern" || override === "baseline") {
396
+ return { variant: override, source: "override" };
397
+ }
398
+ const cached = env[VARIANT_CACHE_ENV_KEY];
399
+ if (cached === "modern" || cached === "baseline") {
400
+ return { variant: cached, source: "cache" };
401
+ }
402
+ const variant = detectAvx2() ? "modern" : "baseline";
403
+ return {
404
+ variant,
405
+ source: "detect",
406
+ cacheEnvKey: VARIANT_CACHE_ENV_KEY,
407
+ cacheEnvValue: variant,
408
+ };
409
+ }
410
+
411
+ function resolveCpuVariant(override) {
412
+ const result = selectCpuVariant({
413
+ arch: process.arch,
414
+ override,
415
+ env: process.env,
416
+ detectAvx2: detectAvx2Support,
417
+ });
418
+ if (result.cacheEnvKey) {
419
+ process.env[result.cacheEnvKey] = result.cacheEnvValue;
420
+ }
421
+ return result.variant;
422
+ }
423
+
424
+ function selectEmbeddedAddonFile(selectedVariant) {
425
+ if (!embeddedAddon) return null;
426
+ const defaultFile = embeddedAddon.files.find(file => file.variant === "default") || null;
427
+ if (process.arch !== "x64") return defaultFile || embeddedAddon.files[0] || null;
428
+ if (selectedVariant === "modern") {
429
+ return (
430
+ embeddedAddon.files.find(file => file.variant === "modern") ||
431
+ embeddedAddon.files.find(file => file.variant === "baseline") ||
432
+ null
433
+ );
434
+ }
435
+ return embeddedAddon.files.find(file => file.variant === "baseline") || null;
436
+ }
437
+
438
+ function readTarString(buffer, offset, length) {
439
+ const end = Math.min(offset + length, buffer.length);
440
+ let stringEnd = offset;
441
+ while (stringEnd < end && buffer[stringEnd] !== 0) stringEnd++;
442
+ return buffer.toString("utf8", offset, stringEnd);
443
+ }
444
+
445
+ function readTarOctal(buffer, offset, length) {
446
+ const value = readTarString(buffer, offset, length).trim();
447
+ if (!value) return 0;
448
+ const parsed = Number.parseInt(value, 8);
449
+ if (!Number.isFinite(parsed)) {
450
+ throw new Error(`Invalid tar octal value: ${value}`);
451
+ }
452
+ return parsed;
453
+ }
454
+
455
+ function isZeroTarBlock(buffer, offset) {
456
+ for (let index = 0; index < 512; index++) {
457
+ if (buffer[offset + index] !== 0) return false;
458
+ }
459
+ return true;
460
+ }
461
+
462
+ function getTarEntryName(header) {
463
+ const name = readTarString(header, 0, 100);
464
+ const prefix = readTarString(header, 345, 155);
465
+ return prefix ? `${prefix}/${name}` : name;
466
+ }
467
+
468
+ function isSafeEmbeddedAddonFilename(filename) {
469
+ return filename.length > 0 && path.basename(filename) === filename && !filename.includes("/") && !filename.includes("\\");
470
+ }
471
+
472
+ function isEmbeddedAddonFileCurrent(targetPath, file) {
473
+ try {
474
+ const stat = fs.statSync(targetPath);
475
+ if (!stat.isFile()) return false;
476
+ return typeof file.size !== "number" || stat.size === file.size;
477
+ } catch (err) {
478
+ if (err && err.code === "ENOENT") return false;
479
+ throw err;
480
+ }
481
+ }
482
+
483
+ function writeEmbeddedAddonFile(targetPath, content) {
484
+ const tempPath = `${targetPath}.tmp.${process.pid}.${Date.now()}`;
485
+ try {
486
+ fs.writeFileSync(tempPath, content, { mode: 0o755 });
487
+ fs.renameSync(tempPath, targetPath);
488
+ } catch (err) {
489
+ try {
490
+ fs.unlinkSync(tempPath);
491
+ } catch {
492
+ // Best-effort cleanup only.
493
+ }
494
+ throw err;
495
+ }
496
+ }
497
+
498
+ export function extractEmbeddedAddonArchive({ archivePath, files, targetDir }) {
499
+ const pending = new Map();
500
+ for (const file of files) {
501
+ if (!isSafeEmbeddedAddonFilename(file.filename)) {
502
+ throw new Error(`Unsafe embedded addon filename: ${file.filename}`);
503
+ }
504
+ const targetPath = path.join(targetDir, file.filename);
505
+ if (!isEmbeddedAddonFileCurrent(targetPath, file)) {
506
+ pending.set(file.filename, file);
507
+ }
508
+ }
509
+ if (pending.size === 0) return [];
510
+
511
+ const archive = zlib.gunzipSync(fs.readFileSync(archivePath));
512
+ const writtenPaths = [];
513
+ let offset = 0;
514
+
515
+ while (offset + 512 <= archive.length) {
516
+ if (isZeroTarBlock(archive, offset)) break;
517
+ const header = archive.subarray(offset, offset + 512);
518
+ const filename = getTarEntryName(header);
519
+ const size = readTarOctal(header, 124, 12);
520
+ const typeflag = header[156] === 0 ? "0" : String.fromCharCode(header[156]);
521
+ offset += 512;
522
+
523
+ if (offset + size > archive.length) {
524
+ throw new Error(`Truncated embedded addon archive entry: ${filename}`);
525
+ }
526
+
527
+ if (!isSafeEmbeddedAddonFilename(filename)) {
528
+ throw new Error(`Unsafe embedded addon archive entry: ${filename}`);
529
+ }
530
+ if (typeflag !== "0") {
531
+ throw new Error(`Unsupported embedded addon archive entry type ${typeflag}: ${filename}`);
532
+ }
533
+
534
+ const file = pending.get(filename);
535
+ if (file) {
536
+ if (typeof file.size === "number" && file.size !== size) {
537
+ throw new Error(`Embedded addon size mismatch for ${filename}: expected ${file.size}, got ${size}`);
538
+ }
539
+ const targetPath = path.join(targetDir, filename);
540
+ writeEmbeddedAddonFile(targetPath, archive.subarray(offset, offset + size));
541
+ pending.delete(filename);
542
+ writtenPaths.push(targetPath);
543
+ }
544
+
545
+ offset += Math.ceil(size / 512) * 512;
546
+ }
547
+
548
+ if (pending.size > 0) {
549
+ throw new Error(`Embedded addon archive missing: ${[...pending.keys()].join(", ")}`);
550
+ }
551
+
552
+ return writtenPaths;
553
+ }
554
+
555
+ function maybeExtractEmbeddedAddon(ctx, errors) {
556
+ if (!ctx.isCompiledBinary || !embeddedAddon) return null;
557
+ if (embeddedAddon.platformTag !== ctx.platformTag || embeddedAddon.version !== ctx.packageVersion) return null;
558
+
559
+ const selectedEmbeddedFile = selectEmbeddedAddonFile(ctx.selectedVariant);
560
+ if (!selectedEmbeddedFile) return null;
561
+ const targetPath = path.join(ctx.versionedDir, selectedEmbeddedFile.filename);
562
+
563
+ startupMarker("native:extractEmbeddedAddon:start");
564
+ try {
565
+ prepareNativeVersionDir(ctx.versionedDir);
566
+ } catch (err) {
567
+ const message = err instanceof Error ? err.message : String(err);
568
+ errors.push(`embedded addon dir: ${message}`);
569
+ return null;
570
+ }
571
+
572
+ if (embeddedAddon.archive) {
573
+ try {
574
+ extractEmbeddedAddonArchive({
575
+ archivePath: embeddedAddon.archive.filePath,
576
+ files: embeddedAddon.files,
577
+ targetDir: ctx.versionedDir,
578
+ });
579
+ if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
580
+ return targetPath;
581
+ }
582
+ errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): missing ${selectedEmbeddedFile.filename}`);
583
+ return null;
584
+ } catch (err) {
585
+ const message = err instanceof Error ? err.message : String(err);
586
+ errors.push(`embedded addon archive (${embeddedAddon.archive.filename}): ${message}`);
587
+ return null;
588
+ }
589
+ }
590
+
591
+ if (isEmbeddedAddonFileCurrent(targetPath, selectedEmbeddedFile)) {
592
+ return targetPath;
593
+ }
594
+ if (!selectedEmbeddedFile.filePath) {
595
+ errors.push(`embedded addon metadata missing file path for ${selectedEmbeddedFile.filename}`);
596
+ return null;
597
+ }
598
+
599
+ try {
600
+ const buffer = fs.readFileSync(selectedEmbeddedFile.filePath);
601
+ fs.writeFileSync(targetPath, buffer);
602
+ return targetPath;
603
+ } catch (err) {
604
+ const message = err instanceof Error ? err.message : String(err);
605
+ errors.push(`embedded addon write (${selectedEmbeddedFile.filename}): ${message}`);
606
+ return null;
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Mirror `leafPackageDir ?? nativeDir` addon binaries to
612
+ * `versionedDir/<filename>.node` on Windows installs so the running process
613
+ * cache path, never on the `node_modules` copy that bun must overwrite on
614
+ * update. No-op on non-Windows, in workspace dev, and for compiled binaries —
615
+ * see `shouldStageNodeModulesAddon` for the gating rules.
616
+ */
617
+ function maybeStageNodeModulesAddon(ctx, errors) {
618
+ if (!ctx.stageFromNodeModules) return null;
619
+
620
+ let stagedPath = null;
621
+ for (const filename of ctx.addonFilenames) {
622
+ const sourcePath = path.join(ctx.leafPackageDir ?? ctx.nativeDir, filename);
623
+ const targetPath = path.join(ctx.versionedDir, filename);
624
+
625
+ if (fs.existsSync(targetPath)) {
626
+ stagedPath = stagedPath || targetPath;
627
+ continue;
628
+ }
629
+ if (!fs.existsSync(sourcePath)) continue;
630
+
631
+ try {
632
+ prepareNativeVersionDir(ctx.versionedDir);
633
+ } catch (err) {
634
+ const message = err instanceof Error ? err.message : String(err);
635
+ errors.push(`staged addon dir: ${message}`);
636
+ continue;
637
+ }
638
+
639
+ try {
640
+ // `copyFileSync` is atomic on Windows (CopyFileW) and avoids holding
641
+ // two large buffers in JS for the read/write dance.
642
+ fs.copyFileSync(sourcePath, targetPath);
643
+ stagedPath = stagedPath || targetPath;
644
+ } catch (err) {
645
+ const message = err instanceof Error ? err.message : String(err);
646
+ errors.push(`staged addon copy (${filename}): ${message}`);
647
+ }
648
+ }
649
+ return stagedPath;
650
+ }
651
+
652
+ export function validateLoadedBindings(ctx, bindings, candidate) {
653
+ // In workspace dev (running out of `packages/natives/native/` rather than a
654
+ // `node_modules` install or a compiled bundle) the local `.node` only gains
655
+ // the renamed sentinel after `bun --cwd=packages/natives run build`. Skip
656
+ // validation there so a stale post-pull dev tree boots while the rebuild
657
+ // completes; install and compiled-binary paths still validate.
658
+ if (ctx.isWorkspaceLoad) return;
659
+ if (typeof bindings[ctx.versionSentinelExport] === "function") return;
660
+
661
+ // The expected sentinel is missing. Distinguish two failure modes by the
662
+ // sentinel the bindings DO carry:
663
+ // - disk stale: the `.node` on disk predates this loader (its own build);
664
+ // reinstalling re-syncs the file.
665
+ // - process stale: an in-place upgrade landed a new release on disk while
666
+ // this process still holds the previous addon generation resident in the
667
+ // dynamic-loader's native-module cache. `require` returns those old
668
+ // exports, which carry the PRIOR sentinel — disk is already consistent,
669
+ // so reinstall is a no-op and only restarting the process re-syncs.
670
+ const residentSentinel = Object.keys(bindings).find(
671
+ key => key !== ctx.versionSentinelExport && /^__piNativesV[A-Za-z0-9_]+$/.test(key),
672
+ );
673
+ // A prior sentinel alone cannot distinguish a resident old module from an
674
+ // actually stale file: `require` returns the same exports in both cases.
675
+ // The restart diagnosis is valid only when the selected file itself carries
676
+ // the current sentinel; otherwise a restart would simply reload stale disk.
677
+ let diskHasExpectedSentinel = false;
678
+ try {
679
+ diskHasExpectedSentinel = fs.readFileSync(candidate).includes(ctx.versionSentinelExport);
680
+ } catch {
681
+ // The successful require above normally guarantees readability. If the
682
+ // file disappears concurrently, retain the safe reinstall diagnosis.
683
+ }
684
+ if (residentSentinel && diskHasExpectedSentinel) {
685
+ const residentVersion = residentSentinel.slice("__piNativesV".length).replace(/_/g, ".");
686
+ throw new Error(
687
+ `Loaded ${candidate}, which exposes the @linxiraos/pi-natives@${residentVersion} version ` +
688
+ `sentinel \`${residentSentinel}\` but not the @${ctx.packageVersion} sentinel ` +
689
+ `\`${ctx.versionSentinelExport}\` this loader expects. omp was upgraded to ` +
690
+ `${ctx.packageVersion} while this session was running; the ${residentVersion} addon is ` +
691
+ "still resident in this process. Disk is already consistent — restart omp to pick up " +
692
+ `${ctx.packageVersion} (reinstalling changes nothing).`,
693
+ );
694
+ }
695
+ throw new Error(
696
+ `Loaded ${candidate} but it does not expose the @linxiraos/pi-natives@${ctx.packageVersion} ` +
697
+ `version sentinel \`${ctx.versionSentinelExport}\`. The .node file on disk is from a different ` +
698
+ "release than this loader — reinstall to re-sync.",
699
+ );
700
+ }
701
+
702
+ /**
703
+ * Install the addon's bounded Tokio runtime now that `dlopen` has returned and
704
+ * the dynamic-loader lock is released. The Rust `#[module_init]` deliberately
705
+ * does NOT build the runtime — spawning worker threads under the loader lock
706
+ * deadlocks on some hosts — so it exposes `__ompInstallTokioRuntime` for the
707
+ * loader to call once, before any async native runs. Best-effort: older addons
708
+ * predating this export simply fall back to napi-rs's default runtime.
709
+ */
710
+ function installNativeTokioRuntime(bindings) {
711
+ const install = bindings.__ompInstallTokioRuntime;
712
+ if (typeof install !== "function") return;
713
+ try {
714
+ install();
715
+ startupMarker("native:tokioRuntime:installed");
716
+ } catch (err) {
717
+ startupMarker(`native:tokioRuntime:failed:${err instanceof Error ? err.message : String(err)}`);
718
+ }
719
+ }
720
+
721
+
722
+ function buildHelpMessage(ctx) {
723
+ if (ctx.isCompiledBinary) {
724
+ const expectedPaths = ctx.addonFilenames.map(filename => ` ${path.join(ctx.versionedDir, filename)}`).join("\n");
725
+ const downloadHints = ctx.addonFilenames
726
+ .map(filename => {
727
+ const downloadUrl = `https://github.com/Linxira-OS/linxira-zeta/releases/latest/download/${filename}`;
728
+ const targetPath = path.join(ctx.versionedDir, filename);
729
+ return ` curl -fsSL "${downloadUrl}" -o "${targetPath}"`;
730
+ })
731
+ .join("\n");
732
+ return (
733
+ `The compiled binary should extract one of:\n${expectedPaths}\n\n` +
734
+ `If missing, delete ${ctx.versionedDir} and re-run, or download manually:\n${downloadHints}`
735
+ );
736
+ }
737
+ return (
738
+ "If installed via npm/bun, try reinstalling: bun install @linxiraos/pi-natives\n" +
739
+ "If developing locally, build with: bun --cwd=packages/natives run build\n" +
740
+ "Explicit targets: bun scripts/bazel-natives.ts <target> --dest packages/natives/native"
741
+ );
742
+ }
743
+
744
+ /**
745
+ * Initialize the loader context: resolves every path, variant, and policy
746
+ * decision once so the inner load loop stays a pure require/validate pipeline.
747
+ * Called from `loadNative()` rather than at module scope so importing pure
748
+ * helpers from this file doesn't trigger AVX2 detection or filesystem probes.
749
+ */
750
+ /**
751
+ * @param {{ nativeDir?: string; platform?: NodeJS.Platform | string; isCompiledBinary?: boolean; leafPackageDir?: string | null }} [overrides]
752
+ */
753
+ export function initLoaderContext(overrides = {}) {
754
+ const platform = overrides.platform ?? process.platform;
755
+ const platformTag = `${platform}-${process.arch}`;
756
+ const packageVersion = packageJson.version;
757
+ const nativeDir = overrides.nativeDir ?? path.join(import.meta.dir, "..", "native");
758
+ const execDir = path.dirname(process.execPath);
759
+ const nativesDir = getNativesDir();
760
+ const versionedDir = path.join(nativesDir, packageVersion);
761
+ const userDataDir =
762
+ platform === "win32"
763
+ ? path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"), "omp")
764
+ : path.join(os.homedir(), ".local", "bin");
765
+
766
+ const isCompiledBinary =
767
+ overrides.isCompiledBinary ??
768
+ detectCompiledBinary({
769
+ embeddedAddon,
770
+ env: process.env,
771
+ importMetaUrl: import.meta.url,
772
+ });
773
+ const normalizedNativeDir = platform === "win32" ? nativeDir.toLowerCase() : nativeDir;
774
+ const isWorkspaceLoad =
775
+ !isCompiledBinary &&
776
+ !normalizedNativeDir.includes("\\node_modules\\") &&
777
+ !normalizedNativeDir.includes("/node_modules/");
778
+ const leafPackageDir =
779
+ isCompiledBinary || isWorkspaceLoad
780
+ ? null
781
+ : overrides.leafPackageDir === undefined
782
+ ? resolveLeafPackageDir(platformTag)
783
+ : overrides.leafPackageDir;
784
+ const stageFromNodeModules = shouldStageNodeModulesAddon({
785
+ platform,
786
+ isCompiledBinary,
787
+ nativeDir: normalizedNativeDir,
788
+ });
789
+
790
+ const selectedVariant = resolveCpuVariant(getVariantOverride());
791
+ const addonFilenames = getAddonFilenames({ tag: platformTag, arch: process.arch, variant: selectedVariant });
792
+ const addonLabel = selectedVariant ? `${platformTag} (${selectedVariant})` : platformTag;
793
+
794
+ const candidates = resolveLoaderCandidates({
795
+ addonFilenames,
796
+ isCompiledBinary,
797
+ stageFromNodeModules,
798
+ nativeDir,
799
+ leafPackageDir,
800
+ execDir,
801
+ versionedDir,
802
+ userDataDir,
803
+ });
804
+
805
+ // Version sentinel emitted by the Rust addon under a `js_name` that encodes
806
+ // the package version (`__piNativesV{major}_{minor}_{patch}`).
807
+ // `scripts/release.ts` bumps the name in `crates/pi-natives/src/lib.rs` in
808
+ // lock-step with the version, so a `.node` from a different release
809
+ // physically cannot expose the symbol this loader is looking for. That
810
+ // turns the silent `<sym> is not a function` crash from a Windows
811
+ // locked-file update into an actionable load-time error.
812
+ const versionSentinelExport = `__piNativesV${packageVersion.replace(/[^A-Za-z0-9]/g, "_")}`;
813
+
814
+ return {
815
+ platformTag,
816
+ packageVersion,
817
+ nativeDir,
818
+ leafPackageDir,
819
+ versionedDir,
820
+ isCompiledBinary,
821
+ stageFromNodeModules,
822
+ selectedVariant,
823
+ addonFilenames,
824
+ addonLabel,
825
+ candidates,
826
+ versionSentinelExport,
827
+ isWorkspaceLoad,
828
+ nativesDir,
829
+ };
830
+ }
831
+
832
+ export function loadNative() {
833
+ startupMarker("native:loadNative:start");
834
+ const ctx = initLoaderContext();
835
+ const require_ = createRequire(import.meta.url);
836
+
837
+ const errors = [];
838
+ const embeddedCandidate = maybeExtractEmbeddedAddon(ctx, errors);
839
+ const stagedCandidate = embeddedCandidate ? null : maybeStageNodeModulesAddon(ctx, errors);
840
+ const prepended = [embeddedCandidate, stagedCandidate].filter(c => typeof c === "string");
841
+ const runtimeCandidates = prepended.length > 0 ? [...prepended, ...ctx.candidates] : ctx.candidates;
842
+
843
+ for (const candidate of runtimeCandidates) {
844
+ try {
845
+ startupMarker(`native:require:${path.basename(candidate)}`);
846
+ const bindings = require_(candidate);
847
+ validateLoadedBindings(ctx, bindings, candidate);
848
+ installNativeTokioRuntime(bindings);
849
+ cleanupStaleNativeVersions({ nativesDir: ctx.nativesDir, currentVersion: ctx.packageVersion });
850
+ startupMarker("native:loadNative:done");
851
+ return bindings;
852
+ } catch (err) {
853
+ const message = err instanceof Error ? err.message : String(err);
854
+ errors.push(`${candidate}: ${message}`);
855
+ }
856
+ }
857
+
858
+ if (!SUPPORTED_PLATFORMS.includes(ctx.platformTag)) {
859
+ throw new Error(
860
+ `Unsupported platform: ${ctx.platformTag}\n` +
861
+ `Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}\n` +
862
+ "If you need support for this platform, please open an issue.",
863
+ );
864
+ }
865
+ const details = errors.map(error => `- ${error}`).join("\n");
866
+ throw new Error(
867
+ `Failed to load pi_natives native addon for ${ctx.addonLabel}.\n\nTried:\n${details}\n\n${buildHelpMessage(ctx)}`,
868
+ );
869
+ }