@gjsify/node-gi 0.44.0 → 0.46.0
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/README.md +31 -9
- package/gi.js +73 -2
- package/gtk-runtime.d.ts +4 -1
- package/gtk-runtime.js +81 -8
- package/index.d.ts +15 -1
- package/index.js +16 -0
- package/native-prebuilds.js +510 -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-x64/node_gi.node +0 -0
- package/src/addon.cc +1 -0
- package/src/calls.cc +102 -26
- package/src/class.cc +22 -9
- package/src/common.h +70 -20
- package/src/marshal.cc +561 -28
- package/src/object.cc +26 -0
- package/src/repo.cc +15 -4
- package/src/toggle.cc +208 -53
- package/system-gi.d.ts +4 -0
- package/system-gi.js +65 -11
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Put EVERY staged prebuild typelib on GI's search path, not only the GTK bundle's.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `@gjsify/*` packages that ship a native library plus its
|
|
5
|
+
// GObject-Introspection typelib declare `"gjsify": { "prebuilds": "<dir>" }` and stage
|
|
6
|
+
// the artifacts under `<dir>/<os>-<arch>/` (ADR 0017). The CLI already finds them —
|
|
7
|
+
// `detectNativePackages()` walks `node_modules` and composes `GI_TYPELIB_PATH` for
|
|
8
|
+
// `gjsify run` — but a bundle started as plain `node app.node.mjs` never goes through
|
|
9
|
+
// the CLI, so it got nothing. The GTK runtime bundle was the one exception, activated
|
|
10
|
+
// env-free by `activateBundledGtkRuntime`; every OTHER prebuilt typelib was reachable
|
|
11
|
+
// only by exporting an environment variable by hand.
|
|
12
|
+
//
|
|
13
|
+
// ADR 0021 makes launcher-free prebuild resolution the rule rather than a courtesy, so
|
|
14
|
+
// the exception was the bug. This module generalises what the GTK bundle already had,
|
|
15
|
+
// using the same two env-free primitives: `prependSearchPath` for the typelib and
|
|
16
|
+
// `prependLibraryPath` for the library it names.
|
|
17
|
+
//
|
|
18
|
+
// MEASURED, and it is what motivated this file. `@gjsify/webkit-native` ships
|
|
19
|
+
// `WebKit-6.0.typelib` + `libgjsifywebkit.dylib` — Apple's WKWebView behind the
|
|
20
|
+
// WebKitGTK 6.0 API (ADR 0022) and the ONLY WebKit any darwin host has. Under Node on
|
|
21
|
+
// macOS, `requireGi('WebKit', '6.0')` failed with "Typelib file for namespace 'WebKit',
|
|
22
|
+
// version '6.0' not found" while the typelib sat installed in `node_modules`, two
|
|
23
|
+
// directories away. Nothing was missing but the search path. That matters beyond one
|
|
24
|
+
// package: ADR 0024 § 4 puts macOS and Windows applications on Node + node-gi, so the
|
|
25
|
+
// runtime that could not see these typelibs is the only runtime those platforms have.
|
|
26
|
+
//
|
|
27
|
+
// ## Why this duplicates `detect-native-packages.ts` instead of importing it
|
|
28
|
+
//
|
|
29
|
+
// That file is the source of truth for prebuild resolution and this one deliberately
|
|
30
|
+
// mirrors its two decisions rather than inventing a second answer. It cannot be
|
|
31
|
+
// imported: node-gi lives OUTSIDE the npm workspace (ADR 0031) and must load with no
|
|
32
|
+
// dependency on the CLI. So the shapes are copied, and named here so a reader can
|
|
33
|
+
// diff them:
|
|
34
|
+
//
|
|
35
|
+
// * `prebuildDirCandidates()` — the directory names to probe, most-specific first.
|
|
36
|
+
// A single `${platform}-${arch}` is NOT enough: `hostPlatformTokens()` calls itself
|
|
37
|
+
// "the SINGLE definition of the musl preference order … so it cannot drift", and
|
|
38
|
+
// the root AGENTS.md keeps the retired `linux-x86_64` spelling readable so
|
|
39
|
+
// pre-rename tarballs still load. Hardcoding one spelling broke both.
|
|
40
|
+
// * `resolvePlatformSibling()` — the SECOND pass, for a package that declares
|
|
41
|
+
// `gjsify.prebuilds` while the artifacts live in a per-target companion
|
|
42
|
+
// (`<name>-<token>`). Walking up from node-gi never enters that package's own
|
|
43
|
+
// `node_modules`, so pass one cannot see a companion nested under it.
|
|
44
|
+
//
|
|
45
|
+
// A LIMIT BOTH RESOLVERS SHARE, measured rather than assumed, because the obvious
|
|
46
|
+
// reading of the paragraph above is wrong. `checkPackage()` requires
|
|
47
|
+
// `gjsify.prebuilds` to be a string before a package is a candidate at all — and the
|
|
48
|
+
// FACADE does not declare it. `@gjsify/webkit-native` declares `gjsify.platforms` plus
|
|
49
|
+
// two `optionalDependencies` and ships `"files": []`, and it is not an exception:
|
|
50
|
+
// counted across the tree, 65 packages declare `gjsify.prebuilds` and all but two ship
|
|
51
|
+
// the directory too, so every one of them is a per-target COMPANION that pass one
|
|
52
|
+
// resolves directly. Eleven facades — rolldown-native, lightningcss-native,
|
|
53
|
+
// oxfmt-native, webgl, webrtc-native, tls-native, http2-native, sab-native,
|
|
54
|
+
// terminal-native, http-soup-bridge, webkit-native — declare the split through
|
|
55
|
+
// `optionalDependencies` and none declares the key.
|
|
56
|
+
//
|
|
57
|
+
// Two consequences, and neither is answered here:
|
|
58
|
+
//
|
|
59
|
+
// * The second pass has NO reachable input today, in this module or in the CLI. It
|
|
60
|
+
// is kept because it is the CLI's shape and the split rollout is ongoing, not
|
|
61
|
+
// because anything currently reaches it — do not read its presence as coverage.
|
|
62
|
+
// * When npm nests a companion under its facade (a version conflict, or pnpm with
|
|
63
|
+
// hoisting off), neither resolver finds the typelib: there is no declaring package
|
|
64
|
+
// to start the walk from. Verified on the darwin VM against a real nested install
|
|
65
|
+
// — `requireGi('WebKit', '6.0')` fails there under both. The caller sees GI's own
|
|
66
|
+
// "Typelib file … not found", which names the namespace but not this cause.
|
|
67
|
+
//
|
|
68
|
+
// That is a gap in the SHARED contract — a facade that declared `gjsify.prebuilds`
|
|
69
|
+
// would close it for both resolvers at once — and it belongs there, not in a third
|
|
70
|
+
// mechanism keyed off `optionalDependencies` here. The ordinary hoisted layout, which
|
|
71
|
+
// is what npm produces absent a conflict, resolves through pass one, and that is what
|
|
72
|
+
// the acceptance below measures.
|
|
73
|
+
//
|
|
74
|
+
// A THIRD LAYOUT, and this one is about the ANCHOR rather than about a facade.
|
|
75
|
+
// `startDir` defaults to `packageRoot`, which is `import.meta.url` — and Node's loader
|
|
76
|
+
// RESOLVES SYMLINKS by default. Where the application reaches this package through a
|
|
77
|
+
// link (`npm link`, a pnpm store, a workspace `node_modules` entry), that anchor lands
|
|
78
|
+
// on the link TARGET, outside the application's tree, and the upward walk never sees
|
|
79
|
+
// the application's `node_modules` at all. Measured on linux-x64, three arms of one
|
|
80
|
+
// tree: node-gi copied in resolves; node-gi symlinked in fails with GI's own "Typelib
|
|
81
|
+
// file … not found"; the same symlinked tree under `node --preserve-symlinks`
|
|
82
|
+
// resolves — which is what pins the cause to the anchor and not to the walk.
|
|
83
|
+
//
|
|
84
|
+
// Left as a recorded limit rather than patched here, because the single anchor is a
|
|
85
|
+
// deliberate choice (a `process.cwd()` second anchor would make which typelibs a
|
|
86
|
+
// LIBRARY loads depend on the shell's working directory) and undoing it in passing
|
|
87
|
+
// would trade a narrow gap for a broad one. The shape a fix would take, if it is
|
|
88
|
+
// wanted, is the CLI's own precedent: `runGjsBundle()` anchors at
|
|
89
|
+
// `dirname(bundlePath)`, so the equivalent here is the application ENTRY —
|
|
90
|
+
// `process.argv[1]`'s directory — which is the application's tree by construction and
|
|
91
|
+
// is not the cwd. That is a decision about scope, so it is named rather than taken.
|
|
92
|
+
|
|
93
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
94
|
+
import { dirname, join, resolve } from 'node:path';
|
|
95
|
+
// `packageRoot` rather than a second `dirname(fileURLToPath(import.meta.url))` here:
|
|
96
|
+
// native-paths.js exists to be the ONE definition of this package's own root, and a
|
|
97
|
+
// second copy of the expression is a second chance to spell it wrong — which is
|
|
98
|
+
// exactly what happened to this one (`new URL(…).pathname`, "/C:/…" on Windows)
|
|
99
|
+
// while the copy in native-paths.js was right the whole time.
|
|
100
|
+
import { packageRoot } from './native-paths.js';
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Mirrors NODE_ARCH_TO_LEGACY_UNAME in detect-native-packages.ts. ALL FOUR rows, not
|
|
104
|
+
* the two this host happens to use: a short table reads as complete and the missing
|
|
105
|
+
* rows fail as "typelib not found" on the arch nobody here tests on.
|
|
106
|
+
*/
|
|
107
|
+
const LEGACY_UNAME_ARCH = { x64: 'x86_64', arm64: 'aarch64', arm: 'armv7', ia32: 'i686' };
|
|
108
|
+
|
|
109
|
+
/** Mirrors ARCH_ALIASES: spellings that name the same arch, folded onto the node one. */
|
|
110
|
+
const ARCH_ALIASES = { x86_64: 'x64', amd64: 'x64', aarch64: 'arm64' };
|
|
111
|
+
|
|
112
|
+
/** Mirrors MUSL_SUFFIX. Linux-only by construction, as the grammar has it. */
|
|
113
|
+
const MUSL_SUFFIX = '-musl';
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Mirrors `canonicalPlatformToken()`: fold the ARCH half onto the node spelling so a
|
|
117
|
+
* package's own declared token compares equal to a host token.
|
|
118
|
+
*
|
|
119
|
+
* Load-bearing rather than tidiness. Without it the declared-spelling probe below
|
|
120
|
+
* compares two RAW strings, which can only ever match a token the probe beside it
|
|
121
|
+
* already adds — so the whole declaration branch was dead code, and a package
|
|
122
|
+
* declaring + staging `linux-amd64` resolved in the CLI and not here.
|
|
123
|
+
*/
|
|
124
|
+
function canonicalPlatformToken(token) {
|
|
125
|
+
const isMusl = token.endsWith(MUSL_SUFFIX);
|
|
126
|
+
const base = isMusl ? token.slice(0, -MUSL_SUFFIX.length) : token;
|
|
127
|
+
const dash = base.indexOf('-');
|
|
128
|
+
if (dash < 0) return token;
|
|
129
|
+
const os = base.slice(0, dash);
|
|
130
|
+
const arch = base.slice(dash + 1);
|
|
131
|
+
if (arch === '') return token;
|
|
132
|
+
return `${os}-${ARCH_ALIASES[arch] ?? arch}${isMusl ? MUSL_SUFFIX : ''}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Directory names to probe for a host, most-specific first.
|
|
137
|
+
*
|
|
138
|
+
* The declared-spelling probe of the CLI's version is folded into the caller, which
|
|
139
|
+
* has the manifest in hand; what is reproduced here is the host-token order and the
|
|
140
|
+
* legacy fallback, which is where the drift would be invisible.
|
|
141
|
+
*/
|
|
142
|
+
function targetCandidates(platform, arch, musl) {
|
|
143
|
+
const canonical = `${platform}-${arch}`;
|
|
144
|
+
const out = musl && platform === 'linux' ? [`${canonical}${MUSL_SUFFIX}`, canonical] : [canonical];
|
|
145
|
+
const legacy = `${platform}-${LEGACY_UNAME_ARCH[arch] ?? arch}`;
|
|
146
|
+
if (!out.includes(legacy)) out.push(legacy);
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Mirrors `resolveHostLibc()`: decide a host's C library from two independently
|
|
152
|
+
* gathered facts. PURE, and exported so a test can pin the decision — the CLI keeps
|
|
153
|
+
* it that way for the same reason, since neither probe answers everywhere.
|
|
154
|
+
*
|
|
155
|
+
* TWO probes, and the SECOND is what makes the default correct:
|
|
156
|
+
*
|
|
157
|
+
* 1. `process.report.…glibcVersionRuntime` — present iff the running process is
|
|
158
|
+
* linked against glibc, and authoritative when it answers. Under GJS it cannot:
|
|
159
|
+
* `@gjsify/process` implements no `report` at all. MEASURED rather than assumed
|
|
160
|
+
* for the other two, because the obvious guess is wrong — bun 1.3.14 and deno
|
|
161
|
+
* 2.1.1 on linux-x64 BOTH answer it (`"2.43"` / `"2.38"`), so the case for probe 2
|
|
162
|
+
* rests on the GJS host and on any build where the field is simply absent, not on
|
|
163
|
+
* a blanket "only node has it".
|
|
164
|
+
* 2. musl's dynamic loader, `/lib/ld-musl-<arch>.so.1`. A fact about the SYSTEM
|
|
165
|
+
* rather than about the process, so it answers wherever probe 1 declines.
|
|
166
|
+
*
|
|
167
|
+
* NEITHER answering means glibc, which is a claim about the evidence and not a guess:
|
|
168
|
+
* probe 2 finding no musl loader means the host has no musl. Reading probe 1's silence
|
|
169
|
+
* as "musl" — which is what this did before, having copied only the first half — makes
|
|
170
|
+
* a host on glibc prefer a `-musl` directory, and a musl-linked library staged there
|
|
171
|
+
* cannot load on the platform it would be chosen for. Silent wrong artifact, not a
|
|
172
|
+
* loud refusal.
|
|
173
|
+
*
|
|
174
|
+
* @param {{platform: string, glibcVersionRuntime?: string, muslLoaderPresent?: boolean}} input
|
|
175
|
+
* @returns {boolean} whether to offer `-musl` directories; false off Linux, where the
|
|
176
|
+
* axis does not exist (npm's own `libc` field is Linux-only).
|
|
177
|
+
*/
|
|
178
|
+
export function resolveHostMusl(input) {
|
|
179
|
+
if (input.platform !== 'linux') return false;
|
|
180
|
+
if (typeof input.glibcVersionRuntime === 'string' && input.glibcVersionRuntime.length > 0) return false;
|
|
181
|
+
return input.muslLoaderPresent === true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Gather the two host facts {@link resolveHostMusl} decides from. */
|
|
185
|
+
function hostIsMusl(platform) {
|
|
186
|
+
if (platform !== 'linux') return false;
|
|
187
|
+
let glibcVersionRuntime;
|
|
188
|
+
try {
|
|
189
|
+
// Two ways probe 1 declines: a missing `report` short-circuits to undefined,
|
|
190
|
+
// while a bare `process` (a GJS host with no polyfill registered) or a partial
|
|
191
|
+
// `getReport` shim THROWS. Both mean the same thing here.
|
|
192
|
+
const header = process.report?.getReport()?.header;
|
|
193
|
+
if (typeof header?.glibcVersionRuntime === 'string') glibcVersionRuntime = header.glibcVersionRuntime;
|
|
194
|
+
} catch {
|
|
195
|
+
// Probe 2 still answers.
|
|
196
|
+
}
|
|
197
|
+
let muslLoaderPresent = false;
|
|
198
|
+
try {
|
|
199
|
+
// Read the directory rather than testing per-arch loader names: those
|
|
200
|
+
// (`ld-musl-x86_64`, `ld-musl-aarch64`, …) are a THIRD arch vocabulary, and
|
|
201
|
+
// this file already carries the two the repo keeps. Throws on a host with no
|
|
202
|
+
// `/lib` at all, which is the same answer as finding no loader in one.
|
|
203
|
+
muslLoaderPresent = readdirSync('/lib').some((f) => f.startsWith('ld-musl-'));
|
|
204
|
+
} catch {
|
|
205
|
+
// No `/lib` to read ⇒ no musl loader installed.
|
|
206
|
+
}
|
|
207
|
+
return resolveHostMusl({ platform, glibcVersionRuntime, muslLoaderPresent });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `@scope/name` + token -> `@scope/name-token`, as `platformPackageName` spells it. */
|
|
211
|
+
function platformPackageName(name, token) {
|
|
212
|
+
return `${name}-${token}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Whether a package name is a GTK runtime bundle, whose typelibs belong to
|
|
217
|
+
* `gtkSource()` and to nothing else (see the skip in the walk below).
|
|
218
|
+
*
|
|
219
|
+
* The UNSCOPED half, and a prefix rather than a substring: the family this module
|
|
220
|
+
* hands over is `gtk-runtime` and `gtk-runtime-<target>`, under any scope. A
|
|
221
|
+
* substring test reaches further than the claim — `@acme/vendored-gtk-runtime-helper`
|
|
222
|
+
* is somebody else's package, and silently dropping its prebuilds is a defect
|
|
223
|
+
* reported as "the typelib is installed and not found", the very complaint this
|
|
224
|
+
* module exists to answer.
|
|
225
|
+
*/
|
|
226
|
+
function isGtkRuntimePackage(name) {
|
|
227
|
+
const unscoped = name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name;
|
|
228
|
+
return unscoped === 'gtk-runtime' || unscoped.startsWith('gtk-runtime-');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const REAL_FS = {
|
|
232
|
+
readDir: (p) => {
|
|
233
|
+
try {
|
|
234
|
+
// A symlink counts as a directory here because that is how npm and pnpm
|
|
235
|
+
// place a workspace or hoisted package, and the walk must follow it.
|
|
236
|
+
// Nothing else asks `isDirectory` about a FILE — the typelib test matches
|
|
237
|
+
// on the name alone, so a symlinked `*.typelib` is not lost to this.
|
|
238
|
+
return readdirSync(p, { withFileTypes: true }).map((e) => ({
|
|
239
|
+
name: e.name,
|
|
240
|
+
isDirectory: e.isDirectory() || e.isSymbolicLink(),
|
|
241
|
+
}));
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
readJson: (p) => {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
isDirectory: (p) => {
|
|
254
|
+
try {
|
|
255
|
+
return statSync(p).isDirectory();
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** Every `node_modules` from `startDir` up to the filesystem root. */
|
|
263
|
+
function nodeModulesChain(startDir, fs) {
|
|
264
|
+
const dirs = [];
|
|
265
|
+
let current = resolve(startDir);
|
|
266
|
+
for (;;) {
|
|
267
|
+
const candidate = join(current, 'node_modules');
|
|
268
|
+
if (fs.isDirectory(candidate)) dirs.push(candidate);
|
|
269
|
+
const parent = dirname(current);
|
|
270
|
+
if (parent === current) break;
|
|
271
|
+
current = parent;
|
|
272
|
+
}
|
|
273
|
+
return dirs;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Package directories inside one `node_modules`, scopes expanded one level, SORTED
|
|
278
|
+
* by package name.
|
|
279
|
+
*
|
|
280
|
+
* The sort is not cosmetic. The up-walk fixes the order BETWEEN `node_modules`
|
|
281
|
+
* levels, but within one level this is a `readdir`, and a directory read order is
|
|
282
|
+
* not a defined order — it is the filesystem's (ext4 hashes it, and the hash moves
|
|
283
|
+
* when the directory is rewritten). Two packages that both stage a typelib would
|
|
284
|
+
* therefore reach GI's search path in an order that can differ between two machines
|
|
285
|
+
* with byte-identical installs, and change on one machine after a reinstall. A
|
|
286
|
+
* search path is first-match-wins, so that is a difference in WHICH typelib loads,
|
|
287
|
+
* reproducing on one machine and not on the other. Sorting costs one comparison per
|
|
288
|
+
* package and makes the answer a function of the tree alone.
|
|
289
|
+
*/
|
|
290
|
+
function packageDirsIn(nodeModules, fs) {
|
|
291
|
+
const out = [];
|
|
292
|
+
for (const entry of fs.readDir(nodeModules)) {
|
|
293
|
+
if (!entry.isDirectory || entry.name.startsWith('.')) continue;
|
|
294
|
+
const full = join(nodeModules, entry.name);
|
|
295
|
+
if (!entry.name.startsWith('@')) {
|
|
296
|
+
out.push({ dir: full, name: entry.name });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
for (const scoped of fs.readDir(full)) {
|
|
300
|
+
if (scoped.isDirectory && !scoped.name.startsWith('.')) {
|
|
301
|
+
out.push({ dir: join(full, scoped.name), name: `${entry.name}/${scoped.name}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Plain code-unit order, not `localeCompare`: package names are ASCII by npm's
|
|
306
|
+
// own rule, and a locale-sensitive collation would hand the determinism this
|
|
307
|
+
// sort exists to establish straight back to the environment.
|
|
308
|
+
return out.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The staged directory of `pkgDir` for this host, or null. */
|
|
312
|
+
function stagedDirFor(pkgDir, manifest, tokens, fs) {
|
|
313
|
+
const declaredDir = manifest?.gjsify?.prebuilds;
|
|
314
|
+
if (typeof declaredDir !== 'string' || declaredDir === '') return null;
|
|
315
|
+
const declaredPlatforms = manifest?.gjsify?.platforms;
|
|
316
|
+
const names = [];
|
|
317
|
+
// The package's OWN declared spelling first, per host token — what makes a
|
|
318
|
+
// pre-rename tarball load without guessing.
|
|
319
|
+
for (const token of tokens) {
|
|
320
|
+
if (Array.isArray(declaredPlatforms)) {
|
|
321
|
+
for (const declared of declaredPlatforms) {
|
|
322
|
+
if (typeof declared !== 'string') continue;
|
|
323
|
+
if (canonicalPlatformToken(declared) === token && !names.includes(declared)) {
|
|
324
|
+
names.push(declared);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!names.includes(token)) names.push(token);
|
|
329
|
+
}
|
|
330
|
+
for (const name of names) {
|
|
331
|
+
const dir = join(pkgDir, declaredDir, name);
|
|
332
|
+
if (!fs.isDirectory(dir)) continue;
|
|
333
|
+
// Name only: a staged typelib may be a symlink, which `readDir` reports as a
|
|
334
|
+
// directory so the package walk can follow one. Nothing is ever a DIRECTORY
|
|
335
|
+
// named `*.typelib`, so the suffix alone is the honest test. A package staging
|
|
336
|
+
// only a `.node` addon or a bare dylib is skipped — those directories would be
|
|
337
|
+
// noise on GI's search path for no namespace.
|
|
338
|
+
if (fs.readDir(dir).some((e) => e.name.endsWith('.typelib'))) return dir;
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The second pass: a facade declares the prebuilds, a per-target companion carries
|
|
345
|
+
* them. Restart the walk from the DECLARING package's own directory, which is the
|
|
346
|
+
* whole trick — `resolvePlatformSibling` in the CLI exists for the same reason and its
|
|
347
|
+
* comment says so.
|
|
348
|
+
*/
|
|
349
|
+
function siblingStagedDir(pkgDir, pkgName, tokens, fs) {
|
|
350
|
+
let dir = resolve(pkgDir);
|
|
351
|
+
for (;;) {
|
|
352
|
+
for (const token of tokens) {
|
|
353
|
+
const siblingName = platformPackageName(pkgName, token);
|
|
354
|
+
const siblingDir = join(dir, 'node_modules', ...siblingName.split('/'));
|
|
355
|
+
const manifest = fs.readJson(join(siblingDir, 'package.json'));
|
|
356
|
+
const staged = stagedDirFor(siblingDir, manifest, tokens, fs);
|
|
357
|
+
if (staged !== null) return staged;
|
|
358
|
+
}
|
|
359
|
+
const parent = resolve(dir, '..');
|
|
360
|
+
if (parent === dir) return null;
|
|
361
|
+
dir = parent;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Directories holding a staged typelib for this host, nearest install first.
|
|
367
|
+
*
|
|
368
|
+
* PURE apart from the injected `fs`, and takes the target as parameters, so the darwin
|
|
369
|
+
* and win32 branches are exercisable from a Linux host — the discipline
|
|
370
|
+
* `detect-native-packages.ts` already states.
|
|
371
|
+
*
|
|
372
|
+
* COST, and it is NOT cheap. Two parts, and the accounting has to name both or the
|
|
373
|
+
* second one grows unwatched: ONE `package.json` read per package in the tree, PLUS
|
|
374
|
+
* the second pass's probes for every package that declares prebuilds and resolves
|
|
375
|
+
* nothing locally — those walk to the filesystem root, per host token, opening a
|
|
376
|
+
* companion manifest that is not there. Measured on a 1691-package install
|
|
377
|
+
* (linux-x64, warm page cache): 1691 + 848 = 2539 reads, ~40 ms; ~306 ms cold. It is
|
|
378
|
+
* paid before any namespace is required, on every addon load, and by an application
|
|
379
|
+
* with no native prebuild at all. Note what the split says: while no facade declares
|
|
380
|
+
* `gjsify.prebuilds` (see the header), those 848 probes find nothing by construction.
|
|
381
|
+
*
|
|
382
|
+
* A cheaper pre-filter was tried and is recorded here because it looked right and was
|
|
383
|
+
* not. Skipping a package that has no `prebuilds/` directory (a cheap `stat`) cut the
|
|
384
|
+
* reads to 109 — and broke the second pass entirely, because a FACADE is exactly the
|
|
385
|
+
* package that declares prebuilds while shipping none, so skipping it skips the
|
|
386
|
+
* sibling walk that is the whole point. A package is a facade only according to its
|
|
387
|
+
* MANIFEST, so the manifest is what has to be read.
|
|
388
|
+
*
|
|
389
|
+
* NOT `@gjsify/webkit-native`, which an earlier revision of this paragraph named as
|
|
390
|
+
* that facade while the module header said the opposite about the same package, in
|
|
391
|
+
* the same file. The manifest settles it — `packages/framework/webkit-native/
|
|
392
|
+
* package.json` declares `gjsify.platforms` and no `gjsify.prebuilds` — and the
|
|
393
|
+
* header is the side that was right. Read it for what that costs.
|
|
394
|
+
*
|
|
395
|
+
* Narrowing the scan to `@gjsify/*` would cut it honestly, but it is a different
|
|
396
|
+
* contract — "any package declaring `gjsify.prebuilds`" is what the CLI implements —
|
|
397
|
+
* and it should be decided rather than smuggled in as an optimisation.
|
|
398
|
+
*/
|
|
399
|
+
export function discoverPrebuiltTypelibDirs({ startDir, platform, arch, musl, fs = REAL_FS }) {
|
|
400
|
+
const tokens = targetCandidates(platform, arch, musl ?? hostIsMusl(platform));
|
|
401
|
+
const found = [];
|
|
402
|
+
const seen = new Set();
|
|
403
|
+
|
|
404
|
+
for (const nodeModules of nodeModulesChain(startDir, fs)) {
|
|
405
|
+
for (const { dir: pkgDir, name } of packageDirsIn(nodeModules, fs)) {
|
|
406
|
+
// Which GTK a process uses is a POLICY decision (ADR 0023) applied in
|
|
407
|
+
// gtk-runtime.js against `gtkSource()`. Its bundle is not staged under a
|
|
408
|
+
// declared `gjsify.prebuilds` at all — it lives at `<pkg>/gtk/` — so this
|
|
409
|
+
// walk would not reach it either way. The skip is kept because the two are
|
|
410
|
+
// one decision apart: a gtk-runtime package that ever DID declare prebuilds
|
|
411
|
+
// would be prepended from here as well, and #920 is what that costs — a
|
|
412
|
+
// bundle's typelibs on the path of a process whose addon is linked against
|
|
413
|
+
// the host's GTK, surfacing as methods resolving onto the wrong entries.
|
|
414
|
+
// No ORDERING fixes it: the case that matters is `gtkSource() === 'system'`,
|
|
415
|
+
// where the GTK activation prepends NOTHING, so there is nothing for a
|
|
416
|
+
// later or earlier prepend to lose to. Excluding the package is the remedy.
|
|
417
|
+
//
|
|
418
|
+
// Matched on the UNSCOPED name's prefix, not `includes`: this claims
|
|
419
|
+
// authority over `@gjsify/gtk-runtime-<target>` and its unscoped spelling,
|
|
420
|
+
// and a substring test would also silently swallow an unrelated
|
|
421
|
+
// `@acme/vendored-gtk-runtime-helper` that this module has no policy about.
|
|
422
|
+
if (isGtkRuntimePackage(name)) continue;
|
|
423
|
+
|
|
424
|
+
const manifest = fs.readJson(join(pkgDir, 'package.json'));
|
|
425
|
+
if (manifest?.gjsify?.prebuilds === undefined) continue;
|
|
426
|
+
|
|
427
|
+
const staged =
|
|
428
|
+
stagedDirFor(pkgDir, manifest, tokens, fs) ??
|
|
429
|
+
siblingStagedDir(pkgDir, manifest.name ?? name, tokens, fs);
|
|
430
|
+
if (staged === null || seen.has(staged)) continue;
|
|
431
|
+
seen.add(staged);
|
|
432
|
+
found.push(staged);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return found;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let activated = null; // memoize: idempotent, like the GTK activation beside it
|
|
439
|
+
|
|
440
|
+
/** TEST-ONLY: allow a spec to run the activation again. */
|
|
441
|
+
export function resetNativePrebuildsForTests() {
|
|
442
|
+
activated = null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Prepend every discovered prebuild directory to GI's typelib AND library search
|
|
447
|
+
* paths.
|
|
448
|
+
*
|
|
449
|
+
* Both paths, because a typelib is only half an answer: `WebKit-6.0.typelib` names
|
|
450
|
+
* `libgjsifywebkit.dylib`, which sits beside it and which dyld would not find either.
|
|
451
|
+
* `gi_repository_prepend_library_path()` is GI's own mechanism and captures nothing at
|
|
452
|
+
* process launch, so this works identically on node, bun and deno with no re-exec and
|
|
453
|
+
* no environment variable — the same reasoning `activateGiLibraryPath` records.
|
|
454
|
+
*
|
|
455
|
+
* Never fatal. An addon predating either binding, an unreadable `node_modules`, or a
|
|
456
|
+
* package with no prebuild for this target all leave the search path exactly as it
|
|
457
|
+
* was; the caller then fails at `requireGi` with GI's own message, which is the
|
|
458
|
+
* behaviour before this module existed.
|
|
459
|
+
*
|
|
460
|
+
* @param {{ prependSearchPath?: (p: string) => void, prependLibraryPath?: (p: string) => void }} native
|
|
461
|
+
* @param {object} [options] test seam; defaults to this module's location and the host
|
|
462
|
+
* @returns {string[]} the directories handed to GI (empty when there was nothing to add)
|
|
463
|
+
*/
|
|
464
|
+
export function activateNativePrebuilds(native, options = {}) {
|
|
465
|
+
if (activated !== null) return activated;
|
|
466
|
+
activated = [];
|
|
467
|
+
if (typeof native?.prependSearchPath !== 'function') return activated;
|
|
468
|
+
|
|
469
|
+
const {
|
|
470
|
+
// This package's own directory, which inside an installed application IS that
|
|
471
|
+
// application's tree — so the upward walk sees every `node_modules` above it.
|
|
472
|
+
// Deliberately NOT merged with `process.cwd()` the way the GJS-side helper in
|
|
473
|
+
// `gi-search-path.ts` does: that one serves a globally installed CLI sitting
|
|
474
|
+
// away from the project, while a cwd anchor here would make which typelibs a
|
|
475
|
+
// library loads depend on the shell's working directory. ADR 0021 § The Node host.
|
|
476
|
+
startDir = packageRoot,
|
|
477
|
+
platform = process.platform,
|
|
478
|
+
arch = process.arch,
|
|
479
|
+
musl,
|
|
480
|
+
fs = REAL_FS,
|
|
481
|
+
} = options;
|
|
482
|
+
|
|
483
|
+
const dirs = discoverPrebuiltTypelibDirs({ startDir, platform, arch, musl, fs });
|
|
484
|
+
// LAST wins with a prepend, so walking in reverse leaves `dirs`' own order —
|
|
485
|
+
// nearest install first — intact in GI's search path.
|
|
486
|
+
for (const dir of [...dirs].reverse()) {
|
|
487
|
+
try {
|
|
488
|
+
native.prependSearchPath(dir);
|
|
489
|
+
} catch {
|
|
490
|
+
// A stubbed/old addon without the binding — this directory did not reach
|
|
491
|
+
// GI, so it must not appear in the returned claim either.
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
// Recorded as soon as the TYPELIB path took it, before the library path is
|
|
495
|
+
// attempted. The two are separate bindings and an addon can carry one without
|
|
496
|
+
// the other; folding both into one try/catch made a directory GI had already
|
|
497
|
+
// accepted vanish from the report because the SECOND call threw. The return
|
|
498
|
+
// value is what a caller diagnoses from, so "reported == handed" has to hold
|
|
499
|
+
// per path, not per pair.
|
|
500
|
+
activated.unshift(dir);
|
|
501
|
+
try {
|
|
502
|
+
if (typeof native.prependLibraryPath === 'function') native.prependLibraryPath(dir);
|
|
503
|
+
} catch {
|
|
504
|
+
// Typelib found, library path unchanged: GI still resolves the namespace
|
|
505
|
+
// and only a g_module_open of the backing library can fail — the state
|
|
506
|
+
// before this module existed, and never a reason to drop the typelib dir.
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return activated;
|
|
510
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/node-gi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"cairo.js",
|
|
57
57
|
"cairo.d.ts",
|
|
58
58
|
"native-paths.js",
|
|
59
|
+
"native-prebuilds.js",
|
|
59
60
|
"overrides",
|
|
60
61
|
"src",
|
|
61
62
|
"binding.gyp",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -110,6 +110,7 @@ static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
110
110
|
exports.Set("getBoxedField", Napi::Function::New(env, GetBoxedField));
|
|
111
111
|
exports.Set("setBoxedField", Napi::Function::New(env, SetBoxedField));
|
|
112
112
|
exports.Set("boxedTypeName", Napi::Function::New(env, BoxedTypeName));
|
|
113
|
+
exports.Set("__boxedAddress", Napi::Function::New(env, BoxedAddress));
|
|
113
114
|
exports.Set("isParamSpecHandle", Napi::Function::New(env, IsParamSpecHandle));
|
|
114
115
|
exports.Set("isFundamentalHandle", Napi::Function::New(env, IsFundamentalHandle));
|
|
115
116
|
exports.Set("paramSpecProp", Napi::Function::New(env, ParamSpecProp));
|