@gjsify/node-gi 0.26.1 → 0.27.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.
@@ -18,43 +18,43 @@ const DEFAULT_IDLE_PRIORITY = 200; // GLib.PRIORITY_DEFAULT_IDLE
18
18
  * @returns {{ run(name?: string): void, quit(name?: string): void, idle_add: Function, timeout_add: Function, timeout_add_seconds: Function, source_remove: Function }}
19
19
  */
20
20
  export function createMainloop(GLib) {
21
- const _mainLoops = Object.create(null);
21
+ const _mainLoops = Object.create(null);
22
22
 
23
- function run(name = '') {
24
- if (!_mainLoops[name]) _mainLoops[name] = GLib.MainLoop.new(null, false);
25
- _mainLoops[name].run();
26
- }
23
+ function run(name = '') {
24
+ if (!_mainLoops[name]) _mainLoops[name] = GLib.MainLoop.new(null, false);
25
+ _mainLoops[name].run();
26
+ }
27
27
 
28
- function quit(name = '') {
29
- if (!_mainLoops[name]) throw new Error('No main loop with this id');
28
+ function quit(name = '') {
29
+ if (!_mainLoops[name]) throw new Error('No main loop with this id');
30
30
 
31
- const loop = _mainLoops[name];
32
- _mainLoops[name] = null;
31
+ const loop = _mainLoops[name];
32
+ _mainLoops[name] = null;
33
33
 
34
- if (!loop.is_running()) throw new Error('Main loop was stopped already');
34
+ if (!loop.is_running()) throw new Error('Main loop was stopped already');
35
35
 
36
- loop.quit();
37
- }
36
+ loop.quit();
37
+ }
38
38
 
39
- // GJS signature is `idle_add(handler, priority)` — handler first — whereas the
40
- // introspected GLib.idle_add is `(priority, handler)`; bridge the argument order.
41
- function idle_add(handler, priority = DEFAULT_IDLE_PRIORITY) {
42
- return GLib.idle_add(priority, handler);
43
- }
39
+ // GJS signature is `idle_add(handler, priority)` — handler first — whereas the
40
+ // introspected GLib.idle_add is `(priority, handler)`; bridge the argument order.
41
+ function idle_add(handler, priority = DEFAULT_IDLE_PRIORITY) {
42
+ return GLib.idle_add(priority, handler);
43
+ }
44
44
 
45
- function timeout_add(timeout, handler, priority = GLib.PRIORITY_DEFAULT) {
46
- return GLib.timeout_add(priority, timeout, handler);
47
- }
45
+ function timeout_add(timeout, handler, priority = GLib.PRIORITY_DEFAULT) {
46
+ return GLib.timeout_add(priority, timeout, handler);
47
+ }
48
48
 
49
- function timeout_add_seconds(timeout, handler, priority = GLib.PRIORITY_DEFAULT) {
50
- return GLib.timeout_add_seconds(priority, timeout, handler);
51
- }
49
+ function timeout_add_seconds(timeout, handler, priority = GLib.PRIORITY_DEFAULT) {
50
+ return GLib.timeout_add_seconds(priority, timeout, handler);
51
+ }
52
52
 
53
- function source_remove(id) {
54
- return GLib.source_remove(id);
55
- }
53
+ function source_remove(id) {
54
+ return GLib.source_remove(id);
55
+ }
56
56
 
57
- return { run, quit, idle_add, timeout_add, timeout_add_seconds, source_remove };
57
+ return { run, quit, idle_add, timeout_add, timeout_add_seconds, source_remove };
58
58
  }
59
59
 
60
60
  export default createMainloop;
@@ -19,111 +19,111 @@
19
19
  * @param {{ requireGi: Function, gettext: any, imports: Record<string, any> }} deps
20
20
  */
