@gjsify/node-gi 0.64.16 → 0.64.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,113 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // The mixed-sysroot check of the Android GI runtime packages (ADR 0117 § 5).
3
+ //
4
+ // Every package that puts native GI libraries into an APK (`@gjsify/gi-runtime-android` and
5
+ // its add-ons) ships a small manifest under `assets/app/gi-runtime/<package>.json` naming
6
+ // the sysroot it was linked against. Two packages built against different sysroots put two
7
+ // glib copies' worth of symbols into one process: it fails at dlopen or, worse, as a struct
8
+ // layout mismatch far from the cause. Gradle merges both AARs without a word, so the first
9
+ // place anything can notice is the app's own startup.
10
+ //
11
+ // This file is the PURE half — manifests in, a sentence out — so Node can test it. Reading
12
+ // the manifests off the device is `readDeviceManifests`, which needs `java.io`.
13
+
14
+ /** Where the manifests land, relative to the app's files dir (`assets/app/**` is extracted there). */
15
+ export const MANIFEST_SUBDIR = 'app/gi-runtime';
16
+
17
+ function pinOf(manifest) {
18
+ const s = manifest?.sysroot;
19
+ return { tag: s?.tag, sha256: s?.sha256 ?? {} };
20
+ }
21
+
22
+ /**
23
+ * Compare the installed packages' sysroot pins.
24
+ *
25
+ * @param {Array<{ package: string, sysroot: { tag: string, sha256: Record<string, string> } }>} manifests
26
+ * @returns {string | null} a sentence naming both packages, or `null` when they agree
27
+ */
28
+ export function describePinMismatch(manifests) {
29
+ if (manifests.length < 2) return null;
30
+ const [first, ...rest] = [...manifests].sort((a, b) => String(a.package).localeCompare(String(b.package)));
31
+ const a = pinOf(first);
32
+ for (const other of rest) {
33
+ const b = pinOf(other);
34
+ const aWhat = `${first.package} (sysroot ${a.tag})`;
35
+ const bWhat = `${other.package} (sysroot ${b.tag})`;
36
+ if (a.tag !== b.tag) {
37
+ return (
38
+ `@gjsify/node-gi: ${aWhat} and ${bWhat} were built against different Android GI ` +
39
+ 'sysroots; two sets of GLib/GTK libraries cannot share one process. Install the ' +
40
+ 'same release of both packages.'
41
+ );
42
+ }
43
+ const abis = new Set([...Object.keys(a.sha256), ...Object.keys(b.sha256)]);
44
+ for (const abi of [...abis].sort()) {
45
+ if (a.sha256[abi] !== b.sha256[abi]) {
46
+ return (
47
+ `@gjsify/node-gi: ${first.package} and ${other.package} both name sysroot ` +
48
+ `${a.tag} but for ${abi} they disagree on its archive digest ` +
49
+ `(${a.sha256[abi] ?? 'none'} vs ${b.sha256[abi] ?? 'none'}); one of them was ` +
50
+ 'built from a different archive. Install the same release of both packages.'
51
+ );
52
+ }
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Read every manifest in `<filesDir>/app/gi-runtime` through `java.io` — NativeScript's own
60
+ * file API, not `node:fs`, which does not exist on the device.
61
+ *
62
+ * @param {string} filesDir
63
+ * @param {any} java the runtime's `java` global
64
+ * @returns {{ manifests: object[], ran: boolean }} `ran` is false when `java.io` is not exposed
65
+ */
66
+ export function readDeviceManifests(filesDir, java) {
67
+ const io = java?.io;
68
+ if (!io?.File || !io?.FileInputStream || !io?.BufferedReader || !io?.InputStreamReader) {
69
+ return { manifests: [], ran: false };
70
+ }
71
+ const dir = new io.File(`${filesDir}/${MANIFEST_SUBDIR}`);
72
+ const files = dir.exists() ? dir.listFiles() : null;
73
+ const manifests = [];
74
+ if (!files) return { manifests, ran: true };
75
+ for (let i = 0; i < files.length; i++) {
76
+ const name = String(files[i].getName());
77
+ if (!name.endsWith('.json')) continue;
78
+ const reader = new io.BufferedReader(new io.InputStreamReader(new io.FileInputStream(files[i]), 'UTF-8'));
79
+ let text = '';
80
+ try {
81
+ for (let line = reader.readLine(); line !== null; line = reader.readLine()) text += `${line}\n`;
82
+ } finally {
83
+ reader.close();
84
+ }
85
+ try {
86
+ manifests.push(JSON.parse(text));
87
+ } catch (e) {
88
+ throw new Error(`@gjsify/node-gi: ${name} in ${MANIFEST_SUBDIR} is not valid JSON (${e.message}).`);
89
+ }
90
+ }
91
+ return { manifests, ran: true };
92
+ }
93
+
94
+ /**
95
+ * Throw when the installed GI runtime packages disagree on the sysroot; warn when the check
96
+ * could not run. Never runs without a `java` global, so Node and iOS are untouched.
97
+ *
98
+ * @param {string | null} filesDir
99
+ * @param {any} java
100
+ */
101
+ export function checkSysrootPins(filesDir, java) {
102
+ if (typeof filesDir !== 'string' || filesDir === '' || !java) return;
103
+ const { manifests, ran } = readDeviceManifests(filesDir, java);
104
+ if (!ran) {
105
+ console.warn(
106
+ '@gjsify/node-gi: the mixed-sysroot check of the GI runtime packages was NOT run — ' +
107
+ 'java.io is not exposed to this bundle.',
108
+ );
109
+ return;
110
+ }
111
+ const problem = describePinMismatch(manifests);
112
+ if (problem !== null) throw new Error(problem);
113
+ }
@@ -41,6 +41,8 @@
41
41
  // JNI_OnLoad, and without that there is no JavaVM to reach GDK
42
42
  // with — see ensureJavaVm().
43
43
 
44
+ import { checkSysrootPins } from './gi-runtime-pin.js';
45
+
44
46
  /** @type {'nativescript'} */
45
47
  export const RUNTIME = 'nativescript';
46
48
 
@@ -375,6 +377,7 @@ export function loadNativeHost() {
375
377
  }
376
378
  const native = load('system_lib://libnode_gi.so');
377
379
  const appDir = appDirectory();
380
+ checkSysrootPins(appDir, globalThis.java);
378
381
  // `app/` is where NativeScript extracts the bundled assets under the files dir.
379
382
  const typelibs = globalThis.__NODE_GI_TYPELIB_PATH ?? (appDir === null ? null : `${appDir}/app/girepository-1.0`);
380
383
  if (typeof typelibs === 'string' && typelibs !== '') native.prependSearchPath(typelibs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.64.16",
3
+ "version": "0.64.18",
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",
@@ -51,6 +51,7 @@
51
51
  "host.node.js",
52
52
  "host.node.foreign.js",
53
53
  "host.nativescript.js",
54
+ "gi-runtime-pin.js",
54
55
  "nativescript.webpack.js",
55
56
  "index.d.ts",
56
57
  "gi.js",
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/loop.cc CHANGED
@@ -219,7 +219,6 @@ static gboolean uv_source_prepare(GSource* base, gint* timeout) {
219
219
  }
220
220
 
221
221
  uv_update_time(s->loop);
222
- DrainMicrotasks();
223
222
 
224
223
  gboolean alive = uv_loop_alive(s->loop);
225
224
  // Toggle whether GLib polls uv's backend fd: an unref'd-but-active uv handle
@@ -256,6 +255,28 @@ static gboolean uv_source_dispatch(GSource* base, GSourceFunc /*callback*/, gpoi
256
255
  static GSourceFuncs uv_source_funcs = {
257
256
  uv_source_prepare, nullptr, uv_source_dispatch, nullptr, nullptr, nullptr,
258
257
  };
258
+
259
+ // Microtask drain, in a source of its own that is only ever PREPARED (never ready).
260
+ // GLib prepares sources in priority order, and the drain runs JS: a source prepared
261
+ // earlier in the same pass may hold a lock until its check() — GDK's Wayland source
262
+ // takes libwayland's reader slot in prepare(). A microtask that then closes a window
263
+ // runs `wl_display_roundtrip` on a slot THIS thread holds and parks forever
264
+ // (`gtk_window_close` -> `gsk_gpu_renderer_unrealize` -> `wsi_wl_swapchain_destroy` ->
265
+ // `wl_display_read_events`: stamped's Node GUI hung after its window closed).
266
+ // GDK's source is prepared even ahead of G_PRIORITY_HIGH, so this one takes G_MININT:
267
+ // it is prepared FIRST and JS runs while nothing is held. It is never dispatched, so it cannot starve GDK redraws the
268
+ // way a high-priority uv source would (see the priority note at the uv source).
269
+ static gboolean drain_source_prepare(GSource* /*base*/, gint* timeout) {
270
+ *timeout = -1;
271
+ if (!g_in_uv_pump) DrainMicrotasks();
272
+ return FALSE;
273
+ }
274
+ static gboolean drain_source_dispatch(GSource*, GSourceFunc, gpointer) {
275
+ return G_SOURCE_CONTINUE;
276
+ }
277
+ static GSourceFuncs drain_source_funcs = {
278
+ drain_source_prepare, nullptr, drain_source_dispatch, nullptr, nullptr, nullptr,
279
+ };
259
280
  #else // _WIN32 — the UvLoopSource, readiness-bounded timer instead of uv-backend-fd-driven.
260
281
  //
261
282
  // Windows/IOCP has no uv backend fd to embed in a GLib GSource, so the co-pump of
@@ -1168,6 +1189,10 @@ Napi::Value StartMainLoop(const Napi::CallbackInfo& info) {
1168
1189
  g_source_set_priority(source, G_PRIORITY_HIGH_IDLE + 30);
1169
1190
  g_source_attach(source, nullptr); // default GLib main context
1170
1191
  g_source_unref(source); // the context holds the surviving ref
1192
+ GSource* drain = g_source_new(&drain_source_funcs, sizeof(GSource));
1193
+ g_source_set_priority(drain, G_MININT);
1194
+ g_source_attach(drain, nullptr);
1195
+ g_source_unref(drain);
1171
1196
  #else // _WIN32 — readiness-bounded UvLoopSource (no uv backend fd on IOCP).
1172
1197
  GSource* source = g_source_new(&uv_source_funcs, sizeof(UvLoopSource));
1173
1198
  UvLoopSource* s = reinterpret_cast<UvLoopSource*>(source);