@gjsify/cli 0.21.0 → 0.23.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/README.md +24 -1
- package/dist/cli.gjs.mjs +257 -184
- package/lib/bundler-pick.js +12 -4
- package/lib/commands/affected.js +22 -10
- package/lib/commands/check.js +38 -29
- package/lib/commands/flatpak/build.js +19 -18
- package/lib/commands/foreach.js +48 -41
- package/lib/commands/info.js +11 -5
- package/lib/commands/install.d.ts +16 -0
- package/lib/commands/install.js +103 -32
- package/lib/commands/onboard.js +6 -1
- package/lib/commands/pack.js +69 -4
- package/lib/commands/run.js +26 -14
- package/lib/commands/trust.js +4 -62
- package/lib/commands/workspace.js +49 -30
- package/lib/utils/bin-shim.d.ts +66 -0
- package/lib/utils/bin-shim.js +246 -0
- package/lib/utils/build-cache.d.ts +42 -6
- package/lib/utils/build-cache.js +114 -42
- package/lib/utils/detect-native-packages.d.ts +112 -6
- package/lib/utils/detect-native-packages.js +201 -29
- package/lib/utils/install-backend-native.d.ts +55 -0
- package/lib/utils/install-backend-native.js +152 -13
- package/lib/utils/install-global.d.ts +16 -0
- package/lib/utils/install-global.js +58 -8
- package/lib/utils/oxc-resolve.js +18 -55
- package/lib/utils/publish-headers.d.ts +2 -3
- package/lib/utils/publish-headers.js +64 -3
- package/lib/utils/publishable-packages.d.ts +22 -0
- package/lib/utils/publishable-packages.js +93 -0
- package/lib/utils/run-gjs.d.ts +6 -7
- package/lib/utils/run-gjs.js +4 -3
- package/lib/utils/spawn.d.ts +82 -0
- package/lib/utils/spawn.js +139 -0
- package/lib/utils/stdin.d.ts +25 -0
- package/lib/utils/stdin.js +132 -0
- package/package.json +23 -18
package/lib/bundler-pick.js
CHANGED
|
@@ -48,10 +48,18 @@ async function loadNpmRolldown() {
|
|
|
48
48
|
// `@gjsify/rolldown-native`'s lib/ is not built yet, issue #497).
|
|
49
49
|
// Fail with the actionable diagnosis instead.
|
|
50
50
|
if (isGjs()) {
|
|
51
|
-
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable ' +
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
51
|
+
throw new Error('gjsify build: no usable bundler engine under GJS — `@gjsify/rolldown-native` is not loadable, ' +
|
|
52
|
+
'and the npm `rolldown` engine is a Rust napi crate that cannot run under GJS.\n' +
|
|
53
|
+
'Most likely cause: the CLI bundle was invoked DIRECTLY (`gjs -m …/cli.gjs.mjs` or by executing ' +
|
|
54
|
+
'dist/cli.gjs.mjs), which bypasses the `gjsify` launcher that exports GI_TYPELIB_PATH / ' +
|
|
55
|
+
'LD_LIBRARY_PATH for the prebuilds. The typelib lookup happens inside the GJS runtime, so those ' +
|
|
56
|
+
'must be set BEFORE the process starts — the CLI cannot repair it from the inside. ' +
|
|
57
|
+
'Run through the `gjsify` bin instead (`node_modules/.bin/gjsify …`, or the globally installed shim), ' +
|
|
58
|
+
'or export the env yourself:\n' +
|
|
59
|
+
' P=<repo>/packages/infra/rolldown-native/prebuilds/linux-x86_64\n' +
|
|
60
|
+
' GI_TYPELIB_PATH=$P LD_LIBRARY_PATH=$P gjs -m …/cli.gjs.mjs build …\n' +
|
|
61
|
+
'Other causes: the JS facade `lib/` is not built (`gjsify workspace @gjsify/rolldown-native build` ' +
|
|
62
|
+
'under Node), or no prebuild exists for this architecture. Running the build under Node also works.');
|
|
55
63
|
}
|
|
56
64
|
// Indirect specifier so Rolldown's static-analysis doesn't try to
|
|
57
65
|
// bundle the npm crate into a GJS target build.
|
package/lib/commands/affected.js
CHANGED
|
@@ -47,9 +47,12 @@
|
|
|
47
47
|
//
|
|
48
48
|
// `--changed-from-stdin` skips `git diff` entirely and reads a newline-
|
|
49
49
|
// separated list of paths from stdin. Useful for local debugging and
|
|
50
|
-
// for the spec suite.
|
|
50
|
+
// for the spec suite. Stdin is slurped via `utils/stdin.ts`, NOT
|
|
51
|
+
// `readFileSync(0)` — `@gjsify/fs` has no numeric-fd path, so the Node
|
|
52
|
+
// idiom opens the relative file `./0` and the committed GJS classifier
|
|
53
|
+
// bundle died with `ENOENT … read '0'` on every `--changed-from-stdin` run.
|
|
51
54
|
import { spawnSync } from 'node:child_process';
|
|
52
|
-
import {
|
|
55
|
+
import { readStdinLines } from '../utils/stdin.js';
|
|
53
56
|
import { discoverWorkspaces, buildDependencyGraph, buildReverseDependencyGraph, affectedClosure, workspacesForChangedFiles, } from '@gjsify/workspace';
|
|
54
57
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
55
58
|
export const affectedCommand = {
|
|
@@ -85,7 +88,7 @@ export const affectedCommand = {
|
|
|
85
88
|
const changedFiles = args['changed-from-stdin']
|
|
86
89
|
? readStdinLines()
|
|
87
90
|
: runGitDiff(rootDir, args.base ?? 'origin/main', args.head ?? 'HEAD');
|
|
88
|
-
const result = classifyAndExpand(workspaces,
|
|
91
|
+
const result = classifyAndExpand(workspaces, changedFiles);
|
|
89
92
|
emit(args.format, result);
|
|
90
93
|
},
|
|
91
94
|
};
|
|
@@ -103,6 +106,11 @@ const GLOBAL_TRIGGERS = [
|
|
|
103
106
|
// can break assertions anywhere. Treat it as a global trigger so a
|
|
104
107
|
// `@gjsify/unit` change re-runs the full suite.
|
|
105
108
|
/^packages\/gjs\/unit\//,
|
|
109
|
+
// The composite action every CI job runs to set itself up. A change here
|
|
110
|
+
// alters the environment of the whole matrix, so it must force a full run
|
|
111
|
+
// EXPLICITLY — it already did, but only by falling through to the
|
|
112
|
+
// conservative "unmatched files" path, which is luck rather than intent.
|
|
113
|
+
/^\.github\/actions\//,
|
|
106
114
|
// Cross-cutting dep + lockfile + root config.
|
|
107
115
|
/^gjsify-lock\.json$/,
|
|
108
116
|
/^package\.json$/,
|
|
@@ -119,7 +127,7 @@ const IGNORE = [
|
|
|
119
127
|
/^refs\//,
|
|
120
128
|
/^website\//,
|
|
121
129
|
/^docs\//,
|
|
122
|
-
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi)\.yml$/,
|
|
130
|
+
/^\.github\/workflows\/(deploy-docs|commitlint|release|audit-runtimes|prebuilds|node-gi|napi|cli-cross-platform|build-ci-image)\.yml$/,
|
|
123
131
|
/^\.githooks\//,
|
|
124
132
|
// @gjsify/node-gi (the Node-native GObject-Introspection engine, Axis 5) is
|
|
125
133
|
// NOT a gjsify workspace member — the GJS-first install/foreach tooling can't
|
|
@@ -128,6 +136,14 @@ const IGNORE = [
|
|
|
128
136
|
// tested on Node). Without this carve-out its files map to no workspace and
|
|
129
137
|
// land in `unmatched`, forcing a conservative full run on every node-gi PR.
|
|
130
138
|
/^packages\/node-gi\//,
|
|
139
|
+
// @gjsify/napi (the N-API host over GJS's SpiderMonkey, the forward mirror of
|
|
140
|
+
// node-gi) is likewise NOT a gjsify workspace member — the GJS-first
|
|
141
|
+
// install/foreach tooling can't build its Vala+C++/meson shim or its node-gyp
|
|
142
|
+
// test addons, so the main workflow neither builds nor tests it. Its own
|
|
143
|
+
// `.github/workflows/napi.yml` is the source of truth. Without this carve-out
|
|
144
|
+
// its files map to no workspace and force a conservative full run on every
|
|
145
|
+
// napi PR.
|
|
146
|
+
/^packages\/napi\//,
|
|
131
147
|
// Flatpak build/distribution tooling (SDK-extension manifest + metainfo).
|
|
132
148
|
// Like `.githooks/`, it has no package-test consumers; its own
|
|
133
149
|
// `tests/e2e/flatpak-sdk-extension` runs on `tests/e2e/**` / global triggers.
|
|
@@ -143,7 +159,7 @@ const IGNORE = [
|
|
|
143
159
|
];
|
|
144
160
|
/** Patterns that suggest a test-only change. */
|
|
145
161
|
const TEST_PATHS = [/\.spec\.[mc]?[tj]sx?$/, /^tests\/(e2e|integration)\//];
|
|
146
|
-
function classifyAndExpand(workspaces,
|
|
162
|
+
function classifyAndExpand(workspaces, changedFiles) {
|
|
147
163
|
const files = changedFiles.map((f) => f.replace(/\\/g, '/')).filter((f) => f.length > 0);
|
|
148
164
|
if (files.length === 0) {
|
|
149
165
|
return {
|
|
@@ -192,7 +208,7 @@ function classifyAndExpand(workspaces, rootDir, changedFiles) {
|
|
|
192
208
|
}
|
|
193
209
|
}
|
|
194
210
|
// Map files → workspaces. Files outside any workspace stay in `unmatched`.
|
|
195
|
-
const { matched, unmatched } = workspacesForChangedFiles(workspaces,
|
|
211
|
+
const { matched, unmatched } = workspacesForChangedFiles(workspaces, remaining);
|
|
196
212
|
// Unmatched-but-not-ignored files are suspicious enough to fall back to
|
|
197
213
|
// the conservative "full run" path. Examples: a new top-level dotfile,
|
|
198
214
|
// a script in `scripts/` we haven't carved out, a refs/-adjacent file.
|
|
@@ -290,10 +306,6 @@ function runGitDiff(cwd, base, head) {
|
|
|
290
306
|
}
|
|
291
307
|
return r.stdout.split('\n').filter(Boolean);
|
|
292
308
|
}
|
|
293
|
-
function readStdinLines() {
|
|
294
|
-
const data = readFileSync(0, 'utf8');
|
|
295
|
-
return data.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
296
|
-
}
|
|
297
309
|
// ─── Output ────────────────────────────────────────────────────────────────
|
|
298
310
|
function emit(format, r) {
|
|
299
311
|
if (format === 'json') {
|
package/lib/commands/check.js
CHANGED
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
// — if both `system-check`-style flags (`--json`) and `check`-style scripts
|
|
15
15
|
// are reachable here, the `--json` flag routes through this command first
|
|
16
16
|
// (since the typescript-check binding is positional `[paths..]`).
|
|
17
|
-
import {
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
19
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
19
20
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
20
21
|
import { join } from 'node:path';
|
|
21
22
|
import { cpus } from 'node:os';
|
|
@@ -26,36 +27,44 @@ import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
|
26
27
|
* indicates failure. Output is forwarded to the parent's stdout/stderr,
|
|
27
28
|
* line-prefixed with the workspace name when `prefix` is set.
|
|
28
29
|
*/
|
|
29
|
-
function runCheck(ws, prefix) {
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
async function runCheck(ws, prefix) {
|
|
31
|
+
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
32
|
+
// `process.exit(…)`, which quits the GLib main loop `spawn()` arms under
|
|
33
|
+
// GJS, so the STREAMING async path is safe here (and required: the
|
|
34
|
+
// blocking one would serialize the parallel workers). See utils/spawn.ts.
|
|
35
|
+
const flushers = [];
|
|
36
|
+
try {
|
|
37
|
+
const { code } = await spawnToCompletion('npm', ['run', 'check', '--if-present'], {
|
|
38
|
+
completion: 'exit',
|
|
32
39
|
cwd: ws.location,
|
|
33
|
-
stdio: prefix === null ? 'inherit' :
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
for (const flush of flushers)
|
|
50
|
-
flush();
|
|
51
|
-
resolve(code ?? 1);
|
|
52
|
-
});
|
|
53
|
-
child.on('error', () => {
|
|
54
|
-
for (const flush of flushers)
|
|
55
|
-
flush();
|
|
56
|
-
resolve(1);
|
|
40
|
+
stdio: prefix === null ? 'inherit' : 'pipe',
|
|
41
|
+
onSpawn: (child) => {
|
|
42
|
+
// Shared with `foreach`: live line-prefixing on a tty, buffered
|
|
43
|
+
// flush-on-close on a non-tty sink. The previous inline
|
|
44
|
+
// forwarder wrote every line live — under GJS
|
|
45
|
+
// `process.stdout.write` is a blocking `Gio.write_all`, so a
|
|
46
|
+
// parallel check into a backpressuring CI pipe could stall the
|
|
47
|
+
// GLib loop the same way the foreach examples build did. See
|
|
48
|
+
// utils/prefixed-output.ts.
|
|
49
|
+
const buffered = !process.stdout.isTTY;
|
|
50
|
+
if (prefix !== null && child.stdout && child.stderr) {
|
|
51
|
+
const tag = `[${prefix}] `;
|
|
52
|
+
flushers.push(prefixLines(child.stdout, process.stdout, tag, buffered));
|
|
53
|
+
flushers.push(prefixLines(child.stderr, process.stderr, tag, buffered));
|
|
54
|
+
}
|
|
55
|
+
},
|
|
57
56
|
});
|
|
58
|
-
|
|
57
|
+
for (const flush of flushers)
|
|
58
|
+
flush();
|
|
59
|
+
return code ?? 1;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Spawn failure (npm not on PATH, …) reports as a failed check —
|
|
63
|
+
// `gjsify check` aggregates codes rather than throwing per workspace.
|
|
64
|
+
for (const flush of flushers)
|
|
65
|
+
flush();
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
export const checkCommand = {
|
|
61
70
|
command: 'check',
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// Replaces the project-local `build-flatpak.sh`-style scripts: same flag
|
|
4
4
|
// shape (manifest, build-dir, install, repo, bundle), plus a `--tarball`
|
|
5
5
|
// helper for Flathub submissions.
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
6
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
8
7
|
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { spawnToCompletion } from '../../utils/spawn.js';
|
|
9
9
|
export const flatpakBuildCommand = {
|
|
10
10
|
command: 'build [manifest]',
|
|
11
11
|
description: 'Build the Flatpak via `flatpak-builder`. Wraps a typical install + export + bundle + tarball pipeline.',
|
|
@@ -115,6 +115,12 @@ export const flatpakBuildCommand = {
|
|
|
115
115
|
await runTar(['-czf', tarballPath, '-C', buildDir, '.'], { verbose: args.verbose });
|
|
116
116
|
}
|
|
117
117
|
console.log(`[gjsify flatpak build] done (${buildDir})`);
|
|
118
|
+
// Terminal command: exit explicitly. Under GJS the async `spawn` used
|
|
119
|
+
// above arms a GLib main loop (`ensureMainLoop`) that only
|
|
120
|
+
// `process.exit()` quits — returning here would park the CLI at 0% CPU
|
|
121
|
+
// after flatpak-builder had already finished. Same reason `foreach` /
|
|
122
|
+
// `workspace` / `check` end this way; see utils/spawn.ts.
|
|
123
|
+
return process.exit(0);
|
|
118
124
|
},
|
|
119
125
|
};
|
|
120
126
|
/** Pick the first JSON file in cwd that looks like a Flatpak manifest. */
|
|
@@ -167,25 +173,20 @@ async function runFlatpak(args, opts) {
|
|
|
167
173
|
async function runTar(args, opts) {
|
|
168
174
|
return runProc('tar', args, opts, { notFoundHint: 'tar not found.' });
|
|
169
175
|
}
|
|
170
|
-
function runProc(cmd, args, opts, extra) {
|
|
176
|
+
async function runProc(cmd, args, opts, extra) {
|
|
171
177
|
if (opts.verbose) {
|
|
172
178
|
console.log(`[gjsify flatpak] ${cmd} ${args.join(' ')}`);
|
|
173
179
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
child.on('exit', (code) => {
|
|
185
|
-
if (code === 0)
|
|
186
|
-
res();
|
|
187
|
-
else
|
|
188
|
-
rej(new Error(`gjsify flatpak: ${cmd} exited with status ${code}`));
|
|
189
|
-
});
|
|
180
|
+
// `completion: 'exit'` — the handler ends in `process.exit(0)` (added for
|
|
181
|
+
// exactly this reason), so the GLib main loop `spawn()` arms under GJS is
|
|
182
|
+
// quit on the way out and the STREAMING path stays available. That matters
|
|
183
|
+
// here more than anywhere: `flatpak-builder` runs for minutes and its live
|
|
184
|
+
// progress is the whole point of `stdio: 'inherit'`. See utils/spawn.ts.
|
|
185
|
+
const { code } = await spawnToCompletion(cmd, args, {
|
|
186
|
+
completion: 'exit',
|
|
187
|
+
notFound: () => new Error(`gjsify flatpak: ${extra.notFoundHint}`),
|
|
190
188
|
});
|
|
189
|
+
if (code !== 0) {
|
|
190
|
+
throw new Error(`gjsify flatpak: ${cmd} exited with status ${code}`);
|
|
191
|
+
}
|
|
191
192
|
}
|
package/lib/commands/foreach.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Output is line-prefixed `[<workspace-name>]` when --parallel is set,
|
|
9
9
|
// matching yarn's interactive flow. Exit code is non-zero if any child
|
|
10
10
|
// process failed; first failure's stderr is forwarded.
|
|
11
|
-
import {
|
|
11
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
12
12
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { cpus } from 'node:os';
|
|
14
14
|
import { buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
@@ -602,47 +602,54 @@ function detectPackageManager() {
|
|
|
602
602
|
return 'gjsify';
|
|
603
603
|
return 'npm';
|
|
604
604
|
}
|
|
605
|
-
function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
605
|
+
async function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
606
|
+
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
607
|
+
// `process.exit(…)` (see the handler's tail), which is what quits the GLib
|
|
608
|
+
// main loop `spawn()` arms under GJS. That keeps foreach on the STREAMING
|
|
609
|
+
// async path: live prefixed output and real parallelism, both of which the
|
|
610
|
+
// blocking path would destroy. See utils/spawn.ts for the full contract.
|
|
611
|
+
let tracked = null;
|
|
612
|
+
const flushers = [];
|
|
613
|
+
try {
|
|
614
|
+
const result = await spawnToCompletion(cmd, args, {
|
|
615
|
+
completion: 'exit',
|
|
612
616
|
cwd,
|
|
613
|
-
stdio: prefix ?
|
|
614
|
-
|
|
617
|
+
stdio: prefix ? 'pipe' : 'inherit',
|
|
618
|
+
// Default FORCE_COLOR=1 unless the user explicitly opted out, so
|
|
619
|
+
// tools that key on `process.stdout.isTTY` (chalk, picocolors, …)
|
|
620
|
+
// still emit ANSI colors under gjsify foreach. Mirrors yarn / npm.
|
|
621
|
+
color: true,
|
|
622
|
+
onSpawn: (child) => {
|
|
623
|
+
tracked = child;
|
|
624
|
+
activeChildren.add(child);
|
|
625
|
+
// Under GJS, `process.stdout.write` is a BLOCKING
|
|
626
|
+
// `Gio.write_all`. Writing each prefixed line LIVE during a
|
|
627
|
+
// PARALLEL foreach to a backpressuring pipe (a CI log
|
|
628
|
+
// collector that drains slowly) stalls the single GLib main
|
|
629
|
+
// loop on a full pipe → every parallel child's pipe backs up →
|
|
630
|
+
// their reads stall → the whole run HANGS. (A tty or a file
|
|
631
|
+
// sink never backpressures, which is why it only bites in CI.)
|
|
632
|
+
// On a NON-tty sink we therefore BUFFER each child's prefixed
|
|
633
|
+
// output and flush it as ONE write when that child closes: the
|
|
634
|
+
// child is already done by then, so its own read can't stall,
|
|
635
|
+
// and concurrent flushes serialize into brief loop stalls
|
|
636
|
+
// instead of a deadlock. On a tty (interactive) we keep live
|
|
637
|
+
// line-prefixing for responsive output.
|
|
638
|
+
const buffered = !process.stdout.isTTY;
|
|
639
|
+
if (prefix && child.stdout && child.stderr) {
|
|
640
|
+
flushers.push(prefixLines(child.stdout, process.stdout, prefix, buffered));
|
|
641
|
+
flushers.push(prefixLines(child.stderr, process.stderr, prefix, buffered));
|
|
642
|
+
}
|
|
643
|
+
},
|
|
615
644
|
});
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
// the single GLib main loop on a full pipe → every parallel child's
|
|
621
|
-
// pipe backs up → their reads stall → the whole run HANGS. (A tty or a
|
|
622
|
-
// file sink never backpressures, which is why it only bites in CI.)
|
|
623
|
-
// On a NON-tty sink we therefore BUFFER each child's prefixed output
|
|
624
|
-
// and flush it as ONE write when that child closes: the child is
|
|
625
|
-
// already done by then, so its own read can't stall, and concurrent
|
|
626
|
-
// flushes serialize into brief loop stalls instead of a deadlock. On a
|
|
627
|
-
// tty (interactive) we keep live line-prefixing for responsive output.
|
|
628
|
-
const buffered = !process.stdout.isTTY;
|
|
629
|
-
const flushers = [];
|
|
630
|
-
if (prefix && child.stdout && child.stderr) {
|
|
631
|
-
flushers.push(prefixLines(child.stdout, process.stdout, prefix, buffered));
|
|
632
|
-
flushers.push(prefixLines(child.stderr, process.stderr, prefix, buffered));
|
|
645
|
+
for (const flush of flushers)
|
|
646
|
+
flush();
|
|
647
|
+
if (result.code !== 0) {
|
|
648
|
+
throw new Error(`${cmd} ${args.join(' ')} exited with code ${result.code}`);
|
|
633
649
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
resolve();
|
|
640
|
-
else
|
|
641
|
-
reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
|
|
642
|
-
});
|
|
643
|
-
child.on('error', (err) => {
|
|
644
|
-
activeChildren.delete(child);
|
|
645
|
-
reject(err);
|
|
646
|
-
});
|
|
647
|
-
});
|
|
650
|
+
}
|
|
651
|
+
finally {
|
|
652
|
+
if (tracked)
|
|
653
|
+
activeChildren.delete(tracked);
|
|
654
|
+
}
|
|
648
655
|
}
|
package/lib/commands/info.js
CHANGED
|
@@ -20,11 +20,17 @@ export const infoCommand = {
|
|
|
20
20
|
const cwd = process.cwd();
|
|
21
21
|
const file = args.file ? resolve(args.file) : null;
|
|
22
22
|
const nativePackages = detectNativePackages(cwd);
|
|
23
|
-
|
|
23
|
+
// Which variables this yields is host-dependent: GI_TYPELIB_PATH
|
|
24
|
+
// everywhere, plus the loader path variable the host actually consults
|
|
25
|
+
// (LD_LIBRARY_PATH on Linux, DYLD_LIBRARY_PATH on macOS, PATH on
|
|
26
|
+
// Windows). Iterate rather than destructuring two fixed names, or the
|
|
27
|
+
// printed environment is wrong off Linux.
|
|
28
|
+
const nativeEnv = buildNativeEnv(nativePackages);
|
|
29
|
+
const envEntries = Object.entries(nativeEnv).filter(([, value]) => value !== undefined);
|
|
24
30
|
if (args.export) {
|
|
25
31
|
// Machine-readable output for eval
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
for (const [key, value] of envEntries)
|
|
33
|
+
console.log(`export ${key}="${value}"`);
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
36
|
// Human-readable output
|
|
@@ -39,8 +45,8 @@ export const infoCommand = {
|
|
|
39
45
|
}
|
|
40
46
|
console.log('');
|
|
41
47
|
console.log('To run your app directly with gjs, set:');
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
for (const [key, value] of envEntries)
|
|
49
|
+
console.log(` export ${key}="${value}"`);
|
|
44
50
|
if (file) {
|
|
45
51
|
console.log(` gjs -m ${file}`);
|
|
46
52
|
}
|
|
@@ -14,4 +14,20 @@ interface InstallOptions {
|
|
|
14
14
|
timeout: number;
|
|
15
15
|
}
|
|
16
16
|
export declare const installCommand: Command<unknown, InstallOptions>;
|
|
17
|
+
export declare function buildBinShim(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[], platform?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
|
20
|
+
*
|
|
21
|
+
* The `sh` shim's gjs-first / node-fallback probe has no Windows counterpart to
|
|
22
|
+
* express: there is no GJS host on Windows at all (no prebuilt `libgjs` — see
|
|
23
|
+
* `website/src/content/docs/platform-support.md`), so a workspace that ships
|
|
24
|
+
* both targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
25
|
+
* `gjs`-invoking shim that fails exactly the way the `sh` one does.
|
|
26
|
+
*
|
|
27
|
+
* Exported + platform-free so it is unit-tested from Linux.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildWorkspaceBinShimsForWindows(wsLocation: string, nodeTarget?: string, gjsTarget?: string, nativePrebuildDirs?: string[]): {
|
|
30
|
+
cmd: string;
|
|
31
|
+
ps1: string;
|
|
32
|
+
};
|
|
17
33
|
export {};
|
package/lib/commands/install.js
CHANGED
|
@@ -30,18 +30,40 @@
|
|
|
30
30
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
31
31
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
32
32
|
import { fileURLToPath } from 'node:url';
|
|
33
|
-
import {
|
|
33
|
+
import { spawnSync } from 'node:child_process';
|
|
34
34
|
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
35
35
|
import { discoverWorkspaces } from '@gjsify/workspace';
|
|
36
36
|
import { buildInstallCommand, detectPackageManager, runMinimalChecks } from '../utils/check-system-deps.js';
|
|
37
|
-
import { detectNativePackages } from '../utils/detect-native-packages.js';
|
|
37
|
+
import { detectNativePackages, libraryPathVar } from '../utils/detect-native-packages.js';
|
|
38
|
+
import { buildLauncherShims } from '../utils/bin-shim.js';
|
|
38
39
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
39
40
|
import { atomicWriteStrict } from '../utils/install-cache-fs.js';
|
|
40
41
|
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
41
42
|
import { nodeBinary } from '../utils/run-node.js';
|
|
43
|
+
import { spawnToCompletion } from '../utils/spawn.js';
|
|
42
44
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
43
45
|
import { binDirOnPath, defaultGlobalLayout, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
44
46
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
47
|
+
/**
|
|
48
|
+
* Link type for the workspace↔workspace directory links.
|
|
49
|
+
*
|
|
50
|
+
* Windows cannot create a *directory symlink* without elevation or Developer
|
|
51
|
+
* Mode (`EPERM`), but any user can create an NTFS **junction** — which is why
|
|
52
|
+
* npm and yarn both use junctions for workspace links. These two call sites had
|
|
53
|
+
* no try/catch, so a non-elevated Windows workspace install hard-failed here.
|
|
54
|
+
*/
|
|
55
|
+
const WORKSPACE_LINK_TYPE = process.platform === 'win32' ? 'junction' : undefined;
|
|
56
|
+
/**
|
|
57
|
+
* The `target` argument for a workspace link.
|
|
58
|
+
*
|
|
59
|
+
* Node normalises a `'junction'` target with `path.resolve()` — i.e. against
|
|
60
|
+
* the process CWD, NOT the link's own directory — so a junction MUST be given
|
|
61
|
+
* an absolute path. POSIX symlinks stay relative so the tree survives being
|
|
62
|
+
* moved.
|
|
63
|
+
*/
|
|
64
|
+
function workspaceLinkTarget(linkPath, absTarget) {
|
|
65
|
+
return WORKSPACE_LINK_TYPE === 'junction' ? resolve(absTarget) : relative(dirname(linkPath), absTarget);
|
|
66
|
+
}
|
|
45
67
|
// Default 30min wall-clock budget for the full install. Big workspaces
|
|
46
68
|
// (212+ sub-packages × 600+ external deps in the gjsify monorepo itself)
|
|
47
69
|
// can legitimately take 10-20 min on a fresh CI install when the npm CDN
|
|
@@ -621,7 +643,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
621
643
|
continue;
|
|
622
644
|
const linkPath = join(target.location, 'node_modules', link.depName);
|
|
623
645
|
parentDirs.add(dirname(linkPath));
|
|
624
|
-
const relTarget =
|
|
646
|
+
const relTarget = workspaceLinkTarget(linkPath, link.targetLocation);
|
|
625
647
|
plans.push({ linkPath, relTarget });
|
|
626
648
|
}
|
|
627
649
|
// Phase 1: one mkdir per unique parent (max ~213 instead of ~793).
|
|
@@ -638,13 +660,13 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
638
660
|
// `{ recursive: true, force: true }` handles every shape (rm
|
|
639
661
|
// no-ops on missing paths under force; recursive covers dirs).
|
|
640
662
|
try {
|
|
641
|
-
await fsp.rm(linkPath, { recursive: true, force: true });
|
|
663
|
+
await fsp.rm(linkPath, { recursive: true, force: true, maxRetries: 10, retryDelay: 100 });
|
|
642
664
|
}
|
|
643
665
|
catch {
|
|
644
666
|
/* unexpected; symlink call below will surface the real
|
|
645
667
|
cause if it persists. */
|
|
646
668
|
}
|
|
647
|
-
await fsp.symlink(relTarget, linkPath);
|
|
669
|
+
await fsp.symlink(relTarget, linkPath, WORKSPACE_LINK_TYPE);
|
|
648
670
|
};
|
|
649
671
|
for (let i = 0; i < Math.min(SYMLINK_CONCURRENCY, plans.length); i++) {
|
|
650
672
|
workers.push((async () => {
|
|
@@ -704,8 +726,7 @@ async function workspaceInstallLocked(cwd, args, signal) {
|
|
|
704
726
|
if (existsHere)
|
|
705
727
|
continue;
|
|
706
728
|
mkdirSync(dirname(linkPath), { recursive: true });
|
|
707
|
-
|
|
708
|
-
symlinkSync(relTarget, linkPath);
|
|
729
|
+
symlinkSync(workspaceLinkTarget(linkPath, ws.location), linkPath, WORKSPACE_LINK_TYPE);
|
|
709
730
|
rootHoisted++;
|
|
710
731
|
}
|
|
711
732
|
if (rootHoisted > 0) {
|
|
@@ -912,16 +933,27 @@ function writeWorkspaceBinShims(cwd, workspaces) {
|
|
|
912
933
|
mkdirSync(wsBinDir, { recursive: true });
|
|
913
934
|
for (const [binName, { nodeTarget, gjsTarget }] of merged) {
|
|
914
935
|
const linkPath = join(wsBinDir, binName);
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
936
|
+
for (const file of [linkPath, `${linkPath}.cmd`, `${linkPath}.ps1`]) {
|
|
937
|
+
try {
|
|
938
|
+
rmSync(file, { force: true, maxRetries: 10, retryDelay: 100 });
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
/* fine */
|
|
942
|
+
}
|
|
920
943
|
}
|
|
921
944
|
writeFileSync(linkPath, buildBinShim(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs), {
|
|
922
945
|
mode: 0o755,
|
|
923
946
|
});
|
|
924
947
|
chmodSync(linkPath, 0o755);
|
|
948
|
+
// Windows executes neither an extension-less file nor a `#!` line —
|
|
949
|
+
// the sh shim above is only reachable from git-bash/MSYS/WSL. Write
|
|
950
|
+
// the `.cmd`/`.ps1` companions cmd.exe and pwsh need, mirroring what
|
|
951
|
+
// npm's cmd-shim lays down next to every linked bin.
|
|
952
|
+
if (process.platform === 'win32') {
|
|
953
|
+
for (const [suffix, contents] of Object.entries(buildWorkspaceBinShimsForWindows(ws.location, nodeTarget, gjsTarget, nativePrebuildDirs))) {
|
|
954
|
+
writeFileSync(`${linkPath}.${suffix}`, contents, { mode: 0o755 });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
925
957
|
wsBinsCreated++;
|
|
926
958
|
}
|
|
927
959
|
}
|
|
@@ -997,19 +1029,25 @@ function scopedOverrideFor(ws, depName, extracted) {
|
|
|
997
1029
|
}
|
|
998
1030
|
return undefined;
|
|
999
1031
|
}
|
|
1000
|
-
function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []) {
|
|
1032
|
+
export function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = [], platform = process.platform) {
|
|
1001
1033
|
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
1002
1034
|
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
1003
1035
|
// GJS-only env preamble — Node ignores GI_TYPELIB_PATH so we scope the
|
|
1004
1036
|
// export to the gjs branch, keeping the shim minimal when no native pkgs
|
|
1005
1037
|
// exist or only the Node bin is in play.
|
|
1038
|
+
//
|
|
1039
|
+
// The library-path variable is host-dependent: `dyld` on macOS never reads
|
|
1040
|
+
// LD_LIBRARY_PATH, so exporting it there silently produced a launcher that
|
|
1041
|
+
// could not load a single `darwin-arm64` prebuild. `libraryPathVar()` is the
|
|
1042
|
+
// one place that mapping lives.
|
|
1043
|
+
const { name: libVar } = libraryPathVar(platform);
|
|
1006
1044
|
const gjsPreamble = nativePrebuildDirs.length === 0
|
|
1007
1045
|
? ''
|
|
1008
1046
|
: (() => {
|
|
1009
1047
|
const joined = `'${nativePrebuildDirs.join(':').replace(/'/g, `'\\''`)}'`;
|
|
1010
1048
|
return (`GI_TYPELIB_PATH=${joined}\${GI_TYPELIB_PATH:+":$GI_TYPELIB_PATH"}\n` +
|
|
1011
|
-
|
|
1012
|
-
`export GI_TYPELIB_PATH
|
|
1049
|
+
`${libVar}=${joined}\${${libVar}:+":$${libVar}"}\n` +
|
|
1050
|
+
`export GI_TYPELIB_PATH ${libVar}\n`);
|
|
1013
1051
|
})();
|
|
1014
1052
|
if (nodeAbs && gjsAbs) {
|
|
1015
1053
|
// GJS-FIRST: prefer the committed Node-free GJS bundle when `gjs` is on
|
|
@@ -1039,6 +1077,37 @@ function buildBinShim(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []
|
|
|
1039
1077
|
return `#!/bin/sh\n${gjsPreamble}exec gjs -m "${gjsAbs}" "$@"\n`;
|
|
1040
1078
|
throw new Error('buildBinShim: either nodeTarget or gjsTarget must be provided');
|
|
1041
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* `.cmd` / `.ps1` companions for {@link buildBinShim}.
|
|
1082
|
+
*
|
|
1083
|
+
* The `sh` shim's gjs-first / node-fallback probe has no Windows counterpart to
|
|
1084
|
+
* express: there is no GJS host on Windows at all (no prebuilt `libgjs` — see
|
|
1085
|
+
* `website/src/content/docs/platform-support.md`), so a workspace that ships
|
|
1086
|
+
* both targets resolves to Node here, and a GJS-only workspace gets an honest
|
|
1087
|
+
* `gjs`-invoking shim that fails exactly the way the `sh` one does.
|
|
1088
|
+
*
|
|
1089
|
+
* Exported + platform-free so it is unit-tested from Linux.
|
|
1090
|
+
*/
|
|
1091
|
+
export function buildWorkspaceBinShimsForWindows(wsLocation, nodeTarget, gjsTarget, nativePrebuildDirs = []) {
|
|
1092
|
+
const nodeAbs = nodeTarget ? join(wsLocation, nodeTarget) : null;
|
|
1093
|
+
const gjsAbs = gjsTarget ? join(wsLocation, gjsTarget) : null;
|
|
1094
|
+
if (nodeAbs)
|
|
1095
|
+
return buildLauncherShims({ interpreter: 'node', target: nodeAbs });
|
|
1096
|
+
if (gjsAbs) {
|
|
1097
|
+
// Windows has no dedicated library-path variable — LoadLibrary searches
|
|
1098
|
+
// PATH — so the prebuild dirs go there, `;`-separated (which is also
|
|
1099
|
+
// GLib's G_SEARCHPATH_SEPARATOR on Windows, hence the same join for
|
|
1100
|
+
// GI_TYPELIB_PATH).
|
|
1101
|
+
const joined = nativePrebuildDirs.join(';');
|
|
1102
|
+
return buildLauncherShims({
|
|
1103
|
+
interpreter: 'gjs',
|
|
1104
|
+
interpreterArgs: ['-m'],
|
|
1105
|
+
target: gjsAbs,
|
|
1106
|
+
prependEnv: nativePrebuildDirs.length === 0 ? {} : { GI_TYPELIB_PATH: joined, PATH: joined },
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
throw new Error('buildWorkspaceBinShimsForWindows: either nodeTarget or gjsTarget must be provided');
|
|
1110
|
+
}
|
|
1042
1111
|
/**
|
|
1043
1112
|
* Walk a workspace's `bin` (Node) + `gjsify.bin` (GJS) declarations
|
|
1044
1113
|
* into a unified `<binName> → {nodeTarget?, gjsTarget?}` map. The
|
|
@@ -1092,23 +1161,25 @@ async function projectInstallViaNpm(args) {
|
|
|
1092
1161
|
await spawnNpm(npmArgs);
|
|
1093
1162
|
}
|
|
1094
1163
|
async function spawnNpm(npmArgs) {
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1164
|
+
// `completion: 'return'` — the `--backend npm` branch of the handler
|
|
1165
|
+
// RETURNS after `runPostInstallChecks()` instead of exiting, so under GJS
|
|
1166
|
+
// this must not leave the main loop `spawn()` arms parked (see
|
|
1167
|
+
// utils/spawn.ts). The npm backend needs a Node host anyway, so the
|
|
1168
|
+
// blocking path costs nothing in practice.
|
|
1169
|
+
return spawnToCompletion('npm', npmArgs, {
|
|
1170
|
+
completion: 'return',
|
|
1171
|
+
notFound: () => new Error('npm not found on PATH — install Node.js first.'),
|
|
1172
|
+
})
|
|
1173
|
+
.then(({ code }) => {
|
|
1174
|
+
if (code !== 0)
|
|
1175
|
+
throw new Error(`npm install exited with code ${code}`);
|
|
1176
|
+
})
|
|
1177
|
+
.catch((err) => {
|
|
1178
|
+
// A raw spawn failure still carries an errno `code`; the mapped
|
|
1179
|
+
// not-found hint and the non-zero-exit error do not, and already
|
|
1180
|
+
// read as finished sentences.
|
|
1181
|
+
const errno = err.code;
|
|
1182
|
+
console.error(errno === undefined ? err.message : `npm install failed: ${err.message}`);
|
|
1112
1183
|
process.exit(1);
|
|
1113
1184
|
});
|
|
1114
1185
|
}
|