@gjsify/node-gi 0.26.1 → 0.27.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/globals.d.ts CHANGED
@@ -6,38 +6,38 @@ import type { GettextModule } from './gettext.js';
6
6
 
7
7
  /** The legacy GJS `imports` object (a minimal Node-backed subset). */
8
8
  export interface GjsImports {
9
- /** `imports.gi.<Ns>` resolves a namespace; `imports.gi.versions.<Ns>` pins a version. */
10
- gi: {
11
- versions: Record<string, string | undefined>;
12
- [namespace: string]: unknown;
13
- };
14
- /** `imports.system` — the `@gjsify/node-gi/system` module (process identity + lifecycle). */
15
- system: SystemModule;
16
- /** `imports.gettext` — the `@gjsify/node-gi/gettext` module (no-translation passthrough). */
17
- gettext: GettextModule;
18
- /** `imports.byteArray` — the legacy GJS byte-array module (GJS semantics: zero-terminated, fatal decode). */
19
- byteArray: {
20
- ByteArray: new (arg?: number | Uint8Array) => {
21
- length: number;
22
- toString(encoding?: string): string;
23
- toGBytes(): unknown;
9
+ /** `imports.gi.<Ns>` resolves a namespace; `imports.gi.versions.<Ns>` pins a version. */
10
+ gi: {
11
+ versions: Record<string, string | undefined>;
12
+ [namespace: string]: unknown;
24
13
  };
25
- fromArray(array: Iterable<number>): unknown;
26
- fromGBytes(bytes: unknown): Uint8Array;
27
- fromString(string: string, encoding?: string): Uint8Array;
28
- toGBytes(array: Uint8Array): unknown;
29
- toString(byteArray: Uint8Array, encoding?: string): string;
30
- };
31
- versions: Record<string, unknown>;
14
+ /** `imports.system` — the `@gjsify/node-gi/system` module (process identity + lifecycle). */
15
+ system: SystemModule;
16
+ /** `imports.gettext` — the `@gjsify/node-gi/gettext` module (no-translation passthrough). */
17
+ gettext: GettextModule;
18
+ /** `imports.byteArray` — the legacy GJS byte-array module (GJS semantics: zero-terminated, fatal decode). */
19
+ byteArray: {
20
+ ByteArray: new (arg?: number | Uint8Array) => {
21
+ length: number;
22
+ toString(encoding?: string): string;
23
+ toGBytes(): unknown;
24
+ };
25
+ fromArray(array: Iterable<number>): unknown;
26
+ fromGBytes(bytes: unknown): Uint8Array;
27
+ fromString(string: string, encoding?: string): Uint8Array;
28
+ toGBytes(array: Uint8Array): unknown;
29
+ toString(byteArray: Uint8Array, encoding?: string): string;
30
+ };
31
+ versions: Record<string, unknown>;
32
32
  }
33
33
 
34
34
  declare global {
35
- var print: (...args: unknown[]) => void;
36
- var printerr: (...args: unknown[]) => void;
37
- var log: (...args: unknown[]) => void;
38
- var logError: (error: unknown, prefix?: string) => void;
39
- var ARGV: string[];
40
- var imports: GjsImports;
35
+ var print: (...args: unknown[]) => void;
36
+ var printerr: (...args: unknown[]) => void;
37
+ var log: (...args: unknown[]) => void;
38
+ var logError: (error: unknown, prefix?: string) => void;
39
+ var ARGV: string[];
40
+ var imports: GjsImports;
41
41
  }
42
42
 
43
43
  /** Install the GJS ambient globals on `globalThis` (idempotent). */
package/globals.js CHANGED
@@ -29,7 +29,7 @@ import { createPackage } from './overrides/package.js';
29
29
  // GJS stringifies each argument with String() and joins with a space (no
30
30
  // util.inspect object formatting) — match that for fidelity.
31
31
  function gjsFormat(args) {
32
- return args.map((a) => String(a)).join(' ');
32
+ return args.map((a) => String(a)).join(' ');
33
33
  }
34
34
 
35
35
  /**
@@ -37,112 +37,118 @@ function gjsFormat(args) {
37
37
  * only defined if absent, so it never clobbers a host that already provides it).
38
38
  */
39
39
  export function installGjsGlobals() {
40
- const g = globalThis;
40
+ const g = globalThis;
41
41
 
42
- if (typeof g.print === 'undefined') {
43
- g.print = (...args) => console.log(gjsFormat(args));
44
- }
45
- if (typeof g.printerr === 'undefined') {
46
- g.printerr = (...args) => console.error(gjsFormat(args));
47
- }
48
- if (typeof g.log === 'undefined') {
49
- // GJS `log` writes to the GLib structured log (stderr/journal).
50
- g.log = (...args) => console.error(gjsFormat(args));
51
- }
52
- if (typeof g.logError === 'undefined') {
53
- g.logError = (error, prefix) => {
54
- const head = prefix ? `${prefix}: ` : '';
55
- const body = error && error.stack ? error.stack : String(error);
56
- console.error(head + body);
57
- };
58
- }
59
- if (typeof g.ARGV === 'undefined') {
60
- // GJS ARGV excludes the interpreter + the script path (Node's argv[0]+[1]).
61
- g.ARGV = (typeof process !== 'undefined' && Array.isArray(process.argv))
62
- ? process.argv.slice(2)
63
- : [];
64
- }
42
+ if (typeof g.print === 'undefined') {
43
+ g.print = (...args) => console.log(gjsFormat(args));
44
+ }
45
+ if (typeof g.printerr === 'undefined') {
46
+ g.printerr = (...args) => console.error(gjsFormat(args));
47
+ }
48
+ if (typeof g.log === 'undefined') {
49
+ // GJS `log` writes to the GLib structured log (stderr/journal).
50
+ g.log = (...args) => console.error(gjsFormat(args));
51
+ }
52
+ if (typeof g.logError === 'undefined') {
53
+ g.logError = (error, prefix) => {
54
+ const head = prefix ? `${prefix}: ` : '';
55
+ const body = error && error.stack ? error.stack : String(error);
56
+ console.error(head + body);
57
+ };
58
+ }
59
+ if (typeof g.ARGV === 'undefined') {
60
+ // GJS ARGV excludes the interpreter + the script path (Node's argv[0]+[1]).
61
+ g.ARGV = typeof process !== 'undefined' && Array.isArray(process.argv) ? process.argv.slice(2) : [];
62
+ }
65
63
 
66
- if (typeof g.imports === 'undefined') {
67
- g.imports = makeImports();
68
- }
64
+ if (typeof g.imports === 'undefined') {
65
+ g.imports = makeImports();
66
+ }
69
67
 
70
- return g.imports;
68
+ return g.imports;
71
69
  }
72
70
 
73
71
  // Build the legacy `imports` object. `imports.gi.<Ns>` resolves a namespace via
74
72
  // the node-gi backend, honouring a version pinned in `imports.gi.versions.<Ns>`
75
73
  // (which GJS code sets before the first access).
76
74
  function makeImports() {
77
- const giVersions = Object.create(null);
78
- const giCache = new Map();
79
- const gi = new Proxy(
80
- { versions: giVersions },
81
- {
82
- get(target, name) {
83
- if (name === 'versions') return giVersions;
84
- if (typeof name !== 'string') return target[name];
85
- if (giCache.has(name)) return giCache.get(name);
86
- const ns = requireGi(name, giVersions[name]);
87
- giCache.set(name, ns);
88
- return ns;
89
- },
90
- has(_target, name) {
91
- return name === 'versions' || typeof name === 'string';
92
- },
93
- },
94
- );
75
+ const giVersions = Object.create(null);
76
+ const giCache = new Map();
77
+ const gi = new Proxy(
78
+ { versions: giVersions },
79
+ {
80
+ get(target, name) {
81
+ if (name === 'versions') return giVersions;
82
+ if (typeof name !== 'string') return target[name];
83
+ if (giCache.has(name)) return giCache.get(name);
84
+ const ns = requireGi(name, giVersions[name]);
85
+ giCache.set(name, ns);
86
+ return ns;
87
+ },
88
+ has(_target, name) {
89
+ return name === 'versions' || typeof name === 'string';
90
+ },
91
+ },
92
+ );
95
93
 
96
- // `imports.system` + `imports.gettext` + `imports.cairo` are the standalone
97
- // module objects (`./system.js` / `./gettext.js` / `./cairo.js`) — the single
98
- // source of truth for those surfaces, shared with the bare `system` / `gettext` /
99
- // `cairo` specifiers on Node.
100
- //
101
- // `imports.signals` is the pure-JS Signals mixin (`addSignalMethods` + the
102
- // `_connect`/`_disconnect`/`_emit`/… primitives), the SAME module the DBus proxy
103
- // surface uses. `imports.mainloop` (a lazy getter — it needs GLib, and a source
104
- // that never touches the main loop should not pull GLib in) is the thin
105
- // GLib.MainLoop convenience layer.
106
- const { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll } =
107
- signalsModule;
108
- const imports = {
109
- gi,
110
- system,
111
- gettext,
112
- cairo,
113
- // `imports.byteArray` — the legacy GJS byte-array module
114
- // (fromGBytes/toGBytes/fromString/toString/fromArray/ByteArray), the seam
115
- // `@gjsify/utils`' gbytesToUint8Array/cli() read subprocess output through.
116
- // GLib is only pulled in lazily (toGBytes), so seeding it here is free.
117
- byteArray: createByteArray(requireGi),
118
- signals: { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll },
119
- // GJS's `imports.searchPath` — `imports.package.init` unshifts the app's
120
- // moduledir onto it. Inert on node-gi (ESM + gi://), but present + safe so
121
- // source that reads/mutates it doesn't throw.
122
- searchPath: [],
123
- versions: Object.create(null),
124
- };
125
- let mainloop;
126
- Object.defineProperty(imports, 'mainloop', {
127
- get() {
128
- if (mainloop === undefined) mainloop = createMainloop(requireGi('GLib', '2.0'));
129
- return mainloop;
130
- },
131
- enumerable: true,
132
- configurable: true,
133
- });
134
- // `imports.package` — the GJS application bootstrap (pkg.init/initGettext/
135
- // initFormat). Lazy like `mainloop`: a bundle that never calls it pulls no GLib.
136
- let pkg;
137
- Object.defineProperty(imports, 'package', {
138
- get() {
139
- if (pkg === undefined) pkg = createPackage({ requireGi, gettext, imports });
140
- return pkg;
141
- },
142
- enumerable: true,
143
- configurable: true,
144
- });
145
- return imports;
94
+ // `imports.system` + `imports.gettext` + `imports.cairo` are the standalone
95
+ // module objects (`./system.js` / `./gettext.js` / `./cairo.js`) — the single
96
+ // source of truth for those surfaces, shared with the bare `system` / `gettext` /
97
+ // `cairo` specifiers on Node.
98
+ //
99
+ // `imports.signals` is the pure-JS Signals mixin (`addSignalMethods` + the
100
+ // `_connect`/`_disconnect`/`_emit`/… primitives), the SAME module the DBus proxy
101
+ // surface uses. `imports.mainloop` (a lazy getter — it needs GLib, and a source
102
+ // that never touches the main loop should not pull GLib in) is the thin
103
+ // GLib.MainLoop convenience layer.
104
+ const { addSignalMethods, _connect, _connectAfter, _disconnect, _emit, _signalHandlerIsConnected, _disconnectAll } =
105
+ signalsModule;
106
+ const imports = {
107
+ gi,
108
+ system,
109
+ gettext,
110
+ cairo,
111
+ // `imports.byteArray` — the legacy GJS byte-array module
112
+ // (fromGBytes/toGBytes/fromString/toString/fromArray/ByteArray), the seam
113
+ // `@gjsify/utils`' gbytesToUint8Array/cli() read subprocess output through.
114
+ // GLib is only pulled in lazily (toGBytes), so seeding it here is free.
115
+ byteArray: createByteArray(requireGi),
116
+ signals: {
117
+ addSignalMethods,
118
+ _connect,
119
+ _connectAfter,
120
+ _disconnect,
121
+ _emit,
122
+ _signalHandlerIsConnected,
123
+ _disconnectAll,
124
+ },
125
+ // GJS's `imports.searchPath` — `imports.package.init` unshifts the app's
126
+ // moduledir onto it. Inert on node-gi (ESM + gi://), but present + safe so
127
+ // source that reads/mutates it doesn't throw.
128
+ searchPath: [],
129
+ versions: Object.create(null),
130
+ };
131
+ let mainloop;
132
+ Object.defineProperty(imports, 'mainloop', {
133
+ get() {
134
+ if (mainloop === undefined) mainloop = createMainloop(requireGi('GLib', '2.0'));
135
+ return mainloop;
136
+ },
137
+ enumerable: true,
138
+ configurable: true,
139
+ });
140
+ // `imports.package` — the GJS application bootstrap (pkg.init/initGettext/
141
+ // initFormat). Lazy like `mainloop`: a bundle that never calls it pulls no GLib.
142
+ let pkg;
143
+ Object.defineProperty(imports, 'package', {
144
+ get() {
145
+ if (pkg === undefined) pkg = createPackage({ requireGi, gettext, imports });
146
+ return pkg;
147
+ },
148
+ enumerable: true,
149
+ configurable: true,
150
+ });
151
+ return imports;
146
152
  }
147
153
 
148
154
  // Side effect on import: seed the globals.
package/gtk-runtime.js CHANGED
@@ -1,14 +1,17 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // @gjsify/node-gi/gtk-runtime — locate + activate a batteries-included, relocated
3
3
  // GTK/GObject-Introspection runtime bundle so gi:// namespaces load with NO system
4
- // or Homebrew GTK (Phase 2 of cross-platform node-gi). Today only darwin-arm64
5
- // ships such a bundle (@gjsify/gtk-runtime-darwin-arm64); this is a no-op on every
6
- // other platform, and harmless when no bundle is present (the addon then uses the
7
- // host's GTK exactly as before).
4
+ // or Homebrew GTK (Phase 2 of cross-platform node-gi). Bundles ship for darwin-arm64,
5
+ // darwin-x64 and win32-x64 (@gjsify/gtk-runtime-<platform>-<arch>); NOTHING below
6
+ // keys on a specific target — the tag is derived from the running process, so a new
7
+ // bundle is found by the mechanism that found the first. This is a no-op on every
8
+ // platform without a bundle, and harmless when no bundle is present (the addon then
9
+ // uses the host's GTK exactly as before).
8
10
  import { createRequire } from 'node:module';
9
11
  import { fileURLToPath } from 'node:url';
10
12
  import { dirname, join } from 'node:path';
11
13
  import { existsSync } from 'node:fs';
14
+ import { pathCovers, splitSearchPath, systemGiLibraryDirs } from './system-gi.js';
12
15
 
13
16
  // NB: node:child_process is intentionally NOT imported at module top level. It is
14
17
  // pulled in lazily (via `require` below) ONLY on the darwin re-exec path — importing
@@ -61,19 +64,36 @@ export function resolveGtkRuntimeBundle() {
61
64
  }
62
65
 
63
66
  /**
64
- * Make the bundled GTK runtime genuinely env-free by RE-EXECING this process once
65
- * with `DYLD_FALLBACK_LIBRARY_PATH` (+ `GI_TYPELIB_PATH`) pointed at the bundle.
67
+ * Make GTK genuinely env-free on macOS by RE-EXECING this process once with
68
+ * `DYLD_FALLBACK_LIBRARY_PATH` pointing at the dylibs a typelib names by bare
69
+ * leaf — the BUNDLED runtime when one ships, otherwise the HOST's system GI stack
70
+ * (`systemGiLibraryDirs()`; Homebrew at either prefix, MacPorts, custom).
66
71
  *
67
72
  * Why a re-exec and not just `process.env` mutation: on macOS, dyld captures
68
73
  * `DYLD_FALLBACK_LIBRARY_PATH` at PROCESS LAUNCH, so setting it from JS never
69
- * affects the running dyld. It is load-bearing beyond the addon's own link
70
- * closure: GObject-Introspection resolves a type's `get_type()` (e.g. for
71
- * `registerClass` subclassing) — and the Pango/Gdk/Graphene backers — via
74
+ * affects the running dyld. Measured on macOS 15.7.8 / Node 24.18.1 — mutating
75
+ * `process.env` before the first `requireGi('Gtk','4.0')` reproduces the failure
76
+ * verbatim. It is load-bearing beyond the addon's own link closure:
77
+ * GObject-Introspection resolves a type's `get_type()` (e.g. for `registerClass`
78
+ * subclassing) — and the Pango/Gdk/Graphene/GdkPixbuf backers — via
72
79
  * `g_module_open(<leaf soname>)`, and dyld will NOT satisfy a bare-leaf dlopen
73
- * from an `@loader_path`-loaded image. Re-launching with the fallback set (exactly
74
- * what the Homebrew-based CI leg does) resolves every such leaf lookup from the
75
- * bundle. Guarded by a sentinel env var so it fires AT MOST ONCE; a no-op unless
76
- * darwin + a bundle is present + the fallback does not already cover it.
80
+ * from an `@loader_path`-loaded image, nor from a prefix absent from its default
81
+ * fallback path. Re-launching with the fallback set (exactly what the
82
+ * Homebrew-based CI legs export by hand) resolves every such leaf lookup.
83
+ *
84
+ * ONE re-exec path for both sources, on purpose. The BUNDLE still wins whenever
85
+ * one is installed and its behaviour here is byte-unchanged (its libDir alone,
86
+ * plus the bundle's `GI_TYPELIB_PATH`) — #920 reverted making a bundle a
87
+ * dependency of this package precisely because an installed bundle silently beats
88
+ * the GTK the addon was built against, so the system branch is reached ONLY when
89
+ * `resolveGtkRuntimeBundle()` finds nothing and it never touches typelib
90
+ * resolution (the host's typelibs are already found; only the loader is blind).
91
+ *
92
+ * Sentinel-guarded so it fires AT MOST ONCE, and skipped entirely when the
93
+ * launching environment already covers the directories — so a CI job or a
94
+ * launcher that exported the variable pays nothing, and the re-exec'd child never
95
+ * re-execs again. A host with no GI stack under any probed or pkg-config-reported
96
+ * prefix yields no directories and is left byte-unchanged.
77
97
  *
78
98
  * MUST be called at module top-level BEFORE the native addon (and thus its GTK
79
99
  * dependency closure) is dlopen'd — the re-exec then loads everything correctly.
@@ -88,23 +108,45 @@ export function maybeReexecForGtkRuntime() {
88
108
  // their DYLD path for non-addon-linked backers is a documented follow-up), so the
89
109
  // darwin path can never spawn a malformed re-exec under a non-Node runtime.
90
110
  if (typeof globalThis.Bun !== 'undefined' || typeof globalThis.Deno !== 'undefined') return;
111
+ // GJS host mode (@gjsify/napi loads this addon inside gjs): the process is
112
+ // `gjs`, so process.execPath/execArgv describe an interpreter that cannot be
113
+ // re-launched with a Node argv — and `createRequire` there is
114
+ // @gjsify/module's polyfill, which rejects synchronous BUILTIN loads, so even
115
+ // REACHING the child_process require below would throw during module init.
116
+ // Nothing needs the repair anyway: gjs itself resolves these leaves (its
117
+ // binary carries the rpath this whole module exists to compensate for).
118
+ // Same probe index.js uses for RUNTIME — as with the Bun/Deno pair above, the
119
+ // check has to live here too because this runs before that const is defined.
120
+ if (typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function') return;
91
121
  if (process.env[REEXEC_SENTINEL]) return; // already re-exec'd — the child path
122
+
123
+ // A bundle, when installed, is the whole answer and stays the ONLY entry (see
124
+ // the #920 note above). Without one, the host's own GI libdirs are the answer.
92
125
  const bundle = resolveGtkRuntimeBundle();
93
- if (!bundle) return; // strict no-op when no bundle is present
126
+ const libDirs = bundle ? [bundle.libDir] : systemGiLibraryDirs();
127
+ if (libDirs.length === 0) return; // no bundle and no host GI stack — nothing to point at
94
128
 
95
129
  const curDyld = process.env.DYLD_FALLBACK_LIBRARY_PATH ?? '';
96
- if (curDyld.split(':').filter(Boolean).includes(bundle.libDir)) return; // already covered
130
+ const curDyldDirs = splitSearchPath(curDyld);
131
+ if (pathCovers(libDirs, curDyldDirs)) return; // the launch env already covers it
97
132
 
98
- // Lazily load node:child_process ONLY here (darwin + bundle present + not yet
99
- // covered) — see the top-of-file note; keeps it out of non-darwin module graphs.
133
+ // Lazily load node:child_process ONLY here (darwin + something to point at +
134
+ // not yet covered) — see the top-of-file note; keeps it out of non-darwin
135
+ // module graphs.
100
136
  const { spawnSync } = require('node:child_process');
101
137
  const curTypelib = process.env.GI_TYPELIB_PATH ?? '';
102
138
  const env = {
103
139
  ...process.env,
104
140
  [REEXEC_SENTINEL]: '1',
105
- DYLD_FALLBACK_LIBRARY_PATH: curDyld ? `${bundle.libDir}:${curDyld}` : `${bundle.libDir}:/usr/lib`,
106
- GI_TYPELIB_PATH: curTypelib ? `${bundle.typelibDir}:${curTypelib}` : bundle.typelibDir,
141
+ // Our dirs first, then whatever the host asked for — falling back to
142
+ // dyld's own default tail so setting the variable never REMOVES /usr/lib.
143
+ DYLD_FALLBACK_LIBRARY_PATH: [...libDirs, ...(curDyldDirs.length > 0 ? curDyldDirs : ['/usr/lib'])].join(':'),
107
144
  };
145
+ // Only a bundle relocates typelibs; a host GI stack's are already on GI's own
146
+ // search path, so the system branch deliberately leaves GI_TYPELIB_PATH alone.
147
+ if (bundle) {
148
+ env.GI_TYPELIB_PATH = curTypelib ? `${bundle.typelibDir}:${curTypelib}` : bundle.typelibDir;
149
+ }
108
150
  // Reproduce this exact invocation (node flags + argv) with the augmented env.
109
151
  const res = spawnSync(process.execPath, [...process.execArgv, ...process.argv.slice(1)], {
110
152
  stdio: 'inherit',
@@ -244,7 +286,9 @@ export function activateBundledGtkRuntime(native) {
244
286
  } else {
245
287
  const existing = process.env.DYLD_FALLBACK_LIBRARY_PATH;
246
288
  if (!existing || !existing.split(':').includes(bundle.libDir)) {
247
- process.env.DYLD_FALLBACK_LIBRARY_PATH = existing ? `${bundle.libDir}:${existing}` : `${bundle.libDir}:/usr/lib`;
289
+ process.env.DYLD_FALLBACK_LIBRARY_PATH = existing
290
+ ? `${bundle.libDir}:${existing}`
291
+ : `${bundle.libDir}:/usr/lib`;
248
292
  }
249
293
  }
250
294