@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.
- package/gi-runtime-pin.js +113 -0
- package/host.nativescript.js +3 -0
- package/package.json +2 -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/loop.cc +26 -1
|
@@ -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
|
+
}
|
package/host.nativescript.js
CHANGED
|
@@ -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.
|
|
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);
|