@gjsify/node-gi 0.41.0 → 0.42.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/gtk-runtime.js CHANGED
@@ -399,14 +399,22 @@ export function maybeWireGtkWindowingEnv() {
399
399
  setIfUnset('GSETTINGS_SCHEMA_DIR', schemaDir);
400
400
 
401
401
  // MODULE_FILE is the load-bearing one on BOTH platforms — it is the cache, and the
402
- // cache is what names the modules. MODULEDIR only resolves a RELATIVE module path in
403
- // that cache, and only where gdk-pixbuf was built relocatable: measured on macOS with
404
- // Homebrew's gdk-pixbuf 2.44.7, the string `GDK_PIXBUF_MODULEDIR` is not in the dylib
405
- // at all, so the darwin bundle's cache instead names each module `@loader_path/…`
406
- // (relative to the image that dlopens it, i.e. the bundle's lib/) and resolves with no
407
- // env beyond this file. gvsbuild's win32 build IS relocatable, so MODULEDIR is what
408
- // its bare-leaf cache needs — hence both are set, and neither platform depends on the
409
- // other's mechanism.
402
+ // cache is what names the modules. It is also the ONLY variable gdk-pixbuf's runtime
403
+ // reads: `GDK_PIXBUF_MODULEDIR` is read by queryloaders.c, the GENERATOR, where it
404
+ // merely picks the directory to scan. This comment used to claim the opposite — that
405
+ // MODULEDIR is "what its bare-leaf cache needs" on win32 — and the win32 builder wrote
406
+ // a bare-leaf cache on the strength of it. gdk-pixbuf 2.44.6's `build_module_path()`
407
+ // joins a relative cache entry with the bundle TOPLEVEL instead, so every leaf resolved
408
+ // to `<bundle>\<leaf>` and no SVG icon has ever decoded from a win32 bundle (#996).
409
+ // The cache is written toplevel-relative now; see scripts/pixbuf-loader-cache.mjs.
410
+ //
411
+ // darwin needs neither: Homebrew's gdk-pixbuf 2.44.7 has no `GDK_PIXBUF_MODULEDIR`
412
+ // string in the dylib at all, so that cache names each module `@loader_path/…`
413
+ // (relative to the image that dlopens it, i.e. the bundle's lib/) and dyld resolves it
414
+ // with no env beyond this file.
415
+ //
416
+ // MODULEDIR is still set, and still only if unset: it costs nothing, and it is what a
417
+ // consumer regenerating the cache in-process would need. Nothing here depends on it.
410
418
  const loaderCache = join(bundle.dir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders.cache');
411
419
  if (existsSync(loaderCache)) {
412
420
  setIfUnset('GDK_PIXBUF_MODULEDIR', join(bundle.dir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders'));
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // @gjsify/node-gi/load-diagnostics — what a user is told when the addon FILE is
3
- // present and still will not load.
3
+ // present and still will not load. TWO causes reach the same catch and they get
4
+ // DIFFERENT answers: the loader could not resolve the specifier, or it resolved
5
+ // it and the OS refused the binary. Only the second is about GTK.
4
6
  //
5
7
  // A MODULE OF ITS OWN, and that is the point: `index.js` loads the native addon
6
8
  // at evaluation time, so anything living there can only be imported by a process
@@ -13,6 +15,41 @@
13
15
  import { hostTarget } from './native-paths.js';
14
16
  import { gtkSource } from './gtk-runtime.js';
15
17
 
18
+ /**
19
+ * Did the loader fail to RESOLVE the specifier, rather than to load the binary?
20
+ *
21
+ * Two entirely different failures arrive at the same `catch`, and only one of
22
+ * them is about GTK:
23
+ *
24
+ * • dlopen/LoadLibrary said no — the file was found, and its dependency
25
+ * closure (or its ABI) is the problem. That is the case the long GTK
26
+ * explanation below describes.
27
+ * • `require()` never got as far as the file. The specifier was neither
28
+ * absolute nor `./`-led, so it was looked up as a MODULE — against this
29
+ * package's directory and node_modules, never against the process's cwd that
30
+ * the `existsSync()` one line earlier had just resolved it against.
31
+ *
32
+ * THE INCIDENT (#996 / PR #1239), and why this discriminator exists at all:
33
+ * node-gi.yml hands the GTK bundle builder a repo-relative `--stage`, the builder
34
+ * passed that straight through as `NODE_GI_NATIVE`, and all four darwin bundle
35
+ * legs failed with `Cannot find module 'packages/node-gi/…'` underneath a
36
+ * confident five-line account of the Homebrew dependency closure ending in
37
+ * "Reinstall @gjsify/gtk-runtime-darwin-arm64". Every word of that advice was
38
+ * wrong for the cause, and it is the advice a stranger would have followed. The
39
+ * path bug itself is fixed in `native-paths.js#nativeCandidates()`; this stays
40
+ * because the next relative path to reach a `require()` will not come from there.
41
+ * @param {unknown} err
42
+ * @returns {boolean}
43
+ */
44
+ function isResolutionFailure(err) {
45
+ if (!err || typeof err !== 'object') return false;
46
+ const { code, message } = /** @type {{ code?: unknown, message?: unknown }} */ (err);
47
+ if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') return true;
48
+ // Bun, Deno and the GJS napi shim raise the same condition without Node's
49
+ // code, so the message is a fallback rather than the primary test.
50
+ return typeof message === 'string' && /cannot find module|module not found/i.test(message);
51
+ }
52
+
16
53
  /**
17
54
  * Turn a failed addon load into something the reader can act on.
18
55
  *
@@ -34,6 +71,27 @@ export function describeAddonLoadFailure(addonPath, err, ctx = {}) {
34
71
  const target = ctx.target ?? hostTarget();
35
72
  const reason = err instanceof Error ? err.message : String(err);
36
73
 
74
+ // A resolution failure gets no GTK answer. Returning early is what keeps the
75
+ // dependency-closure explanation for the case it actually describes — see
76
+ // isResolutionFailure() for what it cost when the two shared one message.
77
+ if (isResolutionFailure(err)) {
78
+ return [
79
+ `@gjsify/node-gi: the native addon path could not be RESOLVED.`,
80
+ ` addon: ${addonPath}`,
81
+ ` cause: ${reason}`,
82
+ '',
83
+ `This is a module-RESOLUTION failure, NOT a missing GTK dependency: the`,
84
+ `loader never reached the file, so nothing here is evidence about glib,`,
85
+ `gobject, girepository or a runtime bundle. A path that is neither absolute`,
86
+ `nor written with a leading ./ is resolved by require() as a MODULE`,
87
+ `specifier — against @gjsify/node-gi's own directory and node_modules,`,
88
+ `never against the working directory of the process that set it.`,
89
+ '',
90
+ `Pass an ABSOLUTE path:`,
91
+ ` NODE_GI_NATIVE=/absolute/path/to/node_gi.node`,
92
+ ].join('\n');
93
+ }
94
+
37
95
  const lines = [
38
96
  `@gjsify/node-gi: the native addon exists but could not be loaded.`,
39
97
  ` addon: ${addonPath}`,
package/native-paths.js CHANGED
@@ -26,7 +26,7 @@
26
26
  // definitions; this file is the authority for node-gi's own binary, and the third
27
27
  // copy is the one that must never be written.
28
28
  import { fileURLToPath } from 'node:url';
29
- import { dirname, join } from 'node:path';
29
+ import { dirname, join, resolve } from 'node:path';
30
30
 
31
31
  /**
32
32
  * The package root. This module sits beside `index.js`, so its own directory IS
@@ -89,6 +89,15 @@ export function buildAddonPath(flavor = 'Release') {
89
89
  * Default order: prefer a shipped prebuild so a consumer needs no C toolchain and
90
90
  * no node-gyp — the only install path Deno supports (it runs no postinstall build
91
91
  * script). Fall back to a locally built addon (Release, then Debug).
92
+ *
93
+ * Every candidate is ABSOLUTE, including a pinned one, and that is load-bearing
94
+ * rather than tidiness: `loadNative()` tests a candidate with `existsSync()` (which
95
+ * reads a relative path against the CWD) and hands the SAME string to `require()`
96
+ * (which reads a bare one as a MODULE specifier, against this package's directory).
97
+ * A relative pin therefore passed the check and failed the load. Resolving HERE,
98
+ * where the string enters the package, is what makes the two ask about one file.
99
+ * The incident it cost, and the misdiagnosis it wore: load-diagnostics.js
100
+ * `isResolutionFailure`.
92
101
  * @param {Record<string, string | undefined>} [env] defaults to `process.env`
93
102
  * @returns {string[]}
94
103
  */
@@ -99,6 +108,9 @@ export function nativeCandidates(env = process.env) {
99
108
  const prefer = env.NODE_GI_NATIVE;
100
109
  if (prefer === 'build') return [release, debug, prebuild];
101
110
  if (prefer === 'prebuild') return [prebuild];
102
- if (prefer) return [prefer]; // an explicit path to a node_gi.node
111
+ // An explicit path to a node_gi.node. `resolve()` keeps an absolute path
112
+ // byte-identical and gives a relative one the cwd meaning `existsSync` would
113
+ // have read into it anyway — so the check and the load name the same file.
114
+ if (prefer) return [resolve(prefer)];
103
115
  return [prebuild, release, debug];
104
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.41.0",
3
+ "version": "0.42.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",
Binary file
Binary file
Binary file
Binary file
Binary file
package/system-gi.js CHANGED
@@ -84,8 +84,14 @@ const PROBED_GI_LIBDIRS = {
84
84
  darwin: ['/opt/homebrew/lib', '/usr/local/lib', '/opt/local/lib'],
85
85
  };
86
86
 
87
- /** The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API. */
88
- const TYPELIB_SUBDIR = 'girepository-1.0';
87
+ /**
88
+ * The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API.
89
+ *
90
+ * Exported so `gi-typelib.ts` shares it rather than keeping a third copy of the
91
+ * one string this whole layout rule turns on. Both mirrors export it, so the
92
+ * agreement suite still compares two files that differ only in language.
93
+ */
94
+ export const TYPELIB_SUBDIR = 'girepository-1.0';
89
95
 
90
96
  /** Memoized `pkg-config` answer: the spawn happens at most once per process. */
91
97
  let pkgConfigDirsCache = null;