@gjsify/cli 0.48.0 → 0.50.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 +165 -157
- package/lib/actions/barrels-generate.d.ts +22 -0
- package/lib/actions/barrels-generate.js +36 -1
- package/lib/actions/build.js +10 -6
- package/lib/commands/barrels.js +19 -2
- package/lib/commands/build.js +4 -0
- package/lib/commands/foreach.js +11 -19
- package/lib/commands/install.js +21 -1
- package/lib/commands/test.d.ts +16 -0
- package/lib/commands/test.js +30 -29
- package/lib/commands/upgrade.js +78 -30
- package/lib/commands/workspace.js +47 -44
- package/lib/config.js +13 -0
- package/lib/index.js +10 -0
- package/lib/types/cli-build-options.d.ts +6 -0
- package/lib/types/config-data.d.ts +14 -0
- package/lib/utils/build-cache.d.ts +15 -4
- package/lib/utils/build-cache.js +62 -67
- package/lib/utils/cli-exit.d.ts +23 -0
- package/lib/utils/cli-exit.js +78 -0
- package/lib/utils/detect-native-packages.d.ts +31 -0
- package/lib/utils/detect-native-packages.js +37 -1
- package/lib/utils/package-inputs.d.ts +81 -0
- package/lib/utils/package-inputs.js +233 -0
- package/lib/utils/ship/changelog.d.ts +15 -14
- package/lib/utils/ship/changelog.js +14 -13
- package/lib/utils/ship/formats.js +23 -2
- package/lib/utils/ship/gzip.d.ts +31 -2
- package/lib/utils/ship/gzip.js +55 -3
- package/lib/utils/ship/plan.d.ts +32 -4
- package/lib/utils/ship/plan.js +61 -10
- package/lib/utils/simple-command.d.ts +17 -2
- package/lib/utils/simple-command.js +9 -4
- package/package.json +19 -19
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
// dependency, in topological build order, then in the target package.
|
|
15
15
|
// Skips deps that don't declare the script (`--if-present` behaviour).
|
|
16
16
|
// Stops on the first failure unless `--continue-on-error` is passed.
|
|
17
|
+
// A local dependency counts whether it is declared with the `workspace:`
|
|
18
|
+
// protocol or with a plain range the local member satisfies (@gjsify/workspace
|
|
19
|
+
// § buildDependencyGraph) — and the expansion SAYS when it found nothing.
|
|
17
20
|
//
|
|
18
21
|
// This replaces the manual `gjsify workspace <A> build && gjsify workspace
|
|
19
22
|
// <B> build && …` chains in root `build:infra` scripts: cascade build
|
|
20
23
|
// failures on uninbuilt workspace deps are the single biggest local-dev
|
|
21
24
|
// pain point in this monorepo.
|
|
22
25
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
23
|
-
import { buildDependencyGraph, discoverWorkspaces, topologicalSort } from '@gjsify/workspace';
|
|
26
|
+
import { affectedClosure, buildDependencyGraph, discoverWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
24
27
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
25
28
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
26
29
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
@@ -135,11 +138,29 @@ export const workspaceCommand = {
|
|
|
135
138
|
// target itself.
|
|
136
139
|
let runList;
|
|
137
140
|
if (withDeps) {
|
|
138
|
-
const
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
141
|
+
const includeDev = args['include-dev'] === true;
|
|
142
|
+
// The SHARED graph, walked by the shared closure — the same two calls
|
|
143
|
+
// `gjsify foreach --with-dependencies` makes. This used to be a private
|
|
144
|
+
// copy of the walk here, and the copy is where the defect lived: it
|
|
145
|
+
// followed `workspace:` edges only, so in a monorepo declaring its local
|
|
146
|
+
// deps by plain semver range it matched nothing, built nothing and exited
|
|
147
|
+
// 0 (#1587). One walk means the rule for "what is a local dependency" has
|
|
148
|
+
// one place to be wrong in.
|
|
149
|
+
const forward = buildDependencyGraph(allWorkspaces, { includeDev });
|
|
150
|
+
const names = affectedClosure(forward, [target.name]);
|
|
151
|
+
const closure = allWorkspaces.filter((ws) => names.has(ws.name));
|
|
152
|
+
reportUnlinked(forward.unlinked, names);
|
|
153
|
+
const graph = buildDependencyGraph(closure, { includeDev });
|
|
142
154
|
runList = topologicalSort(graph);
|
|
155
|
+
if (runList.length === 1) {
|
|
156
|
+
// The expansion found nothing, and it SAYS so. A leaf package
|
|
157
|
+
// legitimately has no local dependencies, but "no local dependencies"
|
|
158
|
+
// and "the flag could not see them" produced the same silent exit 0,
|
|
159
|
+
// and that ambiguity is what cost a Flatpak build, a container run and
|
|
160
|
+
// a manual `ls` to resolve.
|
|
161
|
+
console.error(`gjsify workspace: --with-dependencies found no workspace dependencies of ` +
|
|
162
|
+
`"${target.name}" — running "${script}" in it alone.`);
|
|
163
|
+
}
|
|
143
164
|
}
|
|
144
165
|
else {
|
|
145
166
|
runList = [target];
|
|
@@ -197,48 +218,30 @@ export const workspaceCommand = {
|
|
|
197
218
|
},
|
|
198
219
|
};
|
|
199
220
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
221
|
+
* Name a dependency that points at a local workspace but does not link to it, because
|
|
222
|
+
* the member's version does not satisfy the declared range.
|
|
223
|
+
*
|
|
224
|
+
* Only entries declared BY a package in the closure are printed: an unlinked pair
|
|
225
|
+
* elsewhere in the monorepo is somebody else's business, and printing all of them would
|
|
226
|
+
* bury the one that explains why this closure is the size it is.
|
|
227
|
+
*
|
|
228
|
+
* This is a warning, not a failure, and it deliberately asserts NO outcome, because the
|
|
229
|
+
* outcome depends on which installer placed `node_modules`: npm and yarn fetch such a
|
|
230
|
+
* dependency from the registry — the rule the graph follows, under which leaving it out of
|
|
231
|
+
* the build order is simply right — while `gjsify install` symlinks the local member
|
|
232
|
+
* whatever the range says. Under that one the build order really is short a package, so
|
|
233
|
+
* the message asks for the range and the version to be brought into agreement instead of
|
|
234
|
+
* telling the reader where the files came from.
|
|
204
235
|
*/
|
|
205
|
-
function
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
byName.set(ws.name, ws);
|
|
209
|
-
const seen = new Set();
|
|
210
|
-
const out = [];
|
|
211
|
-
const stack = [target];
|
|
212
|
-
while (stack.length > 0) {
|
|
213
|
-
const ws = stack.pop();
|
|
214
|
-
if (seen.has(ws.name))
|
|
236
|
+
function reportUnlinked(unlinked, closure) {
|
|
237
|
+
for (const entry of unlinked) {
|
|
238
|
+
if (!closure.has(entry.from))
|
|
215
239
|
continue;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
m.dependencies,
|
|
221
|
-
includeDev ? m.devDependencies : undefined,
|
|
222
|
-
m.optionalDependencies,
|
|
223
|
-
// peerDependencies excluded by default (yarn does the same)
|
|
224
|
-
];
|
|
225
|
-
for (const block of blocks) {
|
|
226
|
-
if (!block)
|
|
227
|
-
continue;
|
|
228
|
-
for (const [depName, spec] of Object.entries(block)) {
|
|
229
|
-
if (typeof spec !== 'string')
|
|
230
|
-
continue;
|
|
231
|
-
if (!spec.startsWith('workspace:'))
|
|
232
|
-
continue;
|
|
233
|
-
const dep = byName.get(depName);
|
|
234
|
-
if (!dep)
|
|
235
|
-
continue;
|
|
236
|
-
if (!seen.has(dep.name))
|
|
237
|
-
stack.push(dep);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
+
console.error(`gjsify workspace: "${entry.from}" depends on "${entry.to}": ${entry.spec}, which the local ` +
|
|
241
|
+
`workspace (${entry.to}@${entry.version}) does not satisfy — NOT built as a dependency. ` +
|
|
242
|
+
`npm and yarn take that one from the registry; \`gjsify install\` links the local one ` +
|
|
243
|
+
`anyway, so if it is the local one you mean, make the range admit ${entry.version}.`);
|
|
240
244
|
}
|
|
241
|
-
return out;
|
|
242
245
|
}
|
|
243
246
|
async function runOne(ws, script, extraArgs, runner, verbose, cache) {
|
|
244
247
|
// Build-cache fast path: on a key hit restore the stored outputs and
|
package/lib/config.js
CHANGED
|
@@ -5,6 +5,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
5
5
|
import { isGjs, hostRuntime } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
6
6
|
import { buildAppForRuntime } from './utils/runtimes.js';
|
|
7
7
|
import { DIALECT_APPS, isSourceDialect, SOURCE_DIALECTS } from '@gjsify/rolldown-plugin-gjsify';
|
|
8
|
+
import { GI_RENDERER_APPS } from '@gjsify/resolve-npm';
|
|
8
9
|
/**
|
|
9
10
|
* Does the failed `import()` of a config look like a MODULE-RESOLUTION failure
|
|
10
11
|
* (the config imports something GJS's ESM loader can't resolve for an external
|
|
@@ -269,6 +270,8 @@ export class Config {
|
|
|
269
270
|
configData.exclude = cliArgs.exclude || [];
|
|
270
271
|
if (cliArgs.consoleShim !== undefined)
|
|
271
272
|
configData.consoleShim = cliArgs.consoleShim;
|
|
273
|
+
if (cliArgs.giRenderer !== undefined)
|
|
274
|
+
configData.giRenderer = cliArgs.giRenderer;
|
|
272
275
|
// `--dialect` narrows here rather than at the call site. yargs `choices`
|
|
273
276
|
// already rejects an unknown value typed on the command line, but a
|
|
274
277
|
// `gjsify.dialect` in package.json or `.gjsifyrc.*` never passes through
|
|
@@ -303,6 +306,16 @@ export class Config {
|
|
|
303
306
|
`It composes on ${[...DIALECT_APPS].join(' and ')} only. ` +
|
|
304
307
|
'Drop the dialect, or build one of those targets.');
|
|
305
308
|
}
|
|
309
|
+
// Same rule for the `gi://` arm, and the same reason: `--gi-renderer --app gjs`
|
|
310
|
+
// would parse, resolve and compose nothing, so the flag would be accepted and
|
|
311
|
+
// ignored. On `gjs` and `node` the specifier is already answered — natively and
|
|
312
|
+
// through `requireGi` — so there is nothing for a renderer to substitute for.
|
|
313
|
+
if (configData.giRenderer === true && !GI_RENDERER_APPS.includes(configData.app)) {
|
|
314
|
+
throw new Error(`gjsify build: --gi-renderer has no effect on --app ${configData.app}. ` +
|
|
315
|
+
`It composes on ${GI_RENDERER_APPS.join(' and ')} only — the targets with no GObject ` +
|
|
316
|
+
'introspection to answer `gi://` with. `--app gjs` resolves it natively and `--app node` ' +
|
|
317
|
+
'through @gjsify/node-gi, so neither needs a widget renderer standing in for it.');
|
|
318
|
+
}
|
|
306
319
|
if (cliArgs.globals !== undefined)
|
|
307
320
|
configData.globals = cliArgs.globals;
|
|
308
321
|
// Fallback applied post-merge (not as a yargs `default:`) so a
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// This file is just the executable wrapper around `runCli`.
|
|
7
7
|
import { hideBin } from 'yargs/helpers';
|
|
8
8
|
import { runCli } from './cli-app.js';
|
|
9
|
+
import { exitOnReportedFailure } from './utils/cli-exit.js';
|
|
9
10
|
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
10
11
|
try {
|
|
11
12
|
await runCli(hideBin(process.argv));
|
|
@@ -43,3 +44,12 @@ catch (err) {
|
|
|
43
44
|
process.exitCode = 1;
|
|
44
45
|
gjsExit(1);
|
|
45
46
|
}
|
|
47
|
+
// The catch above is only HALF the entry's exit funnel, and the other half was
|
|
48
|
+
// missing: a handler that reports a failure by assigning `process.exitCode` and
|
|
49
|
+
// returning normally never reaches it. That is the Node idiom and the CLI uses
|
|
50
|
+
// it wherever the failure is already printed in full (`gjsify gresource`,
|
|
51
|
+
// `gjsify gsettings`, `gjsify barrels --check`, `setOxcExitCode`) — and under
|
|
52
|
+
// GJS, which has no atexit hook, nothing read it back, so the process ended 0
|
|
53
|
+
// with the error on stderr. `cli-exit.ts` carries the measurement and why `&&`
|
|
54
|
+
// did not protect against it (JumpLink/Learn6502#180).
|
|
55
|
+
exitOnReportedFailure();
|
|
@@ -36,6 +36,12 @@ export interface CliBuildOptions {
|
|
|
36
36
|
* whatever was typed, and the narrowing happens once, where the value is read.
|
|
37
37
|
*/
|
|
38
38
|
dialect?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve `gi://Ns?version=X` to the target's widget renderer instead of an empty
|
|
41
|
+
* module — `--gi-renderer` (ADR 0034 stage 9). `browser` and `nativescript` targets.
|
|
42
|
+
* Default: unset; see `ConfigData.giRenderer` for why it is opt-in.
|
|
43
|
+
*/
|
|
44
|
+
giRenderer?: boolean;
|
|
39
45
|
/**
|
|
40
46
|
* Comma-separated global identifiers the code needs, e.g.
|
|
41
47
|
* `"fetch,Buffer,process,URL,crypto"`. Each maps to the corresponding
|
|
@@ -96,6 +96,20 @@ export interface ConfigData {
|
|
|
96
96
|
* see the header of `SourceDialect`.
|
|
97
97
|
*/
|
|
98
98
|
dialect?: SourceDialect;
|
|
99
|
+
/**
|
|
100
|
+
* Resolve `gi://Ns?version=X` to the target's widget renderer rather than to an
|
|
101
|
+
* empty module (ADR 0034 stage 9). `--app browser` answers out of
|
|
102
|
+
* `@gjsify/adwaita-web`, `--app nativescript` out of `@gjsify/adwaita-nativescript`;
|
|
103
|
+
* `GI_RENDERERS` in `@gjsify/resolve-npm` is the table, and a target with no row
|
|
104
|
+
* there has no arm. Default: unset.
|
|
105
|
+
*
|
|
106
|
+
* Opt-in, never inferred: a `gi://` import reaching one of those targets today is
|
|
107
|
+
* usually a GJS-only code path pulled in transitively, which the empty module
|
|
108
|
+
* silences on purpose. On by default it would pull a widget toolkit into bundles
|
|
109
|
+
* that never named one, and turn every unanswerable namespace from a silent stub
|
|
110
|
+
* into a hard build failure.
|
|
111
|
+
*/
|
|
112
|
+
giRenderer?: boolean;
|
|
99
113
|
/** Comma-separated global identifiers to register. Format: see CliBuildOptions. */
|
|
100
114
|
globals?: string;
|
|
101
115
|
/**
|
|
@@ -11,9 +11,19 @@ import { type Workspace } from '@gjsify/workspace';
|
|
|
11
11
|
* still on v2's whole-dir ownership and could restore over a sibling unit; a v2
|
|
12
12
|
* manifest for them records `lib`, so it must not be replayed against the
|
|
13
13
|
* per-child units this version stores.
|
|
14
|
+
*
|
|
15
|
+
* v4: the input set is now `packageBuildInputs` instead of `src/**` + the
|
|
16
|
+
* manifests (#1651). Every v3 key was computed over a strictly narrower set,
|
|
17
|
+
* so replaying one would report a hit for a change it could not have seen.
|
|
18
|
+
*/
|
|
19
|
+
export declare const BUILD_CACHE_LAYOUT_VERSION = "v4";
|
|
20
|
+
/**
|
|
21
|
+
* Conventional per-package build output directories, checked in this order.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately the same constant the input definition subtracts: a directory
|
|
24
|
+
* this cache STORES as an output must never also be hashed as an input, or
|
|
25
|
+
* storing it would invalidate the key it was stored under.
|
|
14
26
|
*/
|
|
15
|
-
export declare const BUILD_CACHE_LAYOUT_VERSION = "v3";
|
|
16
|
-
/** Conventional per-package build output directories, checked in this order. */
|
|
17
27
|
export declare const OUTPUT_DIR_CANDIDATES: readonly ["lib", "dist", "dist-templates"];
|
|
18
28
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
19
29
|
export declare const MAX_KEYS_PER_PACKAGE = 2;
|
|
@@ -45,8 +55,9 @@ export declare function sanitizePackageDirName(name: string): string;
|
|
|
45
55
|
/** Streamed sha256 of one file (bounded memory, real contents). */
|
|
46
56
|
export declare function hashFileStream(path: string): string;
|
|
47
57
|
/**
|
|
48
|
-
* Content hash of a package's build INPUTS
|
|
49
|
-
*
|
|
58
|
+
* Content hash of a package's build INPUTS — the set is
|
|
59
|
+
* {@link packageBuildInputs}, shared with `gjsify test`'s freshness check so
|
|
60
|
+
* the two cannot drift about what an input is. Sorted, content-streamed:
|
|
50
61
|
* mtimes do not participate, so a `git checkout` with identical contents
|
|
51
62
|
* hashes identically across machines.
|
|
52
63
|
*/
|
package/lib/utils/build-cache.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// - a toolchain salt (workspace-resolved versions of @gjsify/cli,
|
|
12
12
|
// @gjsify/tsc, rolldown, typescript — a bundler/compiler bump must
|
|
13
13
|
// invalidate everything),
|
|
14
|
-
// - the package's own input hash
|
|
15
|
-
//
|
|
16
|
-
// contents, streamed sha256, sorted for determinism
|
|
14
|
+
// - the package's own input hash — `packageBuildInputs`, the ONE definition
|
|
15
|
+
// of "what are this package's build inputs" (`utils/package-inputs.ts`),
|
|
16
|
+
// hashed by real file contents, streamed sha256, sorted for determinism,
|
|
17
17
|
// - the input hashes of EVERY workspace dependency in the package's
|
|
18
18
|
// transitive dep closure (prod + dev + optional + peer `workspace:`
|
|
19
19
|
// edges). Composing the closure's OWN hashes (instead of recursing into
|
|
@@ -54,6 +54,7 @@ import { dirname, join } from 'node:path';
|
|
|
54
54
|
import { fileURLToPath } from 'node:url';
|
|
55
55
|
import { buildDependencyGraph } from '@gjsify/workspace';
|
|
56
56
|
import { replicateLinkSync } from './dir-link.js';
|
|
57
|
+
import { CONVENTIONAL_OUTPUT_DIRS, packageBuildInputs } from './package-inputs.js';
|
|
57
58
|
/**
|
|
58
59
|
* Bump to invalidate every existing cache entry (schema/semantic changes).
|
|
59
60
|
*
|
|
@@ -66,10 +67,20 @@ import { replicateLinkSync } from './dir-link.js';
|
|
|
66
67
|
* still on v2's whole-dir ownership and could restore over a sibling unit; a v2
|
|
67
68
|
* manifest for them records `lib`, so it must not be replayed against the
|
|
68
69
|
* per-child units this version stores.
|
|
70
|
+
*
|
|
71
|
+
* v4: the input set is now `packageBuildInputs` instead of `src/**` + the
|
|
72
|
+
* manifests (#1651). Every v3 key was computed over a strictly narrower set,
|
|
73
|
+
* so replaying one would report a hit for a change it could not have seen.
|
|
69
74
|
*/
|
|
70
|
-
export const BUILD_CACHE_LAYOUT_VERSION = '
|
|
71
|
-
/**
|
|
72
|
-
|
|
75
|
+
export const BUILD_CACHE_LAYOUT_VERSION = 'v4';
|
|
76
|
+
/**
|
|
77
|
+
* Conventional per-package build output directories, checked in this order.
|
|
78
|
+
*
|
|
79
|
+
* Deliberately the same constant the input definition subtracts: a directory
|
|
80
|
+
* this cache STORES as an output must never also be hashed as an input, or
|
|
81
|
+
* storing it would invalidate the key it was stored under.
|
|
82
|
+
*/
|
|
83
|
+
export const OUTPUT_DIR_CANDIDATES = CONVENTIONAL_OUTPUT_DIRS;
|
|
73
84
|
/** Keep at most this many keys per package; evict the least recently stored/used. */
|
|
74
85
|
export const MAX_KEYS_PER_PACKAGE = 2;
|
|
75
86
|
/** Toolchain packages whose resolved versions salt every cache key. */
|
|
@@ -113,11 +124,50 @@ export function hashFileStream(path) {
|
|
|
113
124
|
}
|
|
114
125
|
return hash.digest('hex');
|
|
115
126
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Content hash of a package's build INPUTS — the set is
|
|
129
|
+
* {@link packageBuildInputs}, shared with `gjsify test`'s freshness check so
|
|
130
|
+
* the two cannot drift about what an input is. Sorted, content-streamed:
|
|
131
|
+
* mtimes do not participate, so a `git checkout` with identical contents
|
|
132
|
+
* hashes identically across machines.
|
|
133
|
+
*/
|
|
134
|
+
export function hashPackageInputs(location) {
|
|
135
|
+
const files = packageBuildInputs(location);
|
|
136
|
+
const hash = createHash('sha256');
|
|
137
|
+
hash.update(`gjsify-build-cache-inputs/${BUILD_CACHE_LAYOUT_VERSION}\n`);
|
|
138
|
+
for (const f of files) {
|
|
139
|
+
if (f.link !== undefined) {
|
|
140
|
+
// Symlink: try content (file links), fall back to the target path.
|
|
141
|
+
let content;
|
|
142
|
+
try {
|
|
143
|
+
content = hashFileStream(f.abs);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
content = `link:${f.link}`;
|
|
147
|
+
}
|
|
148
|
+
hash.update(`${f.rel} ${content}\n`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
hash.update(`${f.rel} ${hashFileStream(f.abs)}\n`);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Vanished mid-hash (concurrent edit) — record the absence so the
|
|
156
|
+
// key still changes vs. the file being present.
|
|
157
|
+
hash.update(`${f.rel} unreadable\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return hash.digest('hex');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Recursive, sorted walk of an OUTPUT tree, yielding `/`-separated relative
|
|
164
|
+
* paths. Directory symlinks are not followed (loop safety) — the link target
|
|
165
|
+
* string participates in the hash instead.
|
|
166
|
+
*
|
|
167
|
+
* Deliberately NOT `packageBuildInputs`: this one is handed a directory that
|
|
168
|
+
* the input walk subtracts, and it must report everything inside it, dot-files
|
|
169
|
+
* included — an emitted `.gresource` is part of what was built.
|
|
170
|
+
*/
|
|
121
171
|
function walkFiles(dir, prefix, out) {
|
|
122
172
|
let entries;
|
|
123
173
|
try {
|
|
@@ -140,11 +190,8 @@ function walkFiles(dir, prefix, out) {
|
|
|
140
190
|
continue;
|
|
141
191
|
}
|
|
142
192
|
if (st.isSymbolicLink()) {
|
|
143
|
-
// File symlinks: hash the resolved content below if readable;
|
|
144
|
-
// dir symlinks: record the target only.
|
|
145
193
|
try {
|
|
146
|
-
|
|
147
|
-
out.push({ rel, abs, link: target });
|
|
194
|
+
out.push({ rel, abs, link: readlinkSync(abs) });
|
|
148
195
|
}
|
|
149
196
|
catch {
|
|
150
197
|
/* dropped mid-walk */
|
|
@@ -158,58 +205,6 @@ function walkFiles(dir, prefix, out) {
|
|
|
158
205
|
}
|
|
159
206
|
}
|
|
160
207
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Content hash of a package's build INPUTS: every file under `src/**`, plus
|
|
163
|
-
* `package.json`, plus root `tsconfig*.json`. Sorted, content-streamed —
|
|
164
|
-
* mtimes do not participate, so a `git checkout` with identical contents
|
|
165
|
-
* hashes identically across machines.
|
|
166
|
-
*/
|
|
167
|
-
export function hashPackageInputs(location) {
|
|
168
|
-
const files = [];
|
|
169
|
-
const pkgJson = join(location, 'package.json');
|
|
170
|
-
if (existsSync(pkgJson))
|
|
171
|
-
files.push({ rel: 'package.json', abs: pkgJson });
|
|
172
|
-
let rootEntries = [];
|
|
173
|
-
try {
|
|
174
|
-
rootEntries = readdirSync(location);
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
/* unreadable package dir — hash degenerates to the empty set */
|
|
178
|
-
}
|
|
179
|
-
for (const entry of rootEntries.sort()) {
|
|
180
|
-
if (/^tsconfig.*\.json$/.test(entry)) {
|
|
181
|
-
files.push({ rel: entry, abs: join(location, entry) });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const srcDir = join(location, 'src');
|
|
185
|
-
if (existsSync(srcDir))
|
|
186
|
-
walkFiles(srcDir, 'src', files);
|
|
187
|
-
const hash = createHash('sha256');
|
|
188
|
-
hash.update(`gjsify-build-cache-inputs/${BUILD_CACHE_LAYOUT_VERSION}\n`);
|
|
189
|
-
for (const f of files) {
|
|
190
|
-
if (f.link !== undefined) {
|
|
191
|
-
// Symlink: try content (file links), fall back to the target path.
|
|
192
|
-
let content;
|
|
193
|
-
try {
|
|
194
|
-
content = hashFileStream(f.abs);
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
content = `link:${f.link}`;
|
|
198
|
-
}
|
|
199
|
-
hash.update(`${f.rel} ${content}\n`);
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
try {
|
|
203
|
-
hash.update(`${f.rel} ${hashFileStream(f.abs)}\n`);
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Vanished mid-hash (concurrent edit) — record the absence so the
|
|
207
|
-
// key still changes vs. the file being present.
|
|
208
|
-
hash.update(`${f.rel} unreadable\n`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return hash.digest('hex');
|
|
212
|
-
}
|
|
213
208
|
/**
|
|
214
209
|
* Mtime-independent content hash of a set of output dirs — used to skip the
|
|
215
210
|
* restore copy when the on-disk outputs already match the cached entry, and
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The status a settled CLI run still owes the shell, or `null` when it owes
|
|
3
|
+
* none.
|
|
4
|
+
*
|
|
5
|
+
* @param exitCode what `process.exitCode` holds now — `number | string | null |
|
|
6
|
+
* undefined`, the full set Node accepts for that property.
|
|
7
|
+
* @param isDaemon whether the command declared itself a daemon. A watch loop
|
|
8
|
+
* RESOLVES as soon as it is armed and keeps running, so its resolution is not
|
|
9
|
+
* an ending to exit on — the same rule `gjsify run`'s fast path applies, and
|
|
10
|
+
* the incident behind it (`gjsify run dev` killing the loop it had just
|
|
11
|
+
* started) is in `daemon-command.ts`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pendingFailureCode(exitCode: number | string | null | undefined, isDaemon: boolean): number | null;
|
|
14
|
+
/**
|
|
15
|
+
* End a settled CLI run on the status its handler reported, on both runtimes.
|
|
16
|
+
*
|
|
17
|
+
* No-op unless a handler actually reported a failure. On Node it is a no-op
|
|
18
|
+
* even then — `gjsExit` finds no `imports.system` and the runtime honours
|
|
19
|
+
* `process.exitCode` at natural shutdown by itself, which is why this does NOT
|
|
20
|
+
* reach for `process.exit()`: that would cut short the stdout flush Node's own
|
|
21
|
+
* idiom exists to allow.
|
|
22
|
+
*/
|
|
23
|
+
export declare function exitOnReportedFailure(): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The CLI has TWO ways to fail, and until now only one of them reached the shell
|
|
2
|
+
// under GJS.
|
|
3
|
+
//
|
|
4
|
+
// A command handler either THROWS — `index.ts` catches it, prints, and calls
|
|
5
|
+
// `gjsExit(1)` — or it REPORTS the failure by assigning `process.exitCode` and
|
|
6
|
+
// returning normally. The second shape is the documented Node idiom (streams
|
|
7
|
+
// flush, the process ends naturally) and the CLI uses it wherever a failure has
|
|
8
|
+
// already been printed in full: `gjsify gresource`/`gjsify gsettings` propagate
|
|
9
|
+
// the code `glib-compile-*` exited with, `gjsify barrels --check` reports drift,
|
|
10
|
+
// `setOxcExitCode` propagates a formatter's verdict.
|
|
11
|
+
//
|
|
12
|
+
// **Under GJS that assignment is a REPORT and nothing more.** There is no atexit
|
|
13
|
+
// hook, so nothing reads `process.exitCode` back at natural shutdown and the
|
|
14
|
+
// process ends 0. Measured on the v0.49.0 bundle, same argv, same broken input:
|
|
15
|
+
//
|
|
16
|
+
// $ node lib/index.js gresource broken.gresource.xml → exit 1
|
|
17
|
+
// $ gjs -m dist/cli.gjs.mjs gresource broken.gresource.xml → exit 0
|
|
18
|
+
// ("[gjsify gresource] glib-compile-resources failed (exit 1)" printed)
|
|
19
|
+
//
|
|
20
|
+
// and the same split for `gjsify gsettings` and `gjsify barrels --check`. The
|
|
21
|
+
// consequence is not a cosmetic status: a package script chaining on it runs the
|
|
22
|
+
// next step over an artifact that was never written —
|
|
23
|
+
//
|
|
24
|
+
// "build:resources": "gjsify gresource … && echo SECOND"
|
|
25
|
+
// → SECOND printed, script exits 0 (JumpLink/Learn6502#180)
|
|
26
|
+
//
|
|
27
|
+
// — which is exactly the shape `&&` is supposed to protect. `gjsify run`'s
|
|
28
|
+
// in-process fast path was already immune (it reads `process.exitCode` back
|
|
29
|
+
// itself, #1568), so the hole survived precisely in the SPAWNED command, i.e.
|
|
30
|
+
// in every chained script and every direct CI invocation.
|
|
31
|
+
//
|
|
32
|
+
// This module is the missing half of the entry's exit funnel, and a sibling of
|
|
33
|
+
// `force-exit.ts`: that one is for a caller that knows the code it wants NOW,
|
|
34
|
+
// this one for the status a settled run left behind. The rule is pure so it can
|
|
35
|
+
// be stated as a test without driving a process to its death.
|
|
36
|
+
//
|
|
37
|
+
// Related, and deliberately distinct: `gjsify/deferred-process-exit` (oxlint)
|
|
38
|
+
// covers the OTHER GJS exit trap, a bare `process.exit()` that does not halt.
|
|
39
|
+
// No lint rule could have caught this one — every `process.exitCode = …` site
|
|
40
|
+
// is correct; the entry was not reading them.
|
|
41
|
+
import { gjsExit } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
42
|
+
import { isDaemonCommand } from './daemon-command.js';
|
|
43
|
+
/**
|
|
44
|
+
* The status a settled CLI run still owes the shell, or `null` when it owes
|
|
45
|
+
* none.
|
|
46
|
+
*
|
|
47
|
+
* @param exitCode what `process.exitCode` holds now — `number | string | null |
|
|
48
|
+
* undefined`, the full set Node accepts for that property.
|
|
49
|
+
* @param isDaemon whether the command declared itself a daemon. A watch loop
|
|
50
|
+
* RESOLVES as soon as it is armed and keeps running, so its resolution is not
|
|
51
|
+
* an ending to exit on — the same rule `gjsify run`'s fast path applies, and
|
|
52
|
+
* the incident behind it (`gjsify run dev` killing the loop it had just
|
|
53
|
+
* started) is in `daemon-command.ts`.
|
|
54
|
+
*/
|
|
55
|
+
export function pendingFailureCode(exitCode, isDaemon) {
|
|
56
|
+
if (isDaemon)
|
|
57
|
+
return null;
|
|
58
|
+
if (exitCode === null || exitCode === undefined)
|
|
59
|
+
return null;
|
|
60
|
+
const code = Number(exitCode);
|
|
61
|
+
// A non-finite value cannot be assigned to `process.exitCode` on Node at
|
|
62
|
+
// all, so there is no failure here to invent one from.
|
|
63
|
+
return Number.isFinite(code) && code !== 0 ? code : null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* End a settled CLI run on the status its handler reported, on both runtimes.
|
|
67
|
+
*
|
|
68
|
+
* No-op unless a handler actually reported a failure. On Node it is a no-op
|
|
69
|
+
* even then — `gjsExit` finds no `imports.system` and the runtime honours
|
|
70
|
+
* `process.exitCode` at natural shutdown by itself, which is why this does NOT
|
|
71
|
+
* reach for `process.exit()`: that would cut short the stdout flush Node's own
|
|
72
|
+
* idiom exists to allow.
|
|
73
|
+
*/
|
|
74
|
+
export function exitOnReportedFailure() {
|
|
75
|
+
const code = pendingFailureCode(process.exitCode, isDaemonCommand());
|
|
76
|
+
if (code !== null)
|
|
77
|
+
gjsExit(code);
|
|
78
|
+
}
|
|
@@ -233,6 +233,37 @@ export declare function resolveHostLibc(input: {
|
|
|
233
233
|
* reach it.
|
|
234
234
|
*/
|
|
235
235
|
export declare function detectHostLibc(platform: string): HostLibc | null;
|
|
236
|
+
/**
|
|
237
|
+
* The packages a musl host had to take the DEFAULT prebuild of, because they ship
|
|
238
|
+
* no `-musl` directory — the fallback {@link hostPlatformTokens} deliberately
|
|
239
|
+
* allows, named so a caller can report it. Returns names and prints nothing: this
|
|
240
|
+
* module resolves, and `libc` is a parameter for the same reason it is on every
|
|
241
|
+
* other function here (there is no musl runner in this repo's CI, so an injected
|
|
242
|
+
* `libc: 'musl'` is the only way the branch executes).
|
|
243
|
+
*
|
|
244
|
+
* Why a fallback that mostly WORKS is worth naming. musl's loader resolves
|
|
245
|
+
* `libc.so.6`/`libm.so.6` to itself, so a glibc-linked prebuild loads — and a
|
|
246
|
+
* bridge that records no libc at all then runs correctly, which is why the
|
|
247
|
+
* fallback exists rather than resolving to null. The one that is NOT agnostic dies
|
|
248
|
+
* at `dlopen` on its first glibc-only symbol, far from its cause. Measured on a
|
|
249
|
+
* OnePlus 6 (postmarketOS v26.06, musl 1.2.6, aarch64, gjs 1.88.1) against the
|
|
250
|
+
* published 0.48.0 train: nine of ten prebuilds loaded, while
|
|
251
|
+
* `@gjsify/lightningcss-native-linux-arm64` answered `gnu_get_libc_version: symbol
|
|
252
|
+
* not found`. `gjsify install` had printed "System dependencies OK", and the first
|
|
253
|
+
* symptom was rolldown reporting `Could not load src/application.css` — the CSS
|
|
254
|
+
* bridge silently absent from the bundle. Nothing in that chain names libc, which
|
|
255
|
+
* is what makes the report worth more than the failure.
|
|
256
|
+
*
|
|
257
|
+
* Membership goes through {@link parsePlatformToken}, not the suffix constant, so
|
|
258
|
+
* this does not become a second reader of the libc half of the token grammar —
|
|
259
|
+
* `darwin-arm64-musl` is a malformed token rather than a musl build, and only the
|
|
260
|
+
* grammar knows that.
|
|
261
|
+
*
|
|
262
|
+
* TODO(open-todos: `-musl` prebuild package is published): naming the fallback is
|
|
263
|
+
* not fixing it. No `-musl` target is published for any bridge, so on a musl host
|
|
264
|
+
* this reports every prebuild in the tree and there is nothing to fall back FROM.
|
|
265
|
+
*/
|
|
266
|
+
export declare function muslPrebuildFallbacks(libc: HostLibc | null, packages: readonly NativePackage[]): string[];
|
|
236
267
|
/**
|
|
237
268
|
* The environment-variable name the host's dynamic loader consults for an
|
|
238
269
|
* additional shared-library search path, plus the separator its search paths
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// win32 AND musl branches unit-testable from a glibc Linux host. The host values
|
|
23
23
|
// are read only at the outermost call site, as a default.
|
|
24
24
|
import { readdirSync, existsSync } from 'node:fs';
|
|
25
|
-
import { join, resolve } from 'node:path';
|
|
25
|
+
import { basename, join, resolve } from 'node:path';
|
|
26
26
|
import { readPackageJson } from './pkg-json.js';
|
|
27
27
|
import { composeDyldFallback, systemGiLibraryDirs } from './system-gi.js';
|
|
28
28
|
/**
|
|
@@ -500,6 +500,42 @@ export function detectHostLibc(platform) {
|
|
|
500
500
|
const muslLoaderPresent = existsSync('/lib') && readdirSync('/lib').some((f) => f.startsWith('ld-musl-'));
|
|
501
501
|
return resolveHostLibc({ platform, glibcVersionRuntime, muslLoaderPresent });
|
|
502
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* The packages a musl host had to take the DEFAULT prebuild of, because they ship
|
|
505
|
+
* no `-musl` directory — the fallback {@link hostPlatformTokens} deliberately
|
|
506
|
+
* allows, named so a caller can report it. Returns names and prints nothing: this
|
|
507
|
+
* module resolves, and `libc` is a parameter for the same reason it is on every
|
|
508
|
+
* other function here (there is no musl runner in this repo's CI, so an injected
|
|
509
|
+
* `libc: 'musl'` is the only way the branch executes).
|
|
510
|
+
*
|
|
511
|
+
* Why a fallback that mostly WORKS is worth naming. musl's loader resolves
|
|
512
|
+
* `libc.so.6`/`libm.so.6` to itself, so a glibc-linked prebuild loads — and a
|
|
513
|
+
* bridge that records no libc at all then runs correctly, which is why the
|
|
514
|
+
* fallback exists rather than resolving to null. The one that is NOT agnostic dies
|
|
515
|
+
* at `dlopen` on its first glibc-only symbol, far from its cause. Measured on a
|
|
516
|
+
* OnePlus 6 (postmarketOS v26.06, musl 1.2.6, aarch64, gjs 1.88.1) against the
|
|
517
|
+
* published 0.48.0 train: nine of ten prebuilds loaded, while
|
|
518
|
+
* `@gjsify/lightningcss-native-linux-arm64` answered `gnu_get_libc_version: symbol
|
|
519
|
+
* not found`. `gjsify install` had printed "System dependencies OK", and the first
|
|
520
|
+
* symptom was rolldown reporting `Could not load src/application.css` — the CSS
|
|
521
|
+
* bridge silently absent from the bundle. Nothing in that chain names libc, which
|
|
522
|
+
* is what makes the report worth more than the failure.
|
|
523
|
+
*
|
|
524
|
+
* Membership goes through {@link parsePlatformToken}, not the suffix constant, so
|
|
525
|
+
* this does not become a second reader of the libc half of the token grammar —
|
|
526
|
+
* `darwin-arm64-musl` is a malformed token rather than a musl build, and only the
|
|
527
|
+
* grammar knows that.
|
|
528
|
+
*
|
|
529
|
+
* TODO(open-todos: `-musl` prebuild package is published): naming the fallback is
|
|
530
|
+
* not fixing it. No `-musl` target is published for any bridge, so on a musl host
|
|
531
|
+
* this reports every prebuild in the tree and there is nothing to fall back FROM.
|
|
532
|
+
*/
|
|
533
|
+
export function muslPrebuildFallbacks(libc, packages) {
|
|
534
|
+
if (libc !== 'musl')
|
|
535
|
+
return [];
|
|
536
|
+
const fellBack = packages.filter((pkg) => parsePlatformToken(basename(pkg.prebuildsDir)).libc !== 'musl');
|
|
537
|
+
return fellBack.map((pkg) => pkg.name);
|
|
538
|
+
}
|
|
503
539
|
/**
|
|
504
540
|
* The environment-variable name the host's dynamic loader consults for an
|
|
505
541
|
* additional shared-library search path, plus the separator its search paths
|