@gjsify/cli 0.39.0 → 0.41.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 +202 -181
- package/lib/cli-app.js +12 -25
- package/lib/commands/create.d.ts +3 -0
- package/lib/commands/create.js +49 -6
- package/lib/commands/flatpak/check.js +1 -1
- package/lib/commands/flatpak/ci.js +1 -1
- package/lib/commands/flatpak/diff.js +1 -1
- package/lib/commands/flatpak/init.js +1 -1
- package/lib/commands/flatpak/scaffold.d.ts +2 -4
- package/lib/commands/flatpak/scaffold.js +25 -298
- package/lib/commands/flatpak/sync-flathub.js +1 -1
- package/lib/commands/foreach.js +34 -20
- package/lib/commands/index.d.ts +2 -0
- package/lib/commands/index.js +2 -0
- package/lib/commands/install.d.ts +1 -0
- package/lib/commands/install.js +32 -1
- package/lib/commands/prune.d.ts +11 -0
- package/lib/commands/prune.js +84 -0
- package/lib/commands/self-update.d.ts +1 -0
- package/lib/commands/self-update.js +55 -0
- package/lib/commands/ship.d.ts +11 -0
- package/lib/commands/ship.js +217 -0
- package/lib/config.d.ts +12 -0
- package/lib/config.js +15 -0
- package/lib/templates/install.mjs.tmpl +8 -3
- package/lib/types/config-data.d.ts +136 -46
- package/lib/utils/app-metadata.d.ts +51 -0
- package/lib/utils/app-metadata.js +338 -0
- package/lib/utils/check-system-deps.d.ts +2 -2
- package/lib/utils/check-system-deps.js +43 -6
- package/lib/utils/cli-runtime-closure.d.ts +23 -0
- package/lib/utils/cli-runtime-closure.js +274 -0
- package/lib/utils/install-provenance.d.ts +44 -0
- package/lib/utils/install-provenance.js +106 -0
- package/lib/utils/prune-prefix.d.ts +83 -0
- package/lib/utils/prune-prefix.js +333 -0
- package/lib/utils/publish-headers.d.ts +13 -0
- package/lib/utils/publish-headers.js +30 -4
- package/lib/utils/ship/ar.d.ts +21 -0
- package/lib/utils/ship/ar.js +57 -0
- package/lib/utils/ship/bytes.d.ts +4 -0
- package/lib/utils/ship/bytes.js +23 -0
- package/lib/utils/ship/cpio.d.ts +22 -0
- package/lib/utils/ship/cpio.js +93 -0
- package/lib/utils/ship/deb.d.ts +24 -0
- package/lib/utils/ship/deb.js +118 -0
- package/lib/utils/ship/depends.d.ts +62 -0
- package/lib/utils/ship/depends.js +221 -0
- package/lib/utils/ship/discover.d.ts +21 -0
- package/lib/utils/ship/discover.js +129 -0
- package/lib/utils/ship/formats.d.ts +5 -0
- package/lib/utils/ship/formats.js +83 -0
- package/lib/utils/ship/gi-namespaces.d.ts +7 -0
- package/lib/utils/ship/gi-namespaces.js +49 -0
- package/lib/utils/ship/gzip.d.ts +2 -0
- package/lib/utils/ship/gzip.js +21 -0
- package/lib/utils/ship/launcher.d.ts +7 -0
- package/lib/utils/ship/launcher.js +37 -0
- package/lib/utils/ship/payload.d.ts +30 -0
- package/lib/utils/ship/payload.js +59 -0
- package/lib/utils/ship/plan.d.ts +34 -0
- package/lib/utils/ship/plan.js +167 -0
- package/lib/utils/ship/rpm-header.d.ts +29 -0
- package/lib/utils/ship/rpm-header.js +170 -0
- package/lib/utils/ship/rpm.d.ts +12 -0
- package/lib/utils/ship/rpm.js +377 -0
- package/lib/utils/ship/scripts.d.ts +31 -0
- package/lib/utils/ship/scripts.js +76 -0
- package/lib/utils/ship/settings.d.ts +60 -0
- package/lib/utils/ship/settings.js +184 -0
- package/lib/utils/ship/stage-writer.d.ts +17 -0
- package/lib/utils/ship/stage-writer.js +49 -0
- package/lib/utils/ship/types.d.ts +114 -0
- package/lib/utils/ship/types.js +10 -0
- package/lib/utils/ship/version.d.ts +17 -0
- package/lib/utils/ship/version.js +55 -0
- package/package.json +27 -18
- /package/lib/templates/{flatpak → app}/desktop.tmpl +0 -0
package/lib/commands/foreach.js
CHANGED
|
@@ -13,9 +13,15 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|
|
13
13
|
import { cpus } from 'node:os';
|
|
14
14
|
import { affectedClosure, buildDependencyGraph, discoverWorkspaces, filterWorkspaces, topologicalSort, } from '@gjsify/workspace';
|
|
15
15
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
16
|
+
import { cliRuntimeClosure } from '../utils/cli-runtime-closure.js';
|
|
16
17
|
import { prefixLines } from '../utils/prefixed-output.js';
|
|
17
18
|
import { BuildCacheRunner, buildCacheEnabledByEnv } from '../utils/build-cache.js';
|
|
18
|
-
|
|
19
|
+
// The ONE runner-selection rule, shared rather than re-derived. This file used to
|
|
20
|
+
// carry its own copy; `workspace.ts` grew the bootstrap-CLI branch and the copy
|
|
21
|
+
// here did not, so a `gjsify foreach` reached from a cold-tree bootstrap would
|
|
22
|
+
// have delegated to npm and lost the shim one level down — silently, and only on
|
|
23
|
+
// the OS legs that bootstrap cold. `onboard.ts` already imports it from here.
|
|
24
|
+
import { detectPackageManager } from './workspace.js';
|
|
19
25
|
// Every child spawned by spawnPrefixed registers here so fail-fast can
|
|
20
26
|
// terminate the whole in-flight set instead of waiting on it. On CI a
|
|
21
27
|
// single failed package used to leave foreach awaiting its still-running
|
|
@@ -425,20 +431,44 @@ export const foreachCommand = {
|
|
|
425
431
|
targets: selected.map((w) => w.name),
|
|
426
432
|
})
|
|
427
433
|
: undefined;
|
|
434
|
+
// The serial prefix: every child of this command BOOTS the CLI, so a
|
|
435
|
+
// package the CLI imports must not be written while siblings are starting
|
|
436
|
+
// (the torn `lib/esm` in `utils/cli-runtime-closure.ts`'s header). Building
|
|
437
|
+
// that closure first — alone, in the order the caller already sorted — is a
|
|
438
|
+
// REORDERING, not extra work: the prefix is removed from the parallel set,
|
|
439
|
+
// so the cost is only the parallelism given up over those few packages.
|
|
440
|
+
//
|
|
441
|
+
// Applied to every PARALLEL invocation, not only to `build`. Which scripts
|
|
442
|
+
// rewrite an output tree is not knowable from a script NAME — `build`,
|
|
443
|
+
// `build:types`, a package's own `dist` step and `clear` all do — and
|
|
444
|
+
// gating on the literal string "build" would be the hand-written list this
|
|
445
|
+
// closure exists to replace. A serial run needs no prefix at all: nothing
|
|
446
|
+
// else is in flight to read what it writes.
|
|
447
|
+
const prefixNames = args.parallel === true ? cliRuntimeClosure(allWorkspaces) : new Set();
|
|
448
|
+
const prefix = prefixNames.size > 0 ? selected.filter((ws) => prefixNames.has(ws.name)) : [];
|
|
449
|
+
const sweep = prefix.length > 0 ? selected.filter((ws) => !prefixNames.has(ws.name)) : selected;
|
|
428
450
|
try {
|
|
451
|
+
if (prefix.length > 0) {
|
|
452
|
+
console.error(`[gjsify foreach] serial prefix: ${prefix.length} package(s) the running CLI imports — ` +
|
|
453
|
+
`${prefix.map((ws) => ws.name).join(', ')}`);
|
|
454
|
+
for (const ws of prefix) {
|
|
455
|
+
console.error(`[gjsify foreach] start ${ws.name} (serial prefix)`);
|
|
456
|
+
await runOne(ws, finalCmd, cmdArgs, /* prefixOutput */ true, verbose, exec, cache);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
429
459
|
if (args.parallel && !args.topological && !args['topological-dev']) {
|
|
430
460
|
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
431
|
-
await runParallel(
|
|
461
|
+
await runParallel(sweep, finalCmd, cmdArgs, jobs, verbose, exec, cache);
|
|
432
462
|
}
|
|
433
463
|
else if (args.parallel) {
|
|
434
464
|
// Topological + parallel: each workspace starts as soon as its
|
|
435
465
|
// deps (in the selected set) have finished. Yarn calls this
|
|
436
466
|
// "topological order with concurrency"; we cap at --jobs.
|
|
437
467
|
const jobs = args.jobs && args.jobs > 0 ? args.jobs : cpus().length;
|
|
438
|
-
await runTopologicalParallel(
|
|
468
|
+
await runTopologicalParallel(sweep, finalCmd, cmdArgs, jobs, verbose, args['topological-dev'] === true, exec, cache);
|
|
439
469
|
}
|
|
440
470
|
else {
|
|
441
|
-
await runSequential(
|
|
471
|
+
await runSequential(sweep, finalCmd, cmdArgs, verbose, exec, cache);
|
|
442
472
|
}
|
|
443
473
|
}
|
|
444
474
|
catch (err) {
|
|
@@ -703,22 +733,6 @@ async function runOne(ws, script, args, prefixOutput, verbose, exec, cache) {
|
|
|
703
733
|
if (cache && before)
|
|
704
734
|
cache.storeAfterSuccess(ws, before);
|
|
705
735
|
}
|
|
706
|
-
function detectPackageManager() {
|
|
707
|
-
// Under GJS there is no Node — neither `npm` nor `yarn` can run scripts, so
|
|
708
|
-
// the runner is `gjsify` itself via the GJS shim on PATH
|
|
709
|
-
// (`ensureGjsifyShimOnPath`). Makes node-free `gjsify foreach` work.
|
|
710
|
-
if (isGjs())
|
|
711
|
-
return 'gjsify';
|
|
712
|
-
// `npm_config_user_agent` is set by npm/yarn/pnpm — first token is
|
|
713
|
-
// `<name>/<version>`. Reuse it so `gjsify foreach build` invoked
|
|
714
|
-
// through `yarn run` keeps using yarn, etc.
|
|
715
|
-
const ua = process.env.npm_config_user_agent ?? '';
|
|
716
|
-
if (ua.startsWith('yarn/'))
|
|
717
|
-
return 'yarn';
|
|
718
|
-
if (ua.startsWith('gjsify/'))
|
|
719
|
-
return 'gjsify';
|
|
720
|
-
return 'npm';
|
|
721
|
-
}
|
|
722
736
|
async function spawnPrefixed(cmd, args, cwd, prefix) {
|
|
723
737
|
// `completion: 'exit'` — every path out of this command's handler ends in
|
|
724
738
|
// `process.exit(…)` (see the handler's tail), which is what quits the GLib
|
package/lib/commands/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './gresource.js';
|
|
|
12
12
|
export * from './gettext.js';
|
|
13
13
|
export * from './gsettings.js';
|
|
14
14
|
export { flatpakCommand } from './flatpak/index.js';
|
|
15
|
+
export * from './ship.js';
|
|
15
16
|
export * from './dlx.js';
|
|
16
17
|
export * from './install.js';
|
|
17
18
|
export * from './foreach.js';
|
|
@@ -25,6 +26,7 @@ export * from './trust.js';
|
|
|
25
26
|
export * from './onboard.js';
|
|
26
27
|
export * from './self-update.js';
|
|
27
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
28
30
|
export * from './uninstall.js';
|
|
29
31
|
export * from './format.js';
|
|
30
32
|
export * from './lint.js';
|
package/lib/commands/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from './gresource.js';
|
|
|
12
12
|
export * from './gettext.js';
|
|
13
13
|
export * from './gsettings.js';
|
|
14
14
|
export { flatpakCommand } from './flatpak/index.js';
|
|
15
|
+
export * from './ship.js';
|
|
15
16
|
export * from './dlx.js';
|
|
16
17
|
export * from './install.js';
|
|
17
18
|
export * from './foreach.js';
|
|
@@ -25,6 +26,7 @@ export * from './trust.js';
|
|
|
25
26
|
export * from './onboard.js';
|
|
26
27
|
export * from './self-update.js';
|
|
27
28
|
export * from './generate-installer.js';
|
|
29
|
+
export * from './prune.js';
|
|
28
30
|
export * from './uninstall.js';
|
|
29
31
|
export * from './format.js';
|
|
30
32
|
export * from './lint.js';
|
package/lib/commands/install.js
CHANGED
|
@@ -32,6 +32,8 @@ import { nodeBinary } from '../utils/run-node.js';
|
|
|
32
32
|
import { spawnToCompletion } from '../utils/spawn.js';
|
|
33
33
|
import { findWorkspaceRoot } from '../utils/workspace-root.js';
|
|
34
34
|
import { binDirOnPath, BUNDLER_ENGINE_PACKAGE, defaultGlobalLayout, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, specToPackageName, } from '../utils/install-global.js';
|
|
35
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
36
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
35
37
|
import { addDependencyEntry, defaultRangeFromVersion, parseSpec, projectSpecsFromPackageJson, readPackageJson, writePackageJson, } from '../utils/pkg-json-edit.js';
|
|
36
38
|
/**
|
|
37
39
|
* Link type for the workspace↔workspace directory links.
|
|
@@ -125,6 +127,11 @@ export const installCommand = {
|
|
|
125
127
|
description: 'Install a REQUIRED dependency even when its os/cpu/libc excludes the target, instead of failing with EBADPLATFORM (npm config key `force`). Incompatible OPTIONAL dependencies stay skipped — npm ignores --force for those too, and a binary that cannot load is not worth downloading.',
|
|
126
128
|
type: 'boolean',
|
|
127
129
|
default: false,
|
|
130
|
+
})
|
|
131
|
+
.option('prune', {
|
|
132
|
+
description: 'After installing, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable. Skipped automatically under --immutable, and whenever --os/--cpu/--libc is given: an install must never delete against a target the user typed.',
|
|
133
|
+
type: 'boolean',
|
|
134
|
+
default: true,
|
|
128
135
|
}),
|
|
129
136
|
handler: async (args) => {
|
|
130
137
|
// Must run before anything resolves — `--os/--cpu/--libc/--force` are npm
|
|
@@ -161,7 +168,7 @@ export const installCommand = {
|
|
|
161
168
|
console.warn(`gjsify install --global ignores --${flag}: global installs do not modify a project package.json.`);
|
|
162
169
|
}
|
|
163
170
|
}
|
|
164
|
-
await installGlobalAndLink(args.packages, { verbose: args.verbose });
|
|
171
|
+
await installGlobalAndLink(args.packages, { verbose: args.verbose, prune: args.prune });
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
167
174
|
// Before the heavy install, so the skew is visible even if a later phase
|
|
@@ -170,6 +177,7 @@ export const installCommand = {
|
|
|
170
177
|
const backend = args.backend ?? process.env.GJSIFY_INSTALL_BACKEND ?? 'native';
|
|
171
178
|
if (backend === 'npm') {
|
|
172
179
|
await projectInstallViaNpm(args);
|
|
180
|
+
pruneProjectPrefix(args);
|
|
173
181
|
await runPostInstallChecks(args);
|
|
174
182
|
return;
|
|
175
183
|
}
|
|
@@ -217,6 +225,7 @@ export const installCommand = {
|
|
|
217
225
|
hardExitTimerId?.unref?.();
|
|
218
226
|
try {
|
|
219
227
|
await projectInstallNative(args, overallController?.signal);
|
|
228
|
+
pruneProjectPrefix(args);
|
|
220
229
|
await runPostInstallChecks(args);
|
|
221
230
|
}
|
|
222
231
|
catch (err) {
|
|
@@ -1198,6 +1207,14 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1198
1207
|
const cliVersion = result.installed.find((r) => r.name === '@gjsify/cli')?.version ?? 'latest';
|
|
1199
1208
|
await installGjsEnginePackages(layout.prefix, cliVersion, { verbose: opts.verbose });
|
|
1200
1209
|
}
|
|
1210
|
+
// Before the LINK on purpose: `linkGlobalBins`' launchers bake the resolved
|
|
1211
|
+
// prebuild directories into GI_TYPELIB_PATH/LD_LIBRARY_PATH, and must not name
|
|
1212
|
+
// a directory this pass is about to delete.
|
|
1213
|
+
if (opts.prune !== false) {
|
|
1214
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
1215
|
+
hint: 'gjsify prune -g --dry-run',
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1201
1218
|
const created = linkGlobalBins(packageNames, layout);
|
|
1202
1219
|
reportLinkedBins(created, layout.binDir);
|
|
1203
1220
|
}
|
|
@@ -1205,6 +1222,20 @@ async function installGlobalAndLink(specs, opts) {
|
|
|
1205
1222
|
lock.release();
|
|
1206
1223
|
}
|
|
1207
1224
|
}
|
|
1225
|
+
/**
|
|
1226
|
+
* The automatic pass for a PROJECT prefix.
|
|
1227
|
+
*
|
|
1228
|
+
* Hooked at the command level rather than inside `installPackages`, so `dlx`'s
|
|
1229
|
+
* throwaway cache prefix — created, used once and dropped — pays nothing for it.
|
|
1230
|
+
*/
|
|
1231
|
+
function pruneProjectPrefix(args) {
|
|
1232
|
+
if (args.prune === false)
|
|
1233
|
+
return;
|
|
1234
|
+
pruneAfterInstall(process.cwd(), resolveHostPlatform({ env: {} }), {
|
|
1235
|
+
immutable: args.immutable === true,
|
|
1236
|
+
hint: 'gjsify prune --dry-run',
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1208
1239
|
function reportLinkedBins(created, binDir) {
|
|
1209
1240
|
if (created.length === 0) {
|
|
1210
1241
|
console.warn('\nNo bins declared (neither `gjsify.bin` nor `bin` in package.json) — nothing was symlinked.');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface PruneOptions {
|
|
3
|
+
global: boolean;
|
|
4
|
+
dryRun: boolean;
|
|
5
|
+
verbose: boolean;
|
|
6
|
+
os?: string;
|
|
7
|
+
cpu?: string;
|
|
8
|
+
libc?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const pruneCommand: Command<unknown, PruneOptions>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// `gjsify prune` — remove installed packages this target cannot use.
|
|
2
|
+
//
|
|
3
|
+
// The repair half of ADR 0025. `install`/`self-update` prune what they would not
|
|
4
|
+
// have placed, but only going forward: a prefix that accreted foreign-platform
|
|
5
|
+
// packages under an older CLI is only reachable by asking. That is this command,
|
|
6
|
+
// and it is why it exists at all rather than the automatic pass alone.
|
|
7
|
+
//
|
|
8
|
+
// `--os/--cpu/--libc` are accepted here and deliberately NOT honoured by the
|
|
9
|
+
// automatic pass: typed on `prune` they are a request ("show me what a darwin-arm64
|
|
10
|
+
// host could not use"), while inherited by an install they would delete the host's
|
|
11
|
+
// own packages. See `automaticPruneRefusal`.
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { acquireInstallLock } from '../utils/install-lock.js';
|
|
14
|
+
import { defaultGlobalLayout } from '../utils/install-global.js';
|
|
15
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
16
|
+
import { executePrune, formatPruneReport, planPrune } from '../utils/prune-prefix.js';
|
|
17
|
+
export const pruneCommand = {
|
|
18
|
+
command: 'prune',
|
|
19
|
+
description: 'Remove installed packages this host cannot use (foreign os/cpu/libc). ' +
|
|
20
|
+
'Reads each package manifest — a package that declares no platform is never touched.',
|
|
21
|
+
builder: (yargs) => yargs
|
|
22
|
+
.option('global', {
|
|
23
|
+
description: 'Prune the user-global XDG prefix (the `install -g` target) instead of the project.',
|
|
24
|
+
type: 'boolean',
|
|
25
|
+
alias: 'g',
|
|
26
|
+
default: false,
|
|
27
|
+
})
|
|
28
|
+
.option('dry-run', {
|
|
29
|
+
description: 'Report what would be removed without touching the filesystem.',
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
default: false,
|
|
32
|
+
})
|
|
33
|
+
.option('verbose', {
|
|
34
|
+
description: 'List every package rather than the first few.',
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
})
|
|
38
|
+
.option('os', {
|
|
39
|
+
description: 'Prune AS IF the host were this OS (linux, darwin, win32). Defaults to this host.',
|
|
40
|
+
type: 'string',
|
|
41
|
+
})
|
|
42
|
+
.option('cpu', {
|
|
43
|
+
description: 'Prune AS IF the host were this architecture (x64, arm64, …). Defaults to this host.',
|
|
44
|
+
type: 'string',
|
|
45
|
+
})
|
|
46
|
+
.option('libc', {
|
|
47
|
+
description: 'Prune AS IF the host used this libc (glibc, musl). Defaults to this host.',
|
|
48
|
+
type: 'string',
|
|
49
|
+
}),
|
|
50
|
+
handler: async (args) => {
|
|
51
|
+
const layout = args.global ? defaultGlobalLayout() : null;
|
|
52
|
+
const prefix = layout ? layout.prefix : process.cwd();
|
|
53
|
+
if (!existsSync(`${prefix}/node_modules`)) {
|
|
54
|
+
console.log(`gjsify prune: nothing installed at ${prefix}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// The flags are read HERE rather than through the npm config keys, so the
|
|
58
|
+
// automatic pass — which refuses to act on an overridden target — cannot be
|
|
59
|
+
// reached by way of this command's own options.
|
|
60
|
+
const target = resolveHostPlatform({
|
|
61
|
+
env: {
|
|
62
|
+
...(args.os ? { npm_config_os: args.os } : {}),
|
|
63
|
+
...(args.cpu ? { npm_config_cpu: args.cpu } : {}),
|
|
64
|
+
...(args.libc ? { npm_config_libc: args.libc } : {}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// A prune racing an install's extract would delete a half-written tree. The
|
|
68
|
+
// lock is the same one every writer of this prefix takes (ADR 0001).
|
|
69
|
+
const lock = await acquireInstallLock(prefix, {});
|
|
70
|
+
let result;
|
|
71
|
+
try {
|
|
72
|
+
result = executePrune(planPrune({ prefix, target }), { dryRun: args.dryRun });
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
lock.release();
|
|
76
|
+
}
|
|
77
|
+
console.log(formatPruneReport(result, { dryRun: args.dryRun, verbose: args.verbose }));
|
|
78
|
+
// Removing nothing is a SUCCESS — idempotent housekeeping, unlike `uninstall`
|
|
79
|
+
// where naming a package that is not there is a mistake worth reporting. Only
|
|
80
|
+
// a removal the user asked for and did not get is a failure.
|
|
81
|
+
if (result.failed.length > 0)
|
|
82
|
+
return process.exit(1);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -25,7 +25,10 @@ import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
|
25
25
|
import { fileURLToPath } from 'node:url';
|
|
26
26
|
import { fetchPackument } from '@gjsify/npm-registry';
|
|
27
27
|
import { installPackages, makeProgressReporter } from '../utils/install-backend.js';
|
|
28
|
+
import { classifyInstall } from '../utils/install-provenance.js';
|
|
28
29
|
import { defaultGlobalLayout, globalLayoutIsDefault, hasBundlerEngineInstalled, installGjsEnginePackages, linkGlobalBins, resolveBinOnPath, } from '../utils/install-global.js';
|
|
30
|
+
import { resolveHostPlatform } from '../utils/platform-check.js';
|
|
31
|
+
import { pruneAfterInstall } from '../utils/prune-prefix.js';
|
|
29
32
|
const PACKAGE_NAME = '@gjsify/cli';
|
|
30
33
|
export const selfUpdateCommand = {
|
|
31
34
|
command: 'self-update',
|
|
@@ -45,6 +48,11 @@ export const selfUpdateCommand = {
|
|
|
45
48
|
description: 'npm dist-tag or pinned version to install (e.g. `latest`, `next`, `0.5.0`).',
|
|
46
49
|
type: 'string',
|
|
47
50
|
default: 'latest',
|
|
51
|
+
})
|
|
52
|
+
.option('prune', {
|
|
53
|
+
description: 'After updating, remove packages an earlier install left behind that this host cannot use (foreign os/cpu/libc). Use --no-prune to disable.',
|
|
54
|
+
type: 'boolean',
|
|
55
|
+
default: true,
|
|
48
56
|
})
|
|
49
57
|
.option('skip-deps', {
|
|
50
58
|
description: 'Update only the @gjsify/cli bundle itself; do not pull its runtime dependencies ' +
|
|
@@ -60,6 +68,24 @@ export const selfUpdateCommand = {
|
|
|
60
68
|
const currentVersion = readCurrentVersion();
|
|
61
69
|
const installedAtPrefix = existsSync(installedPkgJson);
|
|
62
70
|
console.log(`Current ${PACKAGE_NAME}: v${currentVersion ?? '(unknown)'}`);
|
|
71
|
+
// REFUSE, do not warn-and-continue, when something else owns this install.
|
|
72
|
+
//
|
|
73
|
+
// Everything below writes the user-global XDG prefix. Under a `.deb`,
|
|
74
|
+
// `.rpm` or Flatpak that produces a SECOND copy which shadows the one the
|
|
75
|
+
// package manager tracks — so the next `apt upgrade` reverts it, or the two
|
|
76
|
+
// diverge, and the user's report is "I updated and nothing changed". A
|
|
77
|
+
// clean refusal naming the right command is strictly more useful than a
|
|
78
|
+
// successful write to the wrong place; #1064 is the same lesson one layer
|
|
79
|
+
// down, where the write succeeded and PATH still resolved the old binary.
|
|
80
|
+
const provenance = classifyInstall({ selfDir: readSelfDir(), xdgPrefix: layout.prefix });
|
|
81
|
+
if (provenance.managedElsewhere) {
|
|
82
|
+
console.error(`\ngjsify self-update: this gjsify is managed by something else — ${provenance.evidence}.\n` +
|
|
83
|
+
`Updating it from here would write ${layout.prefix} and leave you running two installs.\n\n` +
|
|
84
|
+
`Update with: ${provenance.updateWith}\n`);
|
|
85
|
+
// `return process.exit()` — a bare `process.exit()` is DEFERRED under
|
|
86
|
+
// GJS and the handler would run on into the install it just refused.
|
|
87
|
+
return process.exit(1);
|
|
88
|
+
}
|
|
63
89
|
if (!installedAtPrefix) {
|
|
64
90
|
console.warn(`\nWarning: no @gjsify/cli install found under ${layout.prefix}.\n` +
|
|
65
91
|
`self-update only manages installs created by install.mjs or \`gjsify install -g\`.\n` +
|
|
@@ -157,6 +183,13 @@ export const selfUpdateCommand = {
|
|
|
157
183
|
if (pullDeps) {
|
|
158
184
|
await installGjsEnginePackages(layout.prefix, target, { verbose: false });
|
|
159
185
|
}
|
|
186
|
+
// Same ordering reason as `install -g`: before the LINK, whose launchers bake
|
|
187
|
+
// the resolved prebuild directories into their env.
|
|
188
|
+
if (args.prune !== false) {
|
|
189
|
+
pruneAfterInstall(layout.prefix, resolveHostPlatform({ env: {} }), {
|
|
190
|
+
hint: 'gjsify prune -g --dry-run',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
160
193
|
const linked = linkGlobalBins([PACKAGE_NAME], layout);
|
|
161
194
|
if (linked.length === 0) {
|
|
162
195
|
console.warn('self-update: install completed but no bins were linked — package.json may be missing a `bin` field.');
|
|
@@ -246,6 +279,28 @@ export function verifyPathResolution(opts) {
|
|
|
246
279
|
* `import.meta.url` (the bundle file under GJS, or `lib/index.js` under
|
|
247
280
|
* Node) until it finds a package.json with `name === '@gjsify/cli'`.
|
|
248
281
|
*/
|
|
282
|
+
/**
|
|
283
|
+
* The directory of the running CLI's own package — the input the provenance
|
|
284
|
+
* question is actually about.
|
|
285
|
+
*
|
|
286
|
+
* The same upward walk `readCurrentVersion()` does, and for the same reason: it
|
|
287
|
+
* works from `lib/index.js` under Node and from the published `dist/cli.gjs.mjs`
|
|
288
|
+
* bundle under GJS. Honours the same `GJSIFY_CLI_PACKAGE_JSON` escape hatch, so a
|
|
289
|
+
* test can place a synthetic install anywhere without a real one.
|
|
290
|
+
*/
|
|
291
|
+
function readSelfDir() {
|
|
292
|
+
const override = process.env.GJSIFY_CLI_PACKAGE_JSON;
|
|
293
|
+
if (override)
|
|
294
|
+
return dirname(resolve(override));
|
|
295
|
+
const here = fileURLToPath(import.meta.url);
|
|
296
|
+
let dir = dirname(resolve(here));
|
|
297
|
+
for (let i = 0; i < 8 && dir !== dirname(dir); i++) {
|
|
298
|
+
if (existsSync(join(dir, 'package.json')))
|
|
299
|
+
return dir;
|
|
300
|
+
dir = dirname(dir);
|
|
301
|
+
}
|
|
302
|
+
return dirname(resolve(here));
|
|
303
|
+
}
|
|
249
304
|
function readCurrentVersion() {
|
|
250
305
|
try {
|
|
251
306
|
// Escape hatch for tests: point GJSIFY_CLI_PACKAGE_JSON at a synthetic
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Command } from '../types/index.js';
|
|
2
|
+
interface ShipOptions {
|
|
3
|
+
target?: string[];
|
|
4
|
+
out?: string;
|
|
5
|
+
stage: boolean;
|
|
6
|
+
'skip-build': boolean;
|
|
7
|
+
arch?: string;
|
|
8
|
+
verbose: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare const shipCommand: Command<unknown, ShipOptions>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// `gjsify ship` — turn a built application into something a stranger can
|
|
2
|
+
// install (ADR 0024).
|
|
3
|
+
//
|
|
4
|
+
// One payload, produced once, then wrapped per format. The command itself is
|
|
5
|
+
// thin on purpose: every decision worth testing lives in `utils/ship/*` as a
|
|
6
|
+
// pure function, and what is left here is filesystem work and printing.
|
|
7
|
+
//
|
|
8
|
+
// Scope today is Linux and `--app gjs`: no runtime is bundled, because GJS and
|
|
9
|
+
// GTK come from the distribution and shipping ~100 MiB of them would be cargo
|
|
10
|
+
// cult (ADR 0024 § 4). macOS and Windows layouts, and Flatpak as a target under
|
|
11
|
+
// this command, are the later stages of the same ADR.
|
|
12
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { basename, join, relative, resolve } from 'node:path';
|
|
14
|
+
import { Config } from '../config.js';
|
|
15
|
+
import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
|
|
16
|
+
import { readPackageJson } from '../utils/pkg-json.js';
|
|
17
|
+
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
18
|
+
import { buildDeb } from '../utils/ship/deb.js';
|
|
19
|
+
import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
|
|
20
|
+
import { discoverPayload } from '../utils/ship/discover.js';
|
|
21
|
+
import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
|
|
22
|
+
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
23
|
+
import { buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
24
|
+
import { planOverlay, planStage } from '../utils/ship/plan.js';
|
|
25
|
+
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
26
|
+
import { buildRpm } from '../utils/ship/rpm.js';
|
|
27
|
+
import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
|
|
28
|
+
import { readStage, writeStage } from '../utils/ship/stage-writer.js';
|
|
29
|
+
const LOG = '[gjsify ship]';
|
|
30
|
+
export const shipCommand = {
|
|
31
|
+
command: 'ship',
|
|
32
|
+
description: `Build installable artifacts (${FORMAT_IDS.join(', ')}) from the current project.`,
|
|
33
|
+
builder: (yargs) => yargs
|
|
34
|
+
.option('target', {
|
|
35
|
+
type: 'string',
|
|
36
|
+
array: true,
|
|
37
|
+
description: `Formats to build, comma-separated or repeated. Default: ${FORMAT_IDS.join(',')}.`,
|
|
38
|
+
})
|
|
39
|
+
.option('out', {
|
|
40
|
+
type: 'string',
|
|
41
|
+
normalize: true,
|
|
42
|
+
description: 'Output root, relative to the project. Default: `gjsify.ship.outDir`, else `ship`.',
|
|
43
|
+
})
|
|
44
|
+
.option('stage', {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
default: false,
|
|
47
|
+
description: 'Produce the staged payload and stop, without packing anything.',
|
|
48
|
+
})
|
|
49
|
+
.option('skip-build', {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
default: false,
|
|
52
|
+
description: "Package what is already built instead of running the project's `build` script.",
|
|
53
|
+
})
|
|
54
|
+
.option('arch', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Target architecture in `process.arch` spelling. Default: this host.',
|
|
57
|
+
})
|
|
58
|
+
.option('verbose', {
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
default: false,
|
|
61
|
+
description: 'Print each staged file and the GI namespaces the bundle imports.',
|
|
62
|
+
}),
|
|
63
|
+
handler: async (args) => {
|
|
64
|
+
const projectDir = process.cwd();
|
|
65
|
+
const config = new Config();
|
|
66
|
+
// No `.catch(() => ({}))` here, unlike the flatpak commands: a config
|
|
67
|
+
// file that fails to load would silently become "no config", and the
|
|
68
|
+
// result is a shipped artifact carrying default metadata the author
|
|
69
|
+
// never chose. An unreadable config is an error worth printing.
|
|
70
|
+
const configData = await config.forCommand(projectDir);
|
|
71
|
+
const ship = configData.ship ?? {};
|
|
72
|
+
const flatpak = configData.flatpak ?? {};
|
|
73
|
+
assertShippableTarget(configData.app);
|
|
74
|
+
const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
|
|
75
|
+
// Resolved before anything is built or written: a typo'd `--target`
|
|
76
|
+
// should not cost a build and leave a half-written `ship/stage/`.
|
|
77
|
+
const formats = resolveFormats(args.target ?? ship.targets ?? FORMAT_IDS);
|
|
78
|
+
if (!args['skip-build'])
|
|
79
|
+
await runProjectBuild(projectDir);
|
|
80
|
+
const discovered = discoverPayload({
|
|
81
|
+
projectDir,
|
|
82
|
+
pkg,
|
|
83
|
+
ship,
|
|
84
|
+
flatpakIcon: flatpak.icon,
|
|
85
|
+
declaredBundle: declaredBundlePath(pkg, ship),
|
|
86
|
+
});
|
|
87
|
+
const { settings, metadata, warnings } = resolveShipSettings({
|
|
88
|
+
projectDir,
|
|
89
|
+
pkg,
|
|
90
|
+
ship,
|
|
91
|
+
flatpak,
|
|
92
|
+
cli: { outDir: args.out, arch: args.arch },
|
|
93
|
+
discovered,
|
|
94
|
+
});
|
|
95
|
+
for (const warning of warnings)
|
|
96
|
+
console.warn(`${LOG} ${warning}`);
|
|
97
|
+
const mtime = buildTimestamp(settings.bundlePath);
|
|
98
|
+
const metadataInputs = {
|
|
99
|
+
appId: settings.appId,
|
|
100
|
+
name: settings.name,
|
|
101
|
+
command: settings.binaryName,
|
|
102
|
+
kind: settings.kind,
|
|
103
|
+
metadata,
|
|
104
|
+
configKey: 'gjsify.ship',
|
|
105
|
+
copyrightYear: new Date(mtime * 1000).getUTCFullYear(),
|
|
106
|
+
};
|
|
107
|
+
// Warn rather than fail: an incomplete AppStream component still
|
|
108
|
+
// installs and still runs. It is app STORES that will reject it, and
|
|
109
|
+
// that is a different day's problem than getting the package built.
|
|
110
|
+
for (const missing of validateAppMetadata(metadataInputs)) {
|
|
111
|
+
console.warn(`${LOG} ${missing.field} is not set — ${missing.hint}`);
|
|
112
|
+
}
|
|
113
|
+
const stageInputs = {
|
|
114
|
+
bundleFiles: discovered.bundleFiles,
|
|
115
|
+
launcher: renderLauncher(settings, basename(settings.bundlePath)),
|
|
116
|
+
metainfo: settings.kind === 'app' ? renderMetainfoApp(metadataInputs) : renderMetainfoCli(metadataInputs),
|
|
117
|
+
desktopEntry: settings.kind === 'app' ? renderDesktopEntry(metadataInputs) : undefined,
|
|
118
|
+
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
119
|
+
};
|
|
120
|
+
const outRoot = resolve(projectDir, settings.outDir);
|
|
121
|
+
const stageDir = join(outRoot, 'stage');
|
|
122
|
+
const staged = planStage(settings, stageInputs);
|
|
123
|
+
writeStage(stageDir, staged);
|
|
124
|
+
console.log(`${LOG} staged ${staged.length} file(s) in ${relative(projectDir, stageDir)}/`);
|
|
125
|
+
if (args.verbose)
|
|
126
|
+
for (const file of staged)
|
|
127
|
+
console.log(`${LOG} ${file.path}`);
|
|
128
|
+
if (args.stage)
|
|
129
|
+
return;
|
|
130
|
+
const namespaces = scanGiNamespaces(readFileSync(settings.bundlePath, 'utf-8'));
|
|
131
|
+
if (args.verbose)
|
|
132
|
+
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
133
|
+
const artifacts = [];
|
|
134
|
+
for (const format of formats) {
|
|
135
|
+
artifacts.push(await packOne({ format, settings, stageInputs, staged, stageDir, outRoot, namespaces, mtime }));
|
|
136
|
+
}
|
|
137
|
+
for (const artifact of artifacts) {
|
|
138
|
+
console.log(`${LOG} ${artifact.format}: ${relative(projectDir, artifact.path)} (${artifact.size} bytes)`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
async function packOne(input) {
|
|
143
|
+
const { format, settings, stageDir, outRoot, mtime } = input;
|
|
144
|
+
const overlay = planOverlay(settings, format, input.stageInputs);
|
|
145
|
+
const overlayDir = join(outRoot, 'overlay', format.id);
|
|
146
|
+
writeStage(overlayDir, overlay);
|
|
147
|
+
const payload = readStage([stageDir, overlayDir], [...input.staged, ...overlay]);
|
|
148
|
+
const depends = deriveDepends(format.id, {
|
|
149
|
+
namespaces: input.namespaces,
|
|
150
|
+
kind: settings.kind,
|
|
151
|
+
hasSchemas: settings.schemaFiles.length > 0,
|
|
152
|
+
extra: settings.extraDepends[format.id],
|
|
153
|
+
typelibPackages: settings.typelibPackages,
|
|
154
|
+
minGjsVersion: settings.minGjsVersion,
|
|
155
|
+
});
|
|
156
|
+
for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
|
|
157
|
+
console.warn(`${LOG} ${warning}`);
|
|
158
|
+
const archLabel = format.archName(settings.arch, isArchIndependent(payload));
|
|
159
|
+
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
160
|
+
const bytes = format.id === 'deb' ? await buildDeb(common) : await buildRpm(common);
|
|
161
|
+
const outDir = join(outRoot, 'out');
|
|
162
|
+
mkdirSync(outDir, { recursive: true });
|
|
163
|
+
const target = join(outDir, format.fileName(settings, archLabel));
|
|
164
|
+
writeFileSync(target, bytes);
|
|
165
|
+
return { format: format.id, path: target, size: bytes.byteLength };
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Refuse a build target this command cannot package correctly.
|
|
169
|
+
*
|
|
170
|
+
* The launcher execs `gjs -m <bundle>`, so a `--app node` bundle would produce
|
|
171
|
+
* a package that installs and then fails at startup — and ADR 0024 § 4 says
|
|
172
|
+
* that case needs a BUNDLED Node, whose delivery (fetched at ship time versus a
|
|
173
|
+
* platform package) is still an open decision. Better to say so than to ship
|
|
174
|
+
* the broken half of it.
|
|
175
|
+
*
|
|
176
|
+
* An undeclared target is the common case for a GJS app and is allowed: `app`
|
|
177
|
+
* otherwise defaults to the HOST runtime, which would refuse a perfectly good
|
|
178
|
+
* GJS project merely for running this command under Node.
|
|
179
|
+
*/
|
|
180
|
+
function assertShippableTarget(app) {
|
|
181
|
+
if (app === undefined || app === 'gjs')
|
|
182
|
+
return;
|
|
183
|
+
throw new Error(`gjsify ship: this project declares \`gjsify.app: "${app}"\`, and only \`gjs\` can be packaged today. ` +
|
|
184
|
+
'The launcher execs `gjs -m <bundle>`, so any other target would install and then fail to start. ' +
|
|
185
|
+
'ADR 0024 § 4 has the runtime-per-OS table and what is still open for the other targets.');
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Run the project's own `build` script, as a CHILD process.
|
|
189
|
+
*
|
|
190
|
+
* NOT dispatched in-process, even though `cli-app.ts` exists to allow exactly
|
|
191
|
+
* that: `gjsify run`'s script path ends in `return process.exit(code)` on every
|
|
192
|
+
* branch — deliberately, because under GJS a bare `process.exit` falls through
|
|
193
|
+
* and it needs exactly one exit scheduled. Calling it from here would end the
|
|
194
|
+
* process after the build and never reach the packing, and neither the types
|
|
195
|
+
* nor a `--skip-build` test would have caught it.
|
|
196
|
+
*
|
|
197
|
+
* `completion: 'return'` because this handler returns afterwards; on GJS that
|
|
198
|
+
* selects the blocking path rather than arming a main loop only an exit quits.
|
|
199
|
+
*/
|
|
200
|
+
async function runProjectBuild(projectDir) {
|
|
201
|
+
const pkg = readPackageJson(join(projectDir, 'package.json'));
|
|
202
|
+
if (typeof pkg?.scripts?.build !== 'string') {
|
|
203
|
+
throw new Error('gjsify ship: this project has no `build` script to run. Add one, or pass --skip-build to ' +
|
|
204
|
+
'package what is already built.');
|
|
205
|
+
}
|
|
206
|
+
console.log(`${LOG} running the project's build script`);
|
|
207
|
+
const result = await spawnToCompletion('gjsify', ['run', 'build'], {
|
|
208
|
+
completion: 'return',
|
|
209
|
+
cwd: projectDir,
|
|
210
|
+
stdio: 'inherit',
|
|
211
|
+
notFound: () => new Error('gjsify ship: `gjsify` is not on PATH, so the project build cannot be run. Build the project ' +
|
|
212
|
+
'yourself and pass --skip-build.'),
|
|
213
|
+
});
|
|
214
|
+
if (result.code !== 0) {
|
|
215
|
+
throw new Error(`gjsify ship: the project's build script failed${describeExit(result)}.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
package/lib/config.d.ts
CHANGED
|
@@ -24,5 +24,17 @@ export declare class Config {
|
|
|
24
24
|
globals?: string;
|
|
25
25
|
excludeGlobals?: string[];
|
|
26
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* The config for a command that is NOT a build — `gjsify flatpak …`,
|
|
29
|
+
* `gjsify ship …`.
|
|
30
|
+
*
|
|
31
|
+
* They all want exactly what the loader already produces: the merged
|
|
32
|
+
* `package.json#gjsify` + config-file object, with none of `forBuild`'s
|
|
33
|
+
* build-specific defaulting. Reaching it through `forBuild({} as never)`
|
|
34
|
+
* — five copies of that line before this method existed — lies to the type
|
|
35
|
+
* system and drags a tsconfig read and bundler defaults into a command
|
|
36
|
+
* that has no bundle.
|
|
37
|
+
*/
|
|
38
|
+
forCommand(searchFrom?: string): Promise<ConfigData>;
|
|
27
39
|
forBuild(cliArgs: ArgumentsCamelCase<CliBuildOptions>): Promise<ConfigData>;
|
|
28
40
|
}
|