@gjsify/cli 0.42.0 → 0.43.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 +238 -210
- package/lib/actions/build.d.ts +6 -0
- package/lib/actions/build.js +64 -8
- package/lib/commands/ship.d.ts +2 -0
- package/lib/commands/ship.js +285 -93
- package/lib/types/config-data.d.ts +32 -0
- package/lib/utils/gjs-bundle-guard.d.ts +57 -3
- package/lib/utils/gjs-bundle-guard.js +122 -13
- package/lib/utils/jsx-config.d.ts +45 -0
- package/lib/utils/jsx-config.js +98 -0
- package/lib/utils/onboard-probe.d.ts +23 -0
- package/lib/utils/onboard-probe.js +65 -0
- package/lib/utils/ship/deb.d.ts +3 -3
- package/lib/utils/ship/deb.js +6 -2
- package/lib/utils/ship/depends.d.ts +9 -6
- package/lib/utils/ship/depends.js +25 -3
- package/lib/utils/ship/discover.js +72 -6
- package/lib/utils/ship/formats.js +9 -1
- package/lib/utils/ship/mime.d.ts +19 -0
- package/lib/utils/ship/mime.js +89 -0
- package/lib/utils/ship/payload.d.ts +76 -0
- package/lib/utils/ship/payload.js +91 -1
- package/lib/utils/ship/plan.d.ts +30 -1
- package/lib/utils/ship/plan.js +54 -2
- package/lib/utils/ship/rpm.d.ts +3 -3
- package/lib/utils/ship/rpm.js +20 -3
- package/lib/utils/ship/scripts.d.ts +14 -4
- package/lib/utils/ship/scripts.js +26 -8
- package/lib/utils/ship/settings.js +13 -0
- package/lib/utils/ship/stage-manifest.d.ts +154 -0
- package/lib/utils/ship/stage-manifest.js +408 -0
- package/lib/utils/ship/stage-writer.d.ts +30 -3
- package/lib/utils/ship/stage-writer.js +76 -9
- package/lib/utils/ship/types.d.ts +91 -18
- package/lib/utils/ship/types.js +5 -0
- package/package.json +19 -19
package/lib/actions/build.d.ts
CHANGED
|
@@ -72,6 +72,12 @@ export declare class BuildAction {
|
|
|
72
72
|
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
73
73
|
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
74
74
|
* text scan cannot tell from a statement.
|
|
75
|
+
*
|
|
76
|
+
* The same list also answers the GENERAL form of the question — any bare
|
|
77
|
+
* specifier GJS cannot resolve, `node:` or not — which is why the target's
|
|
78
|
+
* externals predicate and the emitted fileNames are handed over: they are
|
|
79
|
+
* what separates a declared external and a sibling chunk from a dependency
|
|
80
|
+
* that simply is not there.
|
|
75
81
|
*/
|
|
76
82
|
private assertGjsOutputLoadable;
|
|
77
83
|
/**
|
package/lib/actions/build.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runBundle, runWatch, bundleToChunks } from '../bundler-pick.js';
|
|
2
|
-
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG } from '@gjsify/rolldown-plugin-gjsify';
|
|
2
|
+
import { gjsifyPlugin, textLoaderPlugin, resolveShebangLine, NODE_SHEBANG, createGjsExternalsPredicate, globToEntryPoints, } from '@gjsify/rolldown-plugin-gjsify';
|
|
3
3
|
import { isGjs } from '@gjsify/rolldown-plugin-gjsify/runtime';
|
|
4
4
|
import { resolveUserPlugins } from '../utils/resolve-plugin-by-name.js';
|
|
5
5
|
import { resolveGlobalsList, writeRegisterInjectFile, detectAutoGlobals, detectNodeGiGlobals, } from '@gjsify/rolldown-plugin-gjsify/globals';
|
|
@@ -10,7 +10,8 @@ import { chmod, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promis
|
|
|
10
10
|
import { normalizeBundlerOptions, mergeBundlerOptions } from '../utils/normalize-bundler-options.js';
|
|
11
11
|
import { inputSourceDirs, isOutdirInsideSource, libraryOutputLeakError } from '../utils/library-output.js';
|
|
12
12
|
import { detectHtmlEntry, parseHtmlEntry, emitBrowserHtml, htmlOutPathFor } from '../utils/html-entry.js';
|
|
13
|
-
import { assertGjsBundleLoadable } from '../utils/gjs-bundle-guard.js';
|
|
13
|
+
import { assertGjsBundleLoadable, assertGjsBundleParses } from '../utils/gjs-bundle-guard.js';
|
|
14
|
+
import { collectEntryPaths, describeJsxConfig, findJsxEntryPoint, jsxConfigMissingError } from '../utils/jsx-config.js';
|
|
14
15
|
import { giSystemProbes } from '../utils/gi-runtime-paths.js';
|
|
15
16
|
import { escapeRawNulForGjs } from '../utils/gjs-source-escape.js';
|
|
16
17
|
import { assertNodeBundleGlobalsShimmed } from '../utils/node-bundle-guard.js';
|
|
@@ -149,6 +150,21 @@ export class BuildAction {
|
|
|
149
150
|
const multipleBuilds = moduleOutdir && mainOutdir && moduleOutdir !== mainOutdir;
|
|
150
151
|
const pnp = await buildPnpPlugin();
|
|
151
152
|
const pnpPlugins = pnp ? [pnp] : [];
|
|
153
|
+
// The same chain `buildApp` assembles, and it was missing here entirely:
|
|
154
|
+
// a library build resolved NO user plugin and NO user text loader, then
|
|
155
|
+
// emitted the default transform at exit 0. Configuring
|
|
156
|
+
// `gjsify.bundler.plugins` and watching it do nothing is the repo's most
|
|
157
|
+
// expensive failure shape, and `@gjsify/gtk-host` itself builds with
|
|
158
|
+
// `--library` — which is where it was found.
|
|
159
|
+
const userTextLoader = textLoaderPlugin({ loaders: this.configData.loaders });
|
|
160
|
+
const userPlugins = userTextLoader ? [userTextLoader] : [];
|
|
161
|
+
if (userBundler.plugins?.length) {
|
|
162
|
+
// Under GJS a plugin module cannot always be imported directly — the
|
|
163
|
+
// native ESM loader does not follow `package.json#exports` — so the
|
|
164
|
+
// same bundle-then-import escape the app path uses is passed in here.
|
|
165
|
+
const resolved = await resolveUserPlugins(userBundler.plugins, process.cwd(), isGjs() ? { loadModule: (pth, name) => this.loadPluginViaGjsBundle(pth, name, verbose) } : {});
|
|
166
|
+
userPlugins.push(...resolved);
|
|
167
|
+
}
|
|
152
168
|
const results = [];
|
|
153
169
|
if (multipleBuilds) {
|
|
154
170
|
// Both outdirs are derived from module/main — always guard them
|
|
@@ -168,6 +184,7 @@ export class BuildAction {
|
|
|
168
184
|
output: { dir: moduleOutdir },
|
|
169
185
|
userAliases: aliases,
|
|
170
186
|
pnpPlugins,
|
|
187
|
+
userPlugins,
|
|
171
188
|
}));
|
|
172
189
|
const mainFormat = mainOutdir.includes('/cjs') || mainOutExt === '.cjs' ? 'cjs' : 'esm';
|
|
173
190
|
results.push(await runOneLibraryBuild({
|
|
@@ -182,6 +199,7 @@ export class BuildAction {
|
|
|
182
199
|
output: { dir: mainOutdir },
|
|
183
200
|
userAliases: aliases,
|
|
184
201
|
pnpPlugins,
|
|
202
|
+
userPlugins,
|
|
185
203
|
}));
|
|
186
204
|
}
|
|
187
205
|
else {
|
|
@@ -207,6 +225,7 @@ export class BuildAction {
|
|
|
207
225
|
output: { dir: outdir },
|
|
208
226
|
userAliases: aliases,
|
|
209
227
|
pnpPlugins,
|
|
228
|
+
userPlugins,
|
|
210
229
|
}));
|
|
211
230
|
}
|
|
212
231
|
return results;
|
|
@@ -375,10 +394,22 @@ export class BuildAction {
|
|
|
375
394
|
* natively, `@gjsify/rolldown-native` via `synthRolldownOutput`), and it
|
|
376
395
|
* cannot be fooled by a `node:` specifier quoted inside a string, which a
|
|
377
396
|
* text scan cannot tell from a statement.
|
|
397
|
+
*
|
|
398
|
+
* The same list also answers the GENERAL form of the question — any bare
|
|
399
|
+
* specifier GJS cannot resolve, `node:` or not — which is why the target's
|
|
400
|
+
* externals predicate and the emitted fileNames are handed over: they are
|
|
401
|
+
* what separates a declared external and a sibling chunk from a dependency
|
|
402
|
+
* that simply is not there.
|
|
378
403
|
*/
|
|
379
|
-
assertGjsOutputLoadable(result, outfile, outdir) {
|
|
404
|
+
assertGjsOutputLoadable(result, outfile, outdir, userExternal) {
|
|
380
405
|
const chunks = (result.output ?? []).filter((item) => item.type === 'chunk');
|
|
381
|
-
assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle'
|
|
406
|
+
assertGjsBundleLoadable(chunks, outfile ?? outdir ?? 'the GJS bundle', {
|
|
407
|
+
// The target's OWN predicate, not a second list: `externalsPlugin` enforces
|
|
408
|
+
// this exact one, so a specifier the build let through as external cannot be
|
|
409
|
+
// reported as unresolvable here.
|
|
410
|
+
isExternal: createGjsExternalsPredicate(userExternal ?? []),
|
|
411
|
+
emitted: (result.output ?? []).map((item) => item.fileName),
|
|
412
|
+
});
|
|
382
413
|
}
|
|
383
414
|
/**
|
|
384
415
|
* Post-bundle gate for `--app node`: throw when the emitted bundle still
|
|
@@ -544,6 +575,21 @@ export class BuildAction {
|
|
|
544
575
|
async buildApp(app = 'gjs', opts = {}) {
|
|
545
576
|
const { verbose, typescript, exclude, library: pkg, aliases, excludeGlobals } = this.configData;
|
|
546
577
|
const userBundler = normalizeBundlerOptions(this.configData);
|
|
578
|
+
// Refuse a JSX entry with no JSX policy BEFORE bundling anything — see
|
|
579
|
+
// `utils/jsx-config.ts` for what the unset default emits and why it can never be
|
|
580
|
+
// right for this target. The glob expansion is paid only on the failing branch:
|
|
581
|
+
// when JSX is configured there is nothing to name.
|
|
582
|
+
const jsxPolicy = describeJsxConfig({
|
|
583
|
+
transformJsx: userBundler.transform?.jsx,
|
|
584
|
+
tsconfigJsx: typescript?.compilerOptions?.jsx,
|
|
585
|
+
tsconfigJsxImportSource: typescript?.compilerOptions?.jsxImportSource,
|
|
586
|
+
});
|
|
587
|
+
if (app === 'gjs' && !jsxPolicy.configured) {
|
|
588
|
+
const entries = collectEntryPaths(await globToEntryPoints(userBundler.input, exclude));
|
|
589
|
+
const jsxEntry = findJsxEntryPoint(entries);
|
|
590
|
+
if (jsxEntry !== undefined)
|
|
591
|
+
throw jsxConfigMissingError(jsxEntry);
|
|
592
|
+
}
|
|
547
593
|
// --- `--app browser` HTML entry (Vite-style) ---
|
|
548
594
|
// When the entry is an `index.html`, bundle the module its
|
|
549
595
|
// `<script type="module" src>` references (rewrite `input` to that
|
|
@@ -746,7 +792,7 @@ export class BuildAction {
|
|
|
746
792
|
// and appended into `userPlugins` above. Re-emitting the raw
|
|
747
793
|
// entries (which may include `BundlerPluginByName` shapes
|
|
748
794
|
// Rolldown doesn't understand) would crash the build.
|
|
749
|
-
plugins: [...pnpPlugins, ...userPlugins, ...cfg.plugins],
|
|
795
|
+
plugins: [...pnpPlugins, ...cfg.prePlugins, ...userPlugins, ...cfg.plugins],
|
|
750
796
|
};
|
|
751
797
|
if (opts.watch) {
|
|
752
798
|
await this.runWatchLoop(finalOpts, app, outfile, outdir, verbose);
|
|
@@ -762,9 +808,15 @@ export class BuildAction {
|
|
|
762
808
|
if ((app === 'gjs' || app === 'node') && this.configData.shebang) {
|
|
763
809
|
await this.applyShebang(app, outfile, verbose);
|
|
764
810
|
}
|
|
765
|
-
// Refuse to hand back a `--app gjs` bundle stock GJS cannot even load
|
|
811
|
+
// Refuse to hand back a `--app gjs` bundle stock GJS cannot even load: one that
|
|
812
|
+
// imports what the runtime cannot resolve, or — when the transform was told to
|
|
813
|
+
// keep JSX — one that is not parseable JavaScript at all.
|
|
766
814
|
if (app === 'gjs') {
|
|
767
|
-
this.assertGjsOutputLoadable(writeResult, outfile, outdir);
|
|
815
|
+
this.assertGjsOutputLoadable(writeResult, outfile, outdir, userExternal);
|
|
816
|
+
if (jsxPolicy.preserves) {
|
|
817
|
+
const chunks = (writeResult.output ?? []).filter((item) => item.type === 'chunk');
|
|
818
|
+
assertGjsBundleParses(chunks, outfile ?? outdir ?? 'the GJS bundle');
|
|
819
|
+
}
|
|
768
820
|
}
|
|
769
821
|
// Refuse to hand back a `--app node` bundle that reaches for GJS ambient
|
|
770
822
|
// globals nothing seeds. Only meaningful when the shim was NOT injected:
|
|
@@ -882,7 +934,11 @@ async function runOneLibraryBuild(args) {
|
|
|
882
934
|
const merged = mergeBundlerOptions(cfg.options, args.userBundler);
|
|
883
935
|
const finalOpts = {
|
|
884
936
|
...merged,
|
|
885
|
-
|
|
937
|
+
// Same order as the app path: PnP resolves, then the user transforms,
|
|
938
|
+
// then gjsify's own chain. `merged.plugins` is deliberately NOT spread —
|
|
939
|
+
// the user's entries arrive RESOLVED, in `userPlugins`, exactly as
|
|
940
|
+
// `buildApp` passes them.
|
|
941
|
+
plugins: [...args.pnpPlugins, ...cfg.prePlugins, ...args.userPlugins, ...cfg.plugins],
|
|
886
942
|
};
|
|
887
943
|
return await runBundle(finalOpts);
|
|
888
944
|
}
|
package/lib/commands/ship.d.ts
CHANGED
package/lib/commands/ship.js
CHANGED
|
@@ -5,12 +5,23 @@
|
|
|
5
5
|
// thin on purpose: every decision worth testing lives in `utils/ship/*` as a
|
|
6
6
|
// pure function, and what is left here is filesystem work and printing.
|
|
7
7
|
//
|
|
8
|
+
// TWO PHASES, one verb each (ADR 0024 § A2). Phase one ASSEMBLES: it resolves
|
|
9
|
+
// the settings, plans the payload, writes `stage/`, and writes the closure that
|
|
10
|
+
// makes that tree packable somewhere else (`.gjsify-ship-stage.json`, see
|
|
11
|
+
// `utils/ship/stage-manifest.ts`). Phase two FINISHES: `--from-stage <dir>`
|
|
12
|
+
// packs a stage that arrived from another host, with no project, no config and
|
|
13
|
+
// no built bundle in reach. With neither flag both phases run in one process,
|
|
14
|
+
// which is what a developer on one machine wants and what the e2e suite
|
|
15
|
+
// compares the split against byte for byte.
|
|
16
|
+
//
|
|
8
17
|
// Scope today is Linux and `--app gjs`: no runtime is bundled, because GJS and
|
|
9
18
|
// GTK come from the distribution and shipping ~100 MiB of them would be cargo
|
|
10
19
|
// 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
|
|
20
|
+
// this command, are the later stages of the same ADR — and they are the reason
|
|
21
|
+
// the split exists at all: a `.dmg` is a UDIF image over a real HFS+/APFS
|
|
22
|
+
// volume and no Linux host in this tree can write one (§ A1).
|
|
12
23
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { basename, join, relative, resolve } from 'node:path';
|
|
24
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
14
25
|
import { Config } from '../config.js';
|
|
15
26
|
import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
|
|
16
27
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
@@ -20,11 +31,12 @@ import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
|
|
|
20
31
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
21
32
|
import { resolveFormats, FORMAT_IDS } from '../utils/ship/formats.js';
|
|
22
33
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
23
|
-
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent } from '../utils/ship/payload.js';
|
|
24
|
-
import { planOverlay, planStage } from '../utils/ship/plan.js';
|
|
34
|
+
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
35
|
+
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
25
36
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
26
37
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
27
38
|
import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
|
|
39
|
+
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, } from '../utils/ship/stage-manifest.js';
|
|
28
40
|
import { readStage, writeStage } from '../utils/ship/stage-writer.js';
|
|
29
41
|
const LOG = '[gjsify ship]';
|
|
30
42
|
export const shipCommand = {
|
|
@@ -45,6 +57,17 @@ export const shipCommand = {
|
|
|
45
57
|
type: 'boolean',
|
|
46
58
|
default: false,
|
|
47
59
|
description: 'Produce the staged payload and stop, without packing anything.',
|
|
60
|
+
})
|
|
61
|
+
.option('from-stage', {
|
|
62
|
+
type: 'string',
|
|
63
|
+
normalize: true,
|
|
64
|
+
description: 'Pack a staged payload written by an earlier `--stage` run. Needs no project: the stage ' +
|
|
65
|
+
'carries its own closure.',
|
|
66
|
+
})
|
|
67
|
+
.option('expect-target', {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'With --from-stage: refuse unless the stage was assembled for this `<os>-<arch>`, e.g. ' +
|
|
70
|
+
'`linux-arm64`.',
|
|
48
71
|
})
|
|
49
72
|
.option('skip-build', {
|
|
50
73
|
type: 'boolean',
|
|
@@ -61,114 +84,279 @@ export const shipCommand = {
|
|
|
61
84
|
description: 'Print each staged file and the GI namespaces the bundle imports.',
|
|
62
85
|
}),
|
|
63
86
|
handler: async (args) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
// The two phases share nothing but `packOne`, and the branch is first
|
|
88
|
+
// so that `--from-stage` never touches the cwd: a finishing host has no
|
|
89
|
+
// project there, and reading one would make the artifact depend on
|
|
90
|
+
// whatever directory the job happened to start in.
|
|
91
|
+
if (args['from-stage'] !== undefined)
|
|
92
|
+
return await finishFromStage(args, args['from-stage']);
|
|
93
|
+
await assemble(args);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Phase one: resolve, plan, stage, and write the closure — then, unless
|
|
98
|
+
* `--stage` was passed, pack right here.
|
|
99
|
+
*/
|
|
100
|
+
async function assemble(args) {
|
|
101
|
+
const projectDir = process.cwd();
|
|
102
|
+
const config = new Config();
|
|
103
|
+
// No `.catch(() => ({}))` here, unlike the flatpak commands: a config
|
|
104
|
+
// file that fails to load would silently become "no config", and the
|
|
105
|
+
// result is a shipped artifact carrying default metadata the author
|
|
106
|
+
// never chose. An unreadable config is an error worth printing.
|
|
107
|
+
const configData = await config.forCommand(projectDir);
|
|
108
|
+
const ship = configData.ship ?? {};
|
|
109
|
+
const flatpak = configData.flatpak ?? {};
|
|
110
|
+
assertShippableTarget(configData.app);
|
|
111
|
+
const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
|
|
112
|
+
// Resolved before anything is built or written: a typo'd `--target`
|
|
113
|
+
// should not cost a build and leave a half-written `ship/stage/`.
|
|
114
|
+
const formats = resolveFormats(args.target ?? ship.targets ?? FORMAT_IDS);
|
|
115
|
+
if (!args['skip-build'])
|
|
116
|
+
await runProjectBuild(projectDir);
|
|
117
|
+
const discovered = discoverPayload({
|
|
118
|
+
projectDir,
|
|
119
|
+
pkg,
|
|
120
|
+
ship,
|
|
121
|
+
flatpakIcon: flatpak.icon,
|
|
122
|
+
declaredBundle: declaredBundlePath(pkg, ship),
|
|
123
|
+
});
|
|
124
|
+
const { settings, metadata, warnings } = resolveShipSettings({
|
|
125
|
+
projectDir,
|
|
126
|
+
pkg,
|
|
127
|
+
ship,
|
|
128
|
+
flatpak,
|
|
129
|
+
cli: { outDir: args.out, arch: args.arch },
|
|
130
|
+
discovered,
|
|
131
|
+
});
|
|
132
|
+
for (const warning of warnings)
|
|
133
|
+
console.warn(`${LOG} ${warning}`);
|
|
134
|
+
const mtime = buildTimestamp(settings.bundlePath);
|
|
135
|
+
const metadataInputs = {
|
|
136
|
+
appId: settings.appId,
|
|
137
|
+
name: settings.name,
|
|
138
|
+
command: settings.binaryName,
|
|
139
|
+
kind: settings.kind,
|
|
140
|
+
metadata,
|
|
141
|
+
configKey: 'gjsify.ship',
|
|
142
|
+
copyrightYear: new Date(mtime * 1000).getUTCFullYear(),
|
|
143
|
+
};
|
|
144
|
+
// Warn rather than fail: an incomplete AppStream component still
|
|
145
|
+
// installs and still runs. It is app STORES that will reject it, and
|
|
146
|
+
// that is a different day's problem than getting the package built.
|
|
147
|
+
for (const missing of validateAppMetadata(metadataInputs)) {
|
|
148
|
+
console.warn(`${LOG} ${missing.field} is not set — ${missing.hint}`);
|
|
149
|
+
}
|
|
150
|
+
// Printed, not assumed. The text may come from an ancestor — one `LICENSE`
|
|
151
|
+
// at a monorepo root covers every package under it (see `discoverLicense`)
|
|
152
|
+
// — and which file was baked into the copyright is not recoverable from the
|
|
153
|
+
// artifact afterwards without unpacking it.
|
|
154
|
+
if (settings.licenseFile !== undefined && dirname(settings.licenseFile) !== projectDir) {
|
|
155
|
+
console.log(`${LOG} licence text from ${relative(projectDir, settings.licenseFile)} ` +
|
|
156
|
+
'(this package carries none of its own)');
|
|
157
|
+
}
|
|
158
|
+
const stageInputs = {
|
|
159
|
+
bundleFiles: discovered.bundleFiles,
|
|
160
|
+
launcher: renderLauncher(settings, basename(settings.bundlePath)),
|
|
161
|
+
metainfo: settings.kind === 'app' ? renderMetainfoApp(metadataInputs) : renderMetainfoCli(metadataInputs),
|
|
162
|
+
desktopEntry: settings.kind === 'app' ? renderDesktopEntry(metadataInputs) : undefined,
|
|
163
|
+
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
164
|
+
};
|
|
165
|
+
const outRoot = resolve(projectDir, settings.outDir);
|
|
166
|
+
const stageDir = join(outRoot, 'stage');
|
|
167
|
+
const staged = planStage(settings, stageInputs);
|
|
168
|
+
writeStage(stageDir, staged);
|
|
169
|
+
console.log(`${LOG} staged ${staged.length} file(s) in ${relative(projectDir, stageDir)}/`);
|
|
170
|
+
if (args.verbose)
|
|
171
|
+
for (const file of staged)
|
|
172
|
+
console.log(`${LOG} ${file.path}`);
|
|
173
|
+
// Scanned from the BUILD TREE's bundle, which is why it has to be recorded:
|
|
174
|
+
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
175
|
+
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
176
|
+
// tell which staged file is the entry.
|
|
177
|
+
const namespaces = scanGiNamespaces(readFileSync(settings.bundlePath, 'utf-8'));
|
|
178
|
+
if (args.verbose)
|
|
179
|
+
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
180
|
+
const overlay = new Map();
|
|
181
|
+
for (const format of formats)
|
|
182
|
+
overlay.set(format.id, planOverlay(settings, format, stageInputs));
|
|
183
|
+
// The dependency derivation runs HERE as well, and its result is thrown
|
|
184
|
+
// away. It is the only check in the pipeline that can refuse a project
|
|
185
|
+
// (`gi://Nautilus` with no known package, ADR 0024 § 6), and this is the
|
|
186
|
+
// host that has the `package.json` the refusal tells you to edit — letting
|
|
187
|
+
// it fire two jobs later, on a machine with no project, would name a fix
|
|
188
|
+
// nobody there can apply. Facts come from the PLAN, which has the same
|
|
189
|
+
// paths the payload will have.
|
|
190
|
+
const facts = readPayloadFacts(staged);
|
|
191
|
+
for (const format of formats) {
|
|
192
|
+
deriveDepends(format.id, {
|
|
193
|
+
namespaces,
|
|
194
|
+
hasIcons: facts.hasIcons,
|
|
195
|
+
hasSchemas: facts.hasSchemas,
|
|
196
|
+
extra: settings.extraDepends[format.id],
|
|
197
|
+
typelibPackages: settings.typelibPackages,
|
|
198
|
+
bundledTypelibs: facts.bundledTypelibs,
|
|
199
|
+
minGjsVersion: settings.minGjsVersion,
|
|
94
200
|
});
|
|
95
|
-
for (const warning of
|
|
201
|
+
for (const warning of warnAboutGjsFloor(format.id, settings.minGjsVersion))
|
|
202
|
+
console.warn(`${LOG} ${warning}`);
|
|
203
|
+
}
|
|
204
|
+
// Written unconditionally, not only under `--stage`: a `ship/stage/` that
|
|
205
|
+
// is sometimes packable elsewhere and sometimes not is a worse contract
|
|
206
|
+
// than one that always is, and it is what lets the e2e suite compare the
|
|
207
|
+
// one-process artifact with the two-phase one.
|
|
208
|
+
const manifest = writeStageManifest({ stageDir, settings, formats, mtime, namespaces, staged, overlay });
|
|
209
|
+
if (args.stage) {
|
|
210
|
+
console.log(`${LOG} stage manifest: ${formatTarget(manifest.target)}, formats ${manifest.formats.join(', ')}`);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const artifacts = [];
|
|
214
|
+
for (const format of formats) {
|
|
215
|
+
artifacts.push(await packOne({
|
|
216
|
+
format,
|
|
217
|
+
settings,
|
|
218
|
+
arch: settings.arch,
|
|
219
|
+
staged,
|
|
220
|
+
overlay: overlay.get(format.id) ?? [],
|
|
221
|
+
stageDir,
|
|
222
|
+
outRoot,
|
|
223
|
+
namespaces,
|
|
224
|
+
mtime,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
printArtifacts(projectDir, artifacts);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Phase two: pack a stage that was assembled somewhere else.
|
|
231
|
+
*
|
|
232
|
+
* Everything this needs is in the stage. Nothing under the cwd is read, and
|
|
233
|
+
* that is the property under test — `tests/e2e/ship-from-stage` deletes the
|
|
234
|
+
* project tree between the two phases, because without the deletion every
|
|
235
|
+
* accidental reach-back succeeds and the suite proves nothing.
|
|
236
|
+
*/
|
|
237
|
+
async function finishFromStage(args, fromStage) {
|
|
238
|
+
if (args.stage) {
|
|
239
|
+
throw new Error('gjsify ship: --stage and --from-stage are the two halves of one split and cannot run together. ' +
|
|
240
|
+
'`--stage` assembles a payload and stops; `--from-stage <dir>` packs one that is already ' +
|
|
241
|
+
'assembled. Run them as two commands, on the host each belongs on (ADR 0024 § A2).');
|
|
242
|
+
}
|
|
243
|
+
if (args['skip-build']) {
|
|
244
|
+
throw new Error('gjsify ship: --from-stage never runs a build — the payload it packs was built and staged by an ' +
|
|
245
|
+
'earlier run — so --skip-build has nothing to skip. Drop it.');
|
|
246
|
+
}
|
|
247
|
+
if (args.arch !== undefined) {
|
|
248
|
+
throw new Error('gjsify ship: --arch is decided when the stage is assembled, not when it is packed: the launcher, ' +
|
|
249
|
+
'the payload and the recorded target were all fixed then. Pass --arch to ' +
|
|
250
|
+
'`gjsify ship --stage`, and use `--expect-target <os>-<arch>` here to assert which leg you are ' +
|
|
251
|
+
'packing.');
|
|
252
|
+
}
|
|
253
|
+
const cwd = process.cwd();
|
|
254
|
+
const stageDir = resolve(cwd, fromStage);
|
|
255
|
+
const manifest = readStageManifest(stageDir);
|
|
256
|
+
assertExpectedTarget(manifest, args['expect-target']);
|
|
257
|
+
const formats = resolveFormats(args.target ?? manifest.formats);
|
|
258
|
+
assertFormatsStaged(manifest, formats);
|
|
259
|
+
for (const warning of warnOnToolSkew(manifest))
|
|
260
|
+
console.warn(`${LOG} ${warning}`);
|
|
261
|
+
const { mtime, warnings } = resolveStageMtime(manifest);
|
|
262
|
+
for (const warning of warnings)
|
|
263
|
+
console.warn(`${LOG} ${warning}`);
|
|
264
|
+
console.log(`${LOG} packing ${manifest.settings.binaryName} ${manifest.settings.version}-${manifest.settings.release} ` +
|
|
265
|
+
`for ${formatTarget(manifest.target)} from ${relative(cwd, stageDir) || '.'}/`);
|
|
266
|
+
if (args.verbose) {
|
|
267
|
+
for (const file of manifest.staged)
|
|
268
|
+
console.log(`${LOG} ${file.path}`);
|
|
269
|
+
console.log(`${LOG} gi namespaces: ${manifest.namespaces.join(', ') || '(none)'}`);
|
|
270
|
+
}
|
|
271
|
+
// The stage's own directory is not written to — `writeStage` wipes what it
|
|
272
|
+
// is pointed at, and pointing it at an arriving stage would delete the
|
|
273
|
+
// payload. The overlay is the only thing this phase materialises, and it
|
|
274
|
+
// goes beside the artifacts.
|
|
275
|
+
const outRoot = args.out === undefined ? dirname(stageDir) : resolve(cwd, args.out);
|
|
276
|
+
const artifacts = [];
|
|
277
|
+
for (const format of formats) {
|
|
278
|
+
for (const warning of warnAboutGjsFloor(format.id, manifest.settings.minGjsVersion)) {
|
|
96
279
|
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
280
|
}
|
|
140
|
-
|
|
141
|
-
|
|
281
|
+
artifacts.push(await packOne({
|
|
282
|
+
format,
|
|
283
|
+
settings: manifest.settings,
|
|
284
|
+
arch: manifest.target.arch,
|
|
285
|
+
staged: manifest.staged,
|
|
286
|
+
overlay: overlayFiles(manifest, format.id),
|
|
287
|
+
stageDir,
|
|
288
|
+
outRoot,
|
|
289
|
+
namespaces: manifest.namespaces,
|
|
290
|
+
mtime,
|
|
291
|
+
}));
|
|
292
|
+
}
|
|
293
|
+
printArtifacts(cwd, artifacts);
|
|
294
|
+
}
|
|
142
295
|
async function packOne(input) {
|
|
143
296
|
const { format, settings, stageDir, outRoot, mtime } = input;
|
|
144
|
-
|
|
297
|
+
assertOverlayIsLicensed(format.id, settings.binaryName, input.overlay);
|
|
145
298
|
const overlayDir = join(outRoot, 'overlay', format.id);
|
|
146
|
-
writeStage(overlayDir, overlay);
|
|
147
|
-
const payload = readStage([stageDir, overlayDir], [...input.staged, ...overlay]);
|
|
299
|
+
writeStage(overlayDir, input.overlay);
|
|
300
|
+
const payload = readStage([stageDir, overlayDir], [...input.staged, ...input.overlay]);
|
|
301
|
+
// What the package installs, read off what it installs. The three questions
|
|
302
|
+
// this answers used to be answered from the project's file lists, which are
|
|
303
|
+
// absolute paths on the build host and therefore unavailable here.
|
|
304
|
+
const facts = readPayloadFacts(payload);
|
|
148
305
|
const depends = deriveDepends(format.id, {
|
|
149
306
|
namespaces: input.namespaces,
|
|
150
|
-
|
|
151
|
-
hasSchemas:
|
|
307
|
+
hasIcons: facts.hasIcons,
|
|
308
|
+
hasSchemas: facts.hasSchemas,
|
|
152
309
|
extra: settings.extraDepends[format.id],
|
|
153
310
|
typelibPackages: settings.typelibPackages,
|
|
154
|
-
bundledTypelibs:
|
|
311
|
+
bundledTypelibs: facts.bundledTypelibs,
|
|
155
312
|
minGjsVersion: settings.minGjsVersion,
|
|
156
313
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
314
|
+
// `--arch` is a CLAIM about the payload and nothing else compared it to one:
|
|
315
|
+
// an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
|
|
316
|
+
// --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
|
|
317
|
+
// header was written from the claim. Read the two bytes further that settle
|
|
318
|
+
// it before the label reaches a header. Payload-versus-LABEL, never
|
|
319
|
+
// payload-versus-HOST — a host check would refuse the cross-host pack this
|
|
320
|
+
// whole split exists for.
|
|
321
|
+
//
|
|
322
|
+
// And BEFORE the label is chosen, not after — this ordering is load-bearing:
|
|
323
|
+
// `archName` collapses an arch-independent payload to `all`/`noarch`, so
|
|
324
|
+
// asserting afterwards would compare against a label that no longer names an
|
|
325
|
+
// architecture, and the mismatch that matters is exactly the one on a payload
|
|
326
|
+
// that IS architecture-specific.
|
|
327
|
+
assertPayloadMatchesArch(payload, input.arch);
|
|
328
|
+
const archLabel = format.archName(input.arch, isArchIndependent(payload));
|
|
164
329
|
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
165
|
-
|
|
330
|
+
// A SWITCH with a `never` guard, not a ternary. `format.id === 'deb' ? deb : rpm`
|
|
331
|
+
// read as a dispatch and behaved as one only while there were exactly two
|
|
332
|
+
// formats: a third would have taken the else-branch and written an RPM under a
|
|
333
|
+
// `.dmg` name, at exit 0. This is the same closed-vocabulary hazard the format
|
|
334
|
+
// list has, one level worse — a wrong ARTIFACT rather than a rejected
|
|
335
|
+
// declaration — so the compiler owns it now.
|
|
336
|
+
let bytes;
|
|
337
|
+
switch (format.id) {
|
|
338
|
+
case 'deb':
|
|
339
|
+
bytes = await buildDeb(common);
|
|
340
|
+
break;
|
|
341
|
+
case 'rpm':
|
|
342
|
+
bytes = await buildRpm(common);
|
|
343
|
+
break;
|
|
344
|
+
default: {
|
|
345
|
+
const unhandled = format.id;
|
|
346
|
+
throw new Error(`gjsify ship: no packer is wired for format "${String(unhandled)}".`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
166
349
|
const outDir = join(outRoot, 'out');
|
|
167
350
|
mkdirSync(outDir, { recursive: true });
|
|
168
351
|
const target = join(outDir, format.fileName(settings, archLabel));
|
|
169
352
|
writeFileSync(target, bytes);
|
|
170
353
|
return { format: format.id, path: target, size: bytes.byteLength };
|
|
171
354
|
}
|
|
355
|
+
function printArtifacts(base, artifacts) {
|
|
356
|
+
for (const artifact of artifacts) {
|
|
357
|
+
console.log(`${LOG} ${artifact.format}: ${relative(base, artifact.path)} (${artifact.size} bytes)`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
172
360
|
/**
|
|
173
361
|
* Refuse a build target this command cannot package correctly.
|
|
174
362
|
*
|
|
@@ -181,6 +369,10 @@ async function packOne(input) {
|
|
|
181
369
|
* An undeclared target is the common case for a GJS app and is allowed: `app`
|
|
182
370
|
* otherwise defaults to the HOST runtime, which would refuse a perfectly good
|
|
183
371
|
* GJS project merely for running this command under Node.
|
|
372
|
+
*
|
|
373
|
+
* Phase one only, and nothing checks it again in phase two: a stage can only
|
|
374
|
+
* exist because this passed, and the staged `bin/<name>` carries the
|
|
375
|
+
* `exec gjs -m` line that proves it.
|
|
184
376
|
*/
|
|
185
377
|
function assertShippableTarget(app) {
|
|
186
378
|
if (app === undefined || app === 'gjs')
|
|
@@ -328,6 +328,38 @@ export interface AppMetadata {
|
|
|
328
328
|
* https://www.freedesktop.org/software/appstream/docs/sect-Metadata-DesktopApps.html#tag-dapp-kudos
|
|
329
329
|
*/
|
|
330
330
|
kudos?: string[];
|
|
331
|
+
/**
|
|
332
|
+
* File types this package DEFINES for the whole system — a shared-mime-info
|
|
333
|
+
* document staged into `share/mime/packages/`.
|
|
334
|
+
*
|
|
335
|
+
* Distinct from `provides.mimetypes`, which only says the app HANDLES a type.
|
|
336
|
+
* For a standard type (`text/plain`, `application/pdf`) handling is all you
|
|
337
|
+
* need: the distribution already defines it. For a type of your own
|
|
338
|
+
* (`application/x-bauplan`) handling it is not enough and the failure is
|
|
339
|
+
* silent — nothing on the system knows the type exists, so a `.bauplan` file
|
|
340
|
+
* is never recognised as one, `MimeType=` in the desktop entry matches
|
|
341
|
+
* nothing, and double-clicking the file does nothing at all. No error, no
|
|
342
|
+
* log line: the association simply never fires.
|
|
343
|
+
*
|
|
344
|
+
* Every type declared here is added to the handled set automatically, because
|
|
345
|
+
* defining a file type in your own package and then not opening it is not a
|
|
346
|
+
* thing anyone means to do.
|
|
347
|
+
*/
|
|
348
|
+
mimeTypes?: Array<{
|
|
349
|
+
/** `<media>/<subtype>`, e.g. `application/x-bauplan`. */
|
|
350
|
+
type: string;
|
|
351
|
+
/**
|
|
352
|
+
* What a file manager shows instead of the raw type string. Required:
|
|
353
|
+
* a type with no comment is listed to the user as `application/x-bauplan`.
|
|
354
|
+
*/
|
|
355
|
+
comment: string;
|
|
356
|
+
/** Filename patterns, e.g. `["*.bauplan"]`. */
|
|
357
|
+
globs?: string[];
|
|
358
|
+
/** A registered type to inherit from, e.g. `application/zip` for a zip container. */
|
|
359
|
+
subClassOf?: string;
|
|
360
|
+
/** Fallback icon name when no type-specific icon is installed. */
|
|
361
|
+
genericIcon?: string;
|
|
362
|
+
}>;
|
|
331
363
|
/**
|
|
332
364
|
* What this app provides to the system. `<binary>` is auto-included with the
|
|
333
365
|
* value of `command` when omitted — AppStream needs it to register the binary
|