@gjsify/node-gi 0.37.0 → 0.38.1

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/gtk-runtime.d.ts CHANGED
@@ -14,4 +14,12 @@ export function maybePrependGtkRuntimeDllPath(): void;
14
14
  /** Windows: wire the env for a full-windowing bundle's runtime data — GSettings schemas, gdk-pixbuf loaders, icon themes, fontconfig (no-op off win32 / for a display-free bundle). */
15
15
  export function maybeWireGtkWindowingEnv(): void;
16
16
  /** Activate the bundled GTK runtime for the native engine, if one is present. */
17
- export function activateBundledGtkRuntime(native: { prependSearchPath: (p: string) => void }): GtkRuntimeBundle | null;
17
+ export function activateBundledGtkRuntime(native: {
18
+ prependSearchPath: (p: string) => void;
19
+ prependLibraryPath?: (p: string) => void;
20
+ }): GtkRuntimeBundle | null;
21
+
22
+ /** Prepend GI's shared-library search path for the GTK the policy chose. */
23
+ export function activateGiLibraryPath(native: { prependLibraryPath?: (p: string) => void }): string[];
24
+ /** TEST-ONLY: allow the activation to run again. */
25
+ export function resetGiLibraryPathForTests(): void;
package/gtk-runtime.js CHANGED
@@ -11,7 +11,7 @@ import { createRequire } from 'node:module';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { dirname, join } from 'node:path';
13
13
  import { existsSync } from 'node:fs';
14
- import { pathCovers, splitSearchPath, systemGiLibraryDirs } from './system-gi.js';
14
+ import { composeDyldFallback, pathCovers, splitSearchPath, systemGiLibraryDirs } from './system-gi.js';
15
15
  import { nativeCandidates, prebuildAddonPath } from './native-paths.js';
16
16
 
17
17
  // NB: node:child_process is intentionally NOT imported at module top level. It is
