@gjsify/node-gi 0.58.0 → 0.59.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/binding.gyp +2 -0
- package/gettext.js +2 -2
- package/gi.js +15 -15
- package/host.nativescript.js +186 -0
- package/host.node.js +224 -0
- package/index.js +6 -190
- package/nativescript.webpack.js +16 -0
- package/overrides/byte-array.js +24 -2
- package/package.json +12 -1
- package/prebuilds/darwin-arm64/node_gi.node +0 -0
- package/prebuilds/darwin-x64/node_gi.node +0 -0
- package/prebuilds/linux-arm64/node_gi.node +0 -0
- package/prebuilds/linux-x64/node_gi.node +0 -0
- package/prebuilds/win32-arm64/node_gi.node +0 -0
- package/prebuilds/win32-x64/node_gi.node +0 -0
- package/src/addon.cc +7 -0
- package/src/android-gdk.cc +291 -0
- package/src/android-log.cc +187 -0
- package/src/common.h +23 -0
- package/src/loop.cc +478 -141
package/binding.gyp
CHANGED
package/gettext.js
CHANGED
|
@@ -54,7 +54,7 @@ let announcedStaleAddon = false;
|
|
|
54
54
|
function noLocaleBinders(fn) {
|
|
55
55
|
if (!announcedStaleAddon) {
|
|
56
56
|
announcedStaleAddon = true;
|
|
57
|
-
process
|
|
57
|
+
globalThis.process?.emitWarning?.(
|
|
58
58
|
`@gjsify/node-gi: the loaded native addon predates the gettext binders, so ` +
|
|
59
59
|
`${fn}() cannot take effect and messages stay untranslated. Rebuild the addon ` +
|
|
60
60
|
`(npm run rebuild) or update @gjsify/node-gi.`,
|
|
@@ -190,7 +190,7 @@ export function bindtextdomainCodeset(_domainName, _codeset) {
|
|
|
190
190
|
*/
|
|
191
191
|
export const LocaleCategory = haveLocaleBinders
|
|
192
192
|
? native.localeCategories()
|
|
193
|
-
: (FALLBACK_CATEGORIES[process
|
|
193
|
+
: (FALLBACK_CATEGORIES[globalThis.process?.platform] ?? FALLBACK_CATEGORIES.linux);
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
196
|
* The GJS `Gettext` module as a default export — the object shape
|
package/gi.js
CHANGED
|
@@ -7,20 +7,11 @@
|
|
|
7
7
|
// Reference: gjs/modules/esm/gi.js for the require shape; node-gtk (romgrk, MIT) for the
|
|
8
8
|
// binding lineage. Hand-authored JS — the package ships no build step.
|
|
9
9
|
import * as native from './index.js';
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
// dispatch GLib sources would itself be a GLib source nobody dispatches.
|
|
15
|
-
import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
16
|
-
// Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
|
|
17
|
-
// polyfill, whose EventEmitter never emits the runtime's 'beforeExit', so the
|
|
18
|
-
// loop-liveness hooks below would silently never fire.
|
|
19
|
-
import runtimeProcess from 'node:process';
|
|
10
|
+
// Timers, `process` and the windowing env record come from the host seam (host.node.js
|
|
11
|
+
// explains why they are the runtime's own, not the globals; host.nativescript.js is the
|
|
12
|
+
// NativeScript Android variant).
|
|
13
|
+
import { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites } from '#host';
|
|
20
14
|
import { createGioDBus } from './overrides/gio-dbus.js';
|
|
21
|
-
// The windowing loader's record of what it wrote into process.env. Read here and not there
|
|
22
|
-
// because replaying it needs a GLib (mirrorWindowingEnvIntoCrt below).
|
|
23
|
-
import { windowingEnvWrites } from './gtk-runtime.js';
|
|
24
15
|
|
|
25
16
|
// The raw native GObject handle on a wrapped instance, so it can be unwrapped again
|
|
26
17
|
// when passed back into the engine as a GI argument.
|
|
@@ -1082,6 +1073,11 @@ function syncPumpKeepAlive() {
|
|
|
1082
1073
|
pumpHoldsRuntime = hold;
|
|
1083
1074
|
}
|
|
1084
1075
|
|
|
1076
|
+
/** Hosts whose own loop already dispatches GLib, so the portable timer pump must not arm. */
|
|
1077
|
+
function hostPumpsItself() {
|
|
1078
|
+
return native.isNodeRuntime || native.RUNTIME === 'gjs' || native.RUNTIME === 'nativescript';
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1085
1081
|
/**
|
|
1086
1082
|
* Start co-pumping the default GLib main context from a runtime timer (Bun/Deno).
|
|
1087
1083
|
* No-op on Node, where the libuv↔GLib bridge already co-pumps. Reference-counted;
|
|
@@ -1097,7 +1093,7 @@ export function startMainContextPump() {
|
|
|
1097
1093
|
// Node: the libuv↔GLib bridge already co-pumps. GJS: the host loop IS
|
|
1098
1094
|
// GLib's default main context — a JS-timer pump would only iterate the
|
|
1099
1095
|
// context recursively from within its own dispatch.
|
|
1100
|
-
if (
|
|
1096
|
+
if (hostPumpsItself()) return () => {};
|
|
1101
1097
|
pumpRefCount++;
|
|
1102
1098
|
if (pumpTimer === null) {
|
|
1103
1099
|
if (pendingView === null) pendingView = native.makePumpPendingCount();
|
|
@@ -1122,7 +1118,7 @@ export function startMainContextPump() {
|
|
|
1122
1118
|
|
|
1123
1119
|
/** Decrement the main-context pump reference count; clears the timer at zero. */
|
|
1124
1120
|
export function stopMainContextPump() {
|
|
1125
|
-
if (
|
|
1121
|
+
if (hostPumpsItself() || pumpRefCount === 0) return;
|
|
1126
1122
|
pumpRefCount--;
|
|
1127
1123
|
if (pumpRefCount === 0 && pumpTimer !== null) {
|
|
1128
1124
|
clearInterval(pumpTimer);
|
|
@@ -2794,6 +2790,10 @@ export function requireGi(namespace, version) {
|
|
|
2794
2790
|
// ready and re-arm — if GLib still has scheduled work, the armed (ref'd)
|
|
2795
2791
|
// uv timer revives the loop; otherwise the process exits normally.
|
|
2796
2792
|
runtimeProcess.on('beforeExit', () => native.pumpKick());
|
|
2793
|
+
} else if (native.RUNTIME === 'nativescript') {
|
|
2794
|
+
// The native ALooper pump owns GLib dispatch on Android: no beforeExit kick
|
|
2795
|
+
// (the Looper never runs empty) and no portable timer pump.
|
|
2796
|
+
native.startMainLoop();
|
|
2797
2797
|
} else if (native.RUNTIME !== 'gjs') {
|
|
2798
2798
|
// Bun/Deno: no libuv to hook, so the portable timer pump IS the auto-pump.
|
|
2799
2799
|
// Armed HERE, not left to the caller, so that `bun bundle.mjs` /
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi — the NativeScript (Android) host, selected by the `nativescript`
|
|
3
|
+
// condition on `#host` (see nativescript.webpack.js, which adds the condition to
|
|
4
|
+
// @nativescript/webpack's resolver). NativeScript's V8 is a Node-API host WITHOUT Node's
|
|
5
|
+
// module system: no node:fs/module/path/process/timers, no prebuild tree, no re-exec.
|
|
6
|
+
// This file is the whole difference — gi.js and index.js import the same names from
|
|
7
|
+
// `#host` on every runtime.
|
|
8
|
+
//
|
|
9
|
+
// Configuration (globals, set before the first import — this package must not hard-import
|
|
10
|
+
// @nativescript/core):
|
|
11
|
+
// __NODE_GI_APP_DIR app-private writable dir (default: the files dir, derived from
|
|
12
|
+
// com.tns.NativeScriptApplication). HOME / XDG_{DATA,CONFIG,CACHE}_HOME and
|
|
13
|
+
// XDG_RUNTIME_DIR are pointed below it, via android.system.Os.setenv
|
|
14
|
+
// (a libc setenv) before the addon — and so GLib — is loaded: GLib caches
|
|
15
|
+
// these dirs on first use, a later GLib.setenv would be too late.
|
|
16
|
+
// __NODE_GI_TYPELIB_PATH directory holding the *.typelib files (the app bundles them, e.g.
|
|
17
|
+
// `knownFolders.currentApp().path + '/girepository-1.0'`); prepended to
|
|
18
|
+
// the GIRepository search path. Default: `<app dir>/app/girepository-1.0`.
|
|
19
|
+
// __NODE_GI_SYSTEM_DIR root of the bundled GNOME system prefix, holding `share/` and `etc/`
|
|
20
|
+
// as GTK's own Android runtime lays them out (XDG_DATA_DIRS=<root>/share,
|
|
21
|
+
// XDG_CONFIG_DIRS=<root>/etc, plus FONTCONFIG_PATH=<root>/etc/fonts since
|
|
22
|
+
// fontconfig's compiled-in /etc/fonts is Android's own and holds no
|
|
23
|
+
// fonts.conf). Unset ⇒ nothing is written and GLib keeps its /usr/…
|
|
24
|
+
// defaults, which exist on no Android device: a host that bundles its
|
|
25
|
+
// own GSettings schemas, icon themes or GTK resources must set it.
|
|
26
|
+
// GTK does the same from startRuntime() via g_set_user_dirs(), which is
|
|
27
|
+
// not introspectable — see refs/gtk/gdk/android/gdkandroidruntime.c.
|
|
28
|
+
// __NODE_GI_ANDROID_GDK truthy ⇒ bring GDK's Android backend up while loading the addon:
|
|
29
|
+
// gdk_android_initialize(env, app classloader, Application), the pair
|
|
30
|
+
// GTK's RuntimeApplication.startRuntime() passes. Needed before the
|
|
31
|
+
// first Gtk call and NOT done by anything else — GIRepository dlopens
|
|
32
|
+
// libgtk-4.so, which (unlike System.loadLibrary) never runs its
|
|
33
|
+
// JNI_OnLoad, and that JNI_OnLoad passes a NULL context anyway, on
|
|
34
|
+
// which gtk_init's Context.getSystemService() aborts the process.
|
|
35
|
+
// Unset ⇒ libgtk-4.so is not loaded here at all, which is what a
|
|
36
|
+
// GLib-only host wants (src/android-gdk.cc).
|
|
37
|
+
// __NODE_GI_ANDROID_JNI_BOOTSTRAP
|
|
38
|
+
// required by the above: the fully-qualified name of a class in
|
|
39
|
+
// the APK whose static initializer calls
|
|
40
|
+
// System.loadLibrary("node_gi"). Nothing else runs this addon's
|
|
41
|
+
// JNI_OnLoad, and without that there is no JavaVM to reach GDK
|
|
42
|
+
// with — see ensureJavaVm().
|
|
43
|
+
|
|
44
|
+
/** @type {'nativescript'} */
|
|
45
|
+
export const RUNTIME = 'nativescript';
|
|
46
|
+
|
|
47
|
+
// Timers from the global scope: NativeScript has no node:timers, and there is no
|
|
48
|
+
// GLib-backed replacement to avoid here. setImmediate is not a V8 builtin.
|
|
49
|
+
export const setImmediate =
|
|
50
|
+
typeof globalThis.setImmediate === 'function'
|
|
51
|
+
? globalThis.setImmediate.bind(globalThis)
|
|
52
|
+
: (fn, ...args) => globalThis.setTimeout(fn, 0, ...args);
|
|
53
|
+
export const setInterval = globalThis.setInterval.bind(globalThis);
|
|
54
|
+
export const clearInterval = globalThis.clearInterval.bind(globalThis);
|
|
55
|
+
|
|
56
|
+
// The only `process` surface gi.js reads is on('beforeExit'), and NativeScript has no such
|
|
57
|
+
// event: its loop is the Android main Looper, which never "runs empty". The stand-in
|
|
58
|
+
// swallows registrations (nothing needs re-arming — the native ALooper pump owns GLib
|
|
59
|
+
// dispatch). exit() is the honest Android answer: end this process.
|
|
60
|
+
const env = {};
|
|
61
|
+
export const runtimeProcess = {
|
|
62
|
+
env,
|
|
63
|
+
platform: 'android',
|
|
64
|
+
argv: [],
|
|
65
|
+
on() {
|
|
66
|
+
return runtimeProcess;
|
|
67
|
+
},
|
|
68
|
+
once() {
|
|
69
|
+
return runtimeProcess;
|
|
70
|
+
},
|
|
71
|
+
exit(code = 0) {
|
|
72
|
+
const proc = globalThis.android?.os?.Process;
|
|
73
|
+
if (proc) proc.killProcess(proc.myPid());
|
|
74
|
+
throw new Error(`@gjsify/node-gi: process.exit(${code}) is unavailable outside Android`);
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// No bundled-GTK windowing loader on Android: nothing was written, nothing to replay.
|
|
79
|
+
export function windowingEnvWrites() {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// NativeScript's runtime class hands out the Application without @nativescript/core
|
|
84
|
+
// (android.app.ActivityThread is a hidden API and absent from the metadata).
|
|
85
|
+
function deriveAppDir() {
|
|
86
|
+
try {
|
|
87
|
+
return globalThis.com?.tns?.NativeScriptApplication?.getInstance()?.getFilesDir()?.getPath() ?? null;
|
|
88
|
+
} catch {
|
|
89
|
+
// NativeScript surfaces a Java exception as a JS one, and getFilesDir() raises
|
|
90
|
+
// before the Application is fully constructed. No dir is not a failure here: it
|
|
91
|
+
// means the host sets __NODE_GI_APP_DIR itself, or keeps GLib's defaults.
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setNativeEnv(name, value) {
|
|
97
|
+
env[name] = value;
|
|
98
|
+
globalThis.android?.system?.Os?.setenv(name, value, true);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function appDirectory() {
|
|
102
|
+
const set = globalThis.__NODE_GI_APP_DIR;
|
|
103
|
+
if (typeof set === 'string' && set !== '') return set;
|
|
104
|
+
return deriveAppDir();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// The addon needs a JavaVM to hand GDK a JNIEnv, and a native library gets one only from
|
|
108
|
+
// its own JNI_OnLoad — which ART runs when it loads the library on behalf of a class.
|
|
109
|
+
// NativeScript's `system_lib://` is a plain dlopen: the library is mapped and its
|
|
110
|
+
// Node-API exports work, but ART never saw it, so JNI_OnLoad did not run.
|
|
111
|
+
//
|
|
112
|
+
// Loading it a second time from Java does run it, and costs nothing — same soname in the
|
|
113
|
+
// same linker namespace is the same handle, not a second copy. The catch is where the
|
|
114
|
+
// load is issued from, because ART resolves the library against the *calling class's*
|
|
115
|
+
// classloader:
|
|
116
|
+
//
|
|
117
|
+
// java.lang.System.loadLibrary('node_gi') straight from JS fails with "library
|
|
118
|
+
// libnode_gi.so not found": the call arrives through JNI with no Java frame below it,
|
|
119
|
+
// so the caller is the system classloader, whose search path is /system/lib* only.
|
|
120
|
+
// System.load() with the absolute path from ApplicationInfo.nativeLibraryDir does not
|
|
121
|
+
// throw but leaves the VM unset — measured on API 36.
|
|
122
|
+
//
|
|
123
|
+
// So the load has to come from a class in the APK: __NODE_GI_ANDROID_JNI_BOOTSTRAP names
|
|
124
|
+
// one whose static initializer calls System.loadLibrary("node_gi"), and initializing it
|
|
125
|
+
// through Class.forName runs that block under the app's own classloader. Registering the
|
|
126
|
+
// name with the addon first is what lets its JNI_OnLoad cache that classloader, the only
|
|
127
|
+
// moment app classes are visible to it.
|
|
128
|
+
function ensureJavaVm(native) {
|
|
129
|
+
if (typeof native.androidHasJavaVm !== 'function' || native.androidHasJavaVm()) return;
|
|
130
|
+
const bootstrap = globalThis.__NODE_GI_ANDROID_JNI_BOOTSTRAP;
|
|
131
|
+
if (typeof bootstrap !== 'string' || bootstrap === '') {
|
|
132
|
+
throw new Error(
|
|
133
|
+
'@gjsify/node-gi: __NODE_GI_ANDROID_GDK needs __NODE_GI_ANDROID_JNI_BOOTSTRAP — ' +
|
|
134
|
+
'the name of an app class whose static initializer calls ' +
|
|
135
|
+
'System.loadLibrary("node_gi"). Only such a class can run this addon\'s ' +
|
|
136
|
+
'JNI_OnLoad, and without it there is no JavaVM to initialize GDK with.',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
native.androidRegisterJniBootstrap(bootstrap.replace(/\./g, '/'));
|
|
140
|
+
const loader = globalThis.com?.tns?.NativeScriptApplication?.getInstance()?.getClassLoader();
|
|
141
|
+
globalThis.java?.lang?.Class?.forName(bootstrap, true, loader);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function configureEnvironment() {
|
|
145
|
+
const systemDir = globalThis.__NODE_GI_SYSTEM_DIR;
|
|
146
|
+
if (typeof systemDir === 'string' && systemDir !== '') {
|
|
147
|
+
setNativeEnv('XDG_DATA_DIRS', `${systemDir}/share`);
|
|
148
|
+
setNativeEnv('XDG_CONFIG_DIRS', `${systemDir}/etc`);
|
|
149
|
+
setNativeEnv('FONTCONFIG_PATH', `${systemDir}/etc/fonts`);
|
|
150
|
+
}
|
|
151
|
+
const appDir = appDirectory();
|
|
152
|
+
if (appDir === null) return;
|
|
153
|
+
setNativeEnv('HOME', appDir);
|
|
154
|
+
setNativeEnv('XDG_DATA_HOME', `${appDir}/.local/share`);
|
|
155
|
+
setNativeEnv('XDG_CONFIG_HOME', `${appDir}/.config`);
|
|
156
|
+
setNativeEnv('XDG_CACHE_HOME', `${appDir}/.cache`);
|
|
157
|
+
setNativeEnv('XDG_RUNTIME_DIR', `${appDir}/.run`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Load the addon from the APK's native libs and point GI at the app's typelibs.
|
|
162
|
+
* @returns {Record<string, any>} the native engine
|
|
163
|
+
*/
|
|
164
|
+
export function loadNativeHost() {
|
|
165
|
+
configureEnvironment();
|
|
166
|
+
// webpack rewrites __non_webpack_require__ to the runtime's own `require`.
|
|
167
|
+
const load = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : null;
|
|
168
|
+
if (load === null) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
'@gjsify/node-gi: the NativeScript host needs __non_webpack_require__ ' +
|
|
171
|
+
'(bundle with @nativescript/webpack, which adds the `nativescript` resolve condition).',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const native = load('system_lib://libnode_gi.so');
|
|
175
|
+
const appDir = appDirectory();
|
|
176
|
+
// `app/` is where NativeScript extracts the bundled assets under the files dir.
|
|
177
|
+
const typelibs = globalThis.__NODE_GI_TYPELIB_PATH ?? (appDir === null ? null : `${appDir}/app/girepository-1.0`);
|
|
178
|
+
if (typeof typelibs === 'string' && typelibs !== '') native.prependSearchPath(typelibs);
|
|
179
|
+
// The addon names the Context provider rather than guessing: on NativeScript that is
|
|
180
|
+
// the runtime's own Application holder, the same one deriveAppDir() reads.
|
|
181
|
+
if (globalThis.__NODE_GI_ANDROID_GDK && typeof native.androidInitGdk === 'function') {
|
|
182
|
+
ensureJavaVm(native);
|
|
183
|
+
native.androidInitGdk('com/tns/NativeScriptApplication', 'getInstance', '()Landroid/app/Application;');
|
|
184
|
+
}
|
|
185
|
+
return native;
|
|
186
|
+
}
|
package/host.node.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// @gjsify/node-gi — the Node/Bun/Deno/GJS host seam (package.json `imports` → `#host`).
|
|
3
|
+
//
|
|
4
|
+
// Everything in the package that touches a Node builtin or needs the runtime's module
|
|
5
|
+
// system lives behind this one specifier, so a host without them (NativeScript on
|
|
6
|
+
// Android — see host.nativescript.js) swaps ONE file via the `nativescript` condition
|
|
7
|
+
// instead of every importer carrying its own guard. This variant is what index.js and
|
|
8
|
+
// gi.js did inline before the seam existed; behaviour on Node/Bun/Deno/GJS is unchanged.
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { hostTarget, nativeCandidates, packageRoot } from './native-paths.js';
|
|
12
|
+
import { describeAddonLoadFailure } from './load-diagnostics.js';
|
|
13
|
+
import {
|
|
14
|
+
activateBundledGtkRuntime,
|
|
15
|
+
activateBundledOpenGL,
|
|
16
|
+
activateGiLibraryPath,
|
|
17
|
+
maybePrependGtkRuntimeDllPath,
|
|
18
|
+
maybeReexecForGtkRuntime,
|
|
19
|
+
maybeWireGtkWindowingEnv,
|
|
20
|
+
windowingEnvWrites,
|
|
21
|
+
} from './gtk-runtime.js';
|
|
22
|
+
import { activateNativePrebuilds } from './native-prebuilds.js';
|
|
23
|
+
// From node:timers, not the globals, for two reasons: setImmediate is a Node/Bun global
|
|
24
|
+
// but Deno only exposes it here; and a GJS source running through the bridge routinely
|
|
25
|
+
// replaces the global timers with GLib-backed ones (`@gjsify/node-globals/register`
|
|
26
|
+
// swaps them for `GLib.timeout_add`), which deadlocks the pump — the timer meant to
|
|
27
|
+
// dispatch GLib sources would itself be a GLib source nobody dispatches.
|
|
28
|
+
import { setImmediate, setInterval, clearInterval } from 'node:timers';
|
|
29
|
+
// Same reasoning for `process`: `globalThis.process` may be the `@gjsify/process`
|
|
30
|
+
// polyfill, whose EventEmitter never emits the runtime's 'beforeExit', so the
|
|
31
|
+
// loop-liveness hooks in gi.js would silently never fire.
|
|
32
|
+
import runtimeProcess from 'node:process';
|
|
33
|
+
|
|
34
|
+
export { setImmediate, setInterval, clearInterval, runtimeProcess, windowingEnvWrites };
|
|
35
|
+
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const here = packageRoot;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Which JS runtime we are on. The addon loads on all four: Node/Bun/Deno implement
|
|
41
|
+
* Node-API natively, GJS hosts it through the `@gjsify/napi` shim. The libuv main-loop
|
|
42
|
+
* bridge (startMainLoop) is Node-only — Deno exports no libuv symbols and Bun panics on
|
|
43
|
+
* uv_backend_fd — so Bun/Deno use gi.js's portable GLib-iteration pump, and GJS needs
|
|
44
|
+
* neither (its host loop already IS GLib's default main context). Detection order is
|
|
45
|
+
* load-bearing: GJS first via `imports` + `print` (no other runtime defines both), then
|
|
46
|
+
* Bun/Deno, which both expose a `process` shim.
|
|
47
|
+
* @type {'bun' | 'deno' | 'gjs' | 'node'}
|
|
48
|
+
*/
|
|
49
|
+
export const RUNTIME =
|
|
50
|
+
typeof globalThis.imports !== 'undefined' && typeof globalThis.print === 'function'
|
|
51
|
+
? 'gjs'
|
|
52
|
+
: typeof globalThis.Bun !== 'undefined'
|
|
53
|
+
? 'bun'
|
|
54
|
+
: typeof globalThis.Deno !== 'undefined'
|
|
55
|
+
? 'deno'
|
|
56
|
+
: 'node';
|
|
57
|
+
|
|
58
|
+
// GJS host mode: `GjsifyNapi.init()` installs the loader as
|
|
59
|
+
// `globalThis.__gjsifyNapiLoadAddon` and re-installs it if another consumer (the
|
|
60
|
+
// @gjsify/napi L1) already captured + deleted it. Needs the GjsifyNapi typelib
|
|
61
|
+
// resolvable via GI_TYPELIB_PATH/LD_LIBRARY_PATH.
|
|
62
|
+
function gjsLoadAddon(path) {
|
|
63
|
+
let load = globalThis.__gjsifyNapiLoadAddon;
|
|
64
|
+
if (typeof load !== 'function') {
|
|
65
|
+
const GjsifyNapi = globalThis.imports?.gi?.GjsifyNapi;
|
|
66
|
+
if (!GjsifyNapi || !GjsifyNapi.init()) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'@gjsify/node-gi: GJS host mode needs the @gjsify/napi shim — the GjsifyNapi ' +
|
|
69
|
+
'typelib is not loadable (point GI_TYPELIB_PATH and LD_LIBRARY_PATH at its prebuild).',
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
load = globalThis.__gjsifyNapiLoadAddon;
|
|
73
|
+
if (typeof load !== 'function') {
|
|
74
|
+
throw new Error('@gjsify/node-gi: GjsifyNapi.init() did not install the addon loader.');
|
|
75
|
+
}
|
|
76
|
+
// Capture-then-delete, the same no-side-channel discipline as the L1.
|
|
77
|
+
delete globalThis.__gjsifyNapiLoadAddon;
|
|
78
|
+
}
|
|
79
|
+
return load(path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadNative() {
|
|
83
|
+
for (const candidate of nativeCandidates()) {
|
|
84
|
+
if (existsSync(candidate)) {
|
|
85
|
+
try {
|
|
86
|
+
return RUNTIME === 'gjs' ? gjsLoadAddon(candidate) : require(candidate);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
// The addon FILE is here and still would not load — on win32/darwin
|
|
89
|
+
// that means a missing GTK dependency closure, but the raw error
|
|
90
|
+
// names only the .node, in the OS's language, about a file that
|
|
91
|
+
// plainly exists (#1063: `Das angegebene Modul wurde nicht
|
|
92
|
+
// gefunden.` on a present node_gi.node; #994: a missing load-time
|
|
93
|
+
// library surfacing as "Unsupported type void, deriving from
|
|
94
|
+
// fundamental void"). The cause is knowable here, so say it.
|
|
95
|
+
throw new Error(describeAddonLoadFailure(candidate, err), { cause: err });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
throw new Error(
|
|
100
|
+
`@gjsify/node-gi: native addon not found for ${RUNTIME} on ${hostTarget()}. ` +
|
|
101
|
+
`Expected a prebuild at prebuilds/${hostTarget()}/node_gi.node or a local build. ` +
|
|
102
|
+
'Run `node-gyp rebuild` in ' +
|
|
103
|
+
here +
|
|
104
|
+
' (requires a C++ toolchain and the girepository-2.0 / glib-2.0 development headers), ' +
|
|
105
|
+
'or install a package build that ships a prebuild for your platform. ' +
|
|
106
|
+
'Under a bundled GJS app (import.meta anchors at the bundle), set NODE_GI_NATIVE to the addon path.',
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Whether we are on Node.js (the only runtime with the libuv main-loop bridge). */
|
|
111
|
+
const isNodeRuntime = RUNTIME === 'node';
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Load the addon and run every activation that must precede the first namespace.
|
|
115
|
+
* @returns {Record<string, any>} the native engine
|
|
116
|
+
*/
|
|
117
|
+
export function loadNativeHost() {
|
|
118
|
+
// macOS bundled GTK: dyld reads DYLD_FALLBACK_LIBRARY_PATH only at launch, and GI
|
|
119
|
+
// needs it to g_module_open typelib backers (get_type / Pango / Gdk / Graphene) by
|
|
120
|
+
// leaf soname — so the re-exec must happen BEFORE the addon's GTK closure is
|
|
121
|
+
// dlopen'd. Never returns when it re-execs.
|
|
122
|
+
maybeReexecForGtkRuntime();
|
|
123
|
+
|
|
124
|
+
// Windows bundled GTK: Windows re-reads the DLL search path at every LoadLibrary
|
|
125
|
+
// (unlike dyld's launch-time capture), so mutating process.env.PATH in-process
|
|
126
|
+
// before the require() below covers both the static DLL imports and the runtime
|
|
127
|
+
// g_module_open of typelib backers — no re-exec needed.
|
|
128
|
+
maybePrependGtkRuntimeDllPath();
|
|
129
|
+
|
|
130
|
+
// Windows/macOS full-windowing GTK: point GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE /
|
|
131
|
+
// XDG_DATA_DIRS / FONTCONFIG_* at the data a real GTK window needs, and PANGOCAIRO_BACKEND
|
|
132
|
+
// at the one backend that reads any of the font half. Strict no-op for the display-free
|
|
133
|
+
// bundle, which therefore loads byte-unchanged. On win32 these writes reach the Win32
|
|
134
|
+
// environment block ONLY — gi.js replays them into the C runtime's copy on the first
|
|
135
|
+
// requireGi(), which is the environment pango's and fontconfig's getenv() read.
|
|
136
|
+
maybeWireGtkWindowingEnv();
|
|
137
|
+
const native = loadNative();
|
|
138
|
+
|
|
139
|
+
// Activate a relocated GTK/GI runtime bundle (@gjsify/gtk-runtime-<os>-<arch> or a
|
|
140
|
+
// staged prebuilds/<os>-<arch>/gtk/) BEFORE any namespace is required: it prepends the
|
|
141
|
+
// bundle's typelib dir to the GIRepository search path env-free, so gi:// namespaces
|
|
142
|
+
// load with no Homebrew/system GTK. Darwin + win32 only; without a bundle the addon
|
|
143
|
+
// uses the host GTK as before.
|
|
144
|
+
try {
|
|
145
|
+
activateBundledGtkRuntime(native);
|
|
146
|
+
} catch {
|
|
147
|
+
// Never fatal: a missing/partial bundle just leaves the host GTK in charge.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// win32: decide whether the bundle's Mesa opengl32.dll or the host's OpenGL serves GTK, and
|
|
151
|
+
// preload the bundled one when it wins. Here because gtk-4-1.dll imports OPENGL32 statically,
|
|
152
|
+
// so it must happen before any namespace loads GTK. Reports rather than throws.
|
|
153
|
+
activateBundledOpenGL(native);
|
|
154
|
+
|
|
155
|
+
// Every OTHER staged prebuild typelib (@gjsify/webkit-native and its kin): same
|
|
156
|
+
// env-free treatment, so a namespace whose typelib is installed in node_modules
|
|
157
|
+
// resolves under a plain `node app.mjs` and not only under `gjsify run`. Deliberately
|
|
158
|
+
// AFTER the GTK activation — that one owns the bundle-vs-host policy (ADR 0023) and
|
|
159
|
+
// this one skips gtk-runtime-* for exactly that reason.
|
|
160
|
+
try {
|
|
161
|
+
activateNativePrebuilds(native);
|
|
162
|
+
} catch {
|
|
163
|
+
// Same contract as the calls around it: a failed discovery leaves the search
|
|
164
|
+
// path untouched, and the caller gets GI's own "typelib not found".
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// And where the LIBRARIES those typelibs name actually live. Separate from the
|
|
168
|
+
// call above because it is not about a bundle: it applies to whichever GTK the
|
|
169
|
+
// policy chose, and it is the only loader repair bun and deno get on macOS —
|
|
170
|
+
// the re-exec above is Node-shaped and skips them.
|
|
171
|
+
try {
|
|
172
|
+
activateGiLibraryPath(native);
|
|
173
|
+
} catch {
|
|
174
|
+
// Same contract as above: never fatal.
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Cross-runtime microtask checkpoint (Bun/Deno only). Node's napi_make_callback runs
|
|
178
|
+
// the nextTick + microtask checkpoint when its callback scope closes, so promise
|
|
179
|
+
// continuations queued by a loop-dispatched GLib→JS callback drain at the callback
|
|
180
|
+
// boundary even while a blocking GLib.MainLoop.run()/Gio.Application.run() owns the
|
|
181
|
+
// thread; GJS gets the same from SpiderMonkey draining the promise-job queue when the
|
|
182
|
+
// last JS frame exits. Bun and Deno checkpoint NOWHERE there (Deno's
|
|
183
|
+
// napi_make_callback is a plain Function::Call), and with their event loop paused for
|
|
184
|
+
// the blocking run's lifetime the queue never drained: an async DBus method handler
|
|
185
|
+
// never sent its reply and the client timed out, while sync handlers worked. So
|
|
186
|
+
// register the runtime's own drain primitive; the loop-dispatched trampolines
|
|
187
|
+
// (src/signals.cc / calls.cc / class.cc) invoke it after each OUTERMOST callback
|
|
188
|
+
// returns (NodeGiMaybeDrainMicrotasks).
|
|
189
|
+
if (!isNodeRuntime && RUNTIME !== 'gjs') {
|
|
190
|
+
try {
|
|
191
|
+
let drain = null;
|
|
192
|
+
if (RUNTIME === 'bun') {
|
|
193
|
+
// JSC: drains the VM's microtask queue; callable mid-stack by design
|
|
194
|
+
// (Bun's own processTicksAndRejections calls it from JS).
|
|
195
|
+
({ drainMicrotasks: drain } = require('bun:jsc'));
|
|
196
|
+
} else if (typeof globalThis.Deno?.internal === 'symbol') {
|
|
197
|
+
// V8: the checkpoint needs BOTH queues. core.runMicrotasks is only
|
|
198
|
+
// Isolate::PerformMicrotaskCheckpoint, while Deno's node-compat
|
|
199
|
+
// process.nextTick queue is a SEPARATE deno_core queue that only the
|
|
200
|
+
// runtime's own event loop drains — and that loop is paused for the whole
|
|
201
|
+
// lifetime of a blocking run(). node:stream delivers 'end' via nextTick
|
|
202
|
+
// (endReadableNT), so a microtask-only drain got body chunks but never the
|
|
203
|
+
// end: every asset load hung at readyState 3 forever on Deno while the SAME
|
|
204
|
+
// bundle settled on Bun, whose nextTick rides JSC's microtask queue.
|
|
205
|
+
// core.runNextTicks covers both queues in one call, like node's
|
|
206
|
+
// task_queues.js. Regression: test/blocking-run-checkpoint.test.mjs.
|
|
207
|
+
const core = globalThis.Deno[globalThis.Deno.internal]?.core;
|
|
208
|
+
if (typeof core?.runNextTicks === 'function') {
|
|
209
|
+
drain = () => core.runNextTicks();
|
|
210
|
+
} else if (typeof core?.runMicrotasks === 'function') {
|
|
211
|
+
// Older deno_core without runNextTicks: promise continuations
|
|
212
|
+
// settle, nextTick consumers like node:stream 'end' still starve.
|
|
213
|
+
drain = () => core.runMicrotasks();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (typeof drain === 'function') native.setMicrotaskDrain(drain);
|
|
217
|
+
} catch {
|
|
218
|
+
// Unregistered ⇒ promise continuations drain only when the runtime's own loop
|
|
219
|
+
// runs (startMainContextPump), so async DBus replies inside a BLOCKING run stay
|
|
220
|
+
// unavailable. Never fatal — the sync surface is unaffected.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return native;
|
|
224
|
+
}
|