@gjsify/cli 0.43.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 +453 -425
- package/lib/actions/build.js +2 -1
- package/lib/commands/affected-classify.js +7 -5
- package/lib/commands/build.js +6 -0
- package/lib/commands/flatpak/ci.js +10 -2
- package/lib/commands/flatpak/init.js +15 -16
- package/lib/commands/flatpak/utils.d.ts +0 -26
- package/lib/commands/flatpak/utils.js +4 -29
- 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 +911 -70
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/config.js +25 -0
- package/lib/types/cli-build-options.d.ts +11 -0
- package/lib/types/config-data.d.ts +137 -3
- package/lib/utils/flatpak-runtime.d.ts +41 -0
- package/lib/utils/flatpak-runtime.js +54 -0
- 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 +54 -7
- package/lib/utils/ship/depends.js +120 -6
- 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-config.d.ts +66 -0
- package/lib/utils/ship/flatpak-config.js +132 -0
- package/lib/utils/ship/flatpak.d.ts +45 -0
- package/lib/utils/ship/flatpak.js +148 -0
- package/lib/utils/ship/formats.d.ts +116 -3
- package/lib/utils/ship/formats.js +869 -11
- 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 +19 -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 +50 -13
- package/lib/utils/ship/stage-manifest.js +137 -14
- 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 +264 -16
- 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,39 +14,70 @@
|
|
|
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
|
-
//
|
|
23
|
-
|
|
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).
|
|
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';
|
|
26
28
|
import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
|
|
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';
|
|
32
|
-
import {
|
|
35
|
+
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
36
|
+
import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
|
|
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';
|
|
33
44
|
import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
|
|
34
|
-
import { assertPayloadMatchesArch, buildTimestamp, isArchIndependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
45
|
+
import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
|
|
35
46
|
import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
|
|
47
|
+
import { assertHostCanSign, notarizeArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
|
|
36
48
|
import { renderLauncher } from '../utils/ship/launcher.js';
|
|
37
49
|
import { buildRpm } from '../utils/ship/rpm.js';
|
|
38
50
|
import { declaredBundlePath, resolveShipSettings } from '../utils/ship/settings.js';
|
|
39
|
-
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, } from '../utils/ship/stage-manifest.js';
|
|
40
|
-
import { readStage, writeStage } from '../utils/ship/stage-writer.js';
|
|
51
|
+
import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
|
|
52
|
+
import { directorySize, readStage, writePayload, writeStage } from '../utils/ship/stage-writer.js';
|
|
41
53
|
const LOG = '[gjsify ship]';
|
|
42
54
|
export const shipCommand = {
|
|
43
|
-
command: 'ship',
|
|
55
|
+
command: 'ship [os]',
|
|
44
56
|
description: `Build installable artifacts (${FORMAT_IDS.join(', ')}) from the current project.`,
|
|
57
|
+
// `defaultFormatIds` in the flag text, `FORMAT_IDS` in the description:
|
|
58
|
+
// one of them answers "what can this build", the other "what will it build
|
|
59
|
+
// if I say nothing", and they stopped being the same list when a host-bound
|
|
60
|
+
// format arrived — and then stopped again when a second layout did.
|
|
45
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
|
+
})
|
|
46
71
|
.option('target', {
|
|
47
72
|
type: 'string',
|
|
48
73
|
array: true,
|
|
49
|
-
description:
|
|
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(', ')}.`,
|
|
50
81
|
})
|
|
51
82
|
.option('out', {
|
|
52
83
|
type: 'string',
|
|
@@ -77,11 +108,46 @@ export const shipCommand = {
|
|
|
77
108
|
.option('arch', {
|
|
78
109
|
type: 'string',
|
|
79
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.',
|
|
80
145
|
})
|
|
81
146
|
.option('verbose', {
|
|
82
147
|
type: 'boolean',
|
|
83
148
|
default: false,
|
|
84
|
-
description: 'Print each staged file
|
|
149
|
+
description: 'Print each staged file, the GI namespaces the bundle imports, and every tool a ' +
|
|
150
|
+
'host-bound packer invokes.',
|
|
85
151
|
}),
|
|
86
152
|
handler: async (args) => {
|
|
87
153
|
// The two phases share nothing but `packOne`, and the branch is first
|
|
@@ -99,6 +165,10 @@ export const shipCommand = {
|
|
|
99
165
|
*/
|
|
100
166
|
async function assemble(args) {
|
|
101
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);
|
|
102
172
|
const config = new Config();
|
|
103
173
|
// No `.catch(() => ({}))` here, unlike the flatpak commands: a config
|
|
104
174
|
// file that fails to load would silently become "no config", and the
|
|
@@ -108,10 +178,94 @@ async function assemble(args) {
|
|
|
108
178
|
const ship = configData.ship ?? {};
|
|
109
179
|
const flatpak = configData.flatpak ?? {};
|
|
110
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';
|
|
111
187
|
const pkg = (readPackageJson(join(projectDir, 'package.json')) ?? {});
|
|
112
188
|
// Resolved before anything is built or written: a typo'd `--target`
|
|
113
189
|
// should not cost a build and leave a half-written `ship/stage/`.
|
|
114
|
-
|
|
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
|
+
}
|
|
261
|
+
// BEFORE the build, not before the pack. `gjsify ship` runs the project's
|
|
262
|
+
// own `build` script first, so discovering a missing `flatpak-builder`
|
|
263
|
+
// afterwards costs the whole build for a refusal that was knowable up
|
|
264
|
+
// front. Skipped under `--stage`, which is precisely the phase that does
|
|
265
|
+
// NOT need the format's tooling — that asymmetry is the point of the split.
|
|
266
|
+
if (!args.stage)
|
|
267
|
+
for (const format of formats)
|
|
268
|
+
assertCanPack(format);
|
|
115
269
|
if (!args['skip-build'])
|
|
116
270
|
await runProjectBuild(projectDir);
|
|
117
271
|
const discovered = discoverPayload({
|
|
@@ -126,11 +280,18 @@ async function assemble(args) {
|
|
|
126
280
|
pkg,
|
|
127
281
|
ship,
|
|
128
282
|
flatpak,
|
|
129
|
-
cli: { outDir: args.out, arch: args.arch },
|
|
283
|
+
cli: { outDir: args.out, arch: args.arch, layoutOs: layout.os },
|
|
130
284
|
discovered,
|
|
285
|
+
app: configData.app,
|
|
131
286
|
});
|
|
132
287
|
for (const warning of warnings)
|
|
133
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);
|
|
134
295
|
const mtime = buildTimestamp(settings.bundlePath);
|
|
135
296
|
const metadataInputs = {
|
|
136
297
|
appId: settings.appId,
|
|
@@ -155,21 +316,125 @@ async function assemble(args) {
|
|
|
155
316
|
console.log(`${LOG} licence text from ${relative(projectDir, settings.licenseFile)} ` +
|
|
156
317
|
'(this package carries none of its own)');
|
|
157
318
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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({
|
|
161
325
|
metainfo: settings.kind === 'app' ? renderMetainfoApp(metadataInputs) : renderMetainfoCli(metadataInputs),
|
|
162
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,
|
|
163
340
|
licenseText: settings.licenseFile === undefined ? undefined : readFileSync(settings.licenseFile, 'utf-8'),
|
|
164
341
|
};
|
|
165
342
|
const outRoot = resolve(projectDir, settings.outDir);
|
|
166
343
|
const stageDir = join(outRoot, 'stage');
|
|
167
|
-
|
|
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);
|
|
168
368
|
writeStage(stageDir, staged);
|
|
169
|
-
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)}/`);
|
|
170
370
|
if (args.verbose)
|
|
171
371
|
for (const file of staged)
|
|
172
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
|
+
}
|
|
173
438
|
// Scanned from the BUILD TREE's bundle, which is why it has to be recorded:
|
|
174
439
|
// `gi://` specifiers are what the emitted `Depends:` is derived from
|
|
175
440
|
// (ADR 0024 § 6), and the packing host has the staged copy but no way to
|
|
@@ -177,28 +442,62 @@ async function assemble(args) {
|
|
|
177
442
|
const namespaces = scanGiNamespaces(readFileSync(settings.bundlePath, 'utf-8'));
|
|
178
443
|
if (args.verbose)
|
|
179
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.
|
|
180
455
|
const overlay = new Map();
|
|
181
|
-
for (const format of formats)
|
|
182
|
-
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
|
+
}
|
|
183
462
|
// The dependency derivation runs HERE as well, and its result is thrown
|
|
184
463
|
// away. It is the only check in the pipeline that can refuse a project
|
|
185
464
|
// (`gi://Nautilus` with no known package, ADR 0024 § 6), and this is the
|
|
186
465
|
// host that has the `package.json` the refusal tells you to edit — letting
|
|
187
466
|
// it fire two jobs later, on a machine with no project, would name a fix
|
|
188
|
-
// nobody there can apply. Facts come from the
|
|
189
|
-
//
|
|
190
|
-
|
|
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);
|
|
191
474
|
for (const format of formats) {
|
|
192
|
-
|
|
475
|
+
// `format.depends`, not `format.id`: a Flatpak has no package
|
|
476
|
+
// dependency list at all, and asking for one would either invent a
|
|
477
|
+
// third column in the typelib table or silently reuse rpm's — which is
|
|
478
|
+
// the exact defect `check-ship-format-vocabulary.mjs`'s header records
|
|
479
|
+
// from the other direction (a third format taking rpm's package name
|
|
480
|
+
// into a Debian `Depends:`, at exit 0).
|
|
481
|
+
if (format.depends === null)
|
|
482
|
+
continue;
|
|
483
|
+
deriveDepends(format.depends, {
|
|
193
484
|
namespaces,
|
|
194
485
|
hasIcons: facts.hasIcons,
|
|
195
486
|
hasSchemas: facts.hasSchemas,
|
|
196
|
-
|
|
487
|
+
interpreter: settings.app,
|
|
488
|
+
extra: settings.extraDepends[format.depends],
|
|
197
489
|
typelibPackages: settings.typelibPackages,
|
|
198
490
|
bundledTypelibs: facts.bundledTypelibs,
|
|
199
491
|
minGjsVersion: settings.minGjsVersion,
|
|
492
|
+
minNodeVersion: settings.minNodeVersion,
|
|
200
493
|
});
|
|
201
|
-
|
|
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)
|
|
202
501
|
console.warn(`${LOG} ${warning}`);
|
|
203
502
|
}
|
|
204
503
|
// Written unconditionally, not only under `--stage`: a `ship/stage/` that
|
|
@@ -207,7 +506,23 @@ async function assemble(args) {
|
|
|
207
506
|
// one-process artifact with the two-phase one.
|
|
208
507
|
const manifest = writeStageManifest({ stageDir, settings, formats, mtime, namespaces, staged, overlay });
|
|
209
508
|
if (args.stage) {
|
|
210
|
-
|
|
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}`);
|
|
211
526
|
return;
|
|
212
527
|
}
|
|
213
528
|
const artifacts = [];
|
|
@@ -222,10 +537,83 @@ async function assemble(args) {
|
|
|
222
537
|
outRoot,
|
|
223
538
|
namespaces,
|
|
224
539
|
mtime,
|
|
540
|
+
sign,
|
|
541
|
+
notarize,
|
|
542
|
+
verbose: args.verbose,
|
|
225
543
|
}));
|
|
226
544
|
}
|
|
227
545
|
printArtifacts(projectDir, artifacts);
|
|
228
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
|
+
}
|
|
229
617
|
/**
|
|
230
618
|
* Phase two: pack a stage that was assembled somewhere else.
|
|
231
619
|
*
|
|
@@ -250,12 +638,54 @@ async function finishFromStage(args, fromStage) {
|
|
|
250
638
|
'`gjsify ship --stage`, and use `--expect-target <os>-<arch>` here to assert which leg you are ' +
|
|
251
639
|
'packing.');
|
|
252
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
|
+
}
|
|
253
650
|
const cwd = process.cwd();
|
|
254
651
|
const stageDir = resolve(cwd, fromStage);
|
|
255
652
|
const manifest = readStageManifest(stageDir);
|
|
256
653
|
assertExpectedTarget(manifest, args['expect-target']);
|
|
257
|
-
|
|
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);
|
|
258
676
|
assertFormatsStaged(manifest, formats);
|
|
677
|
+
for (const format of formats)
|
|
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);
|
|
259
689
|
for (const warning of warnOnToolSkew(manifest))
|
|
260
690
|
console.warn(`${LOG} ${warning}`);
|
|
261
691
|
const { mtime, warnings } = resolveStageMtime(manifest);
|
|
@@ -275,8 +705,21 @@ async function finishFromStage(args, fromStage) {
|
|
|
275
705
|
const outRoot = args.out === undefined ? dirname(stageDir) : resolve(cwd, args.out);
|
|
276
706
|
const artifacts = [];
|
|
277
707
|
for (const format of formats) {
|
|
278
|
-
|
|
279
|
-
|
|
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);
|
|
714
|
+
if (format.depends !== null) {
|
|
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)
|
|
722
|
+
console.warn(`${LOG} ${warning}`);
|
|
280
723
|
}
|
|
281
724
|
artifacts.push(await packOne({
|
|
282
725
|
format,
|
|
@@ -288,6 +731,9 @@ async function finishFromStage(args, fromStage) {
|
|
|
288
731
|
outRoot,
|
|
289
732
|
namespaces: manifest.namespaces,
|
|
290
733
|
mtime,
|
|
734
|
+
sign,
|
|
735
|
+
notarize,
|
|
736
|
+
verbose: args.verbose,
|
|
291
737
|
}));
|
|
292
738
|
}
|
|
293
739
|
printArtifacts(cwd, artifacts);
|
|
@@ -297,20 +743,32 @@ async function packOne(input) {
|
|
|
297
743
|
assertOverlayIsLicensed(format.id, settings.binaryName, input.overlay);
|
|
298
744
|
const overlayDir = join(outRoot, 'overlay', format.id);
|
|
299
745
|
writeStage(overlayDir, input.overlay);
|
|
300
|
-
|
|
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]);
|
|
301
755
|
// What the package installs, read off what it installs. The three questions
|
|
302
756
|
// this answers used to be answered from the project's file lists, which are
|
|
303
757
|
// absolute paths on the build host and therefore unavailable here.
|
|
304
|
-
const facts = readPayloadFacts(
|
|
305
|
-
const depends =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
758
|
+
const facts = readPayloadFacts(assembled);
|
|
759
|
+
const depends = format.depends === null
|
|
760
|
+
? []
|
|
761
|
+
: deriveDepends(format.depends, {
|
|
762
|
+
namespaces: input.namespaces,
|
|
763
|
+
hasIcons: facts.hasIcons,
|
|
764
|
+
hasSchemas: facts.hasSchemas,
|
|
765
|
+
interpreter: settings.app,
|
|
766
|
+
extra: settings.extraDepends[format.depends],
|
|
767
|
+
typelibPackages: settings.typelibPackages,
|
|
768
|
+
bundledTypelibs: facts.bundledTypelibs,
|
|
769
|
+
minGjsVersion: settings.minGjsVersion,
|
|
770
|
+
minNodeVersion: settings.minNodeVersion,
|
|
771
|
+
});
|
|
314
772
|
// `--arch` is a CLAIM about the payload and nothing else compared it to one:
|
|
315
773
|
// an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
|
|
316
774
|
// --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
|
|
@@ -324,62 +782,445 @@ async function packOne(input) {
|
|
|
324
782
|
// asserting afterwards would compare against a label that no longer names an
|
|
325
783
|
// architecture, and the mismatch that matters is exactly the one on a payload
|
|
326
784
|
// that IS architecture-specific.
|
|
327
|
-
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;
|
|
328
849
|
const archLabel = format.archName(input.arch, isArchIndependent(payload));
|
|
329
850
|
const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
|
|
851
|
+
const outDir = join(outRoot, 'out');
|
|
852
|
+
mkdirSync(outDir, { recursive: true });
|
|
853
|
+
const target = join(outDir, format.fileName(settings, archLabel));
|
|
330
854
|
// A SWITCH with a `never` guard, not a ternary. `format.id === 'deb' ? deb : rpm`
|
|
331
855
|
// read as a dispatch and behaved as one only while there were exactly two
|
|
332
856
|
// formats: a third would have taken the else-branch and written an RPM under a
|
|
333
857
|
// `.dmg` name, at exit 0. This is the same closed-vocabulary hazard the format
|
|
334
858
|
// list has, one level worse — a wrong ARTIFACT rather than a rejected
|
|
335
859
|
// declaration — so the compiler owns it now.
|
|
336
|
-
|
|
860
|
+
//
|
|
861
|
+
// Each branch WRITES `target` rather than returning bytes. It used to return
|
|
862
|
+
// them, which is the right shape only while every packer is a byte writer: a
|
|
863
|
+
// Flatpak is produced by `flatpak build-bundle`, so its bytes exist as a file
|
|
864
|
+
// before this process could hold them, and reading a bundle back into memory
|
|
865
|
+
// to hand it to one `writeFileSync` would be a copy for the sake of a
|
|
866
|
+
// signature. The size is stat'ed off what landed, for every format.
|
|
337
867
|
switch (format.id) {
|
|
338
868
|
case 'deb':
|
|
339
|
-
|
|
869
|
+
writeFileSync(target, await buildDeb(common));
|
|
340
870
|
break;
|
|
341
871
|
case 'rpm':
|
|
342
|
-
|
|
872
|
+
writeFileSync(target, await buildRpm(common));
|
|
873
|
+
break;
|
|
874
|
+
case 'flatpak':
|
|
875
|
+
await buildFlatpakBundle({
|
|
876
|
+
settings,
|
|
877
|
+
stageDir,
|
|
878
|
+
overlayDir,
|
|
879
|
+
stageManifestFile: STAGE_MANIFEST_FILE,
|
|
880
|
+
workDir: join(outRoot, 'flatpak'),
|
|
881
|
+
target,
|
|
882
|
+
archLabel,
|
|
883
|
+
verbose: input.verbose,
|
|
884
|
+
});
|
|
343
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
|
+
}
|
|
344
1005
|
default: {
|
|
345
1006
|
const unhandled = format.id;
|
|
346
1007
|
throw new Error(`gjsify ship: no packer is wired for format "${String(unhandled)}".`);
|
|
347
1008
|
}
|
|
348
1009
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 };
|
|
354
1036
|
}
|
|
355
1037
|
function printArtifacts(base, artifacts) {
|
|
356
1038
|
for (const artifact of artifacts) {
|
|
357
1039
|
console.log(`${LOG} ${artifact.format}: ${relative(base, artifact.path)} (${artifact.size} bytes)`);
|
|
358
1040
|
}
|
|
359
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Refuse a format this host cannot finish, and say which half is missing.
|
|
1044
|
+
*
|
|
1045
|
+
* Two checks and not one, because the fixes differ: the wrong OS needs another
|
|
1046
|
+
* machine, an absent tool needs a package. Both run on the pack path only —
|
|
1047
|
+
* `--stage` deliberately needs neither, which is what makes a Flatpak
|
|
1048
|
+
* assemblable on a host that cannot build one (ADR 0024 § A1).
|
|
1049
|
+
*/
|
|
1050
|
+
function assertCanPack(format) {
|
|
1051
|
+
assertHostCanFinish(format);
|
|
1052
|
+
assertToolsInstalled(format);
|
|
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
|
+
}
|
|
360
1139
|
/**
|
|
361
1140
|
* Refuse a build target this command cannot package correctly.
|
|
362
1141
|
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
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.
|
|
368
1162
|
*
|
|
369
|
-
* An undeclared target is the common case for a GJS app and is allowed
|
|
370
|
-
*
|
|
371
|
-
*
|
|
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.
|
|
372
1167
|
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
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.
|
|
376
1185
|
*/
|
|
377
1186
|
function assertShippableTarget(app) {
|
|
378
|
-
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))
|
|
379
1215
|
return;
|
|
380
|
-
throw new Error(`gjsify ship: this project
|
|
381
|
-
|
|
382
|
-
|
|
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('" / "')}"\`.`);
|
|
383
1224
|
}
|
|
384
1225
|
/**
|
|
385
1226
|
* Run the project's own `build` script, as a CHILD process.
|
|
@@ -409,6 +1250,6 @@ async function runProjectBuild(projectDir) {
|
|
|
409
1250
|
'yourself and pass --skip-build.'),
|
|
410
1251
|
});
|
|
411
1252
|
if (result.code !== 0) {
|
|
412
|
-
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)}.`);
|
|
413
1254
|
}
|
|
414
1255
|
}
|