@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.
- package/README.md +80 -0
- package/native/clipboard.d.ts +9 -0
- package/native/clipboard.js +11 -0
- package/native/desktop.d.ts +4 -0
- package/native/desktop.js +10 -0
- package/native/embedded-addon.js +31 -0
- package/native/index.d.ts +2013 -0
- package/native/index.js +142 -0
- package/native/loader-state.d.ts +129 -0
- package/native/loader-state.js +869 -0
- package/package.json +89 -0
|
@@ -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
|
+
}
|