@gjsify/cli 0.27.1 → 0.29.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 +69 -61
- package/lib/commands/debug.d.ts +1 -0
- package/lib/commands/debug.js +20 -7
- 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/utils/bin-shim.d.ts +26 -0
- package/lib/utils/bin-shim.js +26 -0
- package/lib/utils/check-system-deps.d.ts +34 -0
- package/lib/utils/check-system-deps.js +121 -1
- package/lib/utils/detect-native-packages.d.ts +45 -1
- package/lib/utils/detect-native-packages.js +56 -0
- package/lib/utils/install-backend-native.d.ts +79 -1
- package/lib/utils/install-backend-native.js +139 -32
- 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/debug.d.ts
CHANGED
package/lib/commands/debug.js
CHANGED
|
@@ -43,26 +43,32 @@ function deriveBrowserAppId(name) {
|
|
|
43
43
|
function hasDep(pkg, name) {
|
|
44
44
|
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
45
45
|
}
|
|
46
|
-
function generateEntry(profile, busNameBase, name, version) {
|
|
46
|
+
function generateEntry(profile, busNameBase, name, version, address) {
|
|
47
|
+
const base = JSON.stringify(busNameBase);
|
|
48
|
+
// Only emit the option when an address was actually given: an `address:
|
|
49
|
+
// undefined` in the generated entry would read like a decision, when the
|
|
50
|
+
// bridge's own precedence (env var → published address file → session bus)
|
|
51
|
+
// is what should apply.
|
|
52
|
+
const options = address ? `, { address: ${JSON.stringify(address)} }` : '';
|
|
47
53
|
if (profile === 'storybook') {
|
|
48
54
|
return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
|
|
49
55
|
import { runDevtoolsMcp, storybookProfile } from '@gjsify/devtools-mcp';
|
|
50
56
|
|
|
51
|
-
await runDevtoolsMcp(storybookProfile(${
|
|
57
|
+
await runDevtoolsMcp(storybookProfile(${base}${options}));
|
|
52
58
|
`;
|
|
53
59
|
}
|
|
54
60
|
if (profile === 'browser') {
|
|
55
61
|
return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
|
|
56
62
|
import { runDevtoolsMcp, browserProfile } from '@gjsify/devtools-mcp';
|
|
57
63
|
|
|
58
|
-
await runDevtoolsMcp(browserProfile(${
|
|
64
|
+
await runDevtoolsMcp(browserProfile(${base}${options}));
|
|
59
65
|
`;
|
|
60
66
|
}
|
|
61
67
|
if (profile === 'cdp') {
|
|
62
68
|
return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
|
|
63
69
|
import { runDevtoolsMcp, cdpProfile } from '@gjsify/devtools-mcp';
|
|
64
70
|
|
|
65
|
-
await runDevtoolsMcp(cdpProfile(${
|
|
71
|
+
await runDevtoolsMcp(cdpProfile(${base}${options}));
|
|
66
72
|
`;
|
|
67
73
|
}
|
|
68
74
|
return `// Auto-generated by \`gjsify debug\` — do not edit by hand.
|
|
@@ -71,7 +77,7 @@ import { runDevtoolsMcp } from '@gjsify/devtools-mcp';
|
|
|
71
77
|
await runDevtoolsMcp({
|
|
72
78
|
name: ${JSON.stringify(name)},
|
|
73
79
|
version: ${JSON.stringify(version)},
|
|
74
|
-
busNameBase: ${JSON.stringify(
|
|
80
|
+
busNameBase: ${base},${address ? `\n address: ${JSON.stringify(address)},` : ''}
|
|
75
81
|
});
|
|
76
82
|
`;
|
|
77
83
|
}
|
|
@@ -82,6 +88,12 @@ export const debugCommand = {
|
|
|
82
88
|
.option('bus-name', {
|
|
83
89
|
description: 'App DBus base name (default: gjsify.devtools.busNameBase, or the storybook app-id)',
|
|
84
90
|
type: 'string',
|
|
91
|
+
})
|
|
92
|
+
.option('address', {
|
|
93
|
+
description: 'Peer D-Bus address of the app (unix:path=… / nonce-tcp:…) instead of the session bus — for ' +
|
|
94
|
+
'macOS/Windows, which have no session bus. Default: GJSIFY_DEVTOOLS_ADDRESS, then the address ' +
|
|
95
|
+
'file the app publishes, then the session bus',
|
|
96
|
+
type: 'string',
|
|
85
97
|
})
|
|
86
98
|
.option('profile', {
|
|
87
99
|
description: 'Tool profile: generic | storybook | browser | cdp (default: auto — storybook if @gjsify/storybook is a dep, browser if @gjsify/devtools-browser is, cdp if @gjsify/devtools-cdp is, else generic)',
|
|
@@ -133,9 +145,10 @@ export const debugCommand = {
|
|
|
133
145
|
mkdirSync(cacheDir, { recursive: true });
|
|
134
146
|
const entryPath = join(cacheDir, 'entry.ts');
|
|
135
147
|
const outPath = args.out ? resolve(cwd, args.out) : join(cacheDir, 'bridge.gjs.mjs');
|
|
136
|
-
|
|
148
|
+
const address = args.address ?? config.address;
|
|
149
|
+
writeFileSync(entryPath, generateEntry(profile, busNameBase, name, version, address));
|
|
137
150
|
// stderr only — stdout is the MCP JSON-RPC channel.
|
|
138
|
-
console.error(`[gjsify debug] profile=${profile} bus=${busNameBase}`);
|
|
151
|
+
console.error(`[gjsify debug] profile=${profile} bus=${busNameBase}${address ? ` address=${address}` : ''}`);
|
|
139
152
|
// In-process build (runtime-agnostic), then launch with stdio inherited
|
|
140
153
|
// so the bridge's JSON-RPC stdin/stdout are the client's pipes.
|
|
141
154
|
const { runCli } = await import('../cli-app.js');
|
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/utils/bin-shim.d.ts
CHANGED
|
@@ -119,6 +119,32 @@ export declare function buildLauncherShims(opts: {
|
|
|
119
119
|
* script chain: the self-relative `@loader_path` rpath that
|
|
120
120
|
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
121
121
|
*
|
|
122
|
+
* NOT EXTENDED WITH THE HOST'S GI LIBDIRS — a decision, not an oversight.
|
|
123
|
+
* `buildNativeEnv()` now puts `systemGiLibraryDirs()` on the gjs CHILD's
|
|
124
|
+
* `DYLD_FALLBACK_LIBRARY_PATH`, which repairs every command that goes through this
|
|
125
|
+
* CLI. A launcher that `exec`s a GJS bundle DIRECTLY — `gjsify install -g` on a
|
|
126
|
+
* package whose `gjsify.bin` is a GTK app — has no CLI in the loop and still
|
|
127
|
+
* carries the gap.
|
|
128
|
+
*
|
|
129
|
+
* The MECHANISM would work, and that was measured on the macOS 15.7.8 VM rather
|
|
130
|
+
* than assumed. It is a finer point than the caveat above: what SIP strips is an
|
|
131
|
+
* INHERITED `DYLD_*` at the `/bin/sh` exec, so a value this preamble exports
|
|
132
|
+
* ITSELF survives the following `exec gjs` (an unprotected Homebrew binary) and
|
|
133
|
+
* `Gtk.init()` then succeeds. What is missing is a shape worth having:
|
|
134
|
+
*
|
|
135
|
+
* - BAKING `systemGiLibraryDirs()` in at write time reintroduces precisely the
|
|
136
|
+
* snapshot this function exists to remove — a launcher is routinely written
|
|
137
|
+
* BEFORE `brew install gtk4`, and would bake nothing while saying nothing.
|
|
138
|
+
* - RE-DERIVING it in shell means a THIRD copy of a rule that already has two,
|
|
139
|
+
* pinned in agreement by `system-gi.spec.ts` — and shell can express only one
|
|
140
|
+
* of its three sources (the probed prefixes; `pkg-config` would be a spawn on
|
|
141
|
+
* every launch), so that copy would be knowingly partial, in the one language
|
|
142
|
+
* nothing here type-checks.
|
|
143
|
+
*
|
|
144
|
+
* So the gap is recorded in `status/open-todos.md` with the measurement attached
|
|
145
|
+
* instead of being closed by a mechanism that drifts. Closing it properly means
|
|
146
|
+
* teaching such a launcher to defer to the CLI, not to re-derive.
|
|
147
|
+
*
|
|
122
148
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
123
149
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
124
150
|
* are embedded (entries inside it are dropped — the scan finds them).
|
package/lib/utils/bin-shim.js
CHANGED
|
@@ -303,6 +303,32 @@ function shQuote(s) {
|
|
|
303
303
|
* script chain: the self-relative `@loader_path` rpath that
|
|
304
304
|
* `check-prebuild-loader-path.mjs` enforces is what actually has to carry it.
|
|
305
305
|
*
|
|
306
|
+
* NOT EXTENDED WITH THE HOST'S GI LIBDIRS — a decision, not an oversight.
|
|
307
|
+
* `buildNativeEnv()` now puts `systemGiLibraryDirs()` on the gjs CHILD's
|
|
308
|
+
* `DYLD_FALLBACK_LIBRARY_PATH`, which repairs every command that goes through this
|
|
309
|
+
* CLI. A launcher that `exec`s a GJS bundle DIRECTLY — `gjsify install -g` on a
|
|
310
|
+
* package whose `gjsify.bin` is a GTK app — has no CLI in the loop and still
|
|
311
|
+
* carries the gap.
|
|
312
|
+
*
|
|
313
|
+
* The MECHANISM would work, and that was measured on the macOS 15.7.8 VM rather
|
|
314
|
+
* than assumed. It is a finer point than the caveat above: what SIP strips is an
|
|
315
|
+
* INHERITED `DYLD_*` at the `/bin/sh` exec, so a value this preamble exports
|
|
316
|
+
* ITSELF survives the following `exec gjs` (an unprotected Homebrew binary) and
|
|
317
|
+
* `Gtk.init()` then succeeds. What is missing is a shape worth having:
|
|
318
|
+
*
|
|
319
|
+
* - BAKING `systemGiLibraryDirs()` in at write time reintroduces precisely the
|
|
320
|
+
* snapshot this function exists to remove — a launcher is routinely written
|
|
321
|
+
* BEFORE `brew install gtk4`, and would bake nothing while saying nothing.
|
|
322
|
+
* - RE-DERIVING it in shell means a THIRD copy of a rule that already has two,
|
|
323
|
+
* pinned in agreement by `system-gi.spec.ts` — and shell can express only one
|
|
324
|
+
* of its three sources (the probed prefixes; `pkg-config` would be a spawn on
|
|
325
|
+
* every launch), so that copy would be knowingly partial, in the one language
|
|
326
|
+
* nothing here type-checks.
|
|
327
|
+
*
|
|
328
|
+
* So the gap is recorded in `status/open-todos.md` with the measurement attached
|
|
329
|
+
* instead of being closed by a mechanism that drifts. Closing it properly means
|
|
330
|
+
* teaching such a launcher to defer to the CLI, not to re-derive.
|
|
331
|
+
*
|
|
306
332
|
* @param scanRoot Directory whose `node_modules` is globbed at launch time.
|
|
307
333
|
* @param bakedDirs Prebuild dirs found at write time; entries outside `scanRoot`
|
|
308
334
|
* are embedded (entries inside it are dropped — the scan finds them).
|
|
@@ -52,3 +52,37 @@ export declare function checkGwebgl(cwd: string): DepCheck;
|
|
|
52
52
|
* Returns null when package manager is unknown or no installable deps are missing.
|
|
53
53
|
*/
|
|
54
54
|
export declare function buildInstallCommand(pm: PackageManager, missing: DepCheck[]): string | null;
|
|
55
|
+
export interface TypeSkew {
|
|
56
|
+
typePackage: string;
|
|
57
|
+
pkgConfig: string;
|
|
58
|
+
/** `libraryVersion` declared by the installed `@girs/*` package. */
|
|
59
|
+
declared: string;
|
|
60
|
+
/** `pkg-config --modversion` of the library the host actually has. */
|
|
61
|
+
installed: string;
|
|
62
|
+
/**
|
|
63
|
+
* `ahead` = the types describe a NEWER library than is installed. This is the
|
|
64
|
+
* direction that compiles and then throws, so it is the one worth reporting.
|
|
65
|
+
* `behind` = API the host has is invisible to the compiler; limiting, not wrong.
|
|
66
|
+
*/
|
|
67
|
+
relation: 'ahead' | 'behind';
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Readers the comparison depends on, injectable so every branch is unit-testable
|
|
71
|
+
* from ANY host. Same reasoning as `resolvePrebuildDirName`, which is a pure
|
|
72
|
+
* function of injected values precisely so its darwin/win32 branches are tested
|
|
73
|
+
* from Linux: a check whose only test is "run it on the maintainer's machine and
|
|
74
|
+
* look" is a check whose skew cases are never exercised.
|
|
75
|
+
*/
|
|
76
|
+
export interface TypeSkewReaders {
|
|
77
|
+
/** `libraryVersion` of an installed `@girs/*` package, or null when absent. */
|
|
78
|
+
readDeclared?: (cwd: string, typePackage: string) => string | null;
|
|
79
|
+
/** `pkg-config --modversion` of a library, or null when absent. */
|
|
80
|
+
readInstalled?: (pkgConfig: string) => string | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Compare each installed `@girs/*` package's declared upstream version against the
|
|
84
|
+
* library the host actually has. Only reports a `major.minor` difference: a micro
|
|
85
|
+
* difference does not change the API set, and reporting it would train people to
|
|
86
|
+
* ignore the output.
|
|
87
|
+
*/
|
|
88
|
+
export declare function checkTypeSkew(cwd: string, readers?: TypeSkewReaders): TypeSkew[];
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// is in the project's dependency tree. A user with only @gjsify/fs in their
|
|
12
12
|
// project never sees a warning about libmanette.
|
|
13
13
|
import { execFileSync } from 'node:child_process';
|
|
14
|
-
import { join, resolve } from 'node:path';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
@@ -546,3 +546,123 @@ export function buildInstallCommand(pm, missing) {
|
|
|
546
546
|
}
|
|
547
547
|
return lines.length > 0 ? lines.join('\n ') : null;
|
|
548
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Only libraries whose GIR carries a real `<package version>`, verified against
|
|
551
|
+
* the published packages rather than assumed. A binding whose value turns out
|
|
552
|
+
* degenerate is skipped at runtime by `isDegenerate`, so a wrong entry here
|
|
553
|
+
* cannot produce a false alarm — it can only produce silence.
|
|
554
|
+
*/
|
|
555
|
+
const TYPE_PACKAGE_BINDINGS = [
|
|
556
|
+
{ pkgConfig: 'gtk4', typePackage: '@girs/gtk-4.0', namespaceVersion: '4.0' },
|
|
557
|
+
{ pkgConfig: 'libadwaita-1', typePackage: '@girs/adw-1', namespaceVersion: '1' },
|
|
558
|
+
{ pkgConfig: 'libsoup-3.0', typePackage: '@girs/soup-3.0', namespaceVersion: '3.0' },
|
|
559
|
+
{ pkgConfig: 'glib-2.0', typePackage: '@girs/glib-2.0', namespaceVersion: '2.0' },
|
|
560
|
+
{ pkgConfig: 'gobject-2.0', typePackage: '@girs/gobject-2.0', namespaceVersion: '2.0' },
|
|
561
|
+
{ pkgConfig: 'gio-2.0', typePackage: '@girs/gio-2.0', namespaceVersion: '2.0' },
|
|
562
|
+
{ pkgConfig: 'gstreamer-1.0', typePackage: '@girs/gst-1.0', namespaceVersion: '1.0' },
|
|
563
|
+
{ pkgConfig: 'webkitgtk-6.0', typePackage: '@girs/webkit-6.0', namespaceVersion: '6.0' },
|
|
564
|
+
{ pkgConfig: 'manette-0.2', typePackage: '@girs/manette-0.2', namespaceVersion: '0.2' },
|
|
565
|
+
{ pkgConfig: 'pango', typePackage: '@girs/pango-1.0', namespaceVersion: '1.0' },
|
|
566
|
+
{ pkgConfig: 'harfbuzz', typePackage: '@girs/harfbuzz-0.0', namespaceVersion: '0.0' },
|
|
567
|
+
];
|
|
568
|
+
/**
|
|
569
|
+
* Read `libraryVersion` from an installed `@girs/*` package.
|
|
570
|
+
*
|
|
571
|
+
* Resolves the package's MAIN entry and walks up to its root `package.json`
|
|
572
|
+
* rather than resolving the `package.json` subpath: `@girs/*` exports maps do not
|
|
573
|
+
* expose `./package.json` (verified on all 32 installed packages), so
|
|
574
|
+
* `require.resolve('@girs/gtk-4.0/package.json')` throws
|
|
575
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED`. The same trap silently disabled the
|
|
576
|
+
* `@nativescript/vite` major-version gate until it was found by measurement.
|
|
577
|
+
*/
|
|
578
|
+
function readGirsLibraryVersion(cwd, typePackage) {
|
|
579
|
+
for (const from of [pathToFileURL(join(cwd, '_girs_.js')).href, import.meta.url]) {
|
|
580
|
+
let dir;
|
|
581
|
+
try {
|
|
582
|
+
dir = dirname(createRequire(from).resolve(typePackage));
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
// Walk up to the package root — the entry may sit in a subdirectory.
|
|
588
|
+
for (let i = 0; i < 5; i++) {
|
|
589
|
+
const manifest = join(dir, 'package.json');
|
|
590
|
+
if (existsSync(manifest)) {
|
|
591
|
+
try {
|
|
592
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf-8'));
|
|
593
|
+
if (parsed.name === typePackage)
|
|
594
|
+
return parsed.libraryVersion ?? null;
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return null; // present but unreadable — nothing to compare against
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const parent = dirname(dir);
|
|
601
|
+
if (parent === dir)
|
|
602
|
+
break;
|
|
603
|
+
dir = parent;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
/** `[major, minor]`, or null when the string is not a comparable version. */
|
|
609
|
+
function majorMinor(version) {
|
|
610
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
611
|
+
if (!match)
|
|
612
|
+
return null;
|
|
613
|
+
return [Number(match[1]), Number(match[2])];
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* True when `libraryVersion` is ts-for-gir's namespace-version fallback rather
|
|
617
|
+
* than a real upstream release — `@girs/gdk-4.0`'s `4.0.0` against namespace `4.0`.
|
|
618
|
+
*
|
|
619
|
+
* Compares against every `.0`-padded spelling of the namespace version, not just
|
|
620
|
+
* the three-component one: `4.0` and `4.0.0` are the same claim, and matching only
|
|
621
|
+
* the padded form let the bare spelling through as a bogus 18-minor "behind"
|
|
622
|
+
* finding. Deliberately NOT a major.minor comparison — `@girs/adw-1` has namespace
|
|
623
|
+
* `1` and a real `libraryVersion` of `1.10.0`, which any major-only rule discards.
|
|
624
|
+
*/
|
|
625
|
+
function isDegenerate(declared, namespaceVersion) {
|
|
626
|
+
const trimmed = declared.trim();
|
|
627
|
+
const parts = namespaceVersion.split('.');
|
|
628
|
+
for (let n = parts.length; n <= 3; n++) {
|
|
629
|
+
if (trimmed === [...parts, ...Array(n - parts.length).fill('0')].join('.'))
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Compare each installed `@girs/*` package's declared upstream version against the
|
|
636
|
+
* library the host actually has. Only reports a `major.minor` difference: a micro
|
|
637
|
+
* difference does not change the API set, and reporting it would train people to
|
|
638
|
+
* ignore the output.
|
|
639
|
+
*/
|
|
640
|
+
export function checkTypeSkew(cwd, readers = {}) {
|
|
641
|
+
const readDeclared = readers.readDeclared ?? readGirsLibraryVersion;
|
|
642
|
+
const readInstalled = readers.readInstalled ?? ((pkgConfig) => tryExecFile('pkg-config', ['--modversion', pkgConfig]));
|
|
643
|
+
const findings = [];
|
|
644
|
+
for (const binding of TYPE_PACKAGE_BINDINGS) {
|
|
645
|
+
const declared = readDeclared(cwd, binding.typePackage);
|
|
646
|
+
if (declared === null)
|
|
647
|
+
continue; // types not installed, or no declaration
|
|
648
|
+
if (isDegenerate(declared, binding.namespaceVersion))
|
|
649
|
+
continue;
|
|
650
|
+
const installed = readInstalled(binding.pkgConfig);
|
|
651
|
+
if (installed === null)
|
|
652
|
+
continue; // library absent — the presence check owns that
|
|
653
|
+
const want = majorMinor(declared);
|
|
654
|
+
const have = majorMinor(installed.split('\n')[0] ?? installed);
|
|
655
|
+
if (!want || !have)
|
|
656
|
+
continue;
|
|
657
|
+
if (want[0] === have[0] && want[1] === have[1])
|
|
658
|
+
continue;
|
|
659
|
+
findings.push({
|
|
660
|
+
typePackage: binding.typePackage,
|
|
661
|
+
pkgConfig: binding.pkgConfig,
|
|
662
|
+
declared,
|
|
663
|
+
installed: installed.split('\n')[0] ?? installed,
|
|
664
|
+
relation: want[0] > have[0] || (want[0] === have[0] && want[1] > have[1]) ? 'ahead' : 'behind',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return findings;
|
|
668
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SystemGiOptions } from './system-gi.js';
|
|
1
2
|
export interface NativePackage {
|
|
2
3
|
/** npm package name, e.g. "@gjsify/webgl" */
|
|
3
4
|
name: string;
|
|
@@ -42,8 +43,15 @@ export interface NativeEnv {
|
|
|
42
43
|
GI_TYPELIB_PATH: string;
|
|
43
44
|
/** ELF loader (Linux and other ELF platforms). */
|
|
44
45
|
LD_LIBRARY_PATH?: string;
|
|
45
|
-
/** Mach-O loader (macOS). `dyld` ignores `LD_LIBRARY_PATH` entirely. */
|
|
46
|
+
/** Mach-O loader (macOS), OVERRIDING resolution by leaf. `dyld` ignores `LD_LIBRARY_PATH` entirely. */
|
|
46
47
|
DYLD_LIBRARY_PATH?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Mach-O loader (macOS), consulted only AFTER the normal search fails. This
|
|
50
|
+
* is where the HOST's GI libdirs go — never `DYLD_LIBRARY_PATH`, which would
|
|
51
|
+
* shadow correctly-resolved libraries process-wide. See
|
|
52
|
+
* {@link buildNativeEnv}.
|
|
53
|
+
*/
|
|
54
|
+
DYLD_FALLBACK_LIBRARY_PATH?: string;
|
|
47
55
|
[key: string]: string | undefined;
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
@@ -287,8 +295,44 @@ export declare function libraryPathVar(platform: string): {
|
|
|
287
295
|
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
288
296
|
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
289
297
|
* PATH: …}` would hand a child process two competing entries.
|
|
298
|
+
*
|
|
299
|
+
* ── DARWIN ALSO GETS THE *HOST'S* GI LIBDIRS ─────────────────────────────────
|
|
300
|
+
*
|
|
301
|
+
* Everything above is about the prebuilds gjsify itself ships. On macOS there is
|
|
302
|
+
* a second, independent gap that has nothing to do with gjsify: a typelib names
|
|
303
|
+
* its library by BARE LEAF (`libgtk-4.1.dylib`), and dyld carries neither
|
|
304
|
+
* `/usr/local/lib` nor `/opt/homebrew/lib` in its default fallback path.
|
|
305
|
+
* Homebrew's `gjs` has an rpath into GLIB's keg only, so it resolves no other
|
|
306
|
+
* keg's leaves — measured: bare `gjs -c "imports.gi.Gtk; Gtk.init()"`, no gjsify
|
|
307
|
+
* in the process, dies on that dlopen (full trace in `utils/system-gi.ts`). This
|
|
308
|
+
* is the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info` all
|
|
309
|
+
* pass through, so it is where the child's launch env is repaired.
|
|
310
|
+
*
|
|
311
|
+
* Two properties are load-bearing:
|
|
312
|
+
*
|
|
313
|
+
* 1. **`DYLD_FALLBACK_LIBRARY_PATH`, never `DYLD_LIBRARY_PATH`.** The override
|
|
314
|
+
* variable wins by LEAF for every dylib in the process, including ones the
|
|
315
|
+
* host already resolved correctly — pointing it at a whole system libdir is
|
|
316
|
+
* how you replace a linked-against library with a same-named different one.
|
|
317
|
+
* The fallback is consulted only after the normal search fails, which is
|
|
318
|
+
* exactly the "nothing else found it" case. Consequence, and it is the
|
|
319
|
+
* intended precedence: gjsify's own prebuilds keep priority through the
|
|
320
|
+
* override variable, the system stack fills in behind it.
|
|
321
|
+
* 2. **Our dirs go FIRST, an inherited fallback is kept BEHIND them, and
|
|
322
|
+
* `/usr/lib` is appended when there is none.** Setting the variable
|
|
323
|
+
* REPLACES dyld's own default fallback list, so writing only our dirs would
|
|
324
|
+
* silently remove `/usr/lib` from the search — a host that exported the
|
|
325
|
+
* variable itself already carries whatever tail it wants. Same composition
|
|
326
|
+
* as node-gi's re-exec.
|
|
327
|
+
*
|
|
328
|
+
* Emitted even when `packages` is empty: the gap is in the host's loader, not in
|
|
329
|
+
* a gjsify prebuild, so a plain `gjsify run script.gjs.js` that touches GTK needs
|
|
330
|
+
* it just as much. Empty off darwin — `ld.so`'s configured cache already resolves
|
|
331
|
+
* these leaves on Linux, and Windows re-reads `PATH` per `LoadLibrary`.
|
|
290
332
|
*/
|
|
291
333
|
export declare function buildNativeEnv(packages: NativePackage[], opts?: {
|
|
292
334
|
platform?: string;
|
|
293
335
|
env?: Record<string, string | undefined>;
|
|
336
|
+
/** Injectable for tests — the darwin branch is asserted from Linux. */
|
|
337
|
+
systemGiDirs?: (opts: SystemGiOptions) => string[];
|
|
294
338
|
}): NativeEnv;
|