@gjsify/cli 0.29.0 → 0.30.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/dist/cli.gjs.mjs +20 -13
- package/lib/config.js +1 -1
- package/lib/utils/bin-shim.d.ts +39 -0
- package/lib/utils/bin-shim.js +89 -13
- package/lib/utils/check-system-deps.d.ts +22 -0
- package/lib/utils/check-system-deps.js +38 -4
- package/lib/utils/dlx-cache.js +1 -1
- package/lib/utils/install-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +8 -2
- package/lib/utils/install-global.js +13 -1
- package/lib/utils/run-lifecycle-script.js +41 -25
- package/lib/utils/spawn.d.ts +17 -1
- package/lib/utils/spawn.js +23 -6
- package/package.json +17 -17
package/lib/config.js
CHANGED
|
@@ -223,7 +223,7 @@ export class Config {
|
|
|
223
223
|
const pkg = (await this.readPackageJSON(configFilePath));
|
|
224
224
|
const tsConfig = (await this.readTSConfig(configFilePath));
|
|
225
225
|
tsConfig.reflection ||= cliArgs.reflection;
|
|
226
|
-
// TODO replace with `cliArgs.logLevel`
|
|
226
|
+
// TODO(open-todos: 10 small API gaps): replace with `cliArgs.logLevel`
|
|
227
227
|
configData.verbose = cliArgs.verbose || false;
|
|
228
228
|
configData.exclude = cliArgs.exclude || [];
|
|
229
229
|
if (cliArgs.consoleShim !== undefined)
|
package/lib/utils/bin-shim.d.ts
CHANGED
|
@@ -61,6 +61,17 @@ export declare function buildLauncherShims(opts: {
|
|
|
61
61
|
* path the prebuilds have to be reachable through.
|
|
62
62
|
*/
|
|
63
63
|
prependEnv?: Readonly<Record<string, string>>;
|
|
64
|
+
/**
|
|
65
|
+
* Second interpreter + script to run when the primary interpreter is not on
|
|
66
|
+
* `PATH`. The batch/PowerShell counterpart of {@link buildShLauncher}'s
|
|
67
|
+
* `nodeFallbackAbs` — a Windows host with no `gjs` is the majority case, and
|
|
68
|
+
* the `.cmd`/`.ps1` members are the ones cmd.exe and pwsh actually reach.
|
|
69
|
+
*/
|
|
70
|
+
fallback?: {
|
|
71
|
+
interpreter: string;
|
|
72
|
+
interpreterArgs?: readonly string[];
|
|
73
|
+
target: string;
|
|
74
|
+
};
|
|
64
75
|
}): {
|
|
65
76
|
cmd: string;
|
|
66
77
|
ps1: string;
|
|
@@ -182,6 +193,26 @@ export declare function normalizeBinMap(pkgName: string, bin: string | Record<st
|
|
|
182
193
|
*
|
|
183
194
|
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
184
195
|
* — this must not change how `lodash`'s bin is linked.
|
|
196
|
+
*
|
|
197
|
+
* WHY THE npm MAP COMES BACK TOO
|
|
198
|
+
*
|
|
199
|
+
* The reasoning above is sound and INCOMPLETE: it was written from the Node-less
|
|
200
|
+
* GJS host and is blind to the mirror case. `@gjsify/cli` declares BOTH fields,
|
|
201
|
+
* so a host with no `gjs` — macOS and Windows, the two active port targets —
|
|
202
|
+
* got `exec gjs -m <bundle>`, which is exit 127, while a working Node entry sat
|
|
203
|
+
* beside it in the same package. `cli-cross-platform.yml` recorded that on both
|
|
204
|
+
* OSes for three consecutive releases (v0.27.1, v0.28.0, v0.29.0); it is
|
|
205
|
+
* advisory, so it stayed red. The install even KNEW: its own post-install check
|
|
206
|
+
* printed `Missing required system dependencies: ✗ GJS` and then wrote a
|
|
207
|
+
* gjs-only launcher anyway.
|
|
208
|
+
*
|
|
209
|
+
* So `nodeFallback` carries the npm map alongside, and the LAUNCHER decides per
|
|
210
|
+
* invocation. Deliberately not an install-time host probe: an install-time
|
|
211
|
+
* decision is a SNAPSHOT, and this repo has already paid for that exact mistake
|
|
212
|
+
* once — `buildNativeEnvPreamble` used to bake the install-time prebuild scan,
|
|
213
|
+
* so a package installed later was invisible and v0.24.1 shipped a launcher with
|
|
214
|
+
* no preamble at all. A host probe here fails the same way in reverse: install
|
|
215
|
+
* without gjs, install gjs afterwards, shim still points at Node forever.
|
|
185
216
|
*/
|
|
186
217
|
export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
187
218
|
bin?: string | Record<string, string>;
|
|
@@ -191,6 +222,7 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
191
222
|
}): {
|
|
192
223
|
map: Map<string, string>;
|
|
193
224
|
isGjsBin: boolean;
|
|
225
|
+
nodeFallback: Map<string, string> | null;
|
|
194
226
|
} | null;
|
|
195
227
|
/**
|
|
196
228
|
* The POSIX `sh` launcher gjsify writes for a bin it owns.
|
|
@@ -205,10 +237,17 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
205
237
|
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
206
238
|
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
207
239
|
* then parses JavaScript as shell.
|
|
240
|
+
*
|
|
241
|
+
* `nodeFallbackAbs` makes the runtime choice per INVOCATION rather than per
|
|
242
|
+
* install — see {@link pickBinMap} for why a host probe at install time is the
|
|
243
|
+
* wrong shape. `gjs` stays first: that is today's behaviour on a host carrying
|
|
244
|
+
* both, and which runtime wins is a separate decision from making the shim
|
|
245
|
+
* runnable at all.
|
|
208
246
|
*/
|
|
209
247
|
export declare function buildShLauncher(targetAbs: string, opts: {
|
|
210
248
|
envPreamble?: string;
|
|
211
249
|
isGjsBundle: boolean;
|
|
250
|
+
nodeFallbackAbs?: string;
|
|
212
251
|
}): string;
|
|
213
252
|
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
214
253
|
export declare function isGjsBundlePath(target: string): boolean;
|
package/lib/utils/bin-shim.js
CHANGED
|
@@ -223,26 +223,63 @@ export function buildCmdShim(relTargetPosix, shebang) {
|
|
|
223
223
|
* fails the same way on every OS.
|
|
224
224
|
*/
|
|
225
225
|
export function buildLauncherShims(opts) {
|
|
226
|
-
const { interpreter, interpreterArgs = [], target, prependEnv = {} } = opts;
|
|
226
|
+
const { interpreter, interpreterArgs = [], target, prependEnv = {}, fallback } = opts;
|
|
227
227
|
const winTarget = target.split('/').join('\\');
|
|
228
228
|
const argv = interpreterArgs.length > 0 ? `${interpreterArgs.join(' ')} ` : '';
|
|
229
229
|
const entries = Object.entries(prependEnv);
|
|
230
|
+
const fbTarget = fallback === undefined ? '' : fallback.target.split('/').join('\\');
|
|
231
|
+
const fbArgv = fallback?.interpreterArgs !== undefined && fallback.interpreterArgs.length > 0
|
|
232
|
+
? `${fallback.interpreterArgs.join(' ')} `
|
|
233
|
+
: '';
|
|
230
234
|
let cmd = CMD_HEAD;
|
|
231
235
|
for (const [key, value] of entries)
|
|
232
236
|
cmd += `@SET "${key}=${value};%${key}%"\r\n`;
|
|
233
|
-
|
|
237
|
+
if (fallback === undefined) {
|
|
238
|
+
cmd += `${interpreter} ${argv}"${winTarget}" %*\r\n`;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// `where` is the batch equivalent of `command -v`; it writes to stdout,
|
|
242
|
+
// so both streams are silenced. ERRORLEVEL is read via IF/ELSE rather
|
|
243
|
+
// than `&&`/`||` chaining, which would swallow the child's own exit code.
|
|
244
|
+
cmd +=
|
|
245
|
+
`@where ${interpreter} >NUL 2>NUL\r\n` +
|
|
246
|
+
`@IF %ERRORLEVEL% EQU 0 (\r\n` +
|
|
247
|
+
` ${interpreter} ${argv}"${winTarget}" %*\r\n` +
|
|
248
|
+
`) ELSE (\r\n` +
|
|
249
|
+
` ${fallback.interpreter} ${fbArgv}"${fbTarget}" %*\r\n` +
|
|
250
|
+
`)\r\n`;
|
|
251
|
+
}
|
|
234
252
|
let ps1 = '#!/usr/bin/env pwsh\n';
|
|
235
253
|
for (const [key, value] of entries)
|
|
236
254
|
ps1 += `$env:${key} = "${value};" + $env:${key}\n`;
|
|
237
255
|
const ps1Args = interpreterArgs.map((a) => `"${a}" `).join('');
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
256
|
+
if (fallback === undefined) {
|
|
257
|
+
ps1 +=
|
|
258
|
+
'# Support pipeline input\n' +
|
|
259
|
+
'if ($MyInvocation.ExpectingInput) {\n' +
|
|
260
|
+
` $input | & "${interpreter}" ${ps1Args}"${winTarget}" $args\n` +
|
|
261
|
+
'} else {\n' +
|
|
262
|
+
` & "${interpreter}" ${ps1Args}"${winTarget}" $args\n` +
|
|
263
|
+
'}\n';
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Bind the three parts once, then call once — the alternative (a full
|
|
267
|
+
// if/else around two near-identical invocation blocks, each of which
|
|
268
|
+
// already branches on pipeline input) is four copies of the same call.
|
|
269
|
+
const psList = (a) => a.length === 0 ? '@()' : `@(${a.map((x) => `"${x}"`).join(', ')})`;
|
|
270
|
+
ps1 +=
|
|
271
|
+
`$exe = "${interpreter}"; $exeArgs = ${psList(interpreterArgs)}; $script = "${winTarget}"\n` +
|
|
272
|
+
`if (-not (Get-Command "${interpreter}" -ErrorAction SilentlyContinue)) {\n` +
|
|
273
|
+
` $exe = "${fallback.interpreter}"; $exeArgs = ${psList(fallback.interpreterArgs ?? [])}; $script = "${fbTarget}"\n` +
|
|
274
|
+
'}\n' +
|
|
275
|
+
'# Support pipeline input\n' +
|
|
276
|
+
'if ($MyInvocation.ExpectingInput) {\n' +
|
|
277
|
+
' $input | & $exe @exeArgs $script $args\n' +
|
|
278
|
+
'} else {\n' +
|
|
279
|
+
' & $exe @exeArgs $script $args\n' +
|
|
280
|
+
'}\n';
|
|
281
|
+
}
|
|
282
|
+
ps1 += 'exit $LASTEXITCODE\n';
|
|
246
283
|
return { cmd, ps1 };
|
|
247
284
|
}
|
|
248
285
|
/** POSIX `sh` single-quoting — the one place the escape lives. */
|
|
@@ -437,14 +474,39 @@ export function normalizeBinMap(pkgName, bin) {
|
|
|
437
474
|
*
|
|
438
475
|
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
439
476
|
* — this must not change how `lodash`'s bin is linked.
|
|
477
|
+
*
|
|
478
|
+
* WHY THE npm MAP COMES BACK TOO
|
|
479
|
+
*
|
|
480
|
+
* The reasoning above is sound and INCOMPLETE: it was written from the Node-less
|
|
481
|
+
* GJS host and is blind to the mirror case. `@gjsify/cli` declares BOTH fields,
|
|
482
|
+
* so a host with no `gjs` — macOS and Windows, the two active port targets —
|
|
483
|
+
* got `exec gjs -m <bundle>`, which is exit 127, while a working Node entry sat
|
|
484
|
+
* beside it in the same package. `cli-cross-platform.yml` recorded that on both
|
|
485
|
+
* OSes for three consecutive releases (v0.27.1, v0.28.0, v0.29.0); it is
|
|
486
|
+
* advisory, so it stayed red. The install even KNEW: its own post-install check
|
|
487
|
+
* printed `Missing required system dependencies: ✗ GJS` and then wrote a
|
|
488
|
+
* gjs-only launcher anyway.
|
|
489
|
+
*
|
|
490
|
+
* So `nodeFallback` carries the npm map alongside, and the LAUNCHER decides per
|
|
491
|
+
* invocation. Deliberately not an install-time host probe: an install-time
|
|
492
|
+
* decision is a SNAPSHOT, and this repo has already paid for that exact mistake
|
|
493
|
+
* once — `buildNativeEnvPreamble` used to bake the install-time prebuild scan,
|
|
494
|
+
* so a package installed later was invisible and v0.24.1 shipped a launcher with
|
|
495
|
+
* no preamble at all. A host probe here fails the same way in reverse: install
|
|
496
|
+
* without gjs, install gjs afterwards, shim still points at Node forever.
|
|
440
497
|
*/
|
|
441
498
|
export function pickBinMap(pkgName, pkgJson) {
|
|
442
499
|
const gjsifyBin = pkgJson.gjsify?.bin;
|
|
443
|
-
if (gjsifyBin !== undefined)
|
|
444
|
-
return { map: normalizeBinMap(pkgName, gjsifyBin), isGjsBin: true };
|
|
445
500
|
const npmBin = pkgJson.bin;
|
|
501
|
+
if (gjsifyBin !== undefined) {
|
|
502
|
+
return {
|
|
503
|
+
map: normalizeBinMap(pkgName, gjsifyBin),
|
|
504
|
+
isGjsBin: true,
|
|
505
|
+
nodeFallback: npmBin === undefined ? null : normalizeBinMap(pkgName, npmBin),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
446
508
|
if (npmBin !== undefined)
|
|
447
|
-
return { map: normalizeBinMap(pkgName, npmBin), isGjsBin: false };
|
|
509
|
+
return { map: normalizeBinMap(pkgName, npmBin), isGjsBin: false, nodeFallback: null };
|
|
448
510
|
return null;
|
|
449
511
|
}
|
|
450
512
|
/**
|
|
@@ -460,9 +522,23 @@ export function pickBinMap(pkgName, pkgJson) {
|
|
|
460
522
|
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
461
523
|
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
462
524
|
* then parses JavaScript as shell.
|
|
525
|
+
*
|
|
526
|
+
* `nodeFallbackAbs` makes the runtime choice per INVOCATION rather than per
|
|
527
|
+
* install — see {@link pickBinMap} for why a host probe at install time is the
|
|
528
|
+
* wrong shape. `gjs` stays first: that is today's behaviour on a host carrying
|
|
529
|
+
* both, and which runtime wins is a separate decision from making the shim
|
|
530
|
+
* runnable at all.
|
|
463
531
|
*/
|
|
464
532
|
export function buildShLauncher(targetAbs, opts) {
|
|
465
533
|
const preamble = opts.isGjsBundle ? (opts.envPreamble ?? '') : '';
|
|
534
|
+
if (opts.isGjsBundle && opts.nodeFallbackAbs !== undefined) {
|
|
535
|
+
// The preamble only ever configures the GI search path, so it stays
|
|
536
|
+
// inside the gjs branch — exporting it around a Node exec would be
|
|
537
|
+
// inert at best and misleading to anyone reading the shim.
|
|
538
|
+
return (`#!/bin/sh\nif command -v gjs >/dev/null 2>&1; then\n${preamble}` +
|
|
539
|
+
`exec gjs -m ${shQuote(targetAbs)} "$@"\nfi\n` +
|
|
540
|
+
`exec node ${shQuote(opts.nodeFallbackAbs)} "$@"\n`);
|
|
541
|
+
}
|
|
466
542
|
const exec = opts.isGjsBundle ? `exec gjs -m ${shQuote(targetAbs)} "$@"` : `exec ${shQuote(targetAbs)} "$@"`;
|
|
467
543
|
return `#!/bin/sh\n${preamble}${exec}\n`;
|
|
468
544
|
}
|
|
@@ -28,6 +28,27 @@ export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew
|
|
|
28
28
|
* MacPorts or Nix host, and it is why this must never fall back to "probably brew".
|
|
29
29
|
*/
|
|
30
30
|
export declare function detectPackageManager(): PackageManager;
|
|
31
|
+
interface OptionalDep {
|
|
32
|
+
/** DepCheck id */
|
|
33
|
+
id: string;
|
|
34
|
+
/** Display name */
|
|
35
|
+
name: string;
|
|
36
|
+
/** pkg-config library name */
|
|
37
|
+
pkgName: string;
|
|
38
|
+
}
|
|
39
|
+
/** Optional system deps keyed by their DepCheck id. Exported for the spec's declaration invariant. */
|
|
40
|
+
export declare const OPTIONAL_DEPS: Record<string, OptionalDep>;
|
|
41
|
+
/**
|
|
42
|
+
* Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
|
|
43
|
+
* Generated by walking each package's `gi://` imports (excluding the always-
|
|
44
|
+
* available trio GLib/GObject/Gio).
|
|
45
|
+
*
|
|
46
|
+
* Used to compute the set of optional deps to check for a given project.
|
|
47
|
+
*
|
|
48
|
+
* Exported for the spec's declaration invariant: every id named here must exist in
|
|
49
|
+
* OPTIONAL_DEPS, or the dep is silently never checked.
|
|
50
|
+
*/
|
|
51
|
+
export declare const PACKAGE_DEPS: Record<string, string[]>;
|
|
31
52
|
/**
|
|
32
53
|
* Run all dependency checks. Used by `gjsify check` to show full system status.
|
|
33
54
|
*
|
|
@@ -86,3 +107,4 @@ export interface TypeSkewReaders {
|
|
|
86
107
|
* ignore the output.
|
|
87
108
|
*/
|
|
88
109
|
export declare function checkTypeSkew(cwd: string, readers?: TypeSkewReaders): TypeSkew[];
|
|
110
|
+
export {};
|
|
@@ -87,8 +87,8 @@ export function detectPackageManager() {
|
|
|
87
87
|
}
|
|
88
88
|
return 'unknown';
|
|
89
89
|
}
|
|
90
|
-
/** Optional system deps keyed by their DepCheck id. */
|
|
91
|
-
const OPTIONAL_DEPS = {
|
|
90
|
+
/** Optional system deps keyed by their DepCheck id. Exported for the spec's declaration invariant. */
|
|
91
|
+
export const OPTIONAL_DEPS = {
|
|
92
92
|
manette: { id: 'manette', name: 'libmanette', pkgName: 'manette-0.2' },
|
|
93
93
|
gstreamer: { id: 'gstreamer', name: 'GStreamer', pkgName: 'gstreamer-1.0' },
|
|
94
94
|
'gst-app': { id: 'gst-app', name: 'GStreamer App', pkgName: 'gstreamer-app-1.0' },
|
|
@@ -105,6 +105,21 @@ const OPTIONAL_DEPS = {
|
|
|
105
105
|
// `Run-time dependency X found: NO` error mid-build.
|
|
106
106
|
gnutls: { id: 'gnutls', name: 'GnuTLS', pkgName: 'gnutls' },
|
|
107
107
|
nghttp2: { id: 'nghttp2', name: 'libnghttp2', pkgName: 'libnghttp2' },
|
|
108
|
+
// RUNTIME deps of the shipped prebuilds, not build-time ones — the distinction
|
|
109
|
+
// the two entries above make does not hold for these. Measured on fedora:43
|
|
110
|
+
// provisioned like a CONSUMER (gjs + libsoup3 only), `ldd` over every committed
|
|
111
|
+
// linux-x64 prebuild: three sonames do not resolve, and each makes its typelib
|
|
112
|
+
// UNLOADABLE, which surfaces as `Unsupported type void, deriving from
|
|
113
|
+
// fundamental void` at first construction — a message that names nothing.
|
|
114
|
+
// @gjsify/rolldown-native libjson-glib-1.0.so.0
|
|
115
|
+
// @gjsify/webgl libepoxy.so.0 (gdk-pixbuf already listed)
|
|
116
|
+
// @gjsify/webrtc-native libgstwebrtc-1.0.so.0 (gstreamer already listed)
|
|
117
|
+
// json-glib is the severe one: rolldown-native IS the GJS bundler engine, so a
|
|
118
|
+
// host without it cannot `gjsify build` at all, while `gjsify install` printed
|
|
119
|
+
// "System dependencies OK." — a check passing for the wrong reason.
|
|
120
|
+
'json-glib': { id: 'json-glib', name: 'JSON-GLib', pkgName: 'json-glib-1.0' },
|
|
121
|
+
epoxy: { id: 'epoxy', name: 'libepoxy', pkgName: 'epoxy' },
|
|
122
|
+
'gst-webrtc': { id: 'gst-webrtc', name: 'GStreamer WebRTC', pkgName: 'gstreamer-webrtc-1.0' },
|
|
108
123
|
};
|
|
109
124
|
/**
|
|
110
125
|
* Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
|
|
@@ -112,8 +127,11 @@ const OPTIONAL_DEPS = {
|
|
|
112
127
|
* available trio GLib/GObject/Gio).
|
|
113
128
|
*
|
|
114
129
|
* Used to compute the set of optional deps to check for a given project.
|
|
130
|
+
*
|
|
131
|
+
* Exported for the spec's declaration invariant: every id named here must exist in
|
|
132
|
+
* OPTIONAL_DEPS, or the dep is silently never checked.
|
|
115
133
|
*/
|
|
116
|
-
const PACKAGE_DEPS = {
|
|
134
|
+
export const PACKAGE_DEPS = {
|
|
117
135
|
'@gjsify/gamepad': ['manette'],
|
|
118
136
|
'@gjsify/webaudio': ['gstreamer', 'gst-app'],
|
|
119
137
|
'@gjsify/iframe': ['webkitgtk'],
|
|
@@ -124,13 +142,17 @@ const PACKAGE_DEPS = {
|
|
|
124
142
|
// as a special-case checkNpmPackage rather than checkPkgConfig in
|
|
125
143
|
// runOptionalChecks. Mapping it here so its presence in the project's
|
|
126
144
|
// dep tree triggers the check.
|
|
127
|
-
'@gjsify/webgl': ['gwebgl'],
|
|
145
|
+
'@gjsify/webgl': ['gwebgl', 'gdk-pixbuf', 'epoxy'],
|
|
128
146
|
// Native Vala bridges with `dependency('gnutls')` / `dependency('libnghttp2')`
|
|
129
147
|
// in their meson.build. Optional because the shipped prebuild covers the
|
|
130
148
|
// common-arch user path; only contributors rebuilding from source hit the
|
|
131
149
|
// build-time dep.
|
|
132
150
|
'@gjsify/tls-native': ['gnutls'],
|
|
133
151
|
'@gjsify/http2-native': ['nghttp2'],
|
|
152
|
+
// Unlike the two above, these are LOAD-time: the prebuild is dynamically linked
|
|
153
|
+
// and GIRepository cannot open its backing library without them.
|
|
154
|
+
'@gjsify/rolldown-native': ['json-glib'],
|
|
155
|
+
'@gjsify/webrtc-native': ['gstreamer', 'gst-webrtc'],
|
|
134
156
|
// @gjsify/event-bridge only needs gtk4/gdk which are already in the
|
|
135
157
|
// required set, so it doesn't need an optional entry.
|
|
136
158
|
};
|
|
@@ -367,6 +389,8 @@ const PM_PACKAGES = {
|
|
|
367
389
|
cairo: 'libcairo2-dev',
|
|
368
390
|
gnutls: 'libgnutls28-dev',
|
|
369
391
|
nghttp2: 'libnghttp2-dev',
|
|
392
|
+
'json-glib': 'libjson-glib-dev',
|
|
393
|
+
epoxy: 'libepoxy-dev',
|
|
370
394
|
},
|
|
371
395
|
dnf: {
|
|
372
396
|
gjs: 'gjs',
|
|
@@ -390,6 +414,8 @@ const PM_PACKAGES = {
|
|
|
390
414
|
cairo: 'cairo-devel',
|
|
391
415
|
gnutls: 'gnutls-devel',
|
|
392
416
|
nghttp2: 'libnghttp2-devel',
|
|
417
|
+
'json-glib': 'json-glib-devel',
|
|
418
|
+
epoxy: 'libepoxy-devel',
|
|
393
419
|
},
|
|
394
420
|
pacman: {
|
|
395
421
|
gjs: 'gjs',
|
|
@@ -413,6 +439,8 @@ const PM_PACKAGES = {
|
|
|
413
439
|
cairo: 'cairo',
|
|
414
440
|
gnutls: 'gnutls',
|
|
415
441
|
nghttp2: 'libnghttp2',
|
|
442
|
+
'json-glib': 'json-glib',
|
|
443
|
+
epoxy: 'libepoxy',
|
|
416
444
|
},
|
|
417
445
|
zypper: {
|
|
418
446
|
gjs: 'gjs',
|
|
@@ -436,6 +464,8 @@ const PM_PACKAGES = {
|
|
|
436
464
|
cairo: 'cairo-devel',
|
|
437
465
|
gnutls: 'libgnutls-devel',
|
|
438
466
|
nghttp2: 'libnghttp2-devel',
|
|
467
|
+
'json-glib': 'json-glib-devel',
|
|
468
|
+
epoxy: 'libepoxy-devel',
|
|
439
469
|
},
|
|
440
470
|
apk: {
|
|
441
471
|
gjs: 'gjs',
|
|
@@ -459,6 +489,8 @@ const PM_PACKAGES = {
|
|
|
459
489
|
cairo: 'cairo-dev',
|
|
460
490
|
gnutls: 'gnutls-dev',
|
|
461
491
|
nghttp2: 'nghttp2-dev',
|
|
492
|
+
'json-glib': 'json-glib-dev',
|
|
493
|
+
epoxy: 'libepoxy-dev',
|
|
462
494
|
},
|
|
463
495
|
// macOS. Names verified against homebrew/core rather than inferred from the
|
|
464
496
|
// Linux rows — the mapping is NOT mechanical: Homebrew ships no `-dev`/`-devel`
|
|
@@ -502,6 +534,8 @@ const PM_PACKAGES = {
|
|
|
502
534
|
cairo: 'cairo',
|
|
503
535
|
gnutls: 'gnutls',
|
|
504
536
|
nghttp2: 'nghttp2',
|
|
537
|
+
'json-glib': 'json-glib',
|
|
538
|
+
epoxy: 'libepoxy',
|
|
505
539
|
},
|
|
506
540
|
unknown: {},
|
|
507
541
|
};
|
package/lib/utils/dlx-cache.js
CHANGED
|
@@ -158,7 +158,7 @@ export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
/** Clean up `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`. */
|
|
160
160
|
export function cleanupStalePrepareDirs(cacheDir, _maxAgeMinutes = DEFAULT_TTL_MIN) {
|
|
161
|
-
//
|
|
161
|
+
// TODO(open-todos: 10 small API gaps): out of scope for Phase 1 — pnpm has the same stub. Kept so
|
|
162
162
|
// call sites already exist when we do implement it.
|
|
163
163
|
void cacheDir;
|
|
164
164
|
}
|
|
@@ -250,6 +250,14 @@ export declare function writeBinEntry(opts: {
|
|
|
250
250
|
envPreamble: string;
|
|
251
251
|
prebuildDirs: readonly string[];
|
|
252
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* The package's npm `bin` target, when it declares one ALONGSIDE
|
|
255
|
+
* `gjsify.bin`. The launcher then picks a runtime per invocation instead of
|
|
256
|
+
* hard-coding `gjs` — see {@link pickBinMap}. Absent for a package that
|
|
257
|
+
* declares only one of the two, which is every package but `@gjsify/cli`
|
|
258
|
+
* today.
|
|
259
|
+
*/
|
|
260
|
+
nodeFallbackAbs?: string;
|
|
253
261
|
}): void;
|
|
254
262
|
type Logger = (fmt: string, ...args: unknown[]) => void;
|
|
255
263
|
export {};
|
|
@@ -1766,7 +1766,7 @@ function readShebang(file) {
|
|
|
1766
1766
|
* the same set from the same templates. See `./bin-shim.ts`.
|
|
1767
1767
|
*/
|
|
1768
1768
|
export function writeBinEntry(opts) {
|
|
1769
|
-
const { binDir, binName, targetAbs, gjs } = opts;
|
|
1769
|
+
const { binDir, binName, targetAbs, gjs, nodeFallbackAbs } = opts;
|
|
1770
1770
|
const platform = opts.platform ?? process.platform;
|
|
1771
1771
|
const linkPath = path.join(binDir, binName);
|
|
1772
1772
|
if (gjs) {
|
|
@@ -1775,7 +1775,7 @@ export function writeBinEntry(opts) {
|
|
|
1775
1775
|
// plain symlink (or npm's three-file shim) under this very name.
|
|
1776
1776
|
for (const stale of [linkPath, `${linkPath}.cmd`, `${linkPath}.ps1`])
|
|
1777
1777
|
rmWithRetry(stale);
|
|
1778
|
-
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble: gjs.envPreamble, isGjsBundle: isBundle }));
|
|
1778
|
+
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble: gjs.envPreamble, isGjsBundle: isBundle, nodeFallbackAbs }));
|
|
1779
1779
|
try {
|
|
1780
1780
|
fs.chmodSync(linkPath, 0o755);
|
|
1781
1781
|
}
|
|
@@ -1793,6 +1793,7 @@ export function writeBinEntry(opts) {
|
|
|
1793
1793
|
interpreterArgs: ['-m'],
|
|
1794
1794
|
target: targetAbs,
|
|
1795
1795
|
prependEnv,
|
|
1796
|
+
fallback: nodeFallbackAbs === undefined ? undefined : { interpreter: 'node', target: nodeFallbackAbs },
|
|
1796
1797
|
});
|
|
1797
1798
|
fs.writeFileSync(`${linkPath}.cmd`, shims.cmd);
|
|
1798
1799
|
fs.writeFileSync(`${linkPath}.ps1`, shims.ps1);
|
|
@@ -1872,11 +1873,16 @@ async function linkBins(nodes, prefix, log) {
|
|
|
1872
1873
|
catch {
|
|
1873
1874
|
/* best effort */
|
|
1874
1875
|
}
|
|
1876
|
+
// Only a fallback that EXISTS is worth writing: a launcher naming a
|
|
1877
|
+
// missing Node entry would trade exit 127 for a different exit 127.
|
|
1878
|
+
const fallbackRel = picked.nodeFallback?.get(binName);
|
|
1879
|
+
const fallbackAbs = fallbackRel === undefined ? undefined : path.join(pkgDir, fallbackRel);
|
|
1875
1880
|
writeBinEntry({
|
|
1876
1881
|
binDir,
|
|
1877
1882
|
binName,
|
|
1878
1883
|
targetAbs,
|
|
1879
1884
|
gjs: picked.isGjsBin ? gjsEnvForPrefix() : undefined,
|
|
1885
|
+
nodeFallbackAbs: fallbackAbs !== undefined && fs.existsSync(fallbackAbs) ? fallbackAbs : undefined,
|
|
1880
1886
|
});
|
|
1881
1887
|
created++;
|
|
1882
1888
|
}
|
|
@@ -198,10 +198,19 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
198
198
|
// then tries to parse JavaScript as shell. Plain Node scripts
|
|
199
199
|
// with shebangs (lib/index.js) keep the direct-exec path.
|
|
200
200
|
const isGjsBundle = isGjsBundlePath(targetAbs);
|
|
201
|
+
// A global install extracts the WHOLE package, so when it declares
|
|
202
|
+
// an npm `bin` beside its `gjsify.bin` that entry is on disk here
|
|
203
|
+
// too and the launcher can fall back to it on a host without `gjs`
|
|
204
|
+
// — the same fix as the project-local backend. Guarded on existence
|
|
205
|
+
// rather than assumed: naming a missing Node entry would only trade
|
|
206
|
+
// one exit 127 for another. See `pickBinMap`.
|
|
207
|
+
const fallbackRel = picked.nodeFallback?.get(binName);
|
|
208
|
+
const fallbackCandidate = fallbackRel === undefined ? undefined : path.join(pkgDir, fallbackRel);
|
|
209
|
+
const nodeFallbackAbs = fallbackCandidate !== undefined && fs.existsSync(fallbackCandidate) ? fallbackCandidate : undefined;
|
|
201
210
|
// Only GJS bundles need the GI typelib search path. Plain Node
|
|
202
211
|
// scripts ignore GI_TYPELIB_PATH, so skipping the preamble there
|
|
203
212
|
// keeps the launcher minimal.
|
|
204
|
-
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble, isGjsBundle }));
|
|
213
|
+
fs.writeFileSync(linkPath, buildShLauncher(targetAbs, { envPreamble, isGjsBundle, nodeFallbackAbs }));
|
|
205
214
|
fs.chmodSync(linkPath, 0o755);
|
|
206
215
|
// Windows runs neither an extension-less file nor a `#!` line, so
|
|
207
216
|
// the `sh` launcher above is only reachable from git-bash/MSYS/WSL.
|
|
@@ -224,6 +233,9 @@ export function linkGlobalBins(packageNames, layout) {
|
|
|
224
233
|
interpreterArgs: ['-m'],
|
|
225
234
|
target: targetAbs,
|
|
226
235
|
prependEnv,
|
|
236
|
+
fallback: nodeFallbackAbs === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: { interpreter: 'node', target: nodeFallbackAbs },
|
|
227
239
|
})
|
|
228
240
|
: buildLauncherShims({
|
|
229
241
|
interpreter: shebang?.prog || 'node',
|
|
@@ -7,8 +7,28 @@
|
|
|
7
7
|
// behaves like `yarn run` / `npm run`. That's wrong for the embedded use
|
|
8
8
|
// case where pack/publish needs the lifecycle script to finish, then keep
|
|
9
9
|
// running its own logic. This helper resolves to a Promise that settles
|
|
10
|
-
// when the child exits
|
|
11
|
-
//
|
|
10
|
+
// when the child exits.
|
|
11
|
+
//
|
|
12
|
+
// ## Not calling `process.exit()` is what ARMED the hang, not what avoided it
|
|
13
|
+
//
|
|
14
|
+
// This header used to end "no process.exit, no GLib-mainloop intermingling
|
|
15
|
+
// from `ensureMainLoop`". The second half was exactly backwards, and it is why
|
|
16
|
+
// nobody looked here for #1010.
|
|
17
|
+
//
|
|
18
|
+
// `@gjsify/child_process`'s async `spawn()` calls `ensureMainLoop()`
|
|
19
|
+
// unconditionally — the caller has no say. That arms a GJS main-loop hook whose
|
|
20
|
+
// blocking `g_main_loop_run()` only `process.exit()` tears down. So a helper
|
|
21
|
+
// that spawns asynchronously and deliberately does NOT exit leaves the loop
|
|
22
|
+
// armed forever: `gjsify pack` on a package with a `prepack` wrote its tarball,
|
|
23
|
+
// printed its complete JSON, and then parked at 0% CPU until killed at 5m30s.
|
|
24
|
+
// A package with no lifecycle script exited in under a second, and the same
|
|
25
|
+
// pack under Node exited in 0.68s.
|
|
26
|
+
//
|
|
27
|
+
// `utils/spawn.ts` already owns that contract and makes every call site DECLARE
|
|
28
|
+
// which side of it they are on. This helper was the one spawn in the CLI that
|
|
29
|
+
// bypassed it — the seventh of the "six near-identical wrappers" that file was
|
|
30
|
+
// written to consolidate. It now goes through it with `completion: 'return'`,
|
|
31
|
+
// which is what "settle, then keep running our own logic" means.
|
|
12
32
|
//
|
|
13
33
|
// Matches yarn / npm script semantics:
|
|
14
34
|
// - `shell: true` so `&&` / `|` / env-var refs work
|
|
@@ -16,8 +36,8 @@
|
|
|
16
36
|
// - `npm_lifecycle_event` / `npm_package_name` / `npm_package_version`
|
|
17
37
|
// env vars set
|
|
18
38
|
// - FORCE_COLOR=1 default unless caller overrides
|
|
19
|
-
import { spawn } from 'node:child_process';
|
|
20
39
|
import { delimiter, join } from 'node:path';
|
|
40
|
+
import { describeExit, spawnToCompletion } from './spawn.js';
|
|
21
41
|
import { findWorkspaceRoot } from './workspace-root.js';
|
|
22
42
|
/**
|
|
23
43
|
* Run a lifecycle script defined in `pkg.scripts[name]` from `wsDir`.
|
|
@@ -52,28 +72,24 @@ export async function runLifecycleScript(wsDir, pkg, name, opts = {}) {
|
|
|
52
72
|
npm_package_version: pkg.version ?? '',
|
|
53
73
|
...opts.env,
|
|
54
74
|
};
|
|
55
|
-
// `'
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (code === 0)
|
|
71
|
-
resolveOk();
|
|
72
|
-
else {
|
|
73
|
-
reject(new Error(`gjsify lifecycle-script: "${name}" in ${wsDir} exited with code ${code}`));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
child.on('error', reject);
|
|
75
|
+
// `'ignore'` has no `spawnToCompletion` equivalent and no call site in the
|
|
76
|
+
// tree; folding it onto `'inherit'` would silently start printing where a
|
|
77
|
+
// caller asked for silence, so it is rejected rather than approximated.
|
|
78
|
+
if (opts.stdio === 'ignore') {
|
|
79
|
+
throw new Error('gjsify lifecycle-script: stdio "ignore" is not supported — see utils/spawn.ts');
|
|
80
|
+
}
|
|
81
|
+
const result = await spawnToCompletion(literal, [], {
|
|
82
|
+
// The whole point of this helper: pack/publish waits for the script and
|
|
83
|
+
// then keeps running. Under GJS that is the blocking path, which is what
|
|
84
|
+
// keeps the armed main loop from outliving us — see the header.
|
|
85
|
+
completion: 'return',
|
|
86
|
+
cwd: wsDir,
|
|
87
|
+
env: env,
|
|
88
|
+
shell: true,
|
|
89
|
+
stdio: opts.stdio === 'inherit-stderr' ? 'inherit-stderr' : opts.stdio === 'pipe' ? 'pipe' : 'inherit',
|
|
77
90
|
});
|
|
91
|
+
if (result.code !== 0) {
|
|
92
|
+
throw new Error(`gjsify lifecycle-script: "${name}" in ${wsDir} exited with ${describeExit(result)}`);
|
|
93
|
+
}
|
|
78
94
|
return true;
|
|
79
95
|
}
|
package/lib/utils/spawn.d.ts
CHANGED
|
@@ -33,8 +33,24 @@ export interface SpawnToCompletionOptions {
|
|
|
33
33
|
* `'inherit'` (default) forwards the parent's stdio. `'pipe'` gives the
|
|
34
34
|
* caller `child.stdout` / `child.stderr` via {@link onSpawn} — only
|
|
35
35
|
* meaningful together with `completion: 'exit'` (see {@link onSpawn}).
|
|
36
|
+
*
|
|
37
|
+
* `'inherit-stderr'` inherits, but routes the child's **stdout to the
|
|
38
|
+
* parent's stderr**. For a parent whose own stdout is a machine-readable
|
|
39
|
+
* stream: `gjsify pack --json` emits JSON there, and a lifecycle script's
|
|
40
|
+
* log lines interleaved into it make the result unparseable for every
|
|
41
|
+
* caller that pipes it into `JSON.parse`.
|
|
42
|
+
*/
|
|
43
|
+
stdio?: 'inherit' | 'pipe' | 'inherit-stderr';
|
|
44
|
+
/**
|
|
45
|
+
* Run `cmd` through the platform shell, so `&&`, `|` and env-var
|
|
46
|
+
* references work. `cmd` is then a whole command LINE and `args` must be
|
|
47
|
+
* empty — that is npm/yarn script semantics.
|
|
48
|
+
*
|
|
49
|
+
* Skips the win32 command rewrite by design, per this file's own rule:
|
|
50
|
+
* Node routes a `shell: true` spawn through `%COMSPEC%`, which finds
|
|
51
|
+
* `.cmd` shims on its own, so rewriting the string would corrupt it.
|
|
36
52
|
*/
|
|
37
|
-
|
|
53
|
+
shell?: boolean;
|
|
38
54
|
/**
|
|
39
55
|
* Invoked synchronously with the live `ChildProcess` right after a
|
|
40
56
|
* successful async spawn — the seam for kill-tracking registries and for
|
package/lib/utils/spawn.js
CHANGED
|
@@ -133,34 +133,51 @@ function mustBlock(opts) {
|
|
|
133
133
|
*/
|
|
134
134
|
export function spawnToCompletion(cmd, args, opts) {
|
|
135
135
|
const env = resolveEnv(opts);
|
|
136
|
+
// A shell invocation is a command LINE, not an argv, so the win32 rewrite
|
|
137
|
+
// must not touch it — see `SpawnToCompletionOptions.shell`.
|
|
138
|
+
const invocation = opts.shell
|
|
139
|
+
? { cmd, args: [...args], windowsVerbatimArguments: false }
|
|
140
|
+
: forWin32(cmd, args, env);
|
|
136
141
|
if (mustBlock(opts)) {
|
|
137
142
|
// `'inherit'` is NOT forwarded through `@gjsify/child_process`'s GJS
|
|
138
143
|
// spawnSync path (it drives a private main context via
|
|
139
144
|
// `communicateWithTimeout`), so the child's diagnostics would be
|
|
140
145
|
// silently dropped. Capture them and re-emit after the child exits.
|
|
141
146
|
// stdin stays inherited so an interactive child still reads the tty.
|
|
142
|
-
const
|
|
143
|
-
const r = spawnSync(win.cmd, win.args, {
|
|
147
|
+
const r = spawnSync(invocation.cmd, invocation.args, {
|
|
144
148
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
145
149
|
cwd: opts.cwd,
|
|
146
150
|
env,
|
|
151
|
+
shell: opts.shell,
|
|
147
152
|
maxBuffer: opts.maxBuffer ?? DEFAULT_MAX_BUFFER,
|
|
148
|
-
windowsVerbatimArguments:
|
|
153
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
149
154
|
});
|
|
150
155
|
if (r.error)
|
|
151
156
|
return Promise.reject(mapSpawnError(r.error, opts));
|
|
157
|
+
// The captured stdout goes to the parent's STDERR under
|
|
158
|
+
// `'inherit-stderr'` — same destination the streaming path routes it to,
|
|
159
|
+
// so the two agree on where a lifecycle script's chatter lands.
|
|
160
|
+
const out = opts.stdio === 'inherit-stderr' ? process.stderr : process.stdout;
|
|
152
161
|
if (r.stdout && r.stdout.length > 0)
|
|
153
|
-
|
|
162
|
+
out.write(r.stdout);
|
|
154
163
|
if (r.stderr && r.stderr.length > 0)
|
|
155
164
|
process.stderr.write(r.stderr);
|
|
156
165
|
return Promise.resolve({ code: r.status, signal: r.signal ?? null });
|
|
157
166
|
}
|
|
158
167
|
return new Promise((resolvePromise, reject) => {
|
|
159
|
-
const win =
|
|
168
|
+
const win = invocation;
|
|
160
169
|
const spawnOpts = {
|
|
161
170
|
cwd: opts.cwd,
|
|
162
|
-
|
|
171
|
+
// Numeric fds route the child's matching stream to that fd, so
|
|
172
|
+
// `['inherit', 2, 2]` is stdin inherited, stdout AND stderr → fd 2.
|
|
173
|
+
stdio: opts.stdio === 'pipe'
|
|
174
|
+
? ['ignore', 'pipe', 'pipe']
|
|
175
|
+
: opts.stdio === 'inherit-stderr'
|
|
176
|
+
? ['inherit', 2, 2]
|
|
177
|
+
: 'inherit',
|
|
163
178
|
};
|
|
179
|
+
if (opts.shell)
|
|
180
|
+
spawnOpts.shell = true;
|
|
164
181
|
if (env)
|
|
165
182
|
spawnOpts.env = env;
|
|
166
183
|
if (win.windowsVerbatimArguments)
|