@gjsify/cli 0.44.0 → 0.45.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 +192 -165
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +833 -54
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/types/config-data.d.ts +63 -1
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +50 -0
- package/lib/utils/ship/depends.js +117 -3
- package/lib/utils/ship/discover.js +18 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak.js +1 -1
- package/lib/utils/ship/formats.d.ts +85 -7
- package/lib/utils/ship/formats.js +734 -15
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +362 -17
- package/lib/utils/ship/layout.d.ts +268 -0
- package/lib/utils/ship/layout.js +351 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +417 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +580 -14
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +20 -7
- package/lib/utils/ship/plist.d.ts +58 -0
- package/lib/utils/ship/plist.js +198 -0
- package/lib/utils/ship/rpm.js +9 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +19 -1
- package/lib/utils/ship/settings.js +12 -1
- package/lib/utils/ship/share-dirs.d.ts +23 -0
- package/lib/utils/ship/share-dirs.js +46 -0
- package/lib/utils/ship/signing.d.ts +195 -0
- package/lib/utils/ship/signing.js +317 -0
- package/lib/utils/ship/stage-manifest.d.ts +40 -12
- package/lib/utils/ship/stage-manifest.js +99 -13
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +71 -1
- package/lib/utils/ship/types.d.ts +158 -13
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
package/lib/commands/ship.js
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
// which is what a developer on one machine wants and what the e2e suite
|
|
15
15
|
// compares the split against byte for byte.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
17
|
+
// THREE LAYOUTS AND NINE FORMATS as of #1354 M6, which is what the two-phase
|
|
18
|
+
// split was built for. A Linux artifact bundles no runtime — GJS and GTK come
|
|
19
|
+
// from the distribution and shipping ~100 MiB of them would be cargo cult (ADR
|
|
20
|
+
// 0024 § 4) — while a `.app` and a Windows program directory CARRY theirs, since
|
|
21
|
+
// neither OS ships one (`utils/ship/app-runtime.ts`). And the split earns itself
|
|
22
|
+
// on the two rows that are host-bound: a `.dmg` is a UDIF image over a real
|
|
23
|
+
// HFS+/APFS volume that no Linux host in this tree can write (§ A1), and a
|
|
24
|
+
// signature is made where the identity lives (§ A14).
|
|
23
25
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
24
26
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
25
27
|
import { Config } from '../config.js';
|
|
@@ -27,33 +29,55 @@ import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMe
|
|
|
27
29
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
28
30
|
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
29
31
|
import { buildDeb } from '../utils/ship/deb.js';
|
|
30
|
-
import { deriveDepends, warnAboutGjsFloor } from '../utils/ship/depends.js';
|
|
32
|
+
import { deriveDepends, warnAboutGjsFloor, warnAboutNodeFloor } from '../utils/ship/depends.js';
|
|
33
|
+
import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.js';
|
|
31
34
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
35
|
+
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
32
36
|
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
33
|
-
import {
|
|
37
|
+
import { localizeMetadata } from '../utils/ship/localize-metadata.js';
|
|
38
|
+
import { assertHostCanFinish, assertToolsInstalled, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
|
|
39
|
+
import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout, LAYOUT_NAMES, } from '../utils/ship/layout.js';
|
|
40
|
+
import { isNodeRuntimeTarget, resolveNodeRuntime } from '../utils/ship/node-runtime.js';
|
|
41
|
+
import { compileSchemasForStage } from '../utils/ship/schemas.js';
|
|
42
|
+
import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
|
|
43
|
+
import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
|
|
34
44
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
35
|
-
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
45
|
+
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
36
46
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
47
|
+
import { assertHostCanSign, notarizeArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
37
48
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
38
49
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
39
50
|
import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
|
|
40
51
|
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
|
|
41
|
-
import { readStage, writeStage } from '../utils/ship/stage-writer.js';
|
|
52
|
+
import { directorySize, readStage, writePayload, writeStage } from '../utils/ship/stage-writer.js';
|
|
42
53
|
const LOG = '[gjsify ship]';
|
|
43
54
|
export const shipCommand = {
|
|
44
|
-
command: 'ship',
|
|
55
|
+
command: 'ship [os]',
|
|
45
56
|
description: `Build installable artifacts (${FORMAT_IDS.join(', ')}) from the current project.`,
|
|
46
|
-
// `
|
|
57
|
+
// `defaultFormatIds` in the flag text, `FORMAT_IDS` in the description:
|
|
47
58
|
// one of them answers "what can this build", the other "what will it build
|
|
48
59
|
// if I say nothing", and they stopped being the same list when a host-bound
|
|
49
|
-
// format arrived.
|
|
60
|
+
// format arrived — and then stopped again when a second layout did.
|
|
50
61
|
builder: (yargs) => yargs
|
|
62
|
+
.positional('os', {
|
|
63
|
+
type: 'string',
|
|
64
|
+
// Deliberately no yargs `choices`: `resolveLayout` accepts
|
|
65
|
+
// `win32` beside `windows` — the spelling `--expect-target`
|
|
66
|
+
// prints — and says so when it refuses, which a `choices` list
|
|
67
|
+
// cannot.
|
|
68
|
+
description: `Operating system whose LAYOUT to assemble (${LAYOUT_NAMES.join('|')}, ADR 0024 § A2). ` +
|
|
69
|
+
'Default: this host. Assembly is not host-bound, so any of them can be staged anywhere.',
|
|
70
|
+
})
|
|
51
71
|
.option('target', {
|
|
52
72
|
type: 'string',
|
|
53
73
|
array: true,
|
|
54
|
-
description:
|
|
55
|
-
`
|
|
56
|
-
`${
|
|
74
|
+
description: 'Formats to build, comma-separated or repeated. Default: every format wrapping the ' +
|
|
75
|
+
`target OS's layout that needs no extra tooling — on linux, ` +
|
|
76
|
+
`${defaultFormatIds('linux').join(',')}. Also available for linux, and opt-in because it ` +
|
|
77
|
+
'needs tooling this CLI does not carry: ' +
|
|
78
|
+
`${formatIdsFor('linux')
|
|
79
|
+
.filter((id) => !defaultFormatIds('linux').includes(id))
|
|
80
|
+
.join(', ')}.`,
|
|
57
81
|
})
|
|
58
82
|
.option('out', {
|
|
59
83
|
type: 'string',
|
|
@@ -84,6 +108,40 @@ export const shipCommand = {
|
|
|
84
108
|
.option('arch', {
|
|
85
109
|
type: 'string',
|
|
86
110
|
description: 'Target architecture in `process.arch` spelling. Default: this host.',
|
|
111
|
+
})
|
|
112
|
+
.option('sign', {
|
|
113
|
+
type: 'string',
|
|
114
|
+
// `requiresArg`, and it is not tidiness. `-` is the RESERVED
|
|
115
|
+
// ad-hoc identity — the one value of this flag that needs no
|
|
116
|
+
// developer identity and the only one this project's own CI can
|
|
117
|
+
// use — and yargs-parser treats a lone `-` as a positional by
|
|
118
|
+
// convention (stdin), so `--sign -` parsed as `sign: ''` plus a
|
|
119
|
+
// stray `-` and the strict-mode refusal was "Unknown argument: -".
|
|
120
|
+
// Measured on the first cut of this flag. The cost is that a bare
|
|
121
|
+
// `--sign` with no value is now "Not enough arguments following"
|
|
122
|
+
// rather than the empty-identity skip; that skip stays reachable
|
|
123
|
+
// through `--sign ''` and through the config key, which is where
|
|
124
|
+
// an unset CI variable actually lands.
|
|
125
|
+
requiresArg: true,
|
|
126
|
+
// An IDENTITY, never a certificate (ADR 0024 § A12): `codesign`
|
|
127
|
+
// and `signtool` are both handed a NAME and look the private key
|
|
128
|
+
// up themselves, so this command is never given a secret and has
|
|
129
|
+
// nothing to redact. Deliberately no `default:` — a yargs default
|
|
130
|
+
// is indistinguishable from a value the user typed and would
|
|
131
|
+
// clobber `gjsify.ship.sign.<os>.identity`.
|
|
132
|
+
description: 'Sign the payload with this identity, e.g. a Developer ID name or SHA-1 fingerprint. ' +
|
|
133
|
+
'`-` signs ad-hoc and needs no developer identity. Default: ' +
|
|
134
|
+
'`gjsify.ship.sign.<darwin|win32>.identity`; absent means unsigned, which is a ' +
|
|
135
|
+
'legitimate output.',
|
|
136
|
+
})
|
|
137
|
+
.option('notarize', {
|
|
138
|
+
type: 'string',
|
|
139
|
+
// Same reason as `--sign` one option up, applied for consistency
|
|
140
|
+
// rather than for a measured case: a credential that begins with
|
|
141
|
+
// a dash is not a shape anybody should have to think about.
|
|
142
|
+
requiresArg: true,
|
|
143
|
+
description: 'Submit the signed artifact to Apple with this `notarytool` keychain profile. Needs ' +
|
|
144
|
+
'--sign; darwin only.',
|
|
87
145
|
})
|
|
88
146
|
.option('verbose', {
|
|
89
147
|
type: 'boolean',
|
|
@@ -107,6 +165,10 @@ export const shipCommand = {
|
|
|
107
165
|
*/
|
|
108
166
|
async function assemble(args) {
|
|
109
167
|
const projectDir = process.cwd();
|
|
168
|
+
// FIRST, because everything below is decided per layout: which formats
|
|
169
|
+
// exist, which build target the launcher can run, and where every staged
|
|
170
|
+
// file lands. A bad positional must not cost a build.
|
|
171
|
+
const layout = args.os === undefined ? hostLayout() : resolveLayout(args.os);
|
|
110
172
|
const config = new Config();
|
|
111
173
|
// No `.catch(() => ({}))` here, unlike the flatpak commands: a config
|
|
112
174
|
// file that fails to load would silently become "no config", and the
|
|
@@ -116,10 +178,86 @@ async function assemble(args) {
|
|
|
116
178
|
const ship = configData.ship ?? {};
|
|
117
179
|
const flatpak = configData.flatpak ?? {};
|
|
118
180
|
assertShippableTarget(configData.app);
|
|
181
|
+
// Resolved HERE and not from `settings`, because the format list is decided
|
|
182
|
+
// before anything is built and `resolveShipSettings` runs after. The
|
|
183
|
+
// defaulting rule is the one `resolveShipSettings` states and must stay the
|
|
184
|
+
// same one: an undeclared target means `gjs`, never the host runtime this
|
|
185
|
+
// command happens to run under.
|
|
186
|
+
const interpreter = configData.app === 'node' ? 'node' : 'gjs';
|
|
119
187
|
const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
|
|
120
188
|
// Resolved before anything is built or written: a typo'd `--target`
|
|
121
189
|
// should not cost a build and leave a half-written `ship/stage/`.
|
|
122
|
-
|
|
190
|
+
//
|
|
191
|
+
// THREE sources, three meanings, and collapsing them was a real defect. A
|
|
192
|
+
// `--target` is a claim about THIS run, so a format belonging to another
|
|
193
|
+
// layout is refused by name. `gjsify.ship.targets` is a project-level
|
|
194
|
+
// DEFAULT written once, so it is filtered to this layout instead — passing it
|
|
195
|
+
// through the strict path made `gjsify ship darwin --stage` exit 1 inside
|
|
196
|
+
// this very repository, whose `packages/infra/cli/package.json` declares
|
|
197
|
+
// `targets: ["deb", "rpm"]`, with a message telling the author to run the
|
|
198
|
+
// command they had just run. And the derived default can legitimately be
|
|
199
|
+
// EMPTY, which `resolveFormats` refuses — rightly, for a list a caller typed.
|
|
200
|
+
let formats;
|
|
201
|
+
/** Formats that wrap this layout and cannot run this project — see the derived branch. */
|
|
202
|
+
let unusable = [];
|
|
203
|
+
if (args.target !== undefined) {
|
|
204
|
+
formats = resolveFormats(args.target, layout);
|
|
205
|
+
}
|
|
206
|
+
else if (ship.targets !== undefined) {
|
|
207
|
+
const configured = configuredFormats(ship.targets, layout);
|
|
208
|
+
formats = configured.formats;
|
|
209
|
+
// SAID, not swallowed. The `--target` path refuses a foreign format by
|
|
210
|
+
// name; this one used to shorten the list in silence, so a project whose
|
|
211
|
+
// configured formats all wrap another layout got a stage and no account
|
|
212
|
+
// of where its targets went.
|
|
213
|
+
if (configured.dropped.length > 0) {
|
|
214
|
+
console.log(`${LOG} \`gjsify.ship.targets\` names ${configured.dropped.join(', ')}, which wrap another ` +
|
|
215
|
+
`layout — not built for ${layout.name}. Pass --target to ask for one explicitly.`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// FILTERED BY THE INTERPRETER, and the third of the same shape as the two
|
|
220
|
+
// branches above: a derived default is not a claim anybody made, so a
|
|
221
|
+
// format this project cannot use is dropped rather than refused. It has to
|
|
222
|
+
// be — every format wrapping the darwin layout is `interpreters: ['node']`
|
|
223
|
+
// (there is no relocatable GJS to put in a bundle), so without this
|
|
224
|
+
// `gjsify ship darwin --stage` began exiting 1 on a `--app gjs` project,
|
|
225
|
+
// which is every project this command has today. Measured on the first cut
|
|
226
|
+
// of these rows: the whole audience of M1 lost the ability to assemble the
|
|
227
|
+
// layout M1 added, over a format none of them had asked for.
|
|
228
|
+
const wraps = defaultFormatIds(layout.os).map((id) => FORMATS[id]);
|
|
229
|
+
formats = wraps.filter((format) => format.interpreters.includes(interpreter));
|
|
230
|
+
unusable = wraps.filter((format) => !format.interpreters.includes(interpreter));
|
|
231
|
+
// SAID, like the layout filter one branch up. A stage whose `formats` is
|
|
232
|
+
// empty for this reason looks identical to one whose layout nothing wraps,
|
|
233
|
+
// and the two need different next steps.
|
|
234
|
+
if (unusable.length > 0) {
|
|
235
|
+
console.log(`${LOG} ${unusable.map((format) => format.id).join(', ')} wrap the ${layout.name} layout but ` +
|
|
236
|
+
`cannot run \`gjsify.app: "${interpreter}"\` — ${unusable[0]?.interpreterGap ?? ''}. ` +
|
|
237
|
+
'Assembling the layout only; pass --target to be told the same thing as an error.');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!args.stage)
|
|
241
|
+
assertPackable(formats, layout, args.os === undefined ? 'host' : 'positional', unusable);
|
|
242
|
+
// BEFORE the build, beside `assertCanPack` and for the same reason: a
|
|
243
|
+
// `--sign` this host cannot honour — wrong OS, no tool, a layout that has no
|
|
244
|
+
// signature at all — is knowable up front, and discovering it after the
|
|
245
|
+
// project's own build has run costs the whole build for a refusal nothing
|
|
246
|
+
// learned from building.
|
|
247
|
+
//
|
|
248
|
+
// `ship.sign` is read HERE and nowhere on the `--from-stage` path, and that
|
|
249
|
+
// asymmetry is § A14's amendment to § A1: a format declares where it can be
|
|
250
|
+
// packed, the RUN declares what it can sign with. The finishing host has no
|
|
251
|
+
// project to read a default out of, so there it is the flag or nothing.
|
|
252
|
+
const sign = resolveSignPlan({ flag: args.sign, config: ship.sign, layoutOs: layout.os });
|
|
253
|
+
const notarize = resolveNotaryPlan({ flag: args.notarize, layoutOs: layout.os, sign });
|
|
254
|
+
if (args.stage)
|
|
255
|
+
assertNothingToSignYet(args);
|
|
256
|
+
if (!args.stage) {
|
|
257
|
+
announceSigning(sign, notarize);
|
|
258
|
+
if (sign.kind === 'sign')
|
|
259
|
+
assertHostCanSign(sign.signer);
|
|
260
|
+
}
|
|
123
261
|
// BEFORE the build, not before the pack. `gjsify ship` runs the project's
|
|
124
262
|
// own `build` script first, so discovering a missing `flatpak-builder`
|
|
125
263
|
// afterwards costs the whole build for a refusal that was knowable up
|
|
@@ -142,11 +280,18 @@ async function assemble(args) {
|
|
|
142
280
|
pkg,
|
|
143
281
|
ship,
|
|
144
282
|
flatpak,
|
|
145
|
-
cli: { outDir: args.out, arch: args.arch },
|
|
283
|
+
cli: { outDir: args.out, arch: args.arch, layoutOs: layout.os },
|
|
146
284
|
discovered,
|
|
285
|
+
app: configData.app,
|
|
147
286
|
});
|
|
148
287
|
for (const warning of warnings)
|
|
149
288
|
console.warn(`${LOG} ${warning}`);
|
|
289
|
+
// BEFORE anything is staged, and NOT under `assertCanPack` (which `--stage`
|
|
290
|
+
// deliberately skips): a stage whose launcher execs an interpreter the target
|
|
291
|
+
// runtime does not have is already wrong, and it is the stage that crosses to
|
|
292
|
+
// the packing host.
|
|
293
|
+
for (const format of formats)
|
|
294
|
+
assertFormatCanRunInterpreter(format, settings.app);
|
|
150
295
|
const mtime = buildTimestamp(settings.bundlePath);
|
|
151
296
|
const metadataInputs = {
|
|
152
297
|
appId: settings.appId,
|
|
@@ -171,21 +316,125 @@ async function assemble(args) {
|
|
|
171
316
|
console.log(`${LOG} licence text from ${relative(projectDir, settings.licenseFile)} ` +
|
|
172
317
|
'(this package carries none of its own)');
|
|
173
318
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
319
|
+
// Rendered in English, then TRANSLATED with the catalogues this package
|
|
320
|
+
// already stages. Without this step the two halves never met: `share/locale/`
|
|
321
|
+
// shipped every `.mo` while `Name=` and `<name>` stayed English, so a fully
|
|
322
|
+
// translated app showed an English entry in the app menu and in Software.
|
|
323
|
+
// Both files stay valid either way, which is why nobody reported it.
|
|
324
|
+
const translated = localizeMetadata({
|
|
177
325
|
metainfo: settings.kind === 'app' ? renderMetainfoApp(metadataInputs) : renderMetainfoCli(metadataInputs),
|
|
178
326
|
desktopEntry: settings.kind === 'app' ? renderDesktopEntry(metadataInputs) : undefined,
|
|
327
|
+
}, settings.localeFiles);
|
|
328
|
+
// BEFORE the launcher is rendered, because the launcher's exec line and its two
|
|
329
|
+
// locators are functions of what the stage carries (#1354 M2b). Nothing is
|
|
330
|
+
// resolved for a layout whose interpreter is a package dependency: on Linux the
|
|
331
|
+
// `.deb`/`.rpm` `Depends:` is the promise that `node` or `gjs` is on `PATH`, and
|
|
332
|
+
// carrying a second copy inside the payload would be a private interpreter no
|
|
333
|
+
// distribution update ever patches.
|
|
334
|
+
const runtime = resolveCarriedRuntime(layout, settings, projectDir);
|
|
335
|
+
const stageInputs = {
|
|
336
|
+
bundleFiles: discovered.bundleFiles,
|
|
337
|
+
launcher: renderLauncher(settings, basename(settings.bundlePath), layout, runtime.launcher),
|
|
338
|
+
metainfo: translated.metainfo,
|
|
339
|
+
desktopEntry: translated.desktopEntry,
|
|
179
340
|
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
180
341
|
};
|
|
181
342
|
const outRoot = resolve(projectDir, settings.outDir);
|
|
182
343
|
const stageDir = join(outRoot, 'stage');
|
|
183
|
-
|
|
344
|
+
// Planned once, in the prefix-relative shape, then PLACED. Keeping the two
|
|
345
|
+
// steps apart is what makes "one payload, three layouts" a checkable claim
|
|
346
|
+
// rather than a slogan: the plan is the payload, `placeStage` is the layout
|
|
347
|
+
// map, and `tests/e2e/ship-layout` asserts the three file sets agree modulo
|
|
348
|
+
// exactly that map.
|
|
349
|
+
//
|
|
350
|
+
// The ONE prefix-relative addition a layout makes, and it is not a second
|
|
351
|
+
// plan: a layout with no install step has to carry `gschemas.compiled`,
|
|
352
|
+
// because every launcher points XDG_DATA_DIRS at the staged `share/` and
|
|
353
|
+
// GSettings ABORTS on a schema directory that holds only sources. On Linux the
|
|
354
|
+
// `.deb`/`.rpm` postinst compiles the SYSTEM directory at install time
|
|
355
|
+
// (`utils/ship/scripts.ts`), where our schemas merge with every other
|
|
356
|
+
// package's — shipping a prebuilt cache there would be a file the install step
|
|
357
|
+
// overwrites. So the compile is conditional on the layout and nothing else.
|
|
358
|
+
const planned = [
|
|
359
|
+
...planStage(settings, stageInputs),
|
|
360
|
+
...(layout.os === 'linux'
|
|
361
|
+
? []
|
|
362
|
+
: await compileSchemasForStage({
|
|
363
|
+
schemaFiles: settings.schemaFiles,
|
|
364
|
+
workDir: join(outRoot, 'schemas'),
|
|
365
|
+
})),
|
|
366
|
+
];
|
|
367
|
+
const staged = placeStage(layout, settings, planned, runtime.files);
|
|
184
368
|
writeStage(stageDir, staged);
|
|
185
|
-
console.log(`${LOG} staged ${staged.length} file(s) in ${relative(projectDir, stageDir)}/`);
|
|
369
|
+
console.log(`${LOG} staged ${staged.length} file(s) for ${layout.name} in ${relative(projectDir, stageDir)}/`);
|
|
186
370
|
if (args.verbose)
|
|
187
371
|
for (const file of staged)
|
|
188
372
|
console.log(`${LOG} ${file.path}`);
|
|
373
|
+
// Read the tree back and hold the payload against the label, HERE and not
|
|
374
|
+
// only in `packOne`. That check has always existed, one phase to the right,
|
|
375
|
+
// where it guards the artifact — and this is the first milestone in which the
|
|
376
|
+
// STAGE is the deliverable: darwin and windows have no packer, so a stage
|
|
377
|
+
// assembled `--arch x64` from an arm64 payload reached `--expect-target
|
|
378
|
+
// darwin-x64` with nothing having compared the two. Measured before this
|
|
379
|
+
// line: exit 0, `target: {os: "darwin", arch: "x64"}`, and a Mach-O
|
|
380
|
+
// `cputype` of arm64 inside it.
|
|
381
|
+
//
|
|
382
|
+
// It costs a second read of the payload on the one-shot Linux path, where
|
|
383
|
+
// `packOne` reads it again. That is the price of checking the tree that is
|
|
384
|
+
// shipped rather than the bytes that happen to be in memory, and it is the
|
|
385
|
+
// same reason `readStage` exists at all.
|
|
386
|
+
assertPayloadMatchesArch(readStage([stageDir], staged), settings.arch);
|
|
387
|
+
// What the map carried into a layout that has no install step for it.
|
|
388
|
+
//
|
|
389
|
+
// NOT a warning about this command's output being wrong — the files are the
|
|
390
|
+
// payload's and belong to it. It is the half the layout equality cannot see:
|
|
391
|
+
// three of these are only correct on Linux because a `.deb`/`.rpm` scriptlet
|
|
392
|
+
// compiles or reindexes them at install, and two are freedesktop metadata
|
|
393
|
+
// neither OS reads. Printed so the author of a `.app` knows before the
|
|
394
|
+
// container exists to decide what they become (ADR 0024 stages 4 and 5).
|
|
395
|
+
if (layout.os !== 'linux') {
|
|
396
|
+
const carried = linuxInstallDependent(planned);
|
|
397
|
+
if (carried.length > 0) {
|
|
398
|
+
console.warn(`${LOG} the ${layout.name} layout carries ${carried.length} file(s) whose Linux correctness ` +
|
|
399
|
+
'comes from a package install step it has no equivalent for:');
|
|
400
|
+
// The marker is the whole point of `ShareVerdict`. Every launcher
|
|
401
|
+
// exports XDG_DATA_DIRS at the staged `share/`, so an uncompiled
|
|
402
|
+
// schema directory is not a missing feature — `g_settings_new()`
|
|
403
|
+
// ABORTS on it. Printing that at the same weight as "neither OS reads
|
|
404
|
+
// one" is what buried it in a list of five.
|
|
405
|
+
const marker = { aborts: 'ABORTS: ', unknown: 'UNCLASSIFIED: ', inert: '' };
|
|
406
|
+
for (const { path, verdict, why } of carried) {
|
|
407
|
+
console.warn(`${LOG} ${marker[verdict] ?? ''}${path} — ${why}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// WHAT THE BUNDLE CARRIES, and what it still does not. Printed where the
|
|
411
|
+
// layout's other honesty lives, and the `runtimeGap` is now CONDITIONAL:
|
|
412
|
+
// that string says "the staged launcher execs an interpreter off `PATH`,
|
|
413
|
+
// which a downloaded `.app` cannot assume", and printing it over a stage
|
|
414
|
+
// that carries its own interpreter would be the command telling the author
|
|
415
|
+
// something untrue about the tree it had just written.
|
|
416
|
+
for (const line of runtime.found)
|
|
417
|
+
console.log(`${LOG} carries its own ${line}`);
|
|
418
|
+
for (const line of runtime.missing)
|
|
419
|
+
console.warn(`${LOG} ${line}`);
|
|
420
|
+
if (runtime.launcher.interpreter === undefined && layout.runtimeGap !== undefined) {
|
|
421
|
+
console.warn(`${LOG} ${layout.runtimeGap}`);
|
|
422
|
+
}
|
|
423
|
+
// THE OTHER AXIS, warned about HERE and refused in `packOne`, and the split
|
|
424
|
+
// is the same one `runtimeGap` and `assertFormatCanRunInterpreter` already
|
|
425
|
+
// draw. A stage is a build intermediate, and assembling one for an
|
|
426
|
+
// architecture no runtime exists for is what `tests/e2e/ship-layout` does on
|
|
427
|
+
// purpose: it proves the layout MAP over one payload, and that payload's
|
|
428
|
+
// native file has an architecture. What must not leave is an ARTIFACT — so
|
|
429
|
+
// the refusal sits where an artifact is about to be written and the author
|
|
430
|
+
// is told here, at the `--arch` that caused it, rather than only on the far
|
|
431
|
+
// side of a handoff.
|
|
432
|
+
if (layout.arches !== null && !layout.arches.only.includes(settings.arch)) {
|
|
433
|
+
console.warn(`${LOG} this stage is labelled ${settings.arch} and the ${layout.name} layout has no runtime ` +
|
|
434
|
+
`for it — ${layout.arches.why}. Nothing can pack this stage; ` +
|
|
435
|
+
`\`--arch ${layout.arches.only.join('|')}\` is what produces an artifact.`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
189
438
|
// Scanned from the BUILD TREE's bundle, which is why it has to be recorded:
|
|
190
439
|
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
191
440
|
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
@@ -193,17 +442,35 @@ async function assemble(args) {
|
|
|
193
442
|
const namespaces = scanGiNamespaces(readFileSync(settings.bundlePath, 'utf-8'));
|
|
194
443
|
if (args.verbose)
|
|
195
444
|
console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
|
|
445
|
+
// PLACED, like the payload, and through the same map. `planOverlay` answers a
|
|
446
|
+
// question about the FORMAT (`share/doc/<pkg>/copyright` for Debian policy,
|
|
447
|
+
// `share/licenses/<pkg>/LICENSE` for rpm) in the prefix-relative shape every
|
|
448
|
+
// plan uses; `packOne` then merges the overlay into the payload it reads back,
|
|
449
|
+
// so the two have to be in one coordinate system. On Linux `place()` is the
|
|
450
|
+
// identity for `share/…` and nothing moves. On darwin the licence would
|
|
451
|
+
// otherwise land at the STAGE root, outside `<App>.app` — beside the bundle
|
|
452
|
+
// rather than in it, where `codesign` later refuses it and no user ever finds
|
|
453
|
+
// it. `dirs.other`'s doc already names that hazard for `extraFiles`; the
|
|
454
|
+
// overlay reaches it by the same route.
|
|
196
455
|
const overlay = new Map();
|
|
197
|
-
for (const format of formats)
|
|
198
|
-
overlay.set(format.id, planOverlay(settings, format, stageInputs))
|
|
456
|
+
for (const format of formats) {
|
|
457
|
+
overlay.set(format.id, planOverlay(settings, format, stageInputs).map((file) => ({
|
|
458
|
+
...file,
|
|
459
|
+
path: place(layout, settings, file.path),
|
|
460
|
+
})));
|
|
461
|
+
}
|
|
199
462
|
// The dependency derivation runs HERE as well, and its result is thrown
|
|
200
463
|
// away. It is the only check in the pipeline that can refuse a project
|
|
201
464
|
// (`gi://Nautilus` with no known package, ADR 0024 § 6), and this is the
|
|
202
465
|
// host that has the `package.json` the refusal tells you to edit — letting
|
|
203
466
|
// it fire two jobs later, on a machine with no project, would name a fix
|
|
204
|
-
// nobody there can apply. Facts come from the
|
|
205
|
-
//
|
|
206
|
-
|
|
467
|
+
// nobody there can apply. Facts come from the PREFIX-RELATIVE plan, not from
|
|
468
|
+
// the placed tree: `readPayloadFacts` asks `share/icons/hicolor/…` questions,
|
|
469
|
+
// which is the shape the plan is written in and the shape the only formats
|
|
470
|
+
// that read the answer (`deb`, `rpm`) install into. Reading the placed tree
|
|
471
|
+
// would give the same answer on Linux and quietly give `false` everywhere
|
|
472
|
+
// else the day a format wraps another layout.
|
|
473
|
+
const facts = readPayloadFacts(planned);
|
|
207
474
|
for (const format of formats) {
|
|
208
475
|
// `format.depends`, not `format.id`: a Flatpak has no package
|
|
209
476
|
// dependency list at all, and asking for one would either invent a
|
|
@@ -217,14 +484,21 @@ async function assemble(args) {
|
|
|
217
484
|
namespaces,
|
|
218
485
|
hasIcons: facts.hasIcons,
|
|
219
486
|
hasSchemas: facts.hasSchemas,
|
|
487
|
+
interpreter: settings.app,
|
|
220
488
|
extra: settings.extraDepends[format.depends],
|
|
221
489
|
typelibPackages: settings.typelibPackages,
|
|
222
490
|
bundledTypelibs: facts.bundledTypelibs,
|
|
223
491
|
minGjsVersion: settings.minGjsVersion,
|
|
492
|
+
minNodeVersion: settings.minNodeVersion,
|
|
224
493
|
});
|
|
225
|
-
|
|
494
|
+
// One warning per package, for the interpreter it actually declares. A
|
|
495
|
+
// floor warning about a dependency this package does not emit is noise,
|
|
496
|
+
// and a noisy warning is the one nobody reads when it matters.
|
|
497
|
+
const floorWarnings = settings.app === 'node'
|
|
498
|
+
? warnAboutNodeFloor(format.depends, settings.minNodeVersion)
|
|
499
|
+
: warnAboutGjsFloor(format.depends, settings.minGjsVersion);
|
|
500
|
+
for (const warning of floorWarnings)
|
|
226
501
|
console.warn(`${LOG} ${warning}`);
|
|
227
|
-
}
|
|
228
502
|
}
|
|
229
503
|
// Written unconditionally, not only under `--stage`: a `ship/stage/` that
|
|
230
504
|
// is sometimes packable elsewhere and sometimes not is a worse contract
|
|
@@ -232,7 +506,23 @@ async function assemble(args) {
|
|
|
232
506
|
// one-process artifact with the two-phase one.
|
|
233
507
|
const manifest = writeStageManifest({ stageDir, settings, formats, mtime, namespaces, staged, overlay });
|
|
234
508
|
if (args.stage) {
|
|
235
|
-
|
|
509
|
+
// `(none)` spelled out, because an empty list printed as nothing after
|
|
510
|
+
// "formats " reads as a truncated line rather than as a fact. WHICH fact
|
|
511
|
+
// it is depends on the layout: all three now have formats — three rows each
|
|
512
|
+
// for darwin and windows — so an empty list means this project cannot use
|
|
513
|
+
// them, and printing "none yet" would tell a `--app gjs` author to wait for
|
|
514
|
+
// a milestone that has already landed.
|
|
515
|
+
// Three reasons a list can be empty, and each names a different next step:
|
|
516
|
+
// this project cannot use the formats that wrap the layout; the layout has
|
|
517
|
+
// none; or the configured `targets` all wrap another one, which the branch
|
|
518
|
+
// above has already SAID in its own words rather than repeating here.
|
|
519
|
+
const none = unusable.length > 0
|
|
520
|
+
? `(none — ${unusable.map((format) => format.id).join(' and ')} need \`gjsify.app: "node"\`)`
|
|
521
|
+
: formatIdsFor(layout.os).length === 0
|
|
522
|
+
? `(none — no format wraps the ${layout.name} layout)`
|
|
523
|
+
: '(none asked for)';
|
|
524
|
+
const wraps = manifest.formats.join(', ') || none;
|
|
525
|
+
console.log(`${LOG} stage manifest: ${formatTarget(manifest.target)}, formats ${wraps}`);
|
|
236
526
|
return;
|
|
237
527
|
}
|
|
238
528
|
const artifacts = [];
|
|
@@ -247,11 +537,83 @@ async function assemble(args) {
|
|
|
247
537
|
outRoot,
|
|
248
538
|
namespaces,
|
|
249
539
|
mtime,
|
|
540
|
+
sign,
|
|
541
|
+
notarize,
|
|
250
542
|
verbose: args.verbose,
|
|
251
543
|
}));
|
|
252
544
|
}
|
|
253
545
|
printArtifacts(projectDir, artifacts);
|
|
254
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Refuse `--sign` / `--notarize` on the phase that produces no artifact.
|
|
549
|
+
*
|
|
550
|
+
* `--stage` writes a build intermediate, and a signature over one would be
|
|
551
|
+
* invalidated by the very next thing that happens to it — the packer reads the
|
|
552
|
+
* tree back and the container is built around it (ADR 0024 § A17 fixes that
|
|
553
|
+
* order for the opposite reason: `readStage` compares SIZES, and a re-signed
|
|
554
|
+
* file has a new one). Silently accepting the flag would produce a stage nobody
|
|
555
|
+
* could tell from an unsigned one.
|
|
556
|
+
*/
|
|
557
|
+
function assertNothingToSignYet(args) {
|
|
558
|
+
const given = args.sign !== undefined ? '--sign' : args.notarize !== undefined ? '--notarize' : undefined;
|
|
559
|
+
if (given === undefined)
|
|
560
|
+
return;
|
|
561
|
+
throw new Error(`gjsify ship: ${given} belongs to the finish phase, and --stage produces no artifact to sign — it ` +
|
|
562
|
+
'writes the tree a packing host reads back. Assemble here, then run ' +
|
|
563
|
+
'`gjsify ship --from-stage <dir> --sign <identity>` on the host that holds the identity ' +
|
|
564
|
+
'(ADR 0024 § A2, § A14).');
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Say what this run will do about signatures, once, before any of it happens.
|
|
568
|
+
*
|
|
569
|
+
* PRINTED rather than silent, and to stderr, because ADR 0024 § A13 makes the
|
|
570
|
+
* skip the visible half of "unsigned is the default path": a pipeline that
|
|
571
|
+
* captures the artifact list still shows the line, and an unsigned artifact
|
|
572
|
+
* nobody was told about is how a signature gets claimed that was never made.
|
|
573
|
+
*/
|
|
574
|
+
function announceSigning(sign, notarize) {
|
|
575
|
+
if (sign.kind === 'skip')
|
|
576
|
+
console.warn(`${LOG} ${sign.message}`);
|
|
577
|
+
if (notarize.kind === 'skip' && sign.kind === 'sign')
|
|
578
|
+
console.warn(`${LOG} ${notarize.message}`);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The runtime this artifact carries INSIDE itself, or an empty answer.
|
|
582
|
+
*
|
|
583
|
+
* THE GATE IS THE LAYOUT AND THE INTERPRETER, both. A Linux package declares
|
|
584
|
+
* `Depends: nodejs` and takes the distribution's — carrying a second copy would be
|
|
585
|
+
* a private interpreter no security update ever reaches. And a `--app gjs` payload
|
|
586
|
+
* gets nothing even on macOS: there is no relocatable GJS to put in a bundle
|
|
587
|
+
* (`build-gtk-runtime-darwin.mjs`: "GJS ships no relocation"), which is what
|
|
588
|
+
* `macos-app`'s `interpreters: ['node']` already records, so staging a NODE
|
|
589
|
+
* interpreter in front of a GJS bundle would be an interpreter that cannot read
|
|
590
|
+
* its own payload.
|
|
591
|
+
*
|
|
592
|
+
* `${layout.os}-${settings.arch}` — the target the PAYLOAD was built for, never
|
|
593
|
+
* the host's. Assembling a `darwin-arm64` bundle on a Linux x64 workstation is the
|
|
594
|
+
* supported path (ADR 0024 § A1) and is what this project's own CI does; a
|
|
595
|
+
* host-derived target would resolve the wrong closure, and the mismatch would only
|
|
596
|
+
* surface as a dlopen failure on the user's machine. `assertPayloadMatchesArch`
|
|
597
|
+
* reads the staged tree back afterwards and refuses a closure whose Mach-O
|
|
598
|
+
* `cputype` disagrees with the label — a check the carried runtime is the first
|
|
599
|
+
* thing to make non-vacuous on this layout, since a JavaScript bundle has no
|
|
600
|
+
* cputype to disagree with.
|
|
601
|
+
*/
|
|
602
|
+
function resolveCarriedRuntime(layout, settings, projectDir) {
|
|
603
|
+
const empty = { files: [], launcher: {}, found: [], missing: [] };
|
|
604
|
+
if (layout.os === 'linux' || settings.app !== 'node')
|
|
605
|
+
return empty;
|
|
606
|
+
const target = `${layout.os}-${settings.arch}`;
|
|
607
|
+
if (!isGtkRuntimeTarget(target))
|
|
608
|
+
return empty;
|
|
609
|
+
return stageAppRuntime({
|
|
610
|
+
layout,
|
|
611
|
+
identity: settings,
|
|
612
|
+
target,
|
|
613
|
+
cwd: projectDir,
|
|
614
|
+
interpreter: isNodeRuntimeTarget(target) ? resolveNodeRuntime(target, { cwd: projectDir }) : null,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
255
617
|
/**
|
|
256
618
|
* Phase two: pack a stage that was assembled somewhere else.
|
|
257
619
|
*
|
|
@@ -276,14 +638,54 @@ async function finishFromStage(args, fromStage) {
|
|
|
276
638
|
'`gjsify ship --stage`, and use `--expect-target <os>-<arch>` here to assert which leg you are ' +
|
|
277
639
|
'packing.');
|
|
278
640
|
}
|
|
641
|
+
if (args.os !== undefined) {
|
|
642
|
+
// Same reason as `--arch`, one axis over: the LAYOUT is what phase one
|
|
643
|
+
// wrote — the launcher's language, every staged path, the recorded
|
|
644
|
+
// target. Naming it again here could only agree with the stage or
|
|
645
|
+
// contradict it, and there is no useful behaviour behind either.
|
|
646
|
+
throw new Error(`gjsify ship: the layout is decided when the stage is assembled, so "${args.os}" has nothing to ` +
|
|
647
|
+
"change here — the staged tree already IS one OS's layout. Use `--expect-target <os>-<arch>` " +
|
|
648
|
+
'to assert which one you are packing.');
|
|
649
|
+
}
|
|
279
650
|
const cwd = process.cwd();
|
|
280
651
|
const stageDir = resolve(cwd, fromStage);
|
|
281
652
|
const manifest = readStageManifest(stageDir);
|
|
282
653
|
assertExpectedTarget(manifest, args['expect-target']);
|
|
283
|
-
|
|
654
|
+
// `layoutForOs`, not `resolveLayout`: the manifest is a wire format with one
|
|
655
|
+
// legal spelling per OS, and `readStageManifest` has already refused any other.
|
|
656
|
+
const layout = layoutForOs(manifest.target.os);
|
|
657
|
+
// Before `resolveFormats`, which would otherwise answer a `--target` the
|
|
658
|
+
// caller never typed: a stage for a layout no format wraps carries
|
|
659
|
+
// `formats: []`, and the empty-list refusal there names `--target` and the
|
|
660
|
+
// three Linux formats — three sentences about the wrong thing.
|
|
661
|
+
// THE STAGE'S list, not the layout's. The two agreed while darwin and windows
|
|
662
|
+
// had no formats at all, and stopped the moment two rows wrapped darwin: a
|
|
663
|
+
// `--app gjs` project's darwin stage records `formats: []` against a layout
|
|
664
|
+
// that now has two. Asking the layout would let that stage past this refusal
|
|
665
|
+
// and into `resolveFormats([])`, whose message is about a `--target` the caller
|
|
666
|
+
// never typed.
|
|
667
|
+
if (args.target === undefined) {
|
|
668
|
+
assertPackable(manifest.formats.map((id) => FORMATS[id]), layout, 'stage',
|
|
669
|
+
// What WOULD have wrapped it, so this can tell a `--app gjs` darwin
|
|
670
|
+
// stage apart from a windows one, which nothing wraps at all.
|
|
671
|
+
defaultFormatIds(layout.os)
|
|
672
|
+
.map((id) => FORMATS[id])
|
|
673
|
+
.filter((format) => !format.interpreters.includes(manifest.settings.app)));
|
|
674
|
+
}
|
|
675
|
+
const formats = resolveFormats(args.target ?? manifest.formats, layout);
|
|
284
676
|
assertFormatsStaged(manifest, formats);
|
|
285
677
|
for (const format of formats)
|
|
286
678
|
assertCanPack(format);
|
|
679
|
+
// NO `config` — this phase has no project, by construction (`tests/e2e/
|
|
680
|
+
// ship-from-stage` deletes the project tree between the two phases). So the
|
|
681
|
+
// identity is the flag or nothing, which is also the right answer: § A14
|
|
682
|
+
// amends § A1 to *a format declares where it can be packed; the RUN declares
|
|
683
|
+
// what it can sign with*, and this run is the one holding the keychain.
|
|
684
|
+
const sign = resolveSignPlan({ flag: args.sign, layoutOs: layout.os });
|
|
685
|
+
const notarize = resolveNotaryPlan({ flag: args.notarize, layoutOs: layout.os, sign });
|
|
686
|
+
announceSigning(sign, notarize);
|
|
687
|
+
if (sign.kind === 'sign')
|
|
688
|
+
assertHostCanSign(sign.signer);
|
|
287
689
|
for (const warning of warnOnToolSkew(manifest))
|
|
288
690
|
console.warn(`${LOG} ${warning}`);
|
|
289
691
|
const { mtime, warnings } = resolveStageMtime(manifest);
|
|
@@ -303,10 +705,21 @@ async function finishFromStage(args, fromStage) {
|
|
|
303
705
|
const outRoot = args.out === undefined ? dirname(stageDir) : resolve(cwd, args.out);
|
|
304
706
|
const artifacts = [];
|
|
305
707
|
for (const format of formats) {
|
|
708
|
+
// Re-asserted here and not only in phase one. `--from-stage` can be
|
|
709
|
+
// pointed at any stage with any `--target`, so a stage assembled for
|
|
710
|
+
// deb+rpm can be packed as a Flatpak on the finishing host — the phase
|
|
711
|
+
// that HAS no project and cannot re-read `gjsify.app` from it. The stage
|
|
712
|
+
// manifest carries the answer precisely so this stays checkable.
|
|
713
|
+
assertFormatCanRunInterpreter(format, manifest.settings.app);
|
|
306
714
|
if (format.depends !== null) {
|
|
307
|
-
|
|
715
|
+
// From the stage manifest's settings, which is all this phase has: it
|
|
716
|
+
// may be running on a machine that has never seen the project
|
|
717
|
+
// (ADR 0024 § A2).
|
|
718
|
+
const floorWarnings = manifest.settings.app === 'node'
|
|
719
|
+
? warnAboutNodeFloor(format.depends, manifest.settings.minNodeVersion)
|
|
720
|
+
: warnAboutGjsFloor(format.depends, manifest.settings.minGjsVersion);
|
|
721
|
+
for (const warning of floorWarnings)
|
|
308
722
|
console.warn(`${LOG} ${warning}`);
|
|
309
|
-
}
|
|
310
723
|
}
|
|
311
724
|
artifacts.push(await packOne({
|
|
312
725
|
format,
|
|
@@ -318,6 +731,8 @@ async function finishFromStage(args, fromStage) {
|
|
|
318
731
|
outRoot,
|
|
319
732
|
namespaces: manifest.namespaces,
|
|
320
733
|
mtime,
|
|
734
|
+
sign,
|
|
735
|
+
notarize,
|
|
321
736
|
verbose: args.verbose,
|
|
322
737
|
}));
|
|
323
738
|
}
|
|
@@ -328,21 +743,31 @@ async function packOne(input) {
|
|
|
328
743
|
assertOverlayIsLicensed(format.id, settings.binaryName, input.overlay);
|
|
329
744
|
const overlayDir = join(outRoot, 'overlay', format.id);
|
|
330
745
|
writeStage(overlayDir, input.overlay);
|
|
331
|
-
|
|
746
|
+
// THE PRE-SIGN TREE, and the name says which one it is (ADR 0024 § A17).
|
|
747
|
+
// `readStage` holds each file's SIZE against the stage manifest, and a size is
|
|
748
|
+
// no more re-sign-proof than a digest would be — measured on this tree:
|
|
749
|
+
// append one byte to a staged file and it refuses with "… is 6 bytes in the
|
|
750
|
+
// stage and 5 in its manifest". So the validation runs over what phase one
|
|
751
|
+
// wrote, and the signer below takes its OUTPUT: the signed bytes are computed
|
|
752
|
+
// from the validated ones and therefore cannot exist before them, which is
|
|
753
|
+
// what makes the order structural rather than a comment somebody may reorder.
|
|
754
|
+
const assembled = readStage([stageDir, overlayDir], [...input.staged, ...input.overlay]);
|
|
332
755
|
// What the package installs, read off what it installs. The three questions
|
|
333
756
|
// this answers used to be answered from the project's file lists, which are
|
|
334
757
|
// absolute paths on the build host and therefore unavailable here.
|
|
335
|
-
const facts = readPayloadFacts(
|
|
758
|
+
const facts = readPayloadFacts(assembled);
|
|
336
759
|
const depends = format.depends === null
|
|
337
760
|
? []
|
|
338
761
|
: deriveDepends(format.depends, {
|
|
339
762
|
namespaces: input.namespaces,
|
|
340
763
|
hasIcons: facts.hasIcons,
|
|
341
764
|
hasSchemas: facts.hasSchemas,
|
|
765
|
+
interpreter: settings.app,
|
|
342
766
|
extra: settings.extraDepends[format.depends],
|
|
343
767
|
typelibPackages: settings.typelibPackages,
|
|
344
768
|
bundledTypelibs: facts.bundledTypelibs,
|
|
345
769
|
minGjsVersion: settings.minGjsVersion,
|
|
770
|
+
minNodeVersion: settings.minNodeVersion,
|
|
346
771
|
});
|
|
347
772
|
// `--arch` is a CLAIM about the payload and nothing else compared it to one:
|
|
348
773
|
// an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
|
|
@@ -357,7 +782,70 @@ async function packOne(input) {
|
|
|
357
782
|
// asserting afterwards would compare against a label that no longer names an
|
|
358
783
|
// architecture, and the mismatch that matters is exactly the one on a payload
|
|
359
784
|
// that IS architecture-specific.
|
|
360
|
-
assertPayloadMatchesArch(
|
|
785
|
+
assertPayloadMatchesArch(assembled, input.arch);
|
|
786
|
+
// The second half of the same idea, on the other label this artifact carries:
|
|
787
|
+
// something declares which interpreter runs the app, and until now nothing
|
|
788
|
+
// compared it to the launcher that will. They CAN disagree — a stage assembled
|
|
789
|
+
// by one gjsify and packed by another — and an artifact that promises one
|
|
790
|
+
// interpreter and runs another installs cleanly and dies at first launch.
|
|
791
|
+
//
|
|
792
|
+
// EVERY FORMAT, not only the ones with a `Depends:`. The guard used to run
|
|
793
|
+
// behind `format.depends !== null`, which was the right condition while the
|
|
794
|
+
// only declaration was a package list. The macOS rows have no dependency list
|
|
795
|
+
// and a HARDER promise instead: `interpreters: ['node']`, because there is no
|
|
796
|
+
// relocatable GJS to put in a bundle. So the launcher has to be read for them
|
|
797
|
+
// too. Widening this costs the Flatpak path nothing — its row is
|
|
798
|
+
// `interpreters: ['gjs']` and `assertFormatCanRunInterpreter` has already
|
|
799
|
+
// refused anything else — and the check is asymmetric by construction, so a
|
|
800
|
+
// launcher it cannot resolve stays silent rather than failing a working
|
|
801
|
+
// artifact.
|
|
802
|
+
const layout = layoutForOs(format.layoutOs);
|
|
803
|
+
assertLauncherMatchesInterpreter(assembled, layout, settings, settings.app);
|
|
804
|
+
// The label against the LAYOUT, which is the question one level up from
|
|
805
|
+
// `assertPayloadMatchesArch`: that one asks whether the bytes agree with the
|
|
806
|
+
// label, this asks whether an artifact for that label can exist at all. They
|
|
807
|
+
// are not the same check, and the windows row is where they come apart — a
|
|
808
|
+
// payload of x64 DLLs labelled `arm64` fails the first, while a payload of pure
|
|
809
|
+
// JavaScript labelled `arm64` passes it and still produces a program directory
|
|
810
|
+
// no Windows/ARM machine has a GTK for (#1117).
|
|
811
|
+
//
|
|
812
|
+
// HERE and not at `--stage`, which is the same split `Layout.runtimeGap` draws:
|
|
813
|
+
// a stage is a build intermediate and assembling one for a foreign arch is what
|
|
814
|
+
// `tests/e2e/ship-layout` does on purpose (one payload, three layouts, and that
|
|
815
|
+
// payload's native file has an architecture). What must not leave is an
|
|
816
|
+
// ARTIFACT. `assemble` warns at the `--arch` that caused it.
|
|
817
|
+
//
|
|
818
|
+
// And BEFORE `archName`, deliberately: that function refuses an unknown value
|
|
819
|
+
// too, with a message about a table. This one names the blocker and where it is
|
|
820
|
+
// tracked, which is the difference between "unsupported" and "here is what
|
|
821
|
+
// would have to change".
|
|
822
|
+
assertLayoutSupportsArch(layout, input.arch);
|
|
823
|
+
// AFTER every check above and BEFORE any container below — the seam § A17
|
|
824
|
+
// asks M2's packer to leave, filled. Signing is a MUTATION of the payload
|
|
825
|
+
// (§ A4): what the packers get back is new bytes, not a wrapper, because a
|
|
826
|
+
// Developer-ID-signed main executable will not load ad-hoc-signed dylibs
|
|
827
|
+
// under library validation and all 106 images in the shipped darwin closure
|
|
828
|
+
// are ad-hoc today.
|
|
829
|
+
//
|
|
830
|
+
// Per FORMAT rather than once per run, and that costs darwin a second
|
|
831
|
+
// `codesign` pass over the same images (`macos-app` and `macos-app-zip` are
|
|
832
|
+
// two calls). It is not avoidable by hoisting: the payload a format signs
|
|
833
|
+
// includes that format's own overlay — on darwin the licence, which lands
|
|
834
|
+
// INSIDE `<App>.app` — so two formats have two payloads.
|
|
835
|
+
//
|
|
836
|
+
// `signed/<id>/` is scratch this function owns. The arriving stage is never
|
|
837
|
+
// written to: `writeStage` wipes what it is pointed at, and a re-signed file
|
|
838
|
+
// in it would fail the very `readStage` above on the next run.
|
|
839
|
+
const payload = input.sign.kind === 'sign'
|
|
840
|
+
? await signPayload({
|
|
841
|
+
payload: assembled,
|
|
842
|
+
identity: input.sign.identity,
|
|
843
|
+
signer: input.sign.signer,
|
|
844
|
+
workDir: join(outRoot, 'signed', format.id),
|
|
845
|
+
verbose: input.verbose,
|
|
846
|
+
log: (line) => console.log(`${LOG} ${line}`),
|
|
847
|
+
})
|
|
848
|
+
: assembled;
|
|
361
849
|
const archLabel = format.archName(input.arch, isArchIndependent(payload));
|
|
362
850
|
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
363
851
|
const outDir = join(outRoot, 'out');
|
|
@@ -395,12 +883,156 @@ async function packOne(input) {
|
|
|
395
883
|
verbose: input.verbose,
|
|
396
884
|
});
|
|
397
885
|
break;
|
|
886
|
+
case 'macos-app':
|
|
887
|
+
case 'windows-dir':
|
|
888
|
+
// ONE STATEMENT FOR TWO ROWS, folded because the code really is
|
|
889
|
+
// identical: `Layout.root` is where the two differ, and the first draft
|
|
890
|
+
// of the windows arm said "NO REBASE, which is the one place the two
|
|
891
|
+
// rows differ" over a line byte-identical to this one.
|
|
892
|
+
//
|
|
893
|
+
// No container at all: the artifact IS the payload plus this format's
|
|
894
|
+
// overlay, written out. `writePayload` rather than a directory copy of
|
|
895
|
+
// the stage, so the same two properties every other packer has hold
|
|
896
|
+
// here — modes come from the plan (`readStage` has applied them, and
|
|
897
|
+
// the artifact upload that flattens them cannot reach in between), and
|
|
898
|
+
// the stage's own sidecar stays out because it was never payload.
|
|
899
|
+
//
|
|
900
|
+
// REBASED ON `Layout.root`, which is `<App>.app` on darwin and `''` on
|
|
901
|
+
// windows — the difference between the artifact and a directory
|
|
902
|
+
// containing it, measured as a defect on the first: every staged darwin
|
|
903
|
+
// path already begins with `<App>.app/` and `format.fileName` names the
|
|
904
|
+
// artifact the same thing, so writing them verbatim produced
|
|
905
|
+
// `out/Ship Demo.app/Ship Demo.app/Contents/…`, a folder the Finder does
|
|
906
|
+
// not treat as an application with a real bundle hidden one level down,
|
|
907
|
+
// at exit 0, with `zipinfo` on the sibling zip showing the correct tree
|
|
908
|
+
// the whole time. On windows the stage IS the directory's contents,
|
|
909
|
+
// because an installer picks the parent, and `writePayload` documents
|
|
910
|
+
// `stripPrefix === ''` as a no-op — so the same call is right for both.
|
|
911
|
+
// Neither zip needs a rebase, and for opposite reasons: the darwin
|
|
912
|
+
// archive INHERITS its top level from the staged paths, the windows one
|
|
913
|
+
// SYNTHESISES it (see `windows-dir-zip`).
|
|
914
|
+
writePayload(target, payload, layout.root(settings));
|
|
915
|
+
break;
|
|
916
|
+
case 'macos-app-dmg': {
|
|
917
|
+
// FROM THE STAGE, never from the `out/<App>.app` the row above
|
|
918
|
+
// writes, and that is a design decision rather than an accident of
|
|
919
|
+
// where the bytes were handy. Three reasons, in the order they bite:
|
|
920
|
+
//
|
|
921
|
+
// * a `--target macos-app-dmg` run alone must produce a `.dmg`.
|
|
922
|
+
// Reading the sibling artifact would make this row depend on
|
|
923
|
+
// another row having run, and the only thing that orders them
|
|
924
|
+
// today is `resolveFormats`' alphabetical sort — an invariant
|
|
925
|
+
// nothing states and `--target macos-app-dmg` on its own breaks.
|
|
926
|
+
// * ADR 0024 § A17 fixes the seam for a later `--sign`: `readStage`
|
|
927
|
+
// validates the PRE-sign tree, the re-sign runs after it, and the
|
|
928
|
+
// container is built after that. A packer that opened an artifact
|
|
929
|
+
// would put its container step on the far side of a directory
|
|
930
|
+
// nothing validated.
|
|
931
|
+
// * the modes. `writePayload` applies the PLAN's modes, which
|
|
932
|
+
// `readStage` has already resolved from the manifest; a directory
|
|
933
|
+
// copy would inherit whatever the filesystem — or a CI artifact
|
|
934
|
+
// round trip — left behind.
|
|
935
|
+
//
|
|
936
|
+
// `stripPrefix: ''` and not `layout.root(settings)`, which is the one
|
|
937
|
+
// line where this differs from the `macos-app` arm above: that
|
|
938
|
+
// artifact IS the bundle, so the `<App>.app/` prefix comes off; this
|
|
939
|
+
// one is the VOLUME, and the bundle has to sit inside it or the image
|
|
940
|
+
// mounts showing `Contents/` — a window with no application in it.
|
|
941
|
+
const volumeDir = dmgVolumeDir(outRoot);
|
|
942
|
+
writePayload(volumeDir, payload, '');
|
|
943
|
+
await buildDmgImage({
|
|
944
|
+
volumeName: dmgVolumeName(settings),
|
|
945
|
+
sourceDir: volumeDir,
|
|
946
|
+
target,
|
|
947
|
+
workDir: dirname(volumeDir),
|
|
948
|
+
verbose: input.verbose,
|
|
949
|
+
});
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
case 'macos-app-zip':
|
|
953
|
+
// The SAME payload, in the one container a browser download can be.
|
|
954
|
+
// Written by this tree (`utils/ship/zip.ts`) so the packing host needs
|
|
955
|
+
// no `zip(1)` and `zipinfo` stays an independent reader — ADR 0024
|
|
956
|
+
// § A3's argument for the hand-written deb and rpm, applied again.
|
|
957
|
+
// The paths inside are the staged ones, so the archive expands to
|
|
958
|
+
// `<App>.app/…` and not to a bare `Contents/`.
|
|
959
|
+
writeFileSync(target, buildZip(zipEntriesFromPayload(payload), mtime));
|
|
960
|
+
break;
|
|
961
|
+
case 'windows-dir-zip':
|
|
962
|
+
// The same payload, with the top level the layout deliberately does not
|
|
963
|
+
// carry. `windowsProgramDirName` is the same function the row above
|
|
964
|
+
// names the directory with, so unzipping this archive and running the
|
|
965
|
+
// installer put the app at the same relative path — and a user who
|
|
966
|
+
// unzips it in `Downloads` gets one directory rather than `app\`,
|
|
967
|
+
// `share\` and a `.cmd` loose in it.
|
|
968
|
+
writeFileSync(target, buildZip(zipEntriesFromPayload(payload, windowsProgramDirName(settings)), mtime));
|
|
969
|
+
break;
|
|
970
|
+
case 'msi': {
|
|
971
|
+
// The SAME payload again, and the third artifact to carry it —
|
|
972
|
+
// `payload`, which is `signPayload`'s OUTPUT when an identity was
|
|
973
|
+
// passed and `assembled` otherwise (§ A17). The installer therefore
|
|
974
|
+
// packs the signed bytes by construction rather than by ordering: it
|
|
975
|
+
// cannot read them before the signer has produced them.
|
|
976
|
+
//
|
|
977
|
+
// The source document addresses files by PATH, so the tree is written
|
|
978
|
+
// out once into the work directory and the `.wxs` points at it with
|
|
979
|
+
// paths RELATIVE to that directory — the same shape the Flatpak packer
|
|
980
|
+
// uses, where a manifest also names a directory the packer just laid
|
|
981
|
+
// down. Relative and not absolute because the same document is compiled
|
|
982
|
+
// a second time by WiX on another machine; see `utils/ship/msi.ts`.
|
|
983
|
+
// Not a copy of `out/<App>/`: `--target msi` alone must work, and the
|
|
984
|
+
// directory row may not be in this run's format list at all.
|
|
985
|
+
const workDir = join(outRoot, 'msi');
|
|
986
|
+
writePayload(join(workDir, MSI_PAYLOAD_DIR), payload, layout.root(settings));
|
|
987
|
+
await buildMsi({
|
|
988
|
+
settings,
|
|
989
|
+
// POSIX-separated and RELATIVE to `workDir`, so the `.wxs` plus the
|
|
990
|
+
// tree beside it is a pair that can be handed to a second compiler
|
|
991
|
+
// on another machine — which is what the WiX half of the
|
|
992
|
+
// cross-check does with this exact document.
|
|
993
|
+
files: payload.map((entry) => ({
|
|
994
|
+
path: entry.path,
|
|
995
|
+
source: `${MSI_PAYLOAD_DIR}/${entry.path}`,
|
|
996
|
+
})),
|
|
997
|
+
programDirName: windowsProgramDirName(settings),
|
|
998
|
+
archLabel,
|
|
999
|
+
workDir,
|
|
1000
|
+
target,
|
|
1001
|
+
verbose: input.verbose,
|
|
1002
|
+
});
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
398
1005
|
default: {
|
|
399
1006
|
const unhandled = format.id;
|
|
400
1007
|
throw new Error(`gjsify ship: no packer is wired for format "${String(unhandled)}".`);
|
|
401
1008
|
}
|
|
402
1009
|
}
|
|
403
|
-
|
|
1010
|
+
// The SIZE of a directory is not `statSync().size`, which answers 4096 for the
|
|
1011
|
+
// directory entry — so a `.app` with a 20 MiB bundle would be reported as
|
|
1012
|
+
// "4096 bytes". `artifactKind` is on the descriptor for exactly this.
|
|
1013
|
+
const size = format.artifactKind === 'directory' ? directorySize(target) : statSync(target).size;
|
|
1014
|
+
// LAST, on the artifact that landed — notarisation is a check on a finished,
|
|
1015
|
+
// signed container and there is nothing to submit before one exists.
|
|
1016
|
+
//
|
|
1017
|
+
// A DIRECTORY IS SKIPPED, not refused. `notarytool` takes an archive or an
|
|
1018
|
+
// installer, so a `<App>.app` has no submittable form of its own; on darwin
|
|
1019
|
+
// the zip beside it is the one that goes. Refusing here would make
|
|
1020
|
+
// `--notarize` unusable with the default format set, which is both rows.
|
|
1021
|
+
if (input.notarize.kind === 'notarize') {
|
|
1022
|
+
if (format.artifactKind === 'directory') {
|
|
1023
|
+
console.warn(`${LOG} ${format.id} is a directory, and notarytool submits an archive or an installer — ` +
|
|
1024
|
+
'not notarised. The container format beside it is what carries the ticket.');
|
|
1025
|
+
}
|
|
1026
|
+
else {
|
|
1027
|
+
await notarizeArtifact({
|
|
1028
|
+
plan: input.notarize,
|
|
1029
|
+
artifact: target,
|
|
1030
|
+
verbose: input.verbose,
|
|
1031
|
+
log: (line) => console.log(`${LOG} ${line}`),
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return { format: format.id, path: target, size };
|
|
404
1036
|
}
|
|
405
1037
|
function printArtifacts(base, artifacts) {
|
|
406
1038
|
for (const artifact of artifacts) {
|
|
@@ -419,29 +1051,176 @@ function assertCanPack(format) {
|
|
|
419
1051
|
assertHostCanFinish(format);
|
|
420
1052
|
assertToolsInstalled(format);
|
|
421
1053
|
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Refuse a pack for a layout no format wraps.
|
|
1056
|
+
*
|
|
1057
|
+
* Reached only when the format list was DERIVED — no `--target`, or a
|
|
1058
|
+
* `gjsify.ship.targets` filtered empty by this layout — where the empty set is a
|
|
1059
|
+
* fact about the layout rather than something a caller typed. Without it,
|
|
1060
|
+
* `gjsify ship darwin` would assemble the tree, iterate an empty format list,
|
|
1061
|
+
* print no artifact line and exit 0 — a success that produced nothing, which is
|
|
1062
|
+
* the same shape `resolveFormats` refuses an empty `--target` for.
|
|
1063
|
+
*
|
|
1064
|
+
* `chosenBy` is not decoration. When the layout came from the HOST rather than
|
|
1065
|
+
* from the positional, this is a BEHAVIOUR CHANGE the caller did not ask for:
|
|
1066
|
+
* a bare `gjsify ship` on a Mac used to emit `.deb` + `.rpm`, because the default
|
|
1067
|
+
* format set was host-independent. It is deliberate — the positional means what
|
|
1068
|
+
* it says, and a Linux package built on a Mac is now something you ask for — but
|
|
1069
|
+
* a refusal that does not name the one-word replacement is just a regression.
|
|
1070
|
+
*/
|
|
1071
|
+
function assertPackable(formats, layout, chosenBy, unusable = []) {
|
|
1072
|
+
if (formats.length > 0)
|
|
1073
|
+
return;
|
|
1074
|
+
// A DIFFERENT emptiness, and conflating the two sends the reader to the wrong
|
|
1075
|
+
// fix. "No format wraps this layout" is answered by a later milestone; "the
|
|
1076
|
+
// formats that wrap it cannot run this project's interpreter" is answered by
|
|
1077
|
+
// the project, today — and the two produce byte-identical empty lists.
|
|
1078
|
+
if (unusable.length > 0) {
|
|
1079
|
+
throw new Error(`gjsify ship: ${unusable.map((format) => format.id).join(' and ')} wrap the ${layout.name} ` +
|
|
1080
|
+
`layout, and neither can run this project — ${unusable[0]?.interpreterGap ?? ''}.\n` +
|
|
1081
|
+
` \`gjsify ship ${layout.name} --stage\` assembles the tree anyway; packing it is what ` +
|
|
1082
|
+
'the milestone that stages an interpreter is for (#1354 M2b).');
|
|
1083
|
+
}
|
|
1084
|
+
// A THIRD emptiness, and the one the last throw in this function used to
|
|
1085
|
+
// mis-diagnose. A layout that HAS formats still reaches here with none
|
|
1086
|
+
// SELECTED: `configuredFormats` filters `gjsify.ship.targets` to the layout
|
|
1087
|
+
// being assembled and returns an empty list when they all wrap another one,
|
|
1088
|
+
// and `--stage` records that empty list for the finishing host to read.
|
|
1089
|
+
//
|
|
1090
|
+
// MEASURED before this branch existed, on a `--app node` project carrying
|
|
1091
|
+
// `targets: ["deb","rpm"]` — the key `packages/infra/cli/package.json` itself
|
|
1092
|
+
// carries — with `gjsify ship darwin --stage` then `gjsify ship --from-stage`:
|
|
1093
|
+
// "no format wraps the darwin layout … Keep it for the milestone that packs it
|
|
1094
|
+
// … this is a layout added without a `FORMATS` row". Three rows wrap darwin,
|
|
1095
|
+
// the milestone landed, and the cause was a configured list for another OS.
|
|
1096
|
+
const wrapping = formatIdsFor(layout.os).sort();
|
|
1097
|
+
if (wrapping.length > 0) {
|
|
1098
|
+
// The assembling host is where the answer is, whichever caller got here:
|
|
1099
|
+
// phase two cannot add a format the stage never rendered an overlay for
|
|
1100
|
+
// (`assertFormatsStaged`), so `--target` HERE is not the fix.
|
|
1101
|
+
const where = chosenBy === 'stage' ? `${layout.name} --stage --target` : `${layout.name} --target`;
|
|
1102
|
+
throw new Error(`gjsify ship: this run records no formats for the ${layout.name} layout, so there is nothing ` +
|
|
1103
|
+
`to pack — and it is not that none exist: ${wrapping.join(', ')} wrap it.\n` +
|
|
1104
|
+
' An empty list means none were SELECTED. `gjsify.ship.targets` is filtered to the layout ' +
|
|
1105
|
+
'being assembled,\n' +
|
|
1106
|
+
" so a configured list naming only another OS's formats leaves this one empty and says so " +
|
|
1107
|
+
'at stage time.\n' +
|
|
1108
|
+
` Name one where the payload is assembled: \`gjsify ship ${where} ${wrapping[0]}\`.`);
|
|
1109
|
+
}
|
|
1110
|
+
const linux = defaultFormatIds('linux').join(',');
|
|
1111
|
+
// Three callers, three different next steps. Telling a `--from-stage` caller
|
|
1112
|
+
// to run `gjsify ship <os> --stage` names the command that PRODUCED the stage
|
|
1113
|
+
// they are holding, which is advice to go in a circle.
|
|
1114
|
+
const advice = {
|
|
1115
|
+
host: `This host is ${process.platform}, so \`gjsify ship\` assembled the ${layout.name} layout. ` +
|
|
1116
|
+
'Assembly is not host-bound (ADR 0024 § A1), so the Linux packages are still one word away: ' +
|
|
1117
|
+
`\`gjsify ship linux\` builds ${linux} from right here.`,
|
|
1118
|
+
positional: `\`gjsify ship ${layout.name} --stage\` assembles the payload and stops, which is the whole of ` +
|
|
1119
|
+
'what this command does for that OS today.',
|
|
1120
|
+
stage: `This stage carries the ${layout.name} layout and nothing here can wrap it — a stage is a build ` +
|
|
1121
|
+
'intermediate, not an artifact. Keep it for the milestone that packs it, or re-assemble with ' +
|
|
1122
|
+
'`gjsify ship linux --stage` if a Linux package is what you wanted.',
|
|
1123
|
+
}[chosenBy];
|
|
1124
|
+
// NOW ACTUALLY UNREACHABLE from the three layouts that exist — the branch
|
|
1125
|
+
// above holds every one of them, because `formatIdsFor` answers non-empty for
|
|
1126
|
+
// all three as of #1354 M5. Kept rather than deleted: this is what a fourth
|
|
1127
|
+
// layout gets on the day it is added and before a `FORMATS` row wraps it, the
|
|
1128
|
+
// state windows was in between M1 and M3, and deleting it would make that
|
|
1129
|
+
// day's failure a `TypeError` on an empty list.
|
|
1130
|
+
//
|
|
1131
|
+
// It used to claim this for the whole function while the `targets`-elsewhere
|
|
1132
|
+
// route reached it and got all three of its sentences wrong. A comment saying
|
|
1133
|
+
// "unreachable" is a claim about a predicate; this one now has the predicate
|
|
1134
|
+
// above it in code.
|
|
1135
|
+
throw new Error(`gjsify ship: no format wraps the ${layout.name} layout, so there is nothing to pack. ${advice}` +
|
|
1136
|
+
' Every layout this gjsify knows has one (ADR 0024 stages 2-5), so this is a layout added ' +
|
|
1137
|
+
'without a `FORMATS` row.');
|
|
1138
|
+
}
|
|
422
1139
|
/**
|
|
423
1140
|
* Refuse a build target this command cannot package correctly.
|
|
424
1141
|
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
1142
|
+
* `gjs` and `node` are packageable: `renderLauncher` execs the one
|
|
1143
|
+
* `settings.app` names and `deriveDepends` declares the same one — `gjs >= …`,
|
|
1144
|
+
* or `nodejs (>= 24)` / `nodejs(engine) >= 24`. Until #1354's M0 this said "only
|
|
1145
|
+
* `gjs` can be packaged today", which was true: the launcher execed gjs
|
|
1146
|
+
* unconditionally, so a `--app node` package would have installed and died at
|
|
1147
|
+
* startup.
|
|
1148
|
+
*
|
|
1149
|
+
* `browser` and `nativescript` stay refused, and not for want of a launcher
|
|
1150
|
+
* line: a browser bundle has no process to start, and NativeScript ships as an
|
|
1151
|
+
* APK/IPA through a different pipeline entirely (ADR 0024 § 4).
|
|
1152
|
+
*
|
|
1153
|
+
* DELIBERATELY NOT PER LAYOUT, and the first cut of the layout axis got this
|
|
1154
|
+
* exactly backwards. Reading ADR § 4's runtime table as a per-layout `app`
|
|
1155
|
+
* requirement refused `gjsify.app: "gjs"` for the macOS layout — i.e. refused the
|
|
1156
|
+
* entire audience of this command from assembling a `.app` — while a project with
|
|
1157
|
+
* no `gjsify.app` key sailed through and staged a launcher naming `node` in front
|
|
1158
|
+
* of a GJS bundle. Both halves came from the same mistake: § 4 derives the runtime
|
|
1159
|
+
* a SHIPPED ARTIFACT carries, which is `Layout.shippedRuntime` and is printed as
|
|
1160
|
+
* `Layout.runtimeGap`. What the launcher execs is what the PAYLOAD was built for,
|
|
1161
|
+
* and that is `settings.app` on every layout.
|
|
430
1162
|
*
|
|
431
|
-
* An undeclared target is the common case for a GJS app and is allowed
|
|
432
|
-
*
|
|
433
|
-
*
|
|
1163
|
+
* An undeclared target is the common case for a GJS app and is allowed. It must
|
|
1164
|
+
* NOT be resolved through `Config.forBuild`, whose fallback is the HOST runtime
|
|
1165
|
+
* — that would refuse, or worse silently re-target, a perfectly good GJS project
|
|
1166
|
+
* merely for running this command under Node.
|
|
434
1167
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
1168
|
+
* WHY THIS REFUSES AND `Layout.runtimeGap` ONLY WARNS, since the two describe
|
|
1169
|
+
* neighbouring predicaments and get opposite treatment. A target this command
|
|
1170
|
+
* cannot package would leave here as an ARTIFACT — a `.deb` a stranger installs,
|
|
1171
|
+
* which then fails at startup, with no gate between here and their machine. The
|
|
1172
|
+
* `runtimeGap` warns about something that is not an artifact: it is printed over a
|
|
1173
|
+
* staged tree, and only when that tree carries no interpreter of its own.
|
|
1174
|
+
*
|
|
1175
|
+
* This paragraph used to end "the day a Windows format exists, that warning has to
|
|
1176
|
+
* become this refusal". #1354 M3 is that day, and the refusal it became is
|
|
1177
|
+
* {@link assertFormatCanRunInterpreter} — per FORMAT rather than per layout,
|
|
1178
|
+
* because what a runtime can execute is a property of the container and not of the
|
|
1179
|
+
* tree it wraps. This function stayed exactly as wide as it was.
|
|
1180
|
+
*
|
|
1181
|
+
* Phase one only. Phase two does not re-read `gjsify.app` — it has no project —
|
|
1182
|
+
* but it is not unchecked either: `assertLauncherMatchesInterpreter` compares
|
|
1183
|
+
* the staged `bin/<name>`'s own `exec` line against the dependency about to be
|
|
1184
|
+
* written.
|
|
438
1185
|
*/
|
|
439
1186
|
function assertShippableTarget(app) {
|
|
440
|
-
if (app === undefined || app === 'gjs')
|
|
1187
|
+
if (app === undefined || app === 'gjs' || app === 'node')
|
|
1188
|
+
return;
|
|
1189
|
+
throw new Error(`gjsify ship: this project declares \`gjsify.app: "${app}"\`, and only \`gjs\` and \`node\` can be ` +
|
|
1190
|
+
'packaged. A browser bundle has no process to launch, and a NativeScript app ships as an APK/IPA ' +
|
|
1191
|
+
'through a different pipeline. ADR 0024 § 4 has the runtime-per-OS table.');
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Refuse a format whose runtime cannot provide the interpreter the launcher execs.
|
|
1195
|
+
*
|
|
1196
|
+
* THE HOLE THIS CLOSES, and it was opened by the commit that made `--app node`
|
|
1197
|
+
* packageable. `assertShippableTarget` used to refuse `app: 'node'` outright, so
|
|
1198
|
+
* no format ever saw one. Lifting that made deb and rpm correct and left Flatpak
|
|
1199
|
+
* silently wrong: measured at exit 0, a `--target flatpak --stage` with
|
|
1200
|
+
* `app: 'node'` emitted a manifest with `runtime: org.gnome.Platform`,
|
|
1201
|
+
* `runtime-version: 50`, no `sdk-extensions` and no `append-path`, beside a
|
|
1202
|
+
* launcher that execs `node`. That runtime ships `gjs` and no `node`.
|
|
1203
|
+
*
|
|
1204
|
+
* WHY THE OTHER CHECK DOES NOT CATCH IT. `assertLauncherMatchesInterpreter`
|
|
1205
|
+
* compares the launcher against the DEPENDENCY, and a Flatpak has no dependency
|
|
1206
|
+
* list — `format.depends` is `null`, so both sides come from `settings.app` and
|
|
1207
|
+
* agree by construction. Only the format's runtime knows what it can execute,
|
|
1208
|
+
* which is why the answer is a descriptor field.
|
|
1209
|
+
*
|
|
1210
|
+
* Not a warning. The artifact would install and die at first launch on a user's
|
|
1211
|
+
* machine, which is the failure this whole command is built to make impossible.
|
|
1212
|
+
*/
|
|
1213
|
+
function assertFormatCanRunInterpreter(format, app) {
|
|
1214
|
+
if (format.interpreters.includes(app))
|
|
441
1215
|
return;
|
|
442
|
-
throw new Error(`gjsify ship: this project
|
|
443
|
-
|
|
444
|
-
|
|
1216
|
+
throw new Error(`gjsify ship: this project is \`gjsify.app: "${app}"\`, and the ${format.id} runtime cannot run it — ` +
|
|
1217
|
+
`it provides ${format.interpreters.join(', ')}.\n` +
|
|
1218
|
+
// The ROW's own sentence, not one written here. Hardcoded, this
|
|
1219
|
+
// paragraph told the first `.app` author about
|
|
1220
|
+
// `org.freedesktop.Sdk.Extension.node2x` and the GNOME runtime.
|
|
1221
|
+
` ${format.interpreterGap}.\n` +
|
|
1222
|
+
' Build the formats that wrap this layout and can run it, or ship this app as ' +
|
|
1223
|
+
`\`gjsify.app: "${format.interpreters.join('" / "')}"\`.`);
|
|
445
1224
|
}
|
|
446
1225
|
/**
|
|
447
1226
|
* Run the project's own `build` script, as a CHILD process.
|
|
@@ -471,6 +1250,6 @@ async function runProjectBuild(projectDir) {
|
|
|
471
1250
|
'yourself and pass --skip-build.'),
|
|
472
1251
|
});
|
|
473
1252
|
if (result.code !== 0) {
|
|
474
|
-
throw new Error(`gjsify ship: the project's build script failed${describeExit(result)}.`);
|
|
1253
|
+
throw new Error(`gjsify ship: the project's build script failed with ${describeExit(result)}.`);
|
|
475
1254
|
}
|
|
476
1255
|
}
|