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