21
21
  export function createPackage({ requireGi, gettext, imports }) {
22
- const pkg = {
23
- // Public fields GJS source may read. Populated by init().
24
- name: undefined,
25
- version: undefined,
26
- prefix: undefined,
27
- libdir: undefined,
28
- datadir: undefined,
29
- pkgdatadir: undefined,
30
- pkglibdir: undefined,
31
- moduledir: undefined,
32
- localedir: undefined,
22
+ const pkg = {
23
+ // Public fields GJS source may read. Populated by init().
24
+ name: undefined,
25
+ version: undefined,
26
+ prefix: undefined,
27
+ libdir: undefined,
28
+ datadir: undefined,
29
+ pkgdatadir: undefined,
30
+ pkglibdir: undefined,
31
+ moduledir: undefined,
32
+ localedir: undefined,
33
33
 
34
- /**
35
- * @param {{ name?: string, version?: string, prefix?: string, libdir?: string, datadir?: string }} params
36
- */
37
- init(params = {}) {
38
- // The accessor GJS apps use right after: `pkg.initGettext()`.
39
- globalThis.pkg = pkg;
34
+ /**
35
+ * @param {{ name?: string, version?: string, prefix?: string, libdir?: string, datadir?: string }} params
36
+ */
37
+ init(params = {}) {
38
+ // The accessor GJS apps use right after: `pkg.initGettext()`.
39
+ globalThis.pkg = pkg;
40
40
 
41
- pkg.name = params.name;
42
- pkg.version = params.version;
43
- pkg.prefix = params.prefix;
44
- pkg.libdir = params.libdir;
45
- pkg.datadir = params.datadir ?? (params.prefix ? `${params.prefix}/share` : undefined);
41
+ pkg.name = params.name;
42
+ pkg.version = params.version;
43
+ pkg.prefix = params.prefix;
44
+ pkg.libdir = params.libdir;
45
+ pkg.datadir = params.datadir ?? (params.prefix ? `${params.prefix}/share` : undefined);
46
46
 
47
- // Installed-layout dirs — NOT load-bearing on node-gi (apps carry their own
48
- // constants + register their own resources), computed for source-compat only.
49
- if (pkg.name) {
50
- if (pkg.datadir) {
51
- pkg.pkgdatadir = `${pkg.datadir}/${pkg.name}`;
52
- pkg.moduledir = pkg.pkgdatadir;
53
- pkg.localedir = `${pkg.datadir}/locale`;
54
- }
55
- if (pkg.libdir) pkg.pkglibdir = `${pkg.libdir}/${pkg.name}`;
56
- }
47
+ // Installed-layout dirs — NOT load-bearing on node-gi (apps carry their own
48
+ // constants + register their own resources), computed for source-compat only.
49
+ if (pkg.name) {
50
+ if (pkg.datadir) {
51
+ pkg.pkgdatadir = `${pkg.datadir}/${pkg.name}`;
52
+ pkg.moduledir = pkg.pkgdatadir;
53
+ pkg.localedir = `${pkg.datadir}/locale`;
54
+ }
55
+ if (pkg.libdir) pkg.pkglibdir = `${pkg.libdir}/${pkg.name}`;
56
+ }
57
57
 
58
- // Cheap + safe: keeps ARGV[0]/app-id semantics (window-manager grouping etc.).
59
- try {
60
- const GLib = requireGi('GLib', '2.0');
61
- if (pkg.name && typeof GLib.set_prgname === 'function') GLib.set_prgname(pkg.name);
62
- } catch {
63
- /* GLib not loadable in this context — skip */
64
- }
58
+ // Cheap + safe: keeps ARGV[0]/app-id semantics (window-manager grouping etc.).
59
+ try {
60
+ const GLib = requireGi('GLib', '2.0');
61
+ if (pkg.name && typeof GLib.set_prgname === 'function') GLib.set_prgname(pkg.name);
62
+ } catch {
63
+ /* GLib not loadable in this context — skip */
64
+ }
65
65
 
66
- // GJS unshifts moduledir onto imports.searchPath (script-import machinery).
67
- // node-gi apps use ESM + gi://, so the search path is inert here — but keep
68
- // the array present + the unshift safe so source that reads it doesn't throw.
69
- if (!Array.isArray(imports.searchPath)) imports.searchPath = [];
70
- if (pkg.moduledir) imports.searchPath.unshift(pkg.moduledir);
71
- },
66
+ // GJS unshifts moduledir onto imports.searchPath (script-import machinery).
67
+ // node-gi apps use ESM + gi://, so the search path is inert here — but keep
68
+ // the array present + the unshift safe so source that reads it doesn't throw.
69
+ if (!Array.isArray(imports.searchPath)) imports.searchPath = [];
70
+ if (pkg.moduledir) imports.searchPath.unshift(pkg.moduledir);
71
+ },
72
72
 
73
- initGettext() {
74
- if (pkg.name) {
75
- try {
76
- gettext.bindtextdomain?.(pkg.name, pkg.localedir ?? null);
77
- gettext.textdomain?.(pkg.name);
78
- } catch {
79
- /* passthrough gettext — no catalog, ignore */
80
- }
81
- }
82
- // The globals GJS apps + compiled Blueprint use pervasively.
83
- globalThis._ = (msgid) => gettext.gettext(msgid);
84
- globalThis.C_ = (context, msgid) => gettext.pgettext(context, msgid);
85
- globalThis.N_ = (msgid) => msgid;
86
- },
73
+ initGettext() {
74
+ if (pkg.name) {
75
+ try {
76
+ gettext.bindtextdomain?.(pkg.name, pkg.localedir ?? null);
77
+ gettext.textdomain?.(pkg.name);
78
+ } catch {
79
+ /* passthrough gettext — no catalog, ignore */
80
+ }
81
+ }
82
+ // The globals GJS apps + compiled Blueprint use pervasively.
83
+ globalThis._ = (msgid) => gettext.gettext(msgid);
84
+ globalThis.C_ = (context, msgid) => gettext.pgettext(context, msgid);
85
+ globalThis.N_ = (msgid) => msgid;
86
+ },
87
87
 
88
- initFormat() {
89
- // GJS sets `String.prototype.format = imports.format.format` (backed by the
90
- // native `_format` vprintf). node-gi has no `imports.format`, so supply an
91
- // equivalent JS vprintf here (see makeVprintf).
92
- const vprintf = makeVprintf();
93
- Object.defineProperty(String.prototype, 'format', {
94
- configurable: true,
95
- writable: true,
96
- enumerable: false,
97
- value: function format(...args) {
98
- return vprintf(this, args);
88
+ initFormat() {
89
+ // GJS sets `String.prototype.format = imports.format.format` (backed by the
90
+ // native `_format` vprintf). node-gi has no `imports.format`, so supply an
91
+ // equivalent JS vprintf here (see makeVprintf).
92
+ const vprintf = makeVprintf();
93
+ Object.defineProperty(String.prototype, 'format', {
94
+ configurable: true,
95
+ writable: true,
96
+ enumerable: false,
97
+ value: function format(...args) {
98
+ return vprintf(this, args);
99
+ },
100
+ });
99
101
  },
100
- });
101
- },
102
102
 
103
- // Script-import machinery is not ported — node-gi apps use ESM + gi://. These
104
- // keep the surface present for source that references them (Learn6502 does not).
105
- require() {
106
- /* no-op: apps import via ESM / gi:// on node-gi */
107
- },
108
- requireSymbol() {
109
- return true;
110
- },
111
- checkSymbol() {
112
- return true;
113
- },
114
- start(params = {}) {
115
- pkg.init(params);
116
- if (typeof params.main === 'function') params.main(globalThis.ARGV ? ['', ...globalThis.ARGV] : []);
117
- },
118
- run() {
119
- /* the app drives its own main() on node-gi */
120
- },
121
- initSubmodule() {
122
- /* n/a on node-gi */
123
- },
124
- };
103
+ // Script-import machinery is not ported — node-gi apps use ESM + gi://. These
104
+ // keep the surface present for source that references them (Learn6502 does not).
105
+ require() {
106
+ /* no-op: apps import via ESM / gi:// on node-gi */
107
+ },
108
+ requireSymbol() {
109
+ return true;
110
+ },
111
+ checkSymbol() {
112
+ return true;
113
+ },
114
+ start(params = {}) {
115
+ pkg.init(params);
116
+ if (typeof params.main === 'function') params.main(globalThis.ARGV ? ['', ...globalThis.ARGV] : []);
117
+ },
118
+ run() {
119
+ /* the app drives its own main() on node-gi */
120
+ },
121
+ initSubmodule() {
122
+ /* n/a on node-gi */
123
+ },
124
+ };
125
125
 
126
- return pkg;
126
+ return pkg;
127
127
  }
128
128
 
129
129
  // A small vprintf covering the GJS `String.prototype.format` spec
@@ -131,37 +131,37 @@ export function createPackage({ requireGi, gettext, imports }) {
131
131
  // width, `0`-padding for numerics, and `%%` → literal `%`. Positional `%N$s` is
132
132
  // supported (GJS honours it). Learn6502 only uses `%s`; the rest is fidelity.
133
133
  function makeVprintf() {
134
- return function vprintf(fmt, args) {
135
- let auto = 0;
136
- return String(fmt).replace(
137
- /%(?:(\d+)\$)?(0)?(\d+)?(?:\.(\d+))?([%sdxf])/g,
138
- (match, pos, zero, width, prec, spec) => {
139
- if (spec === '%') return '%';
140
- const arg = pos !== undefined ? args[Number(pos) - 1] : args[auto++];
141
- let str;
142
- switch (spec) {
143
- case 's':
144
- str = String(arg);
145
- break;
146
- case 'd':
147
- str = String(Math.trunc(Number(arg)));
148
- break;
149
- case 'x':
150
- str = Math.trunc(Number(arg)).toString(16);
151
- break;
152
- case 'f':
153
- str = prec !== undefined ? Number(arg).toFixed(Number(prec)) : String(Number(arg));
154
- break;
155
- default:
156
- return match;
157
- }
158
- if (width !== undefined) {
159
- const w = Number(width);
160
- const pad = zero && spec !== 's' ? '0' : ' ';
161
- while (str.length < w) str = pad + str;
162
- }
163
- return str;
164
- },
165
- );
166
- };
134
+ return function vprintf(fmt, args) {
135
+ let auto = 0;
136
+ return String(fmt).replace(
137
+ /%(?:(\d+)\$)?(0)?(\d+)?(?:\.(\d+))?([%sdxf])/g,
138
+ (match, pos, zero, width, prec, spec) => {
139
+ if (spec === '%') return '%';
140
+ const arg = pos !== undefined ? args[Number(pos) - 1] : args[auto++];
141
+ let str;
142
+ switch (spec) {
143
+ case 's':
144
+ str = String(arg);
145
+ break;
146
+ case 'd':
147
+ str = String(Math.trunc(Number(arg)));
148
+ break;
149
+ case 'x':
150
+ str = Math.trunc(Number(arg)).toString(16);
151
+ break;
152
+ case 'f':
153
+ str = prec !== undefined ? Number(arg).toFixed(Number(prec)) : String(Number(arg));
154
+ break;
155
+ default:
156
+ return match;
157
+ }
158
+ if (width !== undefined) {
159
+ const w = Number(width);
160
+ const pad = zero && spec !== 's' ? '0' : ' ';
161
+ while (str.length < w) str = pad + str;
162
+ }
163
+ return str;
164
+ },
165
+ );
166
+ };
167
167
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/node-gi",
3
- "version": "0.26.1",
3
+ "version": "0.27.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",
@@ -52,9 +52,11 @@
52
52
  "gettext.d.ts",
53
53
  "cairo.js",
54
54
  "cairo.d.ts",
55
+ "native-paths.js",
55
56
  "overrides",
56
57
  "src",
57
58
  "binding.gyp",
59
+ "scripts/install.mjs",
58
60
  "scripts/win-gi-gyp-flags.mjs",
59
61
  "prebuilds"
60
62
  ],
@@ -63,6 +65,7 @@
63
65
  "linux-x64",
64
66
  "linux-arm64",
65
67
  "darwin-arm64",
68
+ "darwin-x64",
66
69
  "win32-x64"
67
70
  ],
68
71
  "runtimes": {
@@ -74,7 +77,7 @@
74
77
  "tier": 2
75
78
  },
76
79
  "scripts": {
77
- "install": "node-gyp rebuild",
80
+ "install": "node scripts/install.mjs",
78
81
  "build": "node-gyp rebuild",
79
82
  "build:native": "node-gyp rebuild",
80
83
  "build:prebuild": "node-gyp rebuild && node scripts/stage-prebuild.mjs",
Binary file
Binary file
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ // @gjsify/node-gi — the `install` lifecycle script. Use the SHIPPED PREBUILD when
4
+ // this package carries one for the host, and run the node-gyp source build only
5
+ // when it does not. The source build is the FALLBACK, not the default.
6
+ //
7
+ // THE FAILURE THIS REPLACES. Until this script existed, `package.json` declared
8
+ // `"install": "node-gyp rebuild"`, which npm/yarn/pnpm run on EVERY install — a
9
+ // shipped prebuild could not prevent it. Measured on a `windows-latest` runner
10
+ // against `@gjsify/node-gi@0.26.1`, whose tarball DOES contain
11
+ // `prebuilds/win32-x64/node_gi.node`:
12
+ //
13
+ // win-gi-gyp-flags: import lib for 'girepository-2.0' not found
14
+ // gyp ERR! configure error
15
+ // → npm rolls the install back; the package is not installed at all
16
+ //
17
+ // So on any machine without the GTK/GObject-Introspection development toolchain
18
+ // — i.e. every end user — `npm install @gjsify/node-gi` FAILED, prebuild or not,
19
+ // and it failed by uninstalling the package rather than by degrading. The
20
+ // prebuild was reachable only through `gjsify install`, which deliberately runs
21
+ // no lifecycle scripts; that asymmetry is the whole reason
22
+ // `gjsify showcase --runtime node` worked on Windows while a plain npm
23
+ // dependency on the same version could not be installed.
24
+ //
25
+ // WHY EXISTENCE, NOT LOADABILITY, IS THE GATE. The obvious-looking alternative
26
+ // (`node-gyp-build`) gates on its `node-gyp-build-test` bin, which `require()`s
27
+ // the addon and falls back to a source build if the load throws. On exactly the
28
+ // platforms whose whole point is the prebuild that is a FALSE NEGATIVE: the
29
+ // win32/darwin addon's DLL/dylib closure is only findable after
30
+ // `gtk-runtime.js` has wired the batteries-included GTK bundle into `PATH` /
31
+ // `DYLD_FALLBACK_LIBRARY_PATH`, which `index.js` does at import time and a bare
32
+ // install-time `require()` of the `.node` never does. A load-based gate would
33
+ // therefore call a perfectly good prebuild broken and rebuild — reproducing the
34
+ // exact failure above. Existence of the file the loader actually probes is the
35
+ // honest question at install time; whether it LOADS is what the release
36
+ // workflow's per-target load test answers (release.yml runs
37
+ // `NODE_GI_NATIVE=prebuild node --test test/smoke.test.mjs` on every leg), where
38
+ // there is a real GTK to load against.
39
+ //
40
+ // WHY NOT DEPEND ON `node-gyp-build` (verified, not assumed):
41
+ // • its skip gate is that false-negative load test (above) — the disqualifier;
42
+ // • its probe order is `build/` FIRST, then the prebuild, the OPPOSITE of
43
+ // node-gi's deliberate consumer default (see native-paths.js
44
+ // `nativeCandidates`), so the two would disagree about which binary matters;
45
+ // • it cannot produce ONE actionable diagnosis — on failure it propagates a
46
+ // bare node-gyp exit code, which is the status quo this change removes;
47
+ // • it would add a runtime `dependencies` entry to a Tier-2 package whose dep
48
+ // surface is otherwise just `node-addon-api`, to make a decision this
49
+ // package can make from its own already-shared probe.
50
+ // (It would, contrary to first assumption, MATCH the `node_gi.node` filename:
51
+ // node-gyp-build 4.8.4's `matchTags` treats a missing runtime tag as
52
+ // permissive — checked against the installed copy. The filename was not the
53
+ // blocker; the gate and the message are.)
54
+ //
55
+ // THE SOURCE BUILD MUST KEEP WORKING, and does. `prebuilds/` is gitignored
56
+ // (packages/node-gi/node-gi/.gitignore), so a fresh checkout has none and this
57
+ // script builds exactly as before — which is what the ~12 `npm install
58
+ // --foreground-scripts` steps in node-gi.yml and the three
59
+ // `node-gi-prebuild-*` legs in release.yml rely on to produce
60
+ // `build/Release/node_gi.node` for `scripts/stage-prebuild.mjs`. Depending on a
61
+ // directory being ABSENT is too implicit to be the only guarantee, so the build
62
+ // is also forceable: `NODE_GI_BUILD_FROM_SOURCE=1` (or npm's own
63
+ // `--build-from-source`, which sets `npm_config_build_from_source=true`) skips
64
+ // the prebuild check entirely. `npm run build:native` / `build:prebuild` /
65
+ // `rebuild` still invoke `node-gyp` directly and are untouched by this script.
66
+ //
67
+ // FAIL-SAFE DIRECTION: every uncertain branch builds. Only a positively
68
+ // confirmed prebuild file for this exact host skips the build, so a bug here
69
+ // degrades to the previous behaviour rather than to a package with no addon.
70
+ import { existsSync, readFileSync } from 'node:fs';
71
+ import { spawnSync } from 'node:child_process';
72
+ import { createRequire } from 'node:module';
73
+ import { join } from 'node:path';
74
+ import { hostTarget, packageRoot, prebuildAddonPath } from '../native-paths.js';
75
+
76
+ const target = hostTarget();
77
+
78
+ /** `1`/`true` on an env var, tolerant of the shapes people actually type. */
79
+ const isEnabled = (value) => value === '1' || value === 'true';
80
+
81
+ /**
82
+ * Did the user explicitly ask for a source build? `NODE_GI_BUILD_FROM_SOURCE` is
83
+ * ours; `npm_config_build_from_source` is what npm exports for
84
+ * `--build-from-source` (the flag every node-gyp-build package honours), so a CI
85
+ * job or a maintainer can force the build without knowing node-gi's own name for
86
+ * it.
87
+ */
88
+ const buildFromSourceRequested = () =>
89
+ isEnabled(process.env.NODE_GI_BUILD_FROM_SOURCE) || process.env.npm_config_build_from_source === 'true';
90
+
91
+ /** The `<os>-<arch>` targets this package PROMISES a prebuild for, from its own manifest. */
92
+ function declaredPlatforms() {
93
+ try {
94
+ const manifest = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
95
+ const platforms = manifest?.gjsify?.platforms;
96
+ return Array.isArray(platforms) ? platforms : [];
97
+ } catch {
98
+ // An unreadable own manifest is not worth failing an install over — the
99
+ // diagnosis below just loses one line of context.
100
+ return [];
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Run `node-gyp rebuild` in the package root, inheriting stdio so the real
106
+ * compiler output stays visible (it is the actual diagnosis; the framed message
107
+ * below only tells the reader what to DO about it).
108
+ *
109
+ * node-gyp is resolved the way node-gyp-build's own bin does: prefer node-gyp's
110
+ * JS entry if it happens to be resolvable, else the `node-gyp` shim that npm,
111
+ * yarn and pnpm all put on `PATH` for lifecycle scripts (`node-gyp.cmd` on
112
+ * Windows, hence `shell` there).
113
+ * @returns {{ status: number, error?: Error }}
114
+ */
115
+ function runNodeGyp() {
116
+ const win32 = process.platform === 'win32';
117
+ let command = win32 ? 'node-gyp.cmd' : 'node-gyp';
118
+ let args = ['rebuild'];
119
+ let shell = win32;
120
+ try {
121
+ // Only succeeds when node-gyp is genuinely resolvable from here; npm's
122
+ // bundled copy lives in npm's own tree and is not, which is why the PATH
123
+ // shim above stays the normal path. `createRequire().resolve` rather than
124
+ // `import.meta.resolve` — the latter is only synchronous from Node 20.6,
125
+ // and `engines.node` is `>=20`.
126
+ const entry = createRequire(import.meta.url).resolve('node-gyp/bin/node-gyp.js');
127
+ command = process.execPath;
128
+ args = [entry, 'rebuild'];
129
+ shell = false;
130
+ } catch {
131
+ // Not resolvable — use the PATH shim.
132
+ }
133
+ const res = spawnSync(command, args, { cwd: packageRoot, stdio: 'inherit', shell, windowsHide: true });
134
+ return { status: res.status === null ? 1 : res.status, error: res.error };
135
+ }
136
+
137
+ /**
138
+ * Are we running inside a source CHECKOUT rather than an installed tarball?
139
+ * Keyed on two paths `files` deliberately does not publish (`test/` and the
140
+ * staging tool), because the distinction changes the advice: in a checkout
141
+ * `prebuilds/` is gitignored, so its absence is expected and the source build IS
142
+ * the path; in an installed copy a missing prebuild for a PROMISED target is a
143
+ * packaging regression the consumer should report instead of debugging.
144
+ */
145
+ function isRepoCheckout() {
146
+ return existsSync(join(packageRoot, 'test')) || existsSync(join(packageRoot, 'scripts', 'stage-prebuild.mjs'));
147
+ }
148
+
149
+ /**
150
+ * ONE actionable message for "no usable prebuild AND the source build did not
151
+ * succeed" — the case that used to surface as nothing but a node-gyp stack. It
152
+ * distinguishes the three very different reasons the prebuild was missing,
153
+ * because the reader's next action differs each time: a checkout is SUPPOSED to
154
+ * build; an undeclared platform means "build it yourself or use a supported
155
+ * target"; and a DECLARED platform with no artifact in an installed copy is a
156
+ * packaging regression that should be reported, not worked around (precisely how
157
+ * 0.26.0 shipped `prebuilds/linux-*` only while promising four targets — and
158
+ * nothing told the consumer).
159
+ */
160
+ function reportFailure(status, spawnError) {
161
+ const platforms = declaredPlatforms();
162
+ const promised = platforms.includes(target);
163
+ const checkout = isRepoCheckout();
164
+ const bar = '─'.repeat(78);
165
+ const lines = [
166
+ '',
167
+ bar,
168
+ `@gjsify/node-gi: could not obtain the native addon for ${target}.`,
169
+ '',
170
+ checkout
171
+ ? ' • This is a source checkout, where `prebuilds/` is gitignored — no staged prebuild\n' +
172
+ ' for this host was expected, and the source build IS the path here.'
173
+ : promised
174
+ ? ` • This package PROMISES a prebuild for ${target} (package.json#gjsify.platforms) but\n` +
175
+ ` the installed copy carries no prebuilds/${target}/node_gi.node. That is a packaging\n` +
176
+ ' bug in this release, not a problem with your machine — please report it at\n' +
177
+ ' https://github.com/gjsify/gjsify/issues.'
178
+ : ` • No prebuild ships for ${target}.` +
179
+ (platforms.length > 0 ? ` Prebuilds ship for: ${platforms.join(', ')}.` : ''),
180
+ spawnError
181
+ ? ` • The node-gyp source build could not even be STARTED: ${spawnError.message}\n` +
182
+ ' (node-gyp is normally provided on PATH by npm/yarn/pnpm for install scripts.)'
183
+ : ` • The node-gyp source build above exited ${status}. Its output is the actual diagnosis.`,
184
+ '',
185
+ 'Building from source needs a C++ toolchain plus GLib >= 2.80 development headers',
186
+ 'exposing girepository-2.0, and cairo:',
187
+ ' Fedora dnf install gcc-c++ make python3 pkgconf-pkg-config glib2-devel \\',
188
+ ' gobject-introspection-devel cairo-devel',
189
+ ' Debian/Ubuntu apt install g++ make python3 pkg-config libglib2.0-dev \\',
190
+ ' libgirepository-2.0-dev libcairo2-dev',
191
+ ' macOS brew install gobject-introspection cairo pkg-config',
192
+ ' Windows gvsbuild GTK4 (MSVC ABI), then PKG_CONFIG_PATH=<prefix>\\lib\\pkgconfig',
193
+ '',
194
+ 'Escape hatches:',
195
+ ' NODE_GI_SKIP_NATIVE_BUILD=1 install the JS without an addon (every gi:// import',
196
+ ' then throws until you point NODE_GI_NATIVE at one)',
197
+ ' NODE_GI_NATIVE=/abs/node_gi.node load an addon you built or copied yourself',
198
+ ' NODE_GI_BUILD_FROM_SOURCE=1 build from source even when a prebuild exists',
199
+ bar,
200
+ '',
201
+ ];
202
+ console.error(lines.join('\n'));
203
+ }
204
+
205
+ function main() {
206
+ // Deliberately first: an explicit "do not build" wins over everything, so a
207
+ // consumer on an unsupported platform can complete an install and supply an
208
+ // addon via NODE_GI_NATIVE afterwards.
209
+ if (isEnabled(process.env.NODE_GI_SKIP_NATIVE_BUILD)) {
210
+ console.log(
211
+ `@gjsify/node-gi: NODE_GI_SKIP_NATIVE_BUILD is set — skipping the native addon for ${target}. ` +
212
+ 'gi:// imports will throw until NODE_GI_NATIVE points at a node_gi.node.',
213
+ );
214
+ return 0;
215
+ }
216
+
217
+ const prebuild = prebuildAddonPath();
218
+ if (!buildFromSourceRequested() && existsSync(prebuild)) {
219
+ console.log(
220
+ `@gjsify/node-gi: using the shipped prebuild for ${target} ` +
221
+ `(prebuilds/${target}/node_gi.node) — no node-gyp, no C++ toolchain needed. ` +
222
+ 'Set NODE_GI_BUILD_FROM_SOURCE=1 to build from source anyway.',
223
+ );
224
+ return 0;
225
+ }
226
+
227
+ const { status, error } = runNodeGyp();
228
+ if (status === 0 && !error) return 0;
229
+ reportFailure(status, error);
230
+ // Fail the install rather than half-install. This package IS its native
231
+ // addon: a zero exit here would trade a clear install-time failure for a
232
+ // deferred `import` failure in the consumer's own code, and npm's rollback is
233
+ // the honest outcome. NODE_GI_SKIP_NATIVE_BUILD is the opt-in for anyone who
234
+ // wants the other trade.
235
+ return status === 0 ? 1 : status;
236
+ }
237
+
238
+ process.exit(main());