@gjsify/cli 0.28.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 +76 -61
- package/lib/commands/format.js +17 -8
- package/lib/commands/install.d.ts +27 -0
- package/lib/commands/install.js +25 -8
- package/lib/commands/system-check.js +22 -2
- package/lib/config.js +1 -1
- package/lib/utils/bin-shim.d.ts +65 -0
- package/lib/utils/bin-shim.js +115 -13
- package/lib/utils/check-system-deps.d.ts +56 -0
- package/lib/utils/check-system-deps.js +159 -5
- package/lib/utils/detect-native-packages.d.ts +45 -1
- package/lib/utils/detect-native-packages.js +56 -0
- package/lib/utils/dlx-cache.js +1 -1
- package/lib/utils/install-backend-native.d.ts +87 -1
- package/lib/utils/install-backend-native.js +147 -34
- 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/lib/utils/system-gi.d.ts +71 -0
- package/lib/utils/system-gi.js +221 -0
- package/package.json +17 -17
package/lib/commands/format.js
CHANGED
|
@@ -11,6 +11,15 @@
|
|
|
11
11
|
// toolchain handled is intentionally DROPPED in the oxc migration — no other
|
|
12
12
|
// formatter is wired up for those file types.
|
|
13
13
|
//
|
|
14
|
+
// A BARE `gjsify format` WRITES. `--check` is the read-only CI mode and
|
|
15
|
+
// `--no-write` the read-only local one; there is no flagless report mode.
|
|
16
|
+
// This is what every other formatter in the ecosystem does with its bare
|
|
17
|
+
// invocation (oxfmt itself, `cargo fmt`, `gofmt -w`-less `go fmt`), it is what
|
|
18
|
+
// the sibling `gjsify fix` already did, and the previous report-only default
|
|
19
|
+
// silently did nothing in a loop: `gjsify format` was run repeatedly against
|
|
20
|
+
// two unformatted files while `--check` kept flagging them, because the
|
|
21
|
+
// command that looks like the fix was the diagnosis.
|
|
22
|
+
//
|
|
14
23
|
// `--init` writes recommended `.oxlintrc.json` + `.oxfmtrc.json` templates
|
|
15
24
|
// tuned for GJS/GNOME projects (4-space, single-quote, printWidth 120,
|
|
16
25
|
// trailing-comma all, semicolons always, arrow parens always).
|
|
@@ -28,9 +37,9 @@ export const formatCommand = {
|
|
|
28
37
|
array: true,
|
|
29
38
|
})
|
|
30
39
|
.option('write', {
|
|
31
|
-
description: 'Modify files in place (default: report drift
|
|
40
|
+
description: 'Modify files in place (default: true). Pass --no-write to report drift instead.',
|
|
32
41
|
type: 'boolean',
|
|
33
|
-
default:
|
|
42
|
+
default: true,
|
|
34
43
|
})
|
|
35
44
|
.option('check', {
|
|
36
45
|
description: 'Report formatting drift without modifying files; exit non-zero if any file is unformatted. Useful for CI.',
|
|
@@ -69,13 +78,13 @@ export const formatCommand = {
|
|
|
69
78
|
if (args.check) {
|
|
70
79
|
oxfmtArgs.push('--check');
|
|
71
80
|
}
|
|
72
|
-
else if (args.write) {
|
|
73
|
-
|
|
81
|
+
else if (args.write === false) {
|
|
82
|
+
// `--no-write` — report drift without modifying files. oxfmt's own
|
|
83
|
+
// bare default is `--write` too, so this branch must be explicit.
|
|
84
|
+
oxfmtArgs.push('--list-different');
|
|
74
85
|
}
|
|
75
86
|
else {
|
|
76
|
-
|
|
77
|
-
// default is `--write`, so be explicit with `--list-different`.
|
|
78
|
-
oxfmtArgs.push('--list-different');
|
|
87
|
+
oxfmtArgs.push('--write');
|
|
79
88
|
}
|
|
80
89
|
const configPath = args.configPath ?? findOxfmtConfig(cwd) ?? undefined;
|
|
81
90
|
if (configPath)
|
|
@@ -107,5 +116,5 @@ function handleInit({ cwd, force }) {
|
|
|
107
116
|
};
|
|
108
117
|
writeOne('.oxlintrc.json', loadOxlintTemplate());
|
|
109
118
|
writeOne('.oxfmtrc.json', loadOxfmtTemplate());
|
|
110
|
-
console.log('[gjsify format] Run `gjsify format
|
|
119
|
+
console.log('[gjsify format] Run `gjsify format` to apply the formatter to the project.');
|
|
111
120
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from '../types/index.js';
|
|
2
|
+
import { type PackageJson } from '../utils/pkg-json-edit.js';
|
|
2
3
|
interface InstallOptions {
|
|
3
4
|
packages?: string[];
|
|
4
5
|
global?: boolean;
|
|
@@ -18,6 +19,32 @@ interface InstallOptions {
|
|
|
18
19
|
force?: boolean;
|
|
19
20
|
}
|
|
20
21
|
export declare const installCommand: Command<unknown, InstallOptions>;
|
|
22
|
+
/**
|
|
23
|
+
* Which dependency NAMES are optional across the given manifests.
|
|
24
|
+
*
|
|
25
|
+
* The specs handed to the install backend are flat `"<name>@<range>"` strings —
|
|
26
|
+
* `projectSpecsFromPackageJson` merges the three dependency blocks — so the kind
|
|
27
|
+
* has to travel beside them. It matters for exactly one decision: an
|
|
28
|
+
* incompatible `os`/`cpu`/`libc` skips an OPTIONAL dependency and fails a
|
|
29
|
+
* REQUIRED one (see `NativeInstallOptions.optionalSpecs`).
|
|
30
|
+
*
|
|
31
|
+
* REQUIRED WINS ACROSS MANIFESTS, OPTIONAL WINS WITHIN ONE — and the two halves
|
|
32
|
+
* are different rules for different reasons, so neither is a special case of the
|
|
33
|
+
* other:
|
|
34
|
+
* - Across manifests: in a workspace the same package can be a plain dependency
|
|
35
|
+
* of one member and an optionalDependency of another. Those are two real
|
|
36
|
+
* edges; if one of them is not allowed to be missing, the install is not
|
|
37
|
+
* allowed to silently miss it. Subtracting rather than "last block seen" also
|
|
38
|
+
* makes the answer independent of manifest iteration order.
|
|
39
|
+
* - Within one manifest: a name in BOTH blocks is OPTIONAL, because npm says so
|
|
40
|
+
* ("entries in optionalDependencies will override entries of the same name in
|
|
41
|
+
* dependencies") — there is only ONE edge and the optional block names its
|
|
42
|
+
* kind. Treating it as required is the root-edge twin of the transitive defect
|
|
43
|
+
* `requiredDepEntries` fixes: `optionalDependencies: { fsevents }` alongside a
|
|
44
|
+
* `dependencies: { fsevents }` line would fail every Linux install with
|
|
45
|
+
* EBADPLATFORM over a package npm never installs there.
|
|
46
|
+
*/
|
|
47
|
+
export declare function optionalDependencyNames(manifests: readonly PackageJson[]): Set<string>;
|
|
21
48
|
export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string, scanRoot?: string): string;
|
|
22
49
|
/**
|
|
23
50
|
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
package/lib/commands/install.js
CHANGED
|
@@ -446,21 +446,38 @@ function isWorkspaceRoot(cwd) {
|
|
|
446
446
|
* incompatible `os`/`cpu`/`libc` skips an OPTIONAL dependency and fails a
|
|
447
447
|
* REQUIRED one (see `NativeInstallOptions.optionalSpecs`).
|
|
448
448
|
*
|
|
449
|
-
* REQUIRED WINS
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
449
|
+
* REQUIRED WINS ACROSS MANIFESTS, OPTIONAL WINS WITHIN ONE — and the two halves
|
|
450
|
+
* are different rules for different reasons, so neither is a special case of the
|
|
451
|
+
* other:
|
|
452
|
+
* - Across manifests: in a workspace the same package can be a plain dependency
|
|
453
|
+
* of one member and an optionalDependency of another. Those are two real
|
|
454
|
+
* edges; if one of them is not allowed to be missing, the install is not
|
|
455
|
+
* allowed to silently miss it. Subtracting rather than "last block seen" also
|
|
456
|
+
* makes the answer independent of manifest iteration order.
|
|
457
|
+
* - Within one manifest: a name in BOTH blocks is OPTIONAL, because npm says so
|
|
458
|
+
* ("entries in optionalDependencies will override entries of the same name in
|
|
459
|
+
* dependencies") — there is only ONE edge and the optional block names its
|
|
460
|
+
* kind. Treating it as required is the root-edge twin of the transitive defect
|
|
461
|
+
* `requiredDepEntries` fixes: `optionalDependencies: { fsevents }` alongside a
|
|
462
|
+
* `dependencies: { fsevents }` line would fail every Linux install with
|
|
463
|
+
* EBADPLATFORM over a package npm never installs there.
|
|
454
464
|
*/
|
|
455
|
-
function optionalDependencyNames(manifests) {
|
|
465
|
+
export function optionalDependencyNames(manifests) {
|
|
456
466
|
const optional = new Set();
|
|
457
467
|
const required = new Set();
|
|
458
468
|
for (const manifest of manifests) {
|
|
459
|
-
|
|
469
|
+
const optionalHere = new Set(Object.keys(manifest.optionalDependencies ?? {}));
|
|
470
|
+
for (const name of optionalHere)
|
|
460
471
|
optional.add(name);
|
|
461
472
|
for (const kind of ['dependencies', 'devDependencies']) {
|
|
462
|
-
for (const name of Object.keys(manifest[kind] ?? {}))
|
|
473
|
+
for (const name of Object.keys(manifest[kind] ?? {})) {
|
|
474
|
+
// Overridden by THIS manifest's own optional block — not a
|
|
475
|
+
// required edge, so it must not veto another manifest's optional
|
|
476
|
+
// declaration either.
|
|
477
|
+
if (optionalHere.has(name))
|
|
478
|
+
continue;
|
|
463
479
|
required.add(name);
|
|
480
|
+
}
|
|
464
481
|
}
|
|
465
482
|
}
|
|
466
483
|
for (const name of required)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runAllChecks, detectPackageManager, buildInstallCommand } from '../utils/check-system-deps.js';
|
|
1
|
+
import { runAllChecks, detectPackageManager, buildInstallCommand, checkTypeSkew } from '../utils/check-system-deps.js';
|
|
2
2
|
export const systemCheckCommand = {
|
|
3
3
|
command: 'system-check',
|
|
4
4
|
description: 'Check that required system dependencies (GJS, GTK4, libsoup3, …) are installed. Optional dependencies are detected only when their @gjsify/* package is in your project. (Previously called `gjsify check`; the bare name now runs TypeScript checks across the workspace — see `gjsify check --help`.)',
|
|
@@ -12,11 +12,12 @@ export const systemCheckCommand = {
|
|
|
12
12
|
handler: async (args) => {
|
|
13
13
|
const results = runAllChecks(process.cwd());
|
|
14
14
|
const pm = detectPackageManager();
|
|
15
|
+
const typeSkew = checkTypeSkew(process.cwd());
|
|
15
16
|
const missingRequired = results.filter((r) => !r.found && r.severity === 'required');
|
|
16
17
|
const missingOptional = results.filter((r) => !r.found && r.severity === 'optional');
|
|
17
18
|
const allMissing = [...missingRequired, ...missingOptional];
|
|
18
19
|
if (args.json) {
|
|
19
|
-
console.log(JSON.stringify({ packageManager: pm, deps: results }, null, 2));
|
|
20
|
+
console.log(JSON.stringify({ packageManager: pm, deps: results, typeSkew }, null, 2));
|
|
20
21
|
// Only required deps influence the exit code.
|
|
21
22
|
return process.exit(missingRequired.length > 0 ? 1 : 0);
|
|
22
23
|
}
|
|
@@ -41,6 +42,25 @@ export const systemCheckCommand = {
|
|
|
41
42
|
console.log(` ${icon} ${dep.name}${ver}${requiredBy}`);
|
|
42
43
|
}
|
|
43
44
|
}
|
|
45
|
+
// Printed BEFORE the all-found early return: a version skew is invisible to
|
|
46
|
+
// the presence checks by construction (both sides are installed and both
|
|
47
|
+
// report a version), so "All dependencies found." is exactly the message it
|
|
48
|
+
// would otherwise hide behind.
|
|
49
|
+
if (typeSkew.length > 0) {
|
|
50
|
+
console.log('\nType/runtime version skew:');
|
|
51
|
+
for (const skew of typeSkew) {
|
|
52
|
+
const arrow = skew.relation === 'ahead' ? '⚠' : 'ℹ';
|
|
53
|
+
console.log(` ${arrow} ${skew.typePackage} describes ${skew.declared}, ` +
|
|
54
|
+
`installed ${skew.pkgConfig} is ${skew.installed}`);
|
|
55
|
+
}
|
|
56
|
+
if (typeSkew.some((skew) => skew.relation === 'ahead')) {
|
|
57
|
+
console.log('\n ⚠ types are AHEAD of the installed library: code using newer API\n' +
|
|
58
|
+
' type-checks and then fails at runtime with an undefined property.\n' +
|
|
59
|
+
' Either install the matching library version, or pin the older\n' +
|
|
60
|
+
' @girs/* release. GIRepository cannot detect this — it matches the\n' +
|
|
61
|
+
' API version (e.g. 4.0) only, so any 4.x typelib satisfies gi://Gtk.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
44
64
|
console.log(`\nPackage manager: ${pm}`);
|
|
45
65
|
if (allMissing.length === 0) {
|
|
46
66
|
console.log('\nAll dependencies found.');
|
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;
|
|
@@ -119,6 +130,32 @@ export declare function buildLauncherShims(opts: {
|
|
|
119
130
|
* script chain: the self-relative `@loader_path` rpath that
|
|
120
131
|
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
121
132
|
*
|
|
133
|
+
* NOT EXTENDED WITH THE HOST'S GI LIBDIRS — a decision, not an oversight.
|
|
134
|
+
* `buildNativeEnv()` now puts `systemGiLibraryDirs()` on the gjs CHILD's
|
|
135
|
+
* `DYLD_FALLBACK_LIBRARY_PATH`, which repairs every command that goes through this
|
|
136
|
+
* CLI. A launcher that `exec`s a GJS bundle DIRECTLY — `gjsify install -g` on a
|
|
137
|
+
* package whose `gjsify.bin` is a GTK app — has no CLI in the loop and still
|
|
138
|
+
* carries the gap.
|
|
139
|
+
*
|
|
140
|
+
* The MECHANISM would work, and that was measured on the macOS 15.7.8 VM rather
|
|
141
|
+
* than assumed. It is a finer point than the caveat above: what SIP strips is an
|
|
142
|
+
* INHERITED `DYLD_*` at the `/bin/sh` exec, so a value this preamble exports
|
|
143
|
+
* ITSELF survives the following `exec gjs` (an unprotected Homebrew binary) and
|
|
144
|
+
* `Gtk.init()` then succeeds. What is missing is a shape worth having:
|
|
145
|
+
*
|
|
146
|
+
* - BAKING `systemGiLibraryDirs()` in at write time reintroduces precisely the
|
|
147
|
+
* snapshot this function exists to remove — a launcher is routinely written
|
|
148
|
+
* BEFORE `brew install gtk4`, and would bake nothing while saying nothing.
|
|
149
|
+
* - RE-DERIVING it in shell means a THIRD copy of a rule that already has two,
|
|
150
|
+
* pinned in agreement by `system-gi.spec.ts` — and shell can express only one
|
|
151
|
+
* of its three sources (the probed prefixes; `pkg-config` would be a spawn on
|
|
152
|
+
* every launch), so that copy would be knowingly partial, in the one language
|
|
153
|
+
* nothing here type-checks.
|
|
154
|
+
*
|
|
155
|
+
* So the gap is recorded in `status/open-todos.md` with the measurement attached
|
|
156
|
+
* instead of being closed by a mechanism that drifts. Closing it properly means
|
|
157
|
+
* teaching such a launcher to defer to the CLI, not to re-derive.
|
|
158
|
+
*
|
|
122
159
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
123
160
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
124
161
|
* are embedded (entries inside it are dropped — the scan finds them).
|
|
@@ -156,6 +193,26 @@ export declare function normalizeBinMap(pkgName: string, bin: string | Record<st
|
|
|
156
193
|
*
|
|
157
194
|
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
158
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.
|
|
159
216
|
*/
|
|
160
217
|
export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
161
218
|
bin?: string | Record<string, string>;
|
|
@@ -165,6 +222,7 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
165
222
|
}): {
|
|
166
223
|
map: Map<string, string>;
|
|
167
224
|
isGjsBin: boolean;
|
|
225
|
+
nodeFallback: Map<string, string> | null;
|
|
168
226
|
} | null;
|
|
169
227
|
/**
|
|
170
228
|
* The POSIX `sh` launcher gjsify writes for a bin it owns.
|
|
@@ -179,10 +237,17 @@ export declare function pickBinMap(pkgName: string, pkgJson: {
|
|
|
179
237
|
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
180
238
|
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
181
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.
|
|
182
246
|
*/
|
|
183
247
|
export declare function buildShLauncher(targetAbs: string, opts: {
|
|
184
248
|
envPreamble?: string;
|
|
185
249
|
isGjsBundle: boolean;
|
|
250
|
+
nodeFallbackAbs?: string;
|
|
186
251
|
}): string;
|
|
187
252
|
/** True when a bin target is a GJS-runnable bundle rather than a Node script. */
|
|
188
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. */
|
|
@@ -303,6 +340,32 @@ function shQuote(s) {
|
|
|
303
340
|
* script chain: the self-relative `@loader_path` rpath that
|
|
304
341
|
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
305
342
|
*
|
|
343
|
+
* NOT EXTENDED WITH THE HOST'S GI LIBDIRS — a decision, not an oversight.
|
|
344
|
+
* `buildNativeEnv()` now puts `systemGiLibraryDirs()` on the gjs CHILD's
|
|
345
|
+
* `DYLD_FALLBACK_LIBRARY_PATH`, which repairs every command that goes through this
|
|
346
|
+
* CLI. A launcher that `exec`s a GJS bundle DIRECTLY — `gjsify install -g` on a
|
|
347
|
+
* package whose `gjsify.bin` is a GTK app — has no CLI in the loop and still
|
|
348
|
+
* carries the gap.
|
|
349
|
+
*
|
|
350
|
+
* The MECHANISM would work, and that was measured on the macOS 15.7.8 VM rather
|
|
351
|
+
* than assumed. It is a finer point than the caveat above: what SIP strips is an
|
|
352
|
+
* INHERITED `DYLD_*` at the `/bin/sh` exec, so a value this preamble exports
|
|
353
|
+
* ITSELF survives the following `exec gjs` (an unprotected Homebrew binary) and
|
|
354
|
+
* `Gtk.init()` then succeeds. What is missing is a shape worth having:
|
|
355
|
+
*
|
|
356
|
+
* - BAKING `systemGiLibraryDirs()` in at write time reintroduces precisely the
|
|
357
|
+
* snapshot this function exists to remove — a launcher is routinely written
|
|
358
|
+
* BEFORE `brew install gtk4`, and would bake nothing while saying nothing.
|
|
359
|
+
* - RE-DERIVING it in shell means a THIRD copy of a rule that already has two,
|
|
360
|
+
* pinned in agreement by `system-gi.spec.ts` — and shell can express only one
|
|
361
|
+
* of its three sources (the probed prefixes; `pkg-config` would be a spawn on
|
|
362
|
+
* every launch), so that copy would be knowingly partial, in the one language
|
|
363
|
+
* nothing here type-checks.
|
|
364
|
+
*
|
|
365
|
+
* So the gap is recorded in `status/open-todos.md` with the measurement attached
|
|
366
|
+
* instead of being closed by a mechanism that drifts. Closing it properly means
|
|
367
|
+
* teaching such a launcher to defer to the CLI, not to re-derive.
|
|
368
|
+
*
|
|
306
369
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
307
370
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
308
371
|
* are embedded (entries inside it are dropped — the scan finds them).
|
|
@@ -411,14 +474,39 @@ export function normalizeBinMap(pkgName, bin) {
|
|
|
411
474
|
*
|
|
412
475
|
* `isGjsBin` lets the caller keep plain-Node packages on the plain symlink path
|
|
413
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.
|
|
414
497
|
*/
|
|
415
498
|
export function pickBinMap(pkgName, pkgJson) {
|
|
416
499
|
const gjsifyBin = pkgJson.gjsify?.bin;
|
|
417
|
-
if (gjsifyBin !== undefined)
|
|
418
|
-
return { map: normalizeBinMap(pkgName, gjsifyBin), isGjsBin: true };
|
|
419
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
|
+
}
|
|
420
508
|
if (npmBin !== undefined)
|
|
421
|
-
return { map: normalizeBinMap(pkgName, npmBin), isGjsBin: false };
|
|
509
|
+
return { map: normalizeBinMap(pkgName, npmBin), isGjsBin: false, nodeFallback: null };
|
|
422
510
|
return null;
|
|
423
511
|
}
|
|
424
512
|
/**
|
|
@@ -434,9 +522,23 @@ export function pickBinMap(pkgName, pkgJson) {
|
|
|
434
522
|
* because not every published bundle carries a `#!/usr/bin/env -S gjs -m` line,
|
|
435
523
|
* and direct-exec'ing a shebang-less `.mjs` falls through to `/bin/sh`, which
|
|
436
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.
|
|
437
531
|
*/
|
|
438
532
|
export function buildShLauncher(targetAbs, opts) {
|
|
439
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
|
+
}
|
|
440
542
|
const exec = opts.isGjsBundle ? `exec gjs -m ${shQuote(targetAbs)} "$@"` : `exec ${shQuote(targetAbs)} "$@"`;
|
|
441
543
|
return `#!/bin/sh\n${preamble}${exec}\n`;
|
|
442
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
|
*
|
|
@@ -52,3 +73,38 @@ export declare function checkGwebgl(cwd: string): DepCheck;
|
|
|
52
73
|
* Returns null when package manager is unknown or no installable deps are missing.
|
|
53
74
|
*/
|
|
54
75
|
export declare function buildInstallCommand(pm: PackageManager, missing: DepCheck[]): string | null;
|
|
76
|
+
export interface TypeSkew {
|
|
77
|
+
typePackage: string;
|
|
78
|
+
pkgConfig: string;
|
|
79
|
+
/** `libraryVersion` declared by the installed `@girs/*` package. */
|
|
80
|
+
declared: string;
|
|
81
|
+
/** `pkg-config --modversion` of the library the host actually has. */
|
|
82
|
+
installed: string;
|
|
83
|
+
/**
|
|
84
|
+
* `ahead` = the types describe a NEWER library than is installed. This is the
|
|
85
|
+
* direction that compiles and then throws, so it is the one worth reporting.
|
|
86
|
+
* `behind` = API the host has is invisible to the compiler; limiting, not wrong.
|
|
87
|
+
*/
|
|
88
|
+
relation: 'ahead' | 'behind';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Readers the comparison depends on, injectable so every branch is unit-testable
|
|
92
|
+
* from ANY host. Same reasoning as `resolvePrebuildDirName`, which is a pure
|
|
93
|
+
* function of injected values precisely so its darwin/win32 branches are tested
|
|
94
|
+
* from Linux: a check whose only test is "run it on the maintainer's machine and
|
|
95
|
+
* look" is a check whose skew cases are never exercised.
|
|
96
|
+
*/
|
|
97
|
+
export interface TypeSkewReaders {
|
|
98
|
+
/** `libraryVersion` of an installed `@girs/*` package, or null when absent. */
|
|
99
|
+
readDeclared?: (cwd: string, typePackage: string) => string | null;
|
|
100
|
+
/** `pkg-config --modversion` of a library, or null when absent. */
|
|
101
|
+
readInstalled?: (pkgConfig: string) => string | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Compare each installed `@girs/*` package's declared upstream version against the
|
|
105
|
+
* library the host actually has. Only reports a `major.minor` difference: a micro
|
|
106
|
+
* difference does not change the API set, and reporting it would train people to
|
|
107
|
+
* ignore the output.
|
|
108
|
+
*/
|
|
109
|
+
export declare function checkTypeSkew(cwd: string, readers?: TypeSkewReaders): TypeSkew[];
|
|
110
|
+
export {};
|