@@ -309,9 +309,11 @@ export function maybeReexecForGtkRuntime() {
309
309
  const env = {
310
310
  ...process.env,
311
311
  [REEXEC_SENTINEL]: '1',
312
- // Our dirs first, then whatever the host asked for — falling back to
313
- // dyld's own default tail so setting the variable never REMOVES /usr/lib.
314
- DYLD_FALLBACK_LIBRARY_PATH: [...libDirs, ...(curDyldDirs.length > 0 ? curDyldDirs : ['/usr/lib'])].join(':'),
312
+ // Our dirs first, then whatever the host asked for, then dyld's own
313
+ // default list — setting the variable REPLACES that list, so carrying
314
+ // it is what keeps the re-exec'd child from searching LESS than the
315
+ // process that spawned it.
316
+ DYLD_FALLBACK_LIBRARY_PATH: composeDyldFallback(libDirs),
315
317
  };
316
318
  // Only a bundle relocates typelibs; a host GI stack's are already on GI's own
317
319
  // search path, so the system branch deliberately leaves GI_TYPELIB_PATH alone.
@@ -457,6 +459,57 @@ function gstScannerLeaf() {
457
459
  return process.platform === 'win32' ? 'gst-plugin-scanner.exe' : 'gst-plugin-scanner';
458
460
  }
459
461
 
462
+ let libraryPathActivated = false;
463
+
464
+ /**
465
+ * Tell GI where the shared libraries its typelibs name by BARE LEAF actually are
466
+ * — for whichever GTK the policy chose, and WITHOUT an environment variable.
467
+ *
468
+ * THE GAP THIS CLOSES. A typelib records its backer as `libgtk-4.1.dylib`, and on
469
+ * macOS neither a relocated bundle's `lib` dir nor Homebrew's `/usr/local/lib` is
470
+ * on dyld's search path. Node papers over it in {@link maybeReexecForGtkRuntime}
471
+ * by re-execing with `DYLD_FALLBACK_LIBRARY_PATH` set — a Node-shaped re-exec, so
472
+ * it returns early on bun and deno, which got NO loader repair and died at the
473
+ * first widget behind three errors that all point elsewhere (`GtkWidget is not
474
+ * registered…`, `Adw.Application has no property 'application-id'`, `Gtk.GLArea is
475
+ * not a subclassable GObject type`). Measured on the macOS 15.7.9 x86_64 VM.
476
+ *
477
+ * `gi_repository_prepend_library_path()` is GI's own answer, so nothing is
478
+ * captured at process launch: identical on node, bun and deno, no re-exec, and the
479
+ * environment is left alone. The env paths elsewhere in this module stay — they
480
+ * still cover a dylib pulled in by ANOTHER dylib's link closure, which never
481
+ * passes through GI.
482
+ *
483
+ * The dirs come from `gtkSource()`, not a second opinion: the bundle's `libDir`
484
+ * for `bundle`, `systemGiLibraryDirs()` for `system` — the pair the re-exec
485
+ * composes. Mixing them is the two-copies hazard #920 records. Idempotent, and
486
+ * never fatal: an addon predating the binding changes nothing.
487
+ * @param {{ prependLibraryPath?: (p: string) => void }} native the loaded addon
488
+ * @returns {string[]} the directories handed to GI (empty when there was nothing to add)
489
+ */
490
+ export function activateGiLibraryPath(native) {
491
+ if (libraryPathActivated) return [];
492
+ libraryPathActivated = true;
493
+ if (typeof native?.prependLibraryPath !== 'function') return [];
494
+
495
+ const source = gtkSource();
496
+ const dirs =
497
+ source === 'bundle' ? [resolveGtkRuntimeBundle()?.libDir] : source === 'system' ? systemGiLibraryDirs() : [];
498
+ const applied = [];
499
+ // LAST wins with a prepend, so walking in reverse leaves the array's own
500
+ // order intact in GI's search path.
501
+ for (const dir of dirs.filter(Boolean).reverse()) {
502
+ native.prependLibraryPath(dir);
503
+ applied.unshift(dir);
504
+ }
505
+ return applied;
506
+ }
507
+
508
+ /** TEST-ONLY: allow a spec to run the activation again. */
509
+ export function resetGiLibraryPathForTests() {
510
+ libraryPathActivated = false;
511
+ }
512
+
460
513
  let activated = null; // memoize: the activation result (idempotent)
461
514
 
462
515
  /**
@@ -502,9 +555,10 @@ export function activateBundledGtkRuntime(native) {
502
555
  } else {
503
556
  const existing = process.env.DYLD_FALLBACK_LIBRARY_PATH;
504
557
  if (!existing || !existing.split(':').includes(bundle.libDir)) {
505
- process.env.DYLD_FALLBACK_LIBRARY_PATH = existing
506
- ? `${bundle.libDir}:${existing}`
507
- : `${bundle.libDir}:/usr/lib`;
558
+ // Same composition as the re-exec above: the bundle's libdir first,
559
+ // then the inherited value, then dyld's default list — which this
560
+ // variable REPLACES rather than extends.
561
+ process.env.DYLD_FALLBACK_LIBRARY_PATH = composeDyldFallback([bundle.libDir]);
508
562
  }
509
563
  }
510
564
 
package/index.d.ts CHANGED
@@ -65,6 +65,8 @@ export function setErrorBuilder(
65
65
 
66
66
  /** Prepend a directory to the GIRepository typelib search path. */
67
67
  export function prependSearchPath(path: string): void;
68
+ /** Prepend a directory GI searches for the SHARED LIBRARY a typelib names. */
69
+ export function prependLibraryPath(path: string): void;
68
70
 
69
71
  /**
70
72
  * Invoke a namespace-level GObject-Introspection function (not an instance
@@ -519,6 +521,7 @@ declare const native: {
519
521
  getErrorDomain: typeof getErrorDomain;
520
522
  setErrorBuilder: typeof setErrorBuilder;
521
523
  prependSearchPath: typeof prependSearchPath;
524
+ prependLibraryPath: typeof prependLibraryPath;
522
525
  callFunction: typeof callFunction;
523
526
  callMethod: typeof callMethod;
524
527
  hasMethod: typeof hasMethod;
package/index.js CHANGED
@@ -11,6 +11,7 @@ import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
11
11
  import { describeAddonLoadFailure } from './load-diagnostics.js';
12
12
  import {
13
13
  activateBundledGtkRuntime,
14
+ activateGiLibraryPath,
14
15
  maybePrependGtkRuntimeDllPath,
15
16
  maybeReexecForGtkRuntime,
16
17
  maybeWireGtkWindowingEnv,
@@ -123,6 +124,16 @@ try {
123
124
  // Never fatal: a missing/partial bundle just leaves the host GTK in charge.
124
125
  }
125
126
 
127
+ // And where the LIBRARIES those typelibs name actually live. Separate from the
128
+ // call above because it is not about a bundle: it applies to whichever GTK the
129
+ // policy chose, and it is the only loader repair bun and deno get on macOS —
130
+ // the re-exec above is Node-shaped and skips them.
131
+ try {
132
+ activateGiLibraryPath(native);
133
+ } catch {
134
+ // Same contract as above: never fatal.
135
+ }
136
+
126
137
  // Cross-runtime microtask checkpoint (Bun/Deno only). Node's napi_make_callback runs
127
138
  // the nextTick + microtask checkpoint when its callback scope closes, so promise
128
139
  // continuations queued by a loop-dispatched GLib→JS callback drain at the callback
@@ -182,6 +193,7 @@ export const getEnumValues = native.getEnumValues;
182
193
  export const getErrorDomain = native.getErrorDomain;
183
194
  export const setErrorBuilder = native.setErrorBuilder;
184
195
  export const prependSearchPath = native.prependSearchPath;
196
+ export const prependLibraryPath = native.prependLibraryPath;
185
197
  export const callFunction = native.callFunction;
186
198
  export const callMethod = native.callMethod;
187
199
  export const hasMethod = native.hasMethod;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.37.0",
3
+ "version": "0.38.1",
4
4
  "description": "GObject-Introspection runtime for Node.js — load gi:// namespaces (GLib/GObject/Gio/…) with GJS-compatible semantics. Vendored + rewritten from node-gtk under MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/addon.cc CHANGED
@@ -74,6 +74,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
74
74
  exports.Set("getErrorDomain", Napi::Function::New(env, GetErrorDomain));
75
75
  exports.Set("setErrorBuilder", Napi::Function::New(env, SetErrorBuilder));
76
76
  exports.Set("prependSearchPath", Napi::Function::New(env, PrependSearchPath));
77
+ exports.Set("prependLibraryPath", Napi::Function::New(env, PrependLibraryPath));
77
78
  exports.Set("callFunction", Napi::Function::New(env, CallFunction));
78
79
  exports.Set("callMethod", Napi::Function::New(env, CallMethod));
79
80
  exports.Set("hasMethod", Napi::Function::New(env, HasMethod));
package/src/common.h CHANGED
@@ -550,6 +550,7 @@ Napi::Value GetEnumValues(const Napi::CallbackInfo& info);
550
550
  Napi::Value GetErrorDomain(const Napi::CallbackInfo& info);
551
551
  Napi::Value SetErrorBuilder(const Napi::CallbackInfo& info);
552
552
  Napi::Value PrependSearchPath(const Napi::CallbackInfo& info);
553
+ Napi::Value PrependLibraryPath(const Napi::CallbackInfo& info);
553
554
 
554
555
  // calls.cc
555
556
  Napi::Value CallFunction(const Napi::CallbackInfo& info);
package/src/repo.cc CHANGED
@@ -295,4 +295,30 @@ Napi::Value PrependSearchPath(const Napi::CallbackInfo& info) {
295
295
  return env.Undefined();
296
296
  }
297
297
 
298
+ // prependLibraryPath(path: string) -> void
299
+ // The LIBRARY twin of PrependSearchPath: where GI looks for the shared object a
300
+ // typelib names, as opposed to where it looks for the typelib itself.
301
+ //
302
+ // Native rather than `requireGi('GIRepository').…` because the bundled runtime
303
+ // ships GIRepository 3.0, whose `Repository` this bridge exposes neither as
304
+ // constructible nor with a `get_default()` static — measured, both throw. The C
305
+ // entry point has no such gap and reuses the same `DupDefaultRepository()` handle.
306
+ //
307
+ // It is the env-free repair for a typelib backer named by BARE LEAF: no variable,
308
+ // no re-exec, identical on node, bun and deno. See `gtk-runtime.js`
309
+ // (`activateGiLibraryPath`) for the incident.
310
+ Napi::Value PrependLibraryPath(const Napi::CallbackInfo& info) {
311
+ Napi::Env env = info.Env();
312
+ if (info.Length() < 1 || !info[0].IsString()) {
313
+ Napi::TypeError::New(env, "prependLibraryPath(path: string)")
314
+ .ThrowAsJavaScriptException();
315
+ return env.Undefined();
316
+ }
317
+ std::string path = info[0].As<Napi::String>().Utf8Value();
318
+ GIRepository* repo = DupDefaultRepository();
319
+ gi_repository_prepend_library_path(repo, path.c_str());
320
+ g_object_unref(repo);
321
+ return env.Undefined();
322
+ }
323
+
298
324
  } // namespace nodegi
package/system-gi.js CHANGED
@@ -227,3 +227,43 @@ export function systemGiLibraryDirs({
227
227
  export function pathCovers(wanted, current) {
228
228
  return wanted.every((dir) => current.includes(dir));
229
229
  }
230
+
231
+ /**
232
+ * dyld's OWN default fallback list, verbatim from `dyld(1)`:
233
+ * `$HOME/lib:/usr/local/lib:/lib:/usr/lib`.
234
+ *
235
+ * Load-bearing because `DYLD_FALLBACK_LIBRARY_PATH` REPLACES this list rather
236
+ * than extending it, so a composition that omits part of it hands the child a
237
+ * SMALLER search path than the same child would have had with the variable
238
+ * unset. MEASURED, and it cost a whole platform: both writers used to append a
239
+ * bare `/usr/lib`, dropping `/usr/local/lib` — where every Homebrew GTK library
240
+ * lives on Intel macOS — so every `gjsify` gjs command on such a Mac died in
241
+ * `dlopen(libsoup-3.0.0.dylib)` before it had downloaded anything (the CLI's own
242
+ * fetcher goes through Soup), and so did the Node-free bootstrap the Getting
243
+ * Started page prints. The tell that it was OURS: plain
244
+ * `gjs -c 'imports.gi.Soup'` loads fine with no DYLD variable set.
245
+ * @param {Record<string, string | undefined>} [env]
246
+ * @returns {string[]}
247
+ */
248
+ export function dyldDefaultFallbackDirs(env = process.env) {
249
+ const home = env['HOME'];
250
+ return [...(home ? [`${home}/lib`] : []), '/usr/local/lib', '/lib', '/usr/lib'];
251
+ }
252
+
253
+ /**
254
+ * Compose a `DYLD_FALLBACK_LIBRARY_PATH` value: `wanted`, then the launching
255
+ * environment's own value, then {@link dyldDefaultFallbackDirs} —
256
+ * unconditionally, because the target is a CHILD (a re-exec, or a spawned gjs)
257
+ * and it must never search less than one launched without gjsify. Deduplicated:
258
+ * `systemGiLibraryDirs()` can legitimately yield a directory the tail also
259
+ * names, and a repeat costs the loader a second stat on every miss. First
260
+ * occurrence wins everywhere, so dropping later repeats preserves behaviour.
261
+ * @param {readonly string[]} wanted
262
+ * @param {Record<string, string | undefined>} [env]
263
+ * @returns {string}
264
+ */
265
+ export function composeDyldFallback(wanted, env = process.env) {
266
+ return [
267
+ ...new Set([...wanted, ...splitSearchPath(env['DYLD_FALLBACK_LIBRARY_PATH']), ...dyldDefaultFallbackDirs(env)]),
268
+ ].join(':');
269
+ }