@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
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
// and one filename. What it also proved is what the design did NOT predict —
|
|
9
9
|
// a format can be host-bound (§ A3), so `host` is a descriptor field now.
|
|
10
10
|
import { isOnPath } from '../check-system-deps.js';
|
|
11
|
+
import { DMG_TOOL } from './dmg.js';
|
|
12
|
+
import { LAYOUTS, LAYOUT_NAMES } from './layout.js';
|
|
13
|
+
import { SCHEMA_COMPILER, SCHEMA_COMPILER_HINT } from './schemas.js';
|
|
11
14
|
// `process.arch` → the format's architecture name. Taken from dpkg's own
|
|
12
15
|
// `data/cputable` and rpm's arch table. `arm` cannot be told apart from
|
|
13
16
|
// `armel` by a running process, and Debian's hard-float port is what anything
|
|
@@ -69,6 +72,190 @@ function rpmArch(arch, archIndependent) {
|
|
|
69
72
|
function flatpakArch(arch, _archIndependent) {
|
|
70
73
|
return lookupArch(FLATPAK_ARCH, arch, 'Flatpak');
|
|
71
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* `process.arch` → the name Apple's tools use.
|
|
77
|
+
*
|
|
78
|
+
* Two rows, because two architectures exist: `x86_64` and `arm64` are what
|
|
79
|
+
* `uname -m`, `lipo -archs` and a Mach-O `cputype` all report on the machines
|
|
80
|
+
* macOS runs on. Anything else is REFUSED rather than passed through, for the
|
|
81
|
+
* reason the Flatpak table gives: an unknown value would end up in a filename a
|
|
82
|
+
* user is asked to download, labelling an artifact for a machine that does not
|
|
83
|
+
* exist. Note this is not `arm64` verbatim from `process.arch` by coincidence —
|
|
84
|
+
* it happens to agree, and the day `process.arch` gains a spelling Apple does
|
|
85
|
+
* not use, this table is where it is caught.
|
|
86
|
+
*/
|
|
87
|
+
const MACOS_ARCH = { x64: 'x86_64', arm64: 'arm64' };
|
|
88
|
+
/**
|
|
89
|
+
* A `.app` carries no `noarch`, and unlike Flatpak that is not because the
|
|
90
|
+
* format forbids one — it is because `archIndependent` answers the wrong
|
|
91
|
+
* question here.
|
|
92
|
+
*
|
|
93
|
+
* `all`/`noarch` exist so apt and dnf do not refuse a pure-JavaScript package on
|
|
94
|
+
* a machine of another architecture. Nothing gatekeeps a `.app`: it is dragged
|
|
95
|
+
* into `/Applications` and run. What the label is FOR is the download a user
|
|
96
|
+
* picks between, and that is a claim about which Mac the bundle runs on — which,
|
|
97
|
+
* once M2b stages a runtime, is architecture-specific whatever the JavaScript is.
|
|
98
|
+
* Naming the arch on a payload that happens to be portable is honest; naming
|
|
99
|
+
* `noarch` on one that will not be is not.
|
|
100
|
+
*/
|
|
101
|
+
function macosArch(arch, _archIndependent) {
|
|
102
|
+
return lookupArch(MACOS_ARCH, arch, 'macOS');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `process.arch` → the name Windows and everything that targets it use.
|
|
106
|
+
*
|
|
107
|
+
* ONE ROW, and it is not caution: `wingtk/gvsbuild` hardcodes
|
|
108
|
+
* `self.platform = "x64"` and publishes no arm64 GTK, so there is nothing to build
|
|
109
|
+
* `@gjsify/gtk-runtime-win32-arm64` out of and no GTK for a Windows/ARM artifact
|
|
110
|
+
* to load (#1117). `Layout.arches` refuses the same value one phase earlier, at
|
|
111
|
+
* stage time, with that reason spelled out; this table is the second half — the
|
|
112
|
+
* one that stops a stage assembled by an older gjsify from acquiring a label here.
|
|
113
|
+
*
|
|
114
|
+
* `x64` maps to itself, and unlike the macOS table that is not a coincidence to be
|
|
115
|
+
* caught later: `x64` is the spelling Node's own release archives use
|
|
116
|
+
* (`node-v24.20.0-win-x64.zip`), the spelling gvsbuild's assets use
|
|
117
|
+
* (`GTK4_Gvsbuild_<v>_x64.zip`), and the spelling a `windows-latest x64` runner
|
|
118
|
+
* label uses. `%PROCESSOR_ARCHITECTURE%` says `AMD64` and nothing a user downloads
|
|
119
|
+
* is named after it.
|
|
120
|
+
*/
|
|
121
|
+
const WINDOWS_ARCH = { x64: 'x64' };
|
|
122
|
+
function windowsArch(arch, _archIndependent) {
|
|
123
|
+
return lookupArch(WINDOWS_ARCH, arch, 'Windows');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* What a Windows program-directory name may not be.
|
|
127
|
+
*
|
|
128
|
+
* THREE RULES, not one, because Win32 forbids three different things and an
|
|
129
|
+
* earlier draft called the character class "the Win32 reserved set" while
|
|
130
|
+
* `windowsProgramDirName('CON')`, `'Demo.'` and `'Demo '` all sailed through —
|
|
131
|
+
* each of them producing exactly the failure this check exists to prevent:
|
|
132
|
+
*
|
|
133
|
+
* * the reserved CHARACTERS, plus the control range (a `*` or a `?` cannot be
|
|
134
|
+
* created or extracted at all);
|
|
135
|
+
* * the reserved DEVICE names, with or without an extension — `CON`, `PRN`,
|
|
136
|
+
* `AUX`, `NUL`, `COM1`-`COM9`, `LPT1`-`LPT9` are devices at every path, so
|
|
137
|
+
* `CON\app\app.node.mjs` is not a path;
|
|
138
|
+
* * a trailing dot or space, which the Win32 API silently STRIPS — so the
|
|
139
|
+
* directory an installer creates is not the one the launcher's `%~dp0`
|
|
140
|
+
* resolves against.
|
|
141
|
+
*
|
|
142
|
+
* Refused HERE rather than on the Windows machine, because that machine is on the
|
|
143
|
+
* other side of a download: the artifact assembles at exit 0 on Linux, uploads,
|
|
144
|
+
* and fails on a stranger's box. Same shape and same reason as `layout.ts`'s
|
|
145
|
+
* `BUNDLE_NAME_FORBIDDEN`, one OS over — and a wider rule, because Windows
|
|
146
|
+
* reserves more than HFS+ does.
|
|
147
|
+
*/
|
|
148
|
+
const WINDOWS_NAME_FORBIDDEN = /[<>:"/\\|?*]/;
|
|
149
|
+
const WINDOWS_RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i;
|
|
150
|
+
/**
|
|
151
|
+
* Does this name break one of the three rules?
|
|
152
|
+
*
|
|
153
|
+
* The control range is a code-point test rather than a fourth character class:
|
|
154
|
+
* `no-control-regex` refuses `\u0000-\u001f` inside a literal — correctly, since a
|
|
155
|
+
* control character in a regex is almost always a typo — and the exception here is
|
|
156
|
+
* that the characters ARE the subject. Saying so in code costs one comparison and
|
|
157
|
+
* spends no lint suppression.
|
|
158
|
+
*/
|
|
159
|
+
function windowsNameIsUnusable(name) {
|
|
160
|
+
if (WINDOWS_NAME_FORBIDDEN.test(name) || WINDOWS_RESERVED_NAMES.test(name))
|
|
161
|
+
return true;
|
|
162
|
+
if (/[. ]$/.test(name))
|
|
163
|
+
return true;
|
|
164
|
+
return [...name].some((char) => (char.codePointAt(0) ?? 0) < 0x20);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The directory `windows-dir` produces, and the top level `windows-dir-zip` puts
|
|
168
|
+
* its entries under.
|
|
169
|
+
*
|
|
170
|
+
* ONE function with TWO readers, deliberately: the zip has to expand to the same
|
|
171
|
+
* directory the other row writes, and two literals would be two names a user is
|
|
172
|
+
* asked to believe are one.
|
|
173
|
+
*/
|
|
174
|
+
export function windowsProgramDirName(settings) {
|
|
175
|
+
const name = settings.name;
|
|
176
|
+
// EMPTY FIRST, and it is not defensive: `resolveShipSettings` derives the name
|
|
177
|
+
// as `metadata.name ?? titleCase(binaryName)`, and `??` passes `''` straight
|
|
178
|
+
// through. An empty name makes `windows-dir` write into the output root itself
|
|
179
|
+
// and gives the zip no top level at all — which is precisely the scattering
|
|
180
|
+
// `windows-dir-zip` synthesises one to prevent, reproduced at exit 0 by the
|
|
181
|
+
// function that prevents it. Neither oracle catches it either: `verify-app-zip.sh`
|
|
182
|
+
// takes `basename` of the directory it is handed and would compare the archive
|
|
183
|
+
// against the output root.
|
|
184
|
+
if (name.trim() === '') {
|
|
185
|
+
throw new Error('gjsify ship: the windows layout has no name to call the program directory — ' +
|
|
186
|
+
'`gjsify.ship.name` (or the package name it is derived from) is empty. The artifact IS a ' +
|
|
187
|
+
'directory and its zip expands into one, so an empty name would unpack `app\\`, `share\\` and ' +
|
|
188
|
+
'the launcher loose into whatever folder the user was in.');
|
|
189
|
+
}
|
|
190
|
+
if (windowsNameIsUnusable(name)) {
|
|
191
|
+
throw new Error(`gjsify ship: the windows layout would put this app in a directory called "${name}", and Windows ` +
|
|
192
|
+
'cannot hold it: it reserves < > : " / \\ | ? * and the control characters, reserves CON, PRN, ' +
|
|
193
|
+
'AUX, NUL, COM1-9 and LPT1-9 as device names at every path, and silently strips a trailing dot ' +
|
|
194
|
+
'or space.\n' +
|
|
195
|
+
' The artifact would assemble here and be unextractable — or extract under a different name ' +
|
|
196
|
+
'than\n' +
|
|
197
|
+
' the launcher resolves against. Set `gjsify.ship.name` to the display name you want on disk.');
|
|
198
|
+
}
|
|
199
|
+
return name;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The tools ONE host needs for a format, whichever shape the row declares.
|
|
203
|
+
*
|
|
204
|
+
* The one place the union in {@link RequiredTools} is opened, so a caller cannot
|
|
205
|
+
* read the map and get `undefined.filter` or read the array on a row that has a
|
|
206
|
+
* map. An OS the map does not name answers `[]` — not because nothing is needed
|
|
207
|
+
* there, but because {@link assertHostCanFinish} has already refused that host by
|
|
208
|
+
* the time this is asked, and inventing a tool list for a host that cannot finish
|
|
209
|
+
* the format would answer a question nobody may ask.
|
|
210
|
+
*/
|
|
211
|
+
export function requiredToolsOn(tools, host) {
|
|
212
|
+
if (Array.isArray(tools))
|
|
213
|
+
return tools;
|
|
214
|
+
return tools[host] ?? [];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Every tool any host could need for a format — the question `installHint`'s
|
|
218
|
+
* presence is gated on.
|
|
219
|
+
*
|
|
220
|
+
* Deliberately NOT what {@link assertToolsInstalled} checks. "Does this row need a
|
|
221
|
+
* hint at all" is a property of the ROW; "is this tool here" is a question about
|
|
222
|
+
* one machine, and answering the first with the second would let a row declare a
|
|
223
|
+
* Windows-only tool and no hint, because the gate happened to run on Linux.
|
|
224
|
+
*/
|
|
225
|
+
export function allRequiredTools(tools) {
|
|
226
|
+
if (Array.isArray(tools))
|
|
227
|
+
return tools;
|
|
228
|
+
return Object.values(tools).flat();
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Why a format's runtime provides the interpreters it does — one string per
|
|
232
|
+
* RUNTIME STORY, three stories under nine rows.
|
|
233
|
+
*
|
|
234
|
+
* These are PRINTED: `assertFormatCanRunInterpreter` puts the row's own sentence
|
|
235
|
+
* in front of an author whose project cannot ship this way. Per row they were
|
|
236
|
+
* five redundant copies of one paragraph, so a correction on one row would have
|
|
237
|
+
* left the two beside it telling that author something else about the same fact.
|
|
238
|
+
*/
|
|
239
|
+
// Neither interpreter is restricted on `deb`/`rpm`: `Depends: gjs` and
|
|
240
|
+
// `Depends: nodejs` are both satisfiable from any distribution's own archive, so
|
|
241
|
+
// the artifact's runtime is the machine's.
|
|
242
|
+
const DISTRO_INTERPRETER_GAP = 'a distro package declares its interpreter as a dependency instead of carrying one, and every ' +
|
|
243
|
+
'distribution ships both';
|
|
244
|
+
const MACOS_INTERPRETER_GAP = 'a `.app` a stranger downloads has to CARRY its interpreter, and there is no relocatable GJS to ' +
|
|
245
|
+
'put in one — `packages/node-gi/scripts/build-gtk-runtime-darwin.mjs` records "GJS ships no ' +
|
|
246
|
+
'relocation", which is why ADR 0024 § 4 derives Node on macOS and stage 7 is what would change it';
|
|
247
|
+
const WINDOWS_INTERPRETER_GAP = 'there is no GJS host on Windows at all (ADR 0024 § 4) — not a system one to depend on and not a ' +
|
|
248
|
+
'relocatable one to carry — so a `--app gjs` payload cannot be shipped this way. That is why ' +
|
|
249
|
+
'§ 4 derives Node here, and why `@gjsify/node-runtime-win32-x64` exists';
|
|
250
|
+
/**
|
|
251
|
+
* rpm's `share/licenses/<name>/LICENSE` — eight of the nine rows.
|
|
252
|
+
*
|
|
253
|
+
* `deb` is the one that does not, and the one with a POLICY: Debian § 12.5 puts
|
|
254
|
+
* the copyright at `share/doc/<package>/copyright` and lintian errors without it.
|
|
255
|
+
* Nothing dictates a location for the other eight, so they share one shape and
|
|
256
|
+
* the layout map does the rest.
|
|
257
|
+
*/
|
|
258
|
+
const SHARE_LICENSE_DEST = (binaryName) => `share/licenses/${binaryName}/LICENSE`;
|
|
72
259
|
/** `.deb` and `.rpm` are written by this tree, so they exec nothing and read back with GNU tools. */
|
|
73
260
|
const WRITTEN_HERE = (readWith) => ({
|
|
74
261
|
finishOn: 'any',
|
|
@@ -78,28 +265,37 @@ const WRITTEN_HERE = (readWith) => ({
|
|
|
78
265
|
export const FORMATS = {
|
|
79
266
|
deb: {
|
|
80
267
|
id: 'deb',
|
|
268
|
+
layoutOs: 'linux',
|
|
81
269
|
prefix: '/usr',
|
|
82
270
|
host: WRITTEN_HERE(['ar', 'tar', 'dpkg-deb', 'lintian']),
|
|
83
271
|
depends: 'deb',
|
|
272
|
+
interpreters: ['gjs', 'node'],
|
|
273
|
+
interpreterGap: DISTRO_INTERPRETER_GAP,
|
|
84
274
|
// Debian policy § 12.5: every package ships its copyright in
|
|
85
275
|
// /usr/share/doc/<package>/copyright, and lintian errors without it.
|
|
86
276
|
licenseDest: (binaryName) => `share/doc/${binaryName}/copyright`,
|
|
87
277
|
licenseKind: 'debian-copyright',
|
|
88
278
|
archName: debArch,
|
|
89
279
|
fileName: (s, archLabel) => `${s.binaryName}_${s.version}-${s.release}_${archLabel}.deb`,
|
|
280
|
+
artifactKind: 'file',
|
|
90
281
|
},
|
|
91
282
|
rpm: {
|
|
92
283
|
id: 'rpm',
|
|
284
|
+
layoutOs: 'linux',
|
|
93
285
|
prefix: '/usr',
|
|
94
286
|
host: WRITTEN_HERE(['rpm', 'rpm2cpio', 'cpio']),
|
|
95
287
|
depends: 'rpm',
|
|
96
|
-
|
|
288
|
+
interpreters: ['gjs', 'node'],
|
|
289
|
+
interpreterGap: DISTRO_INTERPRETER_GAP,
|
|
290
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
97
291
|
licenseKind: 'plain',
|
|
98
292
|
archName: rpmArch,
|
|
99
293
|
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.rpm`,
|
|
294
|
+
artifactKind: 'file',
|
|
100
295
|
},
|
|
101
296
|
flatpak: {
|
|
102
297
|
id: 'flatpak',
|
|
298
|
+
layoutOs: 'linux',
|
|
103
299
|
// The one-line difference ADR 0024 § 2 promised. Nothing in the payload
|
|
104
300
|
// changes: the launcher derives its prefix at runtime (§ 3), so the same
|
|
105
301
|
// staged `bin/<name>` works under /usr and under /app.
|
|
@@ -129,15 +325,442 @@ export const FORMATS = {
|
|
|
129
325
|
// which also means the unmapped-namespace refusal ADR 0024 § 6 built for
|
|
130
326
|
// deb and rpm has nothing to say about this format.
|
|
131
327
|
depends: null,
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
328
|
+
// GJS ONLY. `org.gnome.Platform` ships `gjs`; Node is a build-time SDK
|
|
329
|
+
// extension, not a runtime — see the field's doc on `FormatDescriptor`.
|
|
330
|
+
interpreters: ['gjs'],
|
|
331
|
+
interpreterGap: 'a Flatpak runs against `org.gnome.Platform`, which ships `gjs` and no `node`. Node exists only ' +
|
|
332
|
+
'as `org.freedesktop.Sdk.Extension.node2x`, and that extension puts node on the BUILD path, not ' +
|
|
333
|
+
'in the runtime — so the artifact would install and then fail at `exec node`',
|
|
334
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
136
335
|
licenseKind: 'plain',
|
|
137
336
|
archName: flatpakArch,
|
|
138
337
|
// Named after the APP ID, not the binary: the id is the ref the file
|
|
139
338
|
// installs as, and it is what `flatpak install ./file.flatpak` prints.
|
|
140
339
|
fileName: (s, archLabel) => `${s.appId}-${s.version}-${s.release}.${archLabel}.flatpak`,
|
|
340
|
+
artifactKind: 'file',
|
|
341
|
+
},
|
|
342
|
+
// ── macOS (#1354 M2a) ────────────────────────────────────────────────
|
|
343
|
+
//
|
|
344
|
+
// TWO ROWS FOR ONE TREE, and the pair is what `layoutOs` was added for. The
|
|
345
|
+
// bundle and the zip around it wrap the same staged `<App>.app`; they differ
|
|
346
|
+
// only in whether the artifact is a directory a user drags or a file a user
|
|
347
|
+
// downloads. Both are `finishOn: 'any'` — the `.dmg` that is not is the third
|
|
348
|
+
// row below (ADR 0024 § A6, #1354 M4).
|
|
349
|
+
//
|
|
350
|
+
// NEITHER IS SIGNED BY DEFAULT, and unsigned is a deliverable rather than an
|
|
351
|
+
// omission this table hides: `codesign` is macOS-only, so a bundle assembled
|
|
352
|
+
// on Linux is unsigned by construction and Gatekeeper quarantines it on a
|
|
353
|
+
// stranger's Mac. M6 has since landed and holds the other half — `gjsify ship
|
|
354
|
+
// --from-stage --sign <identity>` on a macOS host re-signs the whole Mach-O
|
|
355
|
+
// closure inside the stage (`utils/ship/signing.ts`, § A4). `SIGNERS` is keyed
|
|
356
|
+
// by LAYOUT, so these two rows cannot disagree about their signature.
|
|
357
|
+
'macos-app': {
|
|
358
|
+
id: 'macos-app',
|
|
359
|
+
layoutOs: 'darwin',
|
|
360
|
+
// The bundle IS the prefix. `Contents/` hangs directly off the artifact,
|
|
361
|
+
// so unlike `/usr` or `/app` there is nothing above it — and unlike those
|
|
362
|
+
// two, the launcher does not derive this at runtime: it walks up from
|
|
363
|
+
// `Contents/MacOS` (see `renderAppBundleLauncher`), because a `.app` can
|
|
364
|
+
// be anywhere and `/Applications` is a convention, not a path.
|
|
365
|
+
prefix: '',
|
|
366
|
+
host: {
|
|
367
|
+
// Assembled anywhere, packed anywhere: everything here is a file copy.
|
|
368
|
+
finishOn: 'any',
|
|
369
|
+
// …with ONE exception, and it is the honest one. A non-Linux layout
|
|
370
|
+
// has no install step, so `gschemas.compiled` has to be produced while
|
|
371
|
+
// the tree is being assembled or the bundle aborts at its first
|
|
372
|
+
// `Gio.Settings.new()` (`utils/ship/schemas.ts`). `glib-compile-schemas`
|
|
373
|
+
// is GLib's own and runs on all three OSes, so declaring it does NOT
|
|
374
|
+
// make the format host-bound — `assertToolsInstalled` is deliberately
|
|
375
|
+
// separate from `assertHostCanFinish` for exactly this shape.
|
|
376
|
+
requiredTools: [SCHEMA_COMPILER],
|
|
377
|
+
installHint: SCHEMA_COMPILER_HINT,
|
|
378
|
+
oracle: {
|
|
379
|
+
// CPython's `plistlib` — a DIFFERENT implementation family from
|
|
380
|
+
// the XML this tree writes, and already precedent here
|
|
381
|
+
// (`.github/ship-oracle/verify-modes.py`). Measured on this
|
|
382
|
+
// workstation against a plist with a `<key>` and no value:
|
|
383
|
+
// `plistlib.load` exits 1 naming the line, `plistutil -i` exits 0
|
|
384
|
+
// and prints `<dict/>`, and `xmllint --noout` exits 0 because
|
|
385
|
+
// well-formedness is a weaker question. So `plistlib` is the
|
|
386
|
+
// oracle and the other two are not.
|
|
387
|
+
readWith: ['python3'],
|
|
388
|
+
readOn: ['linux', 'darwin'],
|
|
389
|
+
selfReading: false,
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
// No package list. macOS resolves nothing for an app: whatever the bundle
|
|
393
|
+
// does not carry, it finds on the machine or it does not run — which is
|
|
394
|
+
// what makes M2b (staging a runtime) the milestone that matters, not a
|
|
395
|
+
// dependency field.
|
|
396
|
+
depends: null,
|
|
397
|
+
// NODE ONLY, and it is a measured limit rather than caution.
|
|
398
|
+
// `packages/node-gi/scripts/build-gtk-runtime-darwin.mjs` records that GJS
|
|
399
|
+
// ships no relocation, so there is no relocatable GJS to put inside a
|
|
400
|
+
// bundle a stranger downloads (ADR 0024 § 4, stage 7). A `--app gjs`
|
|
401
|
+
// payload therefore cannot be shipped this way, and
|
|
402
|
+
// `assertFormatCanRunInterpreter` says so by name instead of producing a
|
|
403
|
+
// bundle that dies at `exec gjs`.
|
|
404
|
+
interpreters: ['node'],
|
|
405
|
+
interpreterGap: MACOS_INTERPRETER_GAP,
|
|
406
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
407
|
+
licenseKind: 'plain',
|
|
408
|
+
archName: macosArch,
|
|
409
|
+
// The DISPLAY name, not the binary: this artifact is the thing a user
|
|
410
|
+
// drags into `/Applications`, and its name is what the Finder shows.
|
|
411
|
+
fileName: (s) => `${s.name}.app`,
|
|
412
|
+
artifactKind: 'directory',
|
|
413
|
+
},
|
|
414
|
+
'macos-app-zip': {
|
|
415
|
+
id: 'macos-app-zip',
|
|
416
|
+
layoutOs: 'darwin',
|
|
417
|
+
prefix: '',
|
|
418
|
+
host: {
|
|
419
|
+
finishOn: 'any',
|
|
420
|
+
// The ZIP is written by this tree (`utils/ship/zip.ts`), so the only
|
|
421
|
+
// tool here is the schema compiler the layout needs — see the row
|
|
422
|
+
// above. Writing the archive ourselves is what keeps `zipinfo` an
|
|
423
|
+
// INDEPENDENT reader rather than the other half of a `zip`/`unzip`
|
|
424
|
+
// round trip; same argument ADR 0024 § A3 makes for the deb and rpm.
|
|
425
|
+
requiredTools: [SCHEMA_COMPILER],
|
|
426
|
+
installHint: SCHEMA_COMPILER_HINT,
|
|
427
|
+
oracle: {
|
|
428
|
+
// `zipinfo -l`, not `unzip -Z1`. The failure that matters for a
|
|
429
|
+
// distributed `.app` is a launcher extracted 0644 that will not
|
|
430
|
+
// run, and `-Z1` prints NAMES ONLY — it is structurally blind to
|
|
431
|
+
// it. `zipinfo` prints the Unix mode (`-rwxr-xr-x`) and ships in
|
|
432
|
+
// the same `unzip` package, which is already in the CI image.
|
|
433
|
+
readWith: ['zipinfo'],
|
|
434
|
+
readOn: ['linux', 'darwin'],
|
|
435
|
+
selfReading: false,
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
depends: null,
|
|
439
|
+
interpreters: ['node'],
|
|
440
|
+
interpreterGap: MACOS_INTERPRETER_GAP,
|
|
441
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
442
|
+
licenseKind: 'plain',
|
|
443
|
+
archName: macosArch,
|
|
444
|
+
// The BINARY name here and the display name above, deliberately. This one
|
|
445
|
+
// is a download: it lands in a browser's downloads folder beside other
|
|
446
|
+
// files, so it carries the version and the arch, and it avoids the spaces
|
|
447
|
+
// a display name may contain.
|
|
448
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.zip`,
|
|
449
|
+
artifactKind: 'file',
|
|
450
|
+
},
|
|
451
|
+
// ── macOS (#1354 M4) ─────────────────────────────────────────────────
|
|
452
|
+
//
|
|
453
|
+
// THE THIRD CONTAINER OVER THE SAME TREE, and the first row in this table
|
|
454
|
+
// whose `finishOn` is a real restriction rather than a statement about where
|
|
455
|
+
// the application runs. The two rows above wrap the `<App>.app` in nothing
|
|
456
|
+
// and in a zip, both of which this tree writes; this one wraps it in a UDIF
|
|
457
|
+
// image over an HFS+ volume, which nothing in this tree can write and which
|
|
458
|
+
// `hdiutil` — macOS-only — can. ADR 0024 § A1 is that sentence; § A6 is why
|
|
459
|
+
// writing one ourselves stays rejected.
|
|
460
|
+
'macos-app-dmg': {
|
|
461
|
+
id: 'macos-app-dmg',
|
|
462
|
+
layoutOs: 'darwin',
|
|
463
|
+
// The BUNDLE is still the prefix. A `.dmg` is a volume around the same
|
|
464
|
+
// `<App>.app` the two rows above produce, so nothing in the payload moves
|
|
465
|
+
// — which is the whole claim ADR 0024 § 2 makes about a new format, and
|
|
466
|
+
// the third macOS row is where it stops being a prediction.
|
|
467
|
+
prefix: '',
|
|
468
|
+
host: {
|
|
469
|
+
// THE FIRST `finishOn` THAT IS NOT ABOUT THE APPLICATION. Flatpak is
|
|
470
|
+
// Linux-bound because flatpak runs on Linux; this is darwin-bound
|
|
471
|
+
// because the only UDIF writer in existence is on darwin. A stage
|
|
472
|
+
// assembled on Linux crosses with `--stage` / `--from-stage`, and
|
|
473
|
+
// `assertHostCanFinish`'s refusal names that route.
|
|
474
|
+
finishOn: ['darwin'],
|
|
475
|
+
// `hdiutil` ALONE, and the omission is the decision: the two rows
|
|
476
|
+
// above also declare `glib-compile-schemas`, which is an ASSEMBLY
|
|
477
|
+
// tool — `schemas.ts` runs it while the tree is staged, because a
|
|
478
|
+
// non-Linux layout has no install step to run it later. This is the
|
|
479
|
+
// first row whose pack phase is separated from its assembly by a HOST
|
|
480
|
+
// boundary, and `assertToolsInstalled` fires on the pack path only. So
|
|
481
|
+
// declaring the compiler here would refuse a `--from-stage` pack on a
|
|
482
|
+
// Mac with no GLib — a pack that works, because `gschemas.compiled` is
|
|
483
|
+
// already in the stage that arrived. The assembly-time absence is
|
|
484
|
+
// still caught, by `compileSchemasForStage`'s own ENOENT refusal, on
|
|
485
|
+
// the host that can act on it.
|
|
486
|
+
requiredTools: [DMG_TOOL],
|
|
487
|
+
// Not a package name, because there is none: `hdiutil` ships with
|
|
488
|
+
// macOS and exists nowhere else. `assertHostCanFinish` has already
|
|
489
|
+
// refused every non-darwin host by the time this can fire, so the only
|
|
490
|
+
// reader of this hint is on a Mac whose `/usr/bin` is broken.
|
|
491
|
+
installHint: '`hdiutil` is part of macOS and cannot be installed elsewhere — on a Mac it is /usr/bin/hdiutil, ' +
|
|
492
|
+
'so an absent one means a broken installation rather than a missing package',
|
|
493
|
+
oracle: {
|
|
494
|
+
// NOT `hdiutil verify`, which is hdiutil reading what hdiutil
|
|
495
|
+
// wrote (ADR 0024 § A3 names this format as the case). THREE
|
|
496
|
+
// readers, all on Linux, none of them Apple's and none of them
|
|
497
|
+
// ours:
|
|
498
|
+
//
|
|
499
|
+
// * `7z l` — 7-Zip's own `Dmg` handler over the UDIF container,
|
|
500
|
+
// then its `HFS` handler over the volume inside. Measured on
|
|
501
|
+
// ubuntu-24.04's 7-Zip 23.01: `Dmg`, `HFS` and `APFS` are all
|
|
502
|
+
// in `7z i`.
|
|
503
|
+
// * `dmg2img` — an independent UDIF decoder that writes the raw
|
|
504
|
+
// volume out, so the next reader gets a filesystem rather than
|
|
505
|
+
// an archive listing.
|
|
506
|
+
// * `fsck.hfsplus` (hfsprogs) — Apple's fsck_hfs sources built
|
|
507
|
+
// for Linux, which walks the catalog, the extents overflow
|
|
508
|
+
// file and the volume bitmap. This is the reader that
|
|
509
|
+
// distinguishes "a file is listed" from "the volume is
|
|
510
|
+
// structurally sound".
|
|
511
|
+
//
|
|
512
|
+
// `readOn: ['linux']` and not darwin, deliberately: a reader on
|
|
513
|
+
// the packing host is worth less, and the leg that runs these is
|
|
514
|
+
// the bare `ubuntu-latest` one that already reads the `.deb`.
|
|
515
|
+
readWith: ['7z', 'dmg2img', 'fsck.hfsplus'],
|
|
516
|
+
readOn: ['linux'],
|
|
517
|
+
selfReading: false,
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
// No package list — macOS resolves nothing for an app, exactly as the two
|
|
521
|
+
// rows above record.
|
|
522
|
+
depends: null,
|
|
523
|
+
// NODE ONLY, inherited from the tree this wraps rather than decided here:
|
|
524
|
+
// the image carries the same bundle, and there is no relocatable GJS to
|
|
525
|
+
// put in one.
|
|
526
|
+
interpreters: ['node'],
|
|
527
|
+
interpreterGap: MACOS_INTERPRETER_GAP,
|
|
528
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
529
|
+
licenseKind: 'plain',
|
|
530
|
+
archName: macosArch,
|
|
531
|
+
// The BINARY name, version and arch — the zip's convention and not the
|
|
532
|
+
// bundle's, because this artifact is a DOWNLOAD: it lands in a browser's
|
|
533
|
+
// folder beside other files. The display name is what the VOLUME carries
|
|
534
|
+
// (`dmgVolumeName`), which is the name a user actually reads, in the
|
|
535
|
+
// Finder window the image mounts into.
|
|
536
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.dmg`,
|
|
537
|
+
artifactKind: 'file',
|
|
538
|
+
},
|
|
539
|
+
// ── Windows (#1354 M3) ───────────────────────────────────────────────
|
|
540
|
+
//
|
|
541
|
+
// THE SAME PAIR ONE OS OVER, and the differences from the macOS pair are all
|
|
542
|
+
// the OS's rather than ours:
|
|
543
|
+
//
|
|
544
|
+
// * the DIRECTORY is not the stage root. A `<App>.app` is dragged to
|
|
545
|
+
// `/Applications` as one object, so `Layout.root` puts it in the stage; a
|
|
546
|
+
// Windows program directory is laid INTO `C:\Program Files\<Publisher>\<App>`
|
|
547
|
+
// by an installer that picks the parent, so the stage IS its contents
|
|
548
|
+
// (`layout.ts` records that asymmetry). Which means the ZIP has to
|
|
549
|
+
// SYNTHESISE the top level the `.app` zip inherits — without it the archive
|
|
550
|
+
// expands to a bare `app\`, `share\` and `<name>.cmd` scattered into
|
|
551
|
+
// whatever directory the user was in.
|
|
552
|
+
// * there is no `Info.plist` and no metadata file at all: what a Windows
|
|
553
|
+
// installer says about an application lives in the `.msi`'s own tables
|
|
554
|
+
// (#1354 M5), which is why `Layout.metadata` answers `[]` here.
|
|
555
|
+
// * `x64` alone, and the blocker is upstream — see `WINDOWS_ARCH`.
|
|
556
|
+
//
|
|
557
|
+
// NEITHER IS SIGNED BY DEFAULT, and Windows is the softer of the two asymmetries
|
|
558
|
+
// ADR 0024 § A5 records: Gatekeeper BLOCKS an unsigned `.app`, while SmartScreen
|
|
559
|
+
// only WARNS until per-file download reputation accrues. So an unsigned program
|
|
560
|
+
// directory is a usable artifact in a way an unsigned `.app` is not. `--sign`
|
|
561
|
+
// reaches these rows through `SIGNERS.win32` (signtool — argv unit-tested, the
|
|
562
|
+
// invocation itself UNVERIFIED, since no run here has ever held a certificate);
|
|
563
|
+
// the `.msi` around this tree is the third row below.
|
|
564
|
+
'windows-dir': {
|
|
565
|
+
id: 'windows-dir',
|
|
566
|
+
layoutOs: 'win32',
|
|
567
|
+
// The program directory IS the prefix, and — unlike `/usr` or `/app` — the
|
|
568
|
+
// launcher derives it at run time from `%~dp0` rather than having it baked,
|
|
569
|
+
// because an installer, a portable unzip and a build tree put it in three
|
|
570
|
+
// different places.
|
|
571
|
+
prefix: '',
|
|
572
|
+
host: {
|
|
573
|
+
// Assembled anywhere, packed anywhere: everything here is a file copy.
|
|
574
|
+
// Windows enters at the RUNNING proof, not at the packing.
|
|
575
|
+
finishOn: 'any',
|
|
576
|
+
// The same one exception the `.app` has, for the same reason: a non-Linux
|
|
577
|
+
// layout has no install step, so `gschemas.compiled` is produced while
|
|
578
|
+
// the tree is assembled or the app aborts at its first `Gio.Settings.new()`.
|
|
579
|
+
requiredTools: [SCHEMA_COMPILER],
|
|
580
|
+
installHint: SCHEMA_COMPILER_HINT,
|
|
581
|
+
oracle: {
|
|
582
|
+
// TWO READERS, because the artifact has two halves and no single
|
|
583
|
+
// reader covers both.
|
|
584
|
+
//
|
|
585
|
+
// `cmd` is the authoritative one and it runs on exactly one OS:
|
|
586
|
+
// `cmd.exe` is the only thing that reads a batch launcher, and
|
|
587
|
+
// nothing on Linux parses one. That is why M3's proof is a
|
|
588
|
+
// `windows-latest` leg that unzips this directory and starts the
|
|
589
|
+
// app — assert-the-toolkit-is-absent-first, like
|
|
590
|
+
// `windows-batteries-included`.
|
|
591
|
+
//
|
|
592
|
+
// `python3` is the Linux-runnable half: CPython reads the PE
|
|
593
|
+
// headers of everything the directory CARRIES (`node.exe`, the
|
|
594
|
+
// gvsbuild DLL closure), which is a different implementation
|
|
595
|
+
// family from both PE readers in this repository — the CLI's
|
|
596
|
+
// `readBinaryArch` and `manifest-conformance/lib/binary.mjs`. It
|
|
597
|
+
// reads what the staging DID; it cannot read what the launcher
|
|
598
|
+
// MEANS.
|
|
599
|
+
readWith: ['cmd', 'python3'],
|
|
600
|
+
readOn: ['linux', 'win32'],
|
|
601
|
+
selfReading: false,
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
// No package list. Windows resolves nothing for an application: whatever
|
|
605
|
+
// the directory does not carry, it finds on the machine or it does not run
|
|
606
|
+
// — and on Windows "on the machine" is emptier than on macOS, since there
|
|
607
|
+
// is no Homebrew and no system GTK at all.
|
|
608
|
+
depends: null,
|
|
609
|
+
// NODE ONLY, and here it is not even a relocation question: there is NO GJS
|
|
610
|
+
// host on Windows at all (ADR 0024 § 4), so a `--app gjs` payload has
|
|
611
|
+
// nothing anywhere that could run it.
|
|
612
|
+
interpreters: ['node'],
|
|
613
|
+
interpreterGap: WINDOWS_INTERPRETER_GAP,
|
|
614
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
615
|
+
licenseKind: 'plain',
|
|
616
|
+
archName: windowsArch,
|
|
617
|
+
// The DISPLAY name: this artifact is the directory an installer lays down
|
|
618
|
+
// and a user browses to, so it is named the way the application is.
|
|
619
|
+
fileName: (s) => windowsProgramDirName(s),
|
|
620
|
+
artifactKind: 'directory',
|
|
621
|
+
},
|
|
622
|
+
'windows-dir-zip': {
|
|
623
|
+
id: 'windows-dir-zip',
|
|
624
|
+
layoutOs: 'win32',
|
|
625
|
+
prefix: '',
|
|
626
|
+
host: {
|
|
627
|
+
finishOn: 'any',
|
|
628
|
+
requiredTools: [SCHEMA_COMPILER],
|
|
629
|
+
installHint: SCHEMA_COMPILER_HINT,
|
|
630
|
+
oracle: {
|
|
631
|
+
// `zipinfo -l`, the same independent reader the `.app` zip uses and
|
|
632
|
+
// for the same reason: this tree WRITES the archive
|
|
633
|
+
// (`utils/ship/zip.ts`), so a `zip`/`unzip` round trip would be the
|
|
634
|
+
// two halves of one package agreeing with each other.
|
|
635
|
+
//
|
|
636
|
+
// What it is asked here is a DIFFERENT question, though, and the
|
|
637
|
+
// difference is the OS's: a POSIX mode means nothing to Windows,
|
|
638
|
+
// which decides executability from the extension. What can go wrong
|
|
639
|
+
// with THIS archive is the top level — entries written at the root
|
|
640
|
+
// expand into whatever directory the user was in — so the reader is
|
|
641
|
+
// pointed at the names first and the modes second (the modes still
|
|
642
|
+
// matter to anyone unzipping the artifact on Linux or macOS to
|
|
643
|
+
// inspect it).
|
|
644
|
+
readWith: ['zipinfo'],
|
|
645
|
+
readOn: ['linux', 'darwin'],
|
|
646
|
+
selfReading: false,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
depends: null,
|
|
650
|
+
interpreters: ['node'],
|
|
651
|
+
interpreterGap: WINDOWS_INTERPRETER_GAP,
|
|
652
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
653
|
+
licenseKind: 'plain',
|
|
654
|
+
archName: windowsArch,
|
|
655
|
+
// The BINARY name here and the display name above, exactly as the macOS
|
|
656
|
+
// pair splits them: this one is a download that lands in a browser's
|
|
657
|
+
// folder beside other files, so it carries the version and the arch and
|
|
658
|
+
// avoids the spaces a display name may contain.
|
|
659
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.zip`,
|
|
660
|
+
artifactKind: 'file',
|
|
661
|
+
},
|
|
662
|
+
// ── The Windows installer (#1354 M5) ─────────────────────────────────
|
|
663
|
+
//
|
|
664
|
+
// The THIRD row over the windows layout, and the first in this table whose
|
|
665
|
+
// producer is not this tree. It wraps the same program directory the two rows
|
|
666
|
+
// above wrap; what it adds is the three things a directory cannot do — put
|
|
667
|
+
// itself somewhere, give a user something to click, and come off again.
|
|
668
|
+
//
|
|
669
|
+
// ONE AUTHORED `.wxs`, TWO COMPILERS, and that pair is the whole design (ADR
|
|
670
|
+
// 0024 § A6). `msitools` ships `wixl` AND `msiinfo`, so a wixl-only path would
|
|
671
|
+
// be our writer read back by its own package — `selfReading` with extra steps.
|
|
672
|
+
// Instead the document `utils/ship/msi.ts` renders is compiled by `wixl` on
|
|
673
|
+
// Linux and by WiX v3 on Windows, and each backend's output is read by the
|
|
674
|
+
// OTHER family: `msiexec` (Microsoft's own installer service) installs and RUNS
|
|
675
|
+
// the wixl-built file, `msiinfo` reads back the WiX-built one. Neither leg is a
|
|
676
|
+
// package agreeing with itself.
|
|
677
|
+
//
|
|
678
|
+
// A HAND-WRITTEN MSI STAYS REJECTED, and the reason is specific rather than
|
|
679
|
+
// effort. The three constraints that forced the hand-written `.deb`/`.rpm`
|
|
680
|
+
// writers — must run under GJS, must run offline, the CI image is Fedora — have
|
|
681
|
+
// no subject here: § 4 records that there is NO GJS host on Windows at all, so
|
|
682
|
+
// there is no sandbox in which an `.msi` would have to be produced without
|
|
683
|
+
// distro packages. ≈1 300 lines for nothing this milestone can spend.
|
|
684
|
+
//
|
|
685
|
+
// MSIX STAYS REJECTED until a certificate exists. An unsigned one cannot be
|
|
686
|
+
// installed at all, and a self-signed cert dropped into `TrustedPeople` buys a
|
|
687
|
+
// green leg that proves the leg trusts itself.
|
|
688
|
+
msi: {
|
|
689
|
+
id: 'msi',
|
|
690
|
+
layoutOs: 'win32',
|
|
691
|
+
// Same empty prefix as the directory it wraps, and for a sharper reason
|
|
692
|
+
// here: the install location is a RUNTIME decision. `INSTALLDIR` resolves
|
|
693
|
+
// to `%ProgramFiles%\<App>` by default and to whatever `msiexec
|
|
694
|
+
// INSTALLDIR=…` names otherwise, so a prefix baked at pack time would be a
|
|
695
|
+
// claim the installer is free to contradict. The launcher never needed one
|
|
696
|
+
// anyway — it derives its own from `%~dp0`.
|
|
697
|
+
prefix: '',
|
|
698
|
+
host: {
|
|
699
|
+
// BOTH, and that is the measured answer rather than the ADR's
|
|
700
|
+
// either/or. § A5 wrote "`.msi` is `'any'` if we write it and
|
|
701
|
+
// `['win32']` if WiX does", which was the choice between a hand-written
|
|
702
|
+
// writer and a WiX-only one. The third option is the one that gets an
|
|
703
|
+
// independent reader on both legs: two backends over one document, so
|
|
704
|
+
// the format is finishable on Linux AND on Windows and neither host has
|
|
705
|
+
// to send its stage to the other. `darwin` is deliberately absent —
|
|
706
|
+
// Homebrew does package `msitools`, but nothing in this repository has
|
|
707
|
+
// ever run it there, and a row that claims a host no leg exercises is
|
|
708
|
+
// the shape a `--target msi` on a Mac fails at pack time for.
|
|
709
|
+
finishOn: ['linux', 'win32'],
|
|
710
|
+
// A MAP, not a list, and `RequiredTools` exists for this row alone. The
|
|
711
|
+
// schema compiler is on both sides because it is the LAYOUT's, exactly
|
|
712
|
+
// as on the two rows above — a windows stage has no install step, so
|
|
713
|
+
// `gschemas.compiled` is produced while the tree is assembled or the app
|
|
714
|
+
// aborts at its first `Gio.Settings.new()`.
|
|
715
|
+
//
|
|
716
|
+
// `candle.exe`/`light.exe` carry their extension because `isOnPath` does
|
|
717
|
+
// an `existsSync(join(dir, cmd))` and appends nothing: on Windows the
|
|
718
|
+
// bare word `candle` is not a file, so a row spelling it that way would
|
|
719
|
+
// refuse a host that has WiX installed.
|
|
720
|
+
requiredTools: {
|
|
721
|
+
linux: [SCHEMA_COMPILER, 'wixl'],
|
|
722
|
+
win32: [SCHEMA_COMPILER, 'candle.exe', 'light.exe'],
|
|
723
|
+
},
|
|
724
|
+
installHint: `${SCHEMA_COMPILER_HINT}; the MSI compiler is Fedora: \`sudo dnf install msitools\`, ` +
|
|
725
|
+
'Debian/Ubuntu: `sudo apt install msitools`, Windows: WiX Toolset v3.14 ' +
|
|
726
|
+
'(https://github.com/wixtoolset/wix3/releases) with its `bin` directory on PATH',
|
|
727
|
+
oracle: {
|
|
728
|
+
// TWO READERS, ONE PER BACKEND, and each reads the file the OTHER
|
|
729
|
+
// backend wrote — which is what makes `selfReading: false` true here
|
|
730
|
+
// rather than merely declared.
|
|
731
|
+
//
|
|
732
|
+
// `msiexec` is Windows Installer itself: it INSTALLS the wixl-built
|
|
733
|
+
// file into a prefix and the leg then runs the installed launcher.
|
|
734
|
+
// Installing without running would prove the database parses, not
|
|
735
|
+
// that it laid down something that works — and the artifact's whole
|
|
736
|
+
// claim is the second one.
|
|
737
|
+
//
|
|
738
|
+
// `msiinfo` reads back the WiX-built file on Linux — `tables`,
|
|
739
|
+
// `streams`, `export <table>`, `suminfo`. `suminfo` is what makes
|
|
740
|
+
// the cross-check checkable rather than assumed: it prints the
|
|
741
|
+
// producer (`Application: msitools 0.106.58-a155` on a wixl file),
|
|
742
|
+
// so a leg can assert the file in front of it was NOT written by the
|
|
743
|
+
// package reading it.
|
|
744
|
+
readWith: ['msiexec', 'msiinfo'],
|
|
745
|
+
readOn: ['win32', 'linux'],
|
|
746
|
+
selfReading: false,
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
// Windows resolves nothing for an application — see `windows-dir`. An
|
|
750
|
+
// installer does not change that; it moves the same self-contained tree.
|
|
751
|
+
depends: null,
|
|
752
|
+
interpreters: ['node'],
|
|
753
|
+
interpreterGap: WINDOWS_INTERPRETER_GAP,
|
|
754
|
+
licenseDest: SHARE_LICENSE_DEST,
|
|
755
|
+
licenseKind: 'plain',
|
|
756
|
+
archName: windowsArch,
|
|
757
|
+
// The BINARY name, like the zip beside it and for the same reason: this is a
|
|
758
|
+
// download that lands in a browser's folder next to other files, so it
|
|
759
|
+
// carries the version and the arch and avoids the spaces a display name may
|
|
760
|
+
// contain. The DISPLAY name is what the installer lays down INSIDE — see
|
|
761
|
+
// `windowsProgramDirName`, which both this row and `windows-dir` call.
|
|
762
|
+
fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.msi`,
|
|
763
|
+
artifactKind: 'file',
|
|
141
764
|
},
|
|
142
765
|
};
|
|
143
766
|
// DERIVED, never a second list. `FORMATS` is `Record<FormatId, …>`, so the
|
|
@@ -149,8 +772,8 @@ export const FORMATS = {
|
|
|
149
772
|
// Insertion order is stable for string keys, and `resolveFormats` sorts anyway.
|
|
150
773
|
export const FORMAT_IDS = Object.keys(FORMATS);
|
|
151
774
|
/**
|
|
152
|
-
* What `gjsify ship
|
|
153
|
-
* needs no tool and no particular host.
|
|
775
|
+
* What `gjsify ship <os>` builds when nothing said otherwise — every format that
|
|
776
|
+
* wraps THAT OS's layout and needs no tool and no particular host.
|
|
154
777
|
*
|
|
155
778
|
* A SECOND derivation from the same table, and it has to be one: `FORMAT_IDS`
|
|
156
779
|
* used to be both "every format that exists" and "every format to build by
|
|
@@ -161,8 +784,28 @@ export const FORMAT_IDS = Object.keys(FORMATS);
|
|
|
161
784
|
* packs `@gjsify/cli` itself on a bare ubuntu runner. A host-bound format is
|
|
162
785
|
* opt-in through `--target` or `gjsify.ship.targets`; `--help` still lists all
|
|
163
786
|
* of them.
|
|
787
|
+
*
|
|
788
|
+
* `layoutOs` is the SECOND criterion, and it is what the layout axis needed: a
|
|
789
|
+
* `.app` and its zip and a Windows program directory are all `finishOn: 'any'`,
|
|
790
|
+
* so on the one criterion a bare `gjsify ship` on Linux would start emitting
|
|
791
|
+
* five artifacts (ADR 0024 issue #1354, open question 3). Answered with § A2's
|
|
792
|
+
* positional rather than instead of it — the positional picks the layout, this
|
|
793
|
+
* picks the formats that wrap it, and `gjsify ship` on Linux keeps emitting
|
|
794
|
+
* exactly `deb` and `rpm`.
|
|
795
|
+
*
|
|
796
|
+
* An EMPTY answer is legal and is what darwin and windows give today: their
|
|
797
|
+
* layouts assemble (`--stage`) and no format wraps them yet. `assemble` refuses
|
|
798
|
+
* a pack with nothing to pack rather than exiting 0 having produced no artifact.
|
|
164
799
|
*/
|
|
165
|
-
export
|
|
800
|
+
export function defaultFormatIds(layoutOs) {
|
|
801
|
+
return FORMAT_IDS.filter((id) => FORMATS[id].layoutOs === layoutOs && FORMATS[id].host.finishOn === 'any');
|
|
802
|
+
}
|
|
803
|
+
/** `process.platform` token → the positional that selects it, so a message says `windows`, not `win32`. */
|
|
804
|
+
const LAYOUT_NAME_BY_OS = Object.fromEntries(LAYOUT_NAMES.map((name) => [LAYOUTS[name].os, name]));
|
|
805
|
+
/** Every format that wraps one OS's layout, tool-bound ones included. */
|
|
806
|
+
export function formatIdsFor(layoutOs) {
|
|
807
|
+
return FORMAT_IDS.filter((id) => FORMATS[id].layoutOs === layoutOs);
|
|
808
|
+
}
|
|
166
809
|
/**
|
|
167
810
|
* Refuse a format this host cannot finish, BEFORE anything is built or written.
|
|
168
811
|
*
|
|
@@ -192,22 +835,26 @@ export function assertHostCanFinish(format, host = process.platform) {
|
|
|
192
835
|
* a build: `gjsify ship` runs the project's `build` script first, and finding
|
|
193
836
|
* out afterwards that `flatpak-builder` is absent costs the whole build.
|
|
194
837
|
*/
|
|
195
|
-
export function assertToolsInstalled(format, present = isOnPath) {
|
|
196
|
-
const
|
|
838
|
+
export function assertToolsInstalled(format, present = isOnPath, host = process.platform) {
|
|
839
|
+
const tools = requiredToolsOn(format.host.requiredTools, host);
|
|
840
|
+
const missing = tools.filter((tool) => !present(tool));
|
|
197
841
|
if (missing.length === 0)
|
|
198
842
|
return;
|
|
199
843
|
// The install instruction is the DESCRIPTOR's, not this function's: hardcoded
|
|
200
844
|
// here it was `dnf install flatpak flatpak-builder` for every format that
|
|
201
845
|
// will ever need a tool, which is the branch this table exists to avoid.
|
|
202
846
|
const hint = format.host.installHint;
|
|
203
|
-
throw new Error(`gjsify ship: packing a ${format.id} needs ${
|
|
847
|
+
throw new Error(`gjsify ship: packing a ${format.id} on ${host} needs ${tools.join(' and ')}, and ` +
|
|
204
848
|
`${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not on PATH. ` +
|
|
205
849
|
`${hint === undefined ? 'Install it' : `Install it (${hint})`}, or drop this target — the other ` +
|
|
206
850
|
`formats need no tools at all. \`gjsify ship --stage\` also works without ${missing.join('/')}: ` +
|
|
207
851
|
'the payload is assembled by this CLI and only the container needs them.');
|
|
208
852
|
}
|
|
209
|
-
/**
|
|
210
|
-
|
|
853
|
+
/**
|
|
854
|
+
* The shared half: names in, `FormatId`s out, with the two refusals that hold for
|
|
855
|
+
* every caller.
|
|
856
|
+
*/
|
|
857
|
+
function parseFormatNames(raw, source) {
|
|
211
858
|
const names = raw
|
|
212
859
|
.flatMap((entry) => entry.split(','))
|
|
213
860
|
.map((entry) => entry.trim())
|
|
@@ -217,14 +864,130 @@ export function resolveFormats(raw) {
|
|
|
217
864
|
// fired — the run then died somewhere unrelated.
|
|
218
865
|
const unknown = names.filter((name) => !Object.hasOwn(FORMATS, name));
|
|
219
866
|
if (unknown.length > 0) {
|
|
220
|
-
throw new Error(`gjsify ship: unknown target${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')}. ` +
|
|
867
|
+
throw new Error(`gjsify ship: unknown target${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')} in ${source}. ` +
|
|
221
868
|
`Known targets: ${FORMAT_IDS.join(', ')}.`);
|
|
222
869
|
}
|
|
223
870
|
const unique = [...new Set(names)].sort();
|
|
224
871
|
// An empty list would otherwise stage the payload and pack nothing, exit 0,
|
|
225
872
|
// and print no artifact line — a success that produced no artifact.
|
|
226
873
|
if (unique.length === 0) {
|
|
227
|
-
throw new Error(`gjsify ship:
|
|
874
|
+
throw new Error(`gjsify ship: ${source} named no format. Known targets: ${FORMAT_IDS.join(', ')}.`);
|
|
875
|
+
}
|
|
876
|
+
return unique;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Parse an explicit `--target deb,rpm` into a sorted, deduplicated descriptor list.
|
|
880
|
+
*
|
|
881
|
+
* A format belonging to another layout is an ERROR here, not a filter:
|
|
882
|
+
* `gjsify ship darwin --target deb` is a mistake worth naming, and dropping it
|
|
883
|
+
* silently would stage the darwin layout, pack nothing and exit 0 — the shape
|
|
884
|
+
* the empty-list refusal above already exists to prevent, arriving through a
|
|
885
|
+
* different door.
|
|
886
|
+
*
|
|
887
|
+
* The FLAG is what makes that the right answer, and it is the whole difference
|
|
888
|
+
* from {@link configuredFormats}: a value typed on this command line is a claim
|
|
889
|
+
* about THIS run.
|
|
890
|
+
*/
|
|
891
|
+
export function resolveFormats(raw, layout) {
|
|
892
|
+
const unique = parseFormatNames(raw, '--target');
|
|
893
|
+
const foreign = unique.filter((name) => FORMATS[name].layoutOs !== layout.os);
|
|
894
|
+
if (foreign.length > 0) {
|
|
895
|
+
const wrap = formatIdsFor(layout.os);
|
|
896
|
+
// Deduplicated: two formats wrapping the same layout read as
|
|
897
|
+
// "the linux/linux layout" without it.
|
|
898
|
+
const foreignLayouts = [...new Set(foreign.map((name) => FORMATS[name].layoutOs))];
|
|
899
|
+
throw new Error(`gjsify ship: ${foreign.join(', ')} ${foreign.length > 1 ? 'wrap' : 'wraps'} the ` +
|
|
900
|
+
`${foreignLayouts.join('/')} layout and this run assembles the ${layout.name} one. ` +
|
|
901
|
+
// The empty branch is unreachable from the three layouts that exist
|
|
902
|
+
// — all of them have formats as of #1354 M3 — and is kept for the
|
|
903
|
+
// day a fourth is added before a `FORMATS` row wraps it, which is
|
|
904
|
+
// the state windows was in between M1 and M3.
|
|
905
|
+
(wrap.length === 0
|
|
906
|
+
? `No format wraps the ${layout.name} layout — \`gjsify ship ${layout.name} --stage\` ` +
|
|
907
|
+
'assembles it and stops.'
|
|
908
|
+
: `Formats for this layout: ${wrap.join(', ')}.`) +
|
|
909
|
+
// One suggestion PER foreign layout. `foreign[0]`'s layout was
|
|
910
|
+
// being offered for all of them, two lines under the dedup that
|
|
911
|
+
// exists precisely because several are anticipated — so a mixed
|
|
912
|
+
// `--target` would have printed a command that fails the same way.
|
|
913
|
+
` To build ${foreign.join(', ')}, name the layout each wraps: ` +
|
|
914
|
+
`${foreignLayouts
|
|
915
|
+
.map((os) => `\`gjsify ship ${LAYOUT_NAME_BY_OS[os] ?? os} --target ` +
|
|
916
|
+
`${foreign.filter((name) => FORMATS[name].layoutOs === os).join(',')}\``)
|
|
917
|
+
.join(', ')}.`);
|
|
228
918
|
}
|
|
229
919
|
return unique.map((name) => FORMATS[name]);
|
|
230
920
|
}
|
|
921
|
+
/**
|
|
922
|
+
* The same names from `gjsify.ship.targets`, FILTERED to the current layout.
|
|
923
|
+
*
|
|
924
|
+
* A configured list is a project-level DEFAULT — "when I ship this, build these"
|
|
925
|
+
* — written once and read by every run, so it cannot be a claim about a layout
|
|
926
|
+
* the author had not heard of when they wrote it. Refusing it the way
|
|
927
|
+
* {@link resolveFormats} refuses a flag makes the new positional unusable in
|
|
928
|
+
* every project that has the key, and this repository is the proof: with
|
|
929
|
+
* `targets: ["deb", "rpm"]` in `packages/infra/cli/package.json`,
|
|
930
|
+
* `gjsify ship darwin --stage` exited 1 telling the author to run
|
|
931
|
+
* `gjsify ship darwin --stage`. There was no `--target` value that got a darwin
|
|
932
|
+
* stage out of such a project at all.
|
|
933
|
+
*
|
|
934
|
+
* Filtering is safe HERE and only here, because an empty result is not a silent
|
|
935
|
+
* success: `assemble` refuses a PACK with nothing to pack, and a `--stage` run
|
|
936
|
+
* legitimately wants exactly this — assemble the layout, wrap nothing.
|
|
937
|
+
*/
|
|
938
|
+
export function configuredFormats(raw, layout) {
|
|
939
|
+
const named = parseFormatNames(raw, '`gjsify.ship.targets`');
|
|
940
|
+
const kept = named.filter((name) => FORMATS[name].layoutOs === layout.os);
|
|
941
|
+
// The dropped names are RETURNED rather than swallowed. The `--target` path
|
|
942
|
+
// refuses by name; this one silently produced a shorter list, so a project
|
|
943
|
+
// whose configured formats all belong to another layout got a stage and no
|
|
944
|
+
// explanation of why nothing was packed.
|
|
945
|
+
return {
|
|
946
|
+
formats: kept.map((name) => FORMATS[name]),
|
|
947
|
+
dropped: named.filter((name) => FORMATS[name].layoutOs !== layout.os),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Can this format's container hold a notarisation ticket?
|
|
952
|
+
*
|
|
953
|
+
* A SWITCH WITH A `never` GUARD and not a field, deliberately — the difference
|
|
954
|
+
* from `FormatDescriptor.host` is who owns the fact. Where a format can be PACKED
|
|
955
|
+
* is a property of this project's toolchain and belongs on the descriptor; whether
|
|
956
|
+
* `stapler` will write a ticket into it is Apple's list, unchanged by anything
|
|
957
|
+
* here, and six of the nine rows are only "no" because notarisation is a macOS
|
|
958
|
+
* concept they have nothing to do with. Putting it in a `switch` keeps the answer
|
|
959
|
+
* beside its citation and still makes a tenth format a compile error.
|
|
960
|
+
*
|
|
961
|
+
* `stapler(1)`: *"stapler works only with UDIF disk images, signed \"flat\"
|
|
962
|
+
* installer packages, and certain code-signed executable bundles such as
|
|
963
|
+
* \".app\"."* And Apple's notarisation guide for the exception that decides the
|
|
964
|
+
* zip: *"While you can notarize a ZIP archive, you can't staple to it directly.
|
|
965
|
+
* Instead, run stapler against each item that you added to the archive. Then
|
|
966
|
+
* create a new ZIP file containing the stapled items for distribution."*
|
|
967
|
+
*
|
|
968
|
+
* `macos-app` is `true` here even though `notarytool` cannot SUBMIT a directory:
|
|
969
|
+
* the two questions are separate, and a `.app` is precisely the item Apple's
|
|
970
|
+
* remedy above tells you to staple after submitting the archive around it.
|
|
971
|
+
*/
|
|
972
|
+
export function canCarryTicket(id) {
|
|
973
|
+
switch (id) {
|
|
974
|
+
case 'macos-app':
|
|
975
|
+
case 'macos-app-dmg':
|
|
976
|
+
return true;
|
|
977
|
+
case 'macos-app-zip':
|
|
978
|
+
// Nothing outside darwin has a ticket to carry: `stapler` is Apple's tool
|
|
979
|
+
// and no other OS pre-clears an artifact at all (§ A5 — Windows SmartScreen
|
|
980
|
+
// accrues reputation per file hash instead).
|
|
981
|
+
case 'deb':
|
|
982
|
+
case 'rpm':
|
|
983
|
+
case 'flatpak':
|
|
984
|
+
case 'windows-dir':
|
|
985
|
+
case 'windows-dir-zip':
|
|
986
|
+
case 'msi':
|
|
987
|
+
return false;
|
|
988
|
+
default: {
|
|
989
|
+
const unhandled = id;
|
|
990
|
+
throw new Error(`gjsify ship: no ticket answer for format "${String(unhandled)}".`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|