@gjsify/cli 0.29.0 → 0.31.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 +197 -189
- package/lib/actions/build.d.ts +14 -0
- package/lib/actions/build.js +78 -3
- package/lib/actions/index.d.ts +2 -0
- package/lib/actions/index.js +2 -0
- package/lib/affected-entry.d.ts +1 -0
- package/lib/affected-entry.js +49 -0
- package/lib/bundler-pick.js +84 -19
- package/lib/cli-app.js +20 -0
- package/lib/cli-fail.d.ts +22 -0
- package/lib/cli-fail.js +67 -0
- package/lib/commands/affected.d.ts +2 -2
- package/lib/commands/affected.js +16 -0
- package/lib/commands/install.d.ts +55 -0
- package/lib/commands/install.js +38 -21
- package/lib/commands/run.js +25 -6
- package/lib/commands/test.js +4 -4
- package/lib/commands/tsc.js +59 -11
- package/lib/commands/workspace.js +13 -0
- package/lib/config.js +6 -2
- package/lib/index.js +18 -1
- package/lib/templates/install.mjs.tmpl +27 -2
- package/lib/types/command.d.ts +18 -0
- 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 +88 -1
- package/lib/utils/check-system-deps.js +177 -6
- package/lib/utils/detect-native-packages.js +12 -1
- package/lib/utils/discover-showcases.d.ts +0 -2
- package/lib/utils/discover-showcases.js +0 -1
- package/lib/utils/dlx-cache.d.ts +22 -2
- package/lib/utils/dlx-cache.js +62 -6
- package/lib/utils/force-exit.d.ts +10 -0
- package/lib/utils/force-exit.js +43 -0
- package/lib/utils/gi-search-path.d.ts +36 -0
- package/lib/utils/gi-search-path.js +146 -0
- package/lib/utils/gjs-source-escape.d.ts +10 -0
- package/lib/utils/gjs-source-escape.js +49 -0
- package/lib/utils/gjsify-shim.d.ts +29 -2
- package/lib/utils/gjsify-shim.js +131 -22
- package/lib/utils/install-backend-native.d.ts +8 -0
- package/lib/utils/install-backend-native.js +8 -2
- package/lib/utils/install-global.d.ts +30 -2
- package/lib/utils/install-global.js +51 -5
- package/lib/utils/oxc-resolve.d.ts +1 -1
- package/lib/utils/oxc-resolve.js +10 -6
- 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/lib/utils/stdin.d.ts +3 -13
- package/lib/utils/stdin.js +21 -100
- package/lib/utils/system-gi.js +10 -1
- package/package.json +22 -17
- package/showcases.json +8 -16
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
|
}
|
|
@@ -12,7 +12,26 @@ export interface DepCheck {
|
|
|
12
12
|
/** For optional deps: which @gjsify/* packages need this lib */
|
|
13
13
|
requiredBy?: string[];
|
|
14
14
|
}
|
|
15
|
-
export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew' | 'unknown';
|
|
15
|
+
export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew' | 'winget' | 'unknown';
|
|
16
|
+
/**
|
|
17
|
+
* True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
|
|
18
|
+
* instead of shelling out to `which`(1).
|
|
19
|
+
*
|
|
20
|
+
* `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
|
|
21
|
+
* CI runs in ship without it (only the `which-2.x` rpm provides it), so a
|
|
22
|
+
* `which`-based check silently answers "missing" for EVERY command. That is
|
|
23
|
+
* exactly where this file's answer matters most — `detectPackageManager()`
|
|
24
|
+
* returning `unknown` makes `buildInstallCommand()` return null, so the
|
|
25
|
+
* distro-specific install hint for a missing system library prints nothing on
|
|
26
|
+
* the hosts that needed it. Measured twice on 2026-08-05: ts-for-gir#437 had to
|
|
27
|
+
* discover `json-glib` by hand, bauplaner#40 `libsoup3` — both in such a
|
|
28
|
+
* container, both while this file already declared the dependency.
|
|
29
|
+
*
|
|
30
|
+
* `tests/e2e/helpers.mjs`'s `hasCommand()` carries the same walk for the same
|
|
31
|
+
* reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
|
|
32
|
+
* cannot be imported from the shipped CLI.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isOnPath(cmd: string): boolean;
|
|
16
35
|
/**
|
|
17
36
|
* The package manager whose install command we may suggest.
|
|
18
37
|
*
|
|
@@ -28,6 +47,73 @@ export type PackageManager = 'apt' | 'dnf' | 'pacman' | 'zypper' | 'apk' | 'brew
|
|
|
28
47
|
* MacPorts or Nix host, and it is why this must never fall back to "probably brew".
|
|
29
48
|
*/
|
|
30
49
|
export declare function detectPackageManager(): PackageManager;
|
|
50
|
+
/**
|
|
51
|
+
* The Visual C++ runtime — an undeclared, undiagnosed win32 prerequisite (#997).
|
|
52
|
+
*
|
|
53
|
+
* `@gjsify/gtk-runtime-win32-x64` is built with gvsbuild, i.e. against the MSVC
|
|
54
|
+
* ABI, and its DLLs load into stock (also MSVC-ABI) Node. On a Windows host
|
|
55
|
+
* WITHOUT the Visual C++ redistributable that load fails — and nothing in the
|
|
56
|
+
* toolchain said so. The precedent for why that matters is PR #994: a missing
|
|
57
|
+
* load-time library made a typelib unloadable and the user-visible message was
|
|
58
|
+
* `Unsupported type void, deriving from fundamental void`, which names nothing
|
|
59
|
+
* a user could act on.
|
|
60
|
+
*
|
|
61
|
+
* Probed by FILE PRESENCE in the system directory rather than by asking a
|
|
62
|
+
* package database: the redistributable can arrive with Visual Studio, with
|
|
63
|
+
* another application's installer, or as part of a Windows image, and only one
|
|
64
|
+
* of those routes is visible to a package manager. What the loader actually
|
|
65
|
+
* needs is the DLL, so that is what is checked.
|
|
66
|
+
*
|
|
67
|
+
* `vcruntime140.dll` and `msvcp140.dll` are the C and C++ halves;
|
|
68
|
+
* `vcruntime140_1.dll` carries the x64 exception-handling machinery and is
|
|
69
|
+
* absent from pre-2019 redistributables, which is exactly the "installed, but
|
|
70
|
+
* too old" case that otherwise fails at first use rather than at load.
|
|
71
|
+
*/
|
|
72
|
+
export declare function checkMsvcRuntime(): DepCheck;
|
|
73
|
+
interface OptionalDep {
|
|
74
|
+
/** DepCheck id */
|
|
75
|
+
id: string;
|
|
76
|
+
/** Display name */
|
|
77
|
+
name: string;
|
|
78
|
+
/** pkg-config library name */
|
|
79
|
+
pkgName: string;
|
|
80
|
+
}
|
|
81
|
+
/** Optional system deps keyed by their DepCheck id. Exported for the spec's declaration invariant. */
|
|
82
|
+
export declare const OPTIONAL_DEPS: Record<string, OptionalDep>;
|
|
83
|
+
/**
|
|
84
|
+
* The declared system libraries a single `@gjsify/*` package needs that are NOT
|
|
85
|
+
* present on this host.
|
|
86
|
+
*
|
|
87
|
+
* Exists so a FAILURE can answer the question this file already knows the answer
|
|
88
|
+
* to. The tables below have declared `@gjsify/rolldown-native → json-glib` since
|
|
89
|
+
* #994, complete with per-distro package names — but the only reader was
|
|
90
|
+
* `runAllChecks()`, i.e. `gjsify system-check`, a command the user has to think
|
|
91
|
+
* to run. So when the engine was installed and simply would not LOAD, the build
|
|
92
|
+
* said "@gjsify/rolldown-native is NOT INSTALLED … install it" (it was) or
|
|
93
|
+
* guessed at a bypassed launcher. Two repositories spent a CI round rediscovering
|
|
94
|
+
* a library this file could have named: ts-for-gir#437, bauplaner#40.
|
|
95
|
+
*
|
|
96
|
+
* Cannot throw, by construction rather than by a catch: `checkPkgConfig` goes
|
|
97
|
+
* through `tryExecFile`, which returns null on any failure. That matters because
|
|
98
|
+
* the caller is `diagnoseNativeEngine()`, which runs *while explaining another
|
|
99
|
+
* failure* — a probe that dies there would replace the diagnosis with its own
|
|
100
|
+
* stack.
|
|
101
|
+
*
|
|
102
|
+
* Returns `[]` for an unknown package or one with no declared deps, so a caller
|
|
103
|
+
* needs no special case for "nothing to say".
|
|
104
|
+
*/
|
|
105
|
+
export declare function missingSystemDepsFor(packageName: string): DepCheck[];
|
|
106
|
+
/**
|
|
107
|
+
* Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
|
|
108
|
+
* Generated by walking each package's `gi://` imports (excluding the always-
|
|
109
|
+
* available trio GLib/GObject/Gio).
|
|
110
|
+
*
|
|
111
|
+
* Used to compute the set of optional deps to check for a given project.
|
|
112
|
+
*
|
|
113
|
+
* Exported for the spec's declaration invariant: every id named here must exist in
|
|
114
|
+
* OPTIONAL_DEPS, or the dep is silently never checked.
|
|
115
|
+
*/
|
|
116
|
+
export declare const PACKAGE_DEPS: Record<string, string[]>;
|
|
31
117
|
/**
|
|
32
118
|
* Run all dependency checks. Used by `gjsify check` to show full system status.
|
|
33
119
|
*
|
|
@@ -86,3 +172,4 @@ export interface TypeSkewReaders {
|
|
|
86
172
|
* ignore the output.
|
|
87
173
|
*/
|
|
88
174
|
export declare function checkTypeSkew(cwd: string, readers?: TypeSkewReaders): TypeSkew[];
|
|
175
|
+
export {};
|
|
@@ -24,6 +24,42 @@ function tryExecFile(binary, args) {
|
|
|
24
24
|
return null;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* True when `cmd` is on PATH. Walks `process.env.PATH` with `existsSync`
|
|
29
|
+
* instead of shelling out to `which`(1).
|
|
30
|
+
*
|
|
31
|
+
* `which` is NOT a probe you can rely on: the Fedora 43/44 minimal containers
|
|
32
|
+
* CI runs in ship without it (only the `which-2.x` rpm provides it), so a
|
|
33
|
+
* `which`-based check silently answers "missing" for EVERY command. That is
|
|
34
|
+
* exactly where this file's answer matters most — `detectPackageManager()`
|
|
35
|
+
* returning `unknown` makes `buildInstallCommand()` return null, so the
|
|
36
|
+
* distro-specific install hint for a missing system library prints nothing on
|
|
37
|
+
* the hosts that needed it. Measured twice on 2026-08-05: ts-for-gir#437 had to
|
|
38
|
+
* discover `json-glib` by hand, bauplaner#40 `libsoup3` — both in such a
|
|
39
|
+
* container, both while this file already declared the dependency.
|
|
40
|
+
*
|
|
41
|
+
* `tests/e2e/helpers.mjs`'s `hasCommand()` carries the same walk for the same
|
|
42
|
+
* reason. It stays a second copy on purpose: it is `.mjs` under `tests/` and
|
|
43
|
+
* cannot be imported from the shipped CLI.
|
|
44
|
+
*/
|
|
45
|
+
export function isOnPath(cmd) {
|
|
46
|
+
const pathVar = process.env.PATH;
|
|
47
|
+
if (!pathVar)
|
|
48
|
+
return false;
|
|
49
|
+
const sep = process.platform === 'win32' ? ';' : ':';
|
|
50
|
+
for (const dir of pathVar.split(sep)) {
|
|
51
|
+
if (!dir)
|
|
52
|
+
continue;
|
|
53
|
+
try {
|
|
54
|
+
if (existsSync(join(dir, cmd)))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// An inaccessible PATH entry is not an answer about `cmd`.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
27
63
|
/** Check if a binary exists and optionally capture its version output. */
|
|
28
64
|
function checkBinary(id, name, binary, versionArgs, severity, parseVersion, requiredBy) {
|
|
29
65
|
const out = tryExecFile(binary, versionArgs);
|
|
@@ -79,16 +115,58 @@ function checkNpmPackage(id, name, packageName, cwd, severity, requiredBy) {
|
|
|
79
115
|
* MacPorts or Nix host, and it is why this must never fall back to "probably brew".
|
|
80
116
|
*/
|
|
81
117
|
export function detectPackageManager() {
|
|
118
|
+
// win32 has no `which`, so the loop below would return `unknown` by
|
|
119
|
+
// ACCIDENT rather than by construction — which was the pre-existing
|
|
120
|
+
// behaviour and produced the right answer for the wrong reason. `winget`
|
|
121
|
+
// ships with Windows 11 and is what a user actually has, and it is only
|
|
122
|
+
// ever consulted for the one dependency this platform has an entry for.
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
return tryExecFile('where', ['winget']) !== null ? 'winget' : 'unknown';
|
|
125
|
+
}
|
|
82
126
|
const managers = process.platform === 'darwin' ? ['brew'] : ['apt', 'dnf', 'pacman', 'zypper', 'apk', 'brew'];
|
|
83
127
|
for (const pm of managers) {
|
|
84
|
-
|
|
85
|
-
if (result !== null)
|
|
128
|
+
if (isOnPath(pm))
|
|
86
129
|
return pm;
|
|
87
130
|
}
|
|
88
131
|
return 'unknown';
|
|
89
132
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
133
|
+
/**
|
|
134
|
+
* The Visual C++ runtime — an undeclared, undiagnosed win32 prerequisite (#997).
|
|
135
|
+
*
|
|
136
|
+
* `@gjsify/gtk-runtime-win32-x64` is built with gvsbuild, i.e. against the MSVC
|
|
137
|
+
* ABI, and its DLLs load into stock (also MSVC-ABI) Node. On a Windows host
|
|
138
|
+
* WITHOUT the Visual C++ redistributable that load fails — and nothing in the
|
|
139
|
+
* toolchain said so. The precedent for why that matters is PR #994: a missing
|
|
140
|
+
* load-time library made a typelib unloadable and the user-visible message was
|
|
141
|
+
* `Unsupported type void, deriving from fundamental void`, which names nothing
|
|
142
|
+
* a user could act on.
|
|
143
|
+
*
|
|
144
|
+
* Probed by FILE PRESENCE in the system directory rather than by asking a
|
|
145
|
+
* package database: the redistributable can arrive with Visual Studio, with
|
|
146
|
+
* another application's installer, or as part of a Windows image, and only one
|
|
147
|
+
* of those routes is visible to a package manager. What the loader actually
|
|
148
|
+
* needs is the DLL, so that is what is checked.
|
|
149
|
+
*
|
|
150
|
+
* `vcruntime140.dll` and `msvcp140.dll` are the C and C++ halves;
|
|
151
|
+
* `vcruntime140_1.dll` carries the x64 exception-handling machinery and is
|
|
152
|
+
* absent from pre-2019 redistributables, which is exactly the "installed, but
|
|
153
|
+
* too old" case that otherwise fails at first use rather than at load.
|
|
154
|
+
*/
|
|
155
|
+
export function checkMsvcRuntime() {
|
|
156
|
+
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT ?? 'C:\\Windows';
|
|
157
|
+
const required = ['vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll'];
|
|
158
|
+
const missing = required.filter((dll) => !existsSync(`${systemRoot}\\System32\\${dll}`));
|
|
159
|
+
return {
|
|
160
|
+
id: 'msvc-runtime',
|
|
161
|
+
name: missing.length === 0
|
|
162
|
+
? 'Microsoft Visual C++ runtime'
|
|
163
|
+
: `Microsoft Visual C++ runtime (missing ${missing.join(', ')} — the GTK bundle's DLLs are MSVC-ABI and will not load)`,
|
|
164
|
+
found: missing.length === 0,
|
|
165
|
+
severity: 'required',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** Optional system deps keyed by their DepCheck id. Exported for the spec's declaration invariant. */
|
|
169
|
+
export const OPTIONAL_DEPS = {
|
|
92
170
|
manette: { id: 'manette', name: 'libmanette', pkgName: 'manette-0.2' },
|
|
93
171
|
gstreamer: { id: 'gstreamer', name: 'GStreamer', pkgName: 'gstreamer-1.0' },
|
|
94
172
|
'gst-app': { id: 'gst-app', name: 'GStreamer App', pkgName: 'gstreamer-app-1.0' },
|
|
@@ -105,15 +183,74 @@ const OPTIONAL_DEPS = {
|
|
|
105
183
|
// `Run-time dependency X found: NO` error mid-build.
|
|
106
184
|
gnutls: { id: 'gnutls', name: 'GnuTLS', pkgName: 'gnutls' },
|
|
107
185
|
nghttp2: { id: 'nghttp2', name: 'libnghttp2', pkgName: 'libnghttp2' },
|
|
186
|
+
// RUNTIME deps of the shipped prebuilds, not build-time ones — the distinction
|
|
187
|
+
// the two entries above make does not hold for these. Measured on fedora:43
|
|
188
|
+
// provisioned like a CONSUMER (gjs + libsoup3 only), `ldd` over every committed
|
|
189
|
+
// linux-x64 prebuild: three sonames do not resolve, and each makes its typelib
|
|
190
|
+
// UNLOADABLE, which surfaces as `Unsupported type void, deriving from
|
|
191
|
+
// fundamental void` at first construction — a message that names nothing.
|
|
192
|
+
// @gjsify/rolldown-native libjson-glib-1.0.so.0
|
|
193
|
+
// @gjsify/webgl libepoxy.so.0 (gdk-pixbuf already listed)
|
|
194
|
+
// @gjsify/webrtc-native libgstwebrtc-1.0.so.0 (gstreamer already listed)
|
|
195
|
+
// json-glib is the severe one: rolldown-native IS the GJS bundler engine, so a
|
|
196
|
+
// host without it cannot `gjsify build` at all, while `gjsify install` printed
|
|
197
|
+
// "System dependencies OK." — a check passing for the wrong reason.
|
|
198
|
+
'json-glib': { id: 'json-glib', name: 'JSON-GLib', pkgName: 'json-glib-1.0' },
|
|
199
|
+
epoxy: { id: 'epoxy', name: 'libepoxy', pkgName: 'epoxy' },
|
|
200
|
+
'gst-webrtc': { id: 'gst-webrtc', name: 'GStreamer WebRTC', pkgName: 'gstreamer-webrtc-1.0' },
|
|
108
201
|
};
|
|
202
|
+
/**
|
|
203
|
+
* The declared system libraries a single `@gjsify/*` package needs that are NOT
|
|
204
|
+
* present on this host.
|
|
205
|
+
*
|
|
206
|
+
* Exists so a FAILURE can answer the question this file already knows the answer
|
|
207
|
+
* to. The tables below have declared `@gjsify/rolldown-native → json-glib` since
|
|
208
|
+
* #994, complete with per-distro package names — but the only reader was
|
|
209
|
+
* `runAllChecks()`, i.e. `gjsify system-check`, a command the user has to think
|
|
210
|
+
* to run. So when the engine was installed and simply would not LOAD, the build
|
|
211
|
+
* said "@gjsify/rolldown-native is NOT INSTALLED … install it" (it was) or
|
|
212
|
+
* guessed at a bypassed launcher. Two repositories spent a CI round rediscovering
|
|
213
|
+
* a library this file could have named: ts-for-gir#437, bauplaner#40.
|
|
214
|
+
*
|
|
215
|
+
* Cannot throw, by construction rather than by a catch: `checkPkgConfig` goes
|
|
216
|
+
* through `tryExecFile`, which returns null on any failure. That matters because
|
|
217
|
+
* the caller is `diagnoseNativeEngine()`, which runs *while explaining another
|
|
218
|
+
* failure* — a probe that dies there would replace the diagnosis with its own
|
|
219
|
+
* stack.
|
|
220
|
+
*
|
|
221
|
+
* Returns `[]` for an unknown package or one with no declared deps, so a caller
|
|
222
|
+
* needs no special case for "nothing to say".
|
|
223
|
+
*/
|
|
224
|
+
export function missingSystemDepsFor(packageName) {
|
|
225
|
+
const ids = PACKAGE_DEPS[packageName];
|
|
226
|
+
if (!ids)
|
|
227
|
+
return [];
|
|
228
|
+
const missing = [];
|
|
229
|
+
for (const id of ids) {
|
|
230
|
+
const dep = OPTIONAL_DEPS[id];
|
|
231
|
+
// An id with no OPTIONAL_DEPS entry is not "present"; it is unanswerable
|
|
232
|
+
// here. The spec's declaration invariant is what keeps that set empty —
|
|
233
|
+
// silently reporting it as found would be the failure mode this whole
|
|
234
|
+
// function exists to end.
|
|
235
|
+
if (!dep?.pkgName)
|
|
236
|
+
continue;
|
|
237
|
+
const result = checkPkgConfig(dep.id, dep.name, dep.pkgName, 'optional', [packageName]);
|
|
238
|
+
if (!result.found)
|
|
239
|
+
missing.push(result);
|
|
240
|
+
}
|
|
241
|
+
return missing;
|
|
242
|
+
}
|
|
109
243
|
/**
|
|
110
244
|
* Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
|
|
111
245
|
* Generated by walking each package's `gi://` imports (excluding the always-
|
|
112
246
|
* available trio GLib/GObject/Gio).
|
|
113
247
|
*
|
|
114
248
|
* Used to compute the set of optional deps to check for a given project.
|
|
249
|
+
*
|
|
250
|
+
* Exported for the spec's declaration invariant: every id named here must exist in
|
|
251
|
+
* OPTIONAL_DEPS, or the dep is silently never checked.
|
|
115
252
|
*/
|
|
116
|
-
const PACKAGE_DEPS = {
|
|
253
|
+
export const PACKAGE_DEPS = {
|
|
117
254
|
'@gjsify/gamepad': ['manette'],
|
|
118
255
|
'@gjsify/webaudio': ['gstreamer', 'gst-app'],
|
|
119
256
|
'@gjsify/iframe': ['webkitgtk'],
|
|
@@ -124,13 +261,17 @@ const PACKAGE_DEPS = {
|
|
|
124
261
|
// as a special-case checkNpmPackage rather than checkPkgConfig in
|
|
125
262
|
// runOptionalChecks. Mapping it here so its presence in the project's
|
|
126
263
|
// dep tree triggers the check.
|
|
127
|
-
'@gjsify/webgl': ['gwebgl'],
|
|
264
|
+
'@gjsify/webgl': ['gwebgl', 'gdk-pixbuf', 'epoxy'],
|
|
128
265
|
// Native Vala bridges with `dependency('gnutls')` / `dependency('libnghttp2')`
|
|
129
266
|
// in their meson.build. Optional because the shipped prebuild covers the
|
|
130
267
|
// common-arch user path; only contributors rebuilding from source hit the
|
|
131
268
|
// build-time dep.
|
|
132
269
|
'@gjsify/tls-native': ['gnutls'],
|
|
133
270
|
'@gjsify/http2-native': ['nghttp2'],
|
|
271
|
+
// Unlike the two above, these are LOAD-time: the prebuild is dynamically linked
|
|
272
|
+
// and GIRepository cannot open its backing library without them.
|
|
273
|
+
'@gjsify/rolldown-native': ['json-glib'],
|
|
274
|
+
'@gjsify/webrtc-native': ['gstreamer', 'gst-webrtc'],
|
|
134
275
|
// @gjsify/event-bridge only needs gtk4/gdk which are already in the
|
|
135
276
|
// required set, so it doesn't need an optional entry.
|
|
136
277
|
};
|
|
@@ -243,6 +384,12 @@ export function runMinimalChecks() {
|
|
|
243
384
|
results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'required' });
|
|
244
385
|
// GJS
|
|
245
386
|
results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], 'required', (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
|
|
387
|
+
// The one win32 system dependency that is real and was undiagnosed (#997).
|
|
388
|
+
// Checked at INSTALL time, not just in `system-check`, because the failure
|
|
389
|
+
// it prevents is a bare `ERR_DLOPEN_FAILED` on a `.node` that is present —
|
|
390
|
+
// by then the user has no thread to pull.
|
|
391
|
+
if (process.platform === 'win32')
|
|
392
|
+
results.push(checkMsvcRuntime());
|
|
246
393
|
return results;
|
|
247
394
|
}
|
|
248
395
|
/** Check gwebgl npm package (project first, CLI fallback). Optional — only needed by @gjsify/webgl users. */
|
|
@@ -367,6 +514,8 @@ const PM_PACKAGES = {
|
|
|
367
514
|
cairo: 'libcairo2-dev',
|
|
368
515
|
gnutls: 'libgnutls28-dev',
|
|
369
516
|
nghttp2: 'libnghttp2-dev',
|
|
517
|
+
'json-glib': 'libjson-glib-dev',
|
|
518
|
+
epoxy: 'libepoxy-dev',
|
|
370
519
|
},
|
|
371
520
|
dnf: {
|
|
372
521
|
gjs: 'gjs',
|
|
@@ -390,6 +539,8 @@ const PM_PACKAGES = {
|
|
|
390
539
|
cairo: 'cairo-devel',
|
|
391
540
|
gnutls: 'gnutls-devel',
|
|
392
541
|
nghttp2: 'libnghttp2-devel',
|
|
542
|
+
'json-glib': 'json-glib-devel',
|
|
543
|
+
epoxy: 'libepoxy-devel',
|
|
393
544
|
},
|
|
394
545
|
pacman: {
|
|
395
546
|
gjs: 'gjs',
|
|
@@ -413,6 +564,8 @@ const PM_PACKAGES = {
|
|
|
413
564
|
cairo: 'cairo',
|
|
414
565
|
gnutls: 'gnutls',
|
|
415
566
|
nghttp2: 'libnghttp2',
|
|
567
|
+
'json-glib': 'json-glib',
|
|
568
|
+
epoxy: 'libepoxy',
|
|
416
569
|
},
|
|
417
570
|
zypper: {
|
|
418
571
|
gjs: 'gjs',
|
|
@@ -436,6 +589,8 @@ const PM_PACKAGES = {
|
|
|
436
589
|
cairo: 'cairo-devel',
|
|
437
590
|
gnutls: 'libgnutls-devel',
|
|
438
591
|
nghttp2: 'libnghttp2-devel',
|
|
592
|
+
'json-glib': 'json-glib-devel',
|
|
593
|
+
epoxy: 'libepoxy-devel',
|
|
439
594
|
},
|
|
440
595
|
apk: {
|
|
441
596
|
gjs: 'gjs',
|
|
@@ -459,6 +614,8 @@ const PM_PACKAGES = {
|
|
|
459
614
|
cairo: 'cairo-dev',
|
|
460
615
|
gnutls: 'gnutls-dev',
|
|
461
616
|
nghttp2: 'nghttp2-dev',
|
|
617
|
+
'json-glib': 'json-glib-dev',
|
|
618
|
+
epoxy: 'libepoxy-dev',
|
|
462
619
|
},
|
|
463
620
|
// macOS. Names verified against homebrew/core rather than inferred from the
|
|
464
621
|
// Linux rows — the mapping is NOT mechanical: Homebrew ships no `-dev`/`-devel`
|
|
@@ -502,6 +659,17 @@ const PM_PACKAGES = {
|
|
|
502
659
|
cairo: 'cairo',
|
|
503
660
|
gnutls: 'gnutls',
|
|
504
661
|
nghttp2: 'nghttp2',
|
|
662
|
+
'json-glib': 'json-glib',
|
|
663
|
+
epoxy: 'libepoxy',
|
|
664
|
+
},
|
|
665
|
+
// Deliberately ONE entry. Windows is not a GJS host, so `gjs`, the
|
|
666
|
+
// pkg-config toolchain and every `-devel` library above have no winget
|
|
667
|
+
// package that would help — offering one would be a wrong answer rather
|
|
668
|
+
// than a missing one, and `buildInstallCommand` already skips any id with
|
|
669
|
+
// no mapping. The Visual C++ runtime is the one win32 system dependency
|
|
670
|
+
// that IS real, installable, and currently undiagnosed (#997).
|
|
671
|
+
winget: {
|
|
672
|
+
'msvc-runtime': 'Microsoft.VCRedist.2015+.x64',
|
|
505
673
|
},
|
|
506
674
|
unknown: {},
|
|
507
675
|
};
|
|
@@ -513,6 +681,9 @@ const PM_INSTALL_PREFIX = {
|
|
|
513
681
|
apk: 'sudo apk add',
|
|
514
682
|
// No `sudo`: Homebrew refuses to run as root and tells you so.
|
|
515
683
|
brew: 'brew install',
|
|
684
|
+
// `--id` because winget's fuzzy name match is ambiguous for this package,
|
|
685
|
+
// and it is the spelling Microsoft's own docs use.
|
|
686
|
+
winget: 'winget install --id',
|
|
516
687
|
unknown: '',
|
|
517
688
|
};
|
|
518
689
|
/**
|
|
@@ -642,7 +642,18 @@ export function buildNativeEnv(packages, opts = {}) {
|
|
|
642
642
|
const systemDirs = (opts.systemGiDirs ?? systemGiLibraryDirs)({ platform, env });
|
|
643
643
|
if (systemDirs.length > 0) {
|
|
644
644
|
const inherited = splitSearchPath(env[DYLD_FALLBACK_VAR]);
|
|
645
|
-
|
|
645
|
+
// Deduplicated, and not for tidiness: `systemGiLibraryDirs()` accepts a
|
|
646
|
+
// libdir stated outright through `GI_TYPELIB_PATH` on directory
|
|
647
|
+
// existence alone, so a host that names `/usr/lib/girepository-1.0`
|
|
648
|
+
// there puts `/usr/lib` in `systemDirs` — and the `/usr/lib` tail below
|
|
649
|
+
// then repeated it. A search path that lists the same directory twice
|
|
650
|
+
// asks the loader to stat it twice for every miss, and it makes the
|
|
651
|
+
// value the `$ …` echo prints read as though the CLI had composed
|
|
652
|
+
// something it did not. First occurrence wins in every loader, so
|
|
653
|
+
// dropping later repeats is behaviour-preserving.
|
|
654
|
+
out[DYLD_FALLBACK_VAR] = [
|
|
655
|
+
...new Set([...systemDirs, ...(inherited.length > 0 ? inherited : ['/usr/lib'])]),
|
|
656
|
+
].join(':');
|
|
646
657
|
}
|
|
647
658
|
return out;
|
|
648
659
|
}
|
|
@@ -7,8 +7,6 @@ export interface ShowcaseInfo {
|
|
|
7
7
|
category: string;
|
|
8
8
|
/** Description for the list view */
|
|
9
9
|
description: string;
|
|
10
|
-
/** Whether the showcase needs the gwebgl native prebuild. */
|
|
11
|
-
needsWebgl: boolean;
|
|
12
10
|
}
|
|
13
11
|
/**
|
|
14
12
|
* Read the curated showcase list from `showcases.json`. Returns showcases
|
|
@@ -43,7 +43,6 @@ export function discoverShowcases() {
|
|
|
43
43
|
packageName: e.package,
|
|
44
44
|
category: e.category,
|
|
45
45
|
description: e.description ?? '',
|
|
46
|
-
needsWebgl: Boolean(e.needsWebgl),
|
|
47
46
|
}));
|
|
48
47
|
showcases.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
49
48
|
return showcases;
|
package/lib/utils/dlx-cache.d.ts
CHANGED
|
@@ -54,8 +54,28 @@ export declare function getValidCachedPkg(cacheDir: string, maxAgeMinutes?: numb
|
|
|
54
54
|
export declare function symlinkSwap(cacheDir: string, prepareDir: string, opts?: {
|
|
55
55
|
junctions?: boolean;
|
|
56
56
|
}): string;
|
|
57
|
-
/**
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Remove `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`.
|
|
59
|
+
*
|
|
60
|
+
* Every `gjsify dlx` miss creates a fresh prepare dir and swaps the `pkg` link
|
|
61
|
+
* onto it; the previous one is then unreferenced and stayed on disk forever,
|
|
62
|
+
* because this function accepted a TTL and did nothing with it. A full npm tree
|
|
63
|
+
* per miss makes that the largest thing gjsify writes to a user's cache.
|
|
64
|
+
*
|
|
65
|
+
* Three rules keep it from deleting something it should not:
|
|
66
|
+
*
|
|
67
|
+
* 1. **Only names `makePrepareDir` could have produced** (`<hex>-<hex>`). The
|
|
68
|
+
* cache dir also holds `pkg` and, briefly, `pkg.tmp-<ts>-<pid>` links from
|
|
69
|
+
* a concurrent swap — and a name-shape guard is what makes "delete the
|
|
70
|
+
* rest" safe against anything a future version puts here.
|
|
71
|
+
* 2. **Never the LIVE target**, whatever its age. A long-lived cache entry
|
|
72
|
+
* that is still current is exactly the one worth keeping, and it ages past
|
|
73
|
+
* any TTL while being used on every run.
|
|
74
|
+
* 3. **Never throws.** This is housekeeping on a best-effort path: a losing
|
|
75
|
+
* race (a parallel dlx removing the same dir) or a read-only cache must not
|
|
76
|
+
* fail the command the user actually ran.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cleanupStalePrepareDirs(cacheDir: string, maxAgeMinutes?: number): void;
|
|
59
79
|
/** Resolve absolute path to the installed package's directory inside cache. */
|
|
60
80
|
export declare function resolveInstalledPkgDir(cachedRoot: string, pkgName: string): string;
|
|
61
81
|
export {};
|