@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.
Files changed (87) hide show
  1. package/dist/cli.gjs.mjs +453 -425
  2. package/lib/actions/build.js +2 -1
  3. package/lib/commands/affected-classify.js +7 -5
  4. package/lib/commands/build.js +6 -0
  5. package/lib/commands/flatpak/ci.js +10 -2
  6. package/lib/commands/flatpak/init.js +15 -16
  7. package/lib/commands/flatpak/utils.d.ts +0 -26
  8. package/lib/commands/flatpak/utils.js +4 -29
  9. package/lib/commands/gettext.d.ts +2 -1
  10. package/lib/commands/gettext.js +105 -47
  11. package/lib/commands/onboard.d.ts +7 -0
  12. package/lib/commands/onboard.js +349 -96
  13. package/lib/commands/ship.d.ts +3 -0
  14. package/lib/commands/ship.js +911 -70
  15. package/lib/commands/trust.d.ts +6 -0
  16. package/lib/commands/trust.js +1 -1
  17. package/lib/config.js +25 -0
  18. package/lib/types/cli-build-options.d.ts +11 -0
  19. package/lib/types/config-data.d.ts +137 -3
  20. package/lib/utils/flatpak-runtime.d.ts +41 -0
  21. package/lib/utils/flatpak-runtime.js +54 -0
  22. package/lib/utils/msgfmt-merge.d.ts +35 -0
  23. package/lib/utils/msgfmt-merge.js +102 -0
  24. package/lib/utils/npm-otp.d.ts +27 -2
  25. package/lib/utils/npm-otp.js +82 -7
  26. package/lib/utils/onboard-discovery.d.ts +68 -0
  27. package/lib/utils/onboard-discovery.js +178 -0
  28. package/lib/utils/onboard-probe.d.ts +118 -1
  29. package/lib/utils/onboard-probe.js +153 -4
  30. package/lib/utils/prompt-output.d.ts +12 -0
  31. package/lib/utils/prompt-output.js +54 -0
  32. package/lib/utils/prompt.js +22 -2
  33. package/lib/utils/publishable-packages.d.ts +1 -1
  34. package/lib/utils/publishable-packages.js +1 -0
  35. package/lib/utils/resolve-npm-package.d.ts +56 -0
  36. package/lib/utils/resolve-npm-package.js +69 -0
  37. package/lib/utils/ship/app-runtime.d.ts +214 -0
  38. package/lib/utils/ship/app-runtime.js +429 -0
  39. package/lib/utils/ship/depends.d.ts +54 -7
  40. package/lib/utils/ship/depends.js +120 -6
  41. package/lib/utils/ship/discover.js +18 -1
  42. package/lib/utils/ship/dmg.d.ts +71 -0
  43. package/lib/utils/ship/dmg.js +181 -0
  44. package/lib/utils/ship/flatpak-config.d.ts +66 -0
  45. package/lib/utils/ship/flatpak-config.js +132 -0
  46. package/lib/utils/ship/flatpak.d.ts +45 -0
  47. package/lib/utils/ship/flatpak.js +148 -0
  48. package/lib/utils/ship/formats.d.ts +116 -3
  49. package/lib/utils/ship/formats.js +869 -11
  50. package/lib/utils/ship/launcher.d.ts +13 -3
  51. package/lib/utils/ship/launcher.js +362 -17
  52. package/lib/utils/ship/layout.d.ts +268 -0
  53. package/lib/utils/ship/layout.js +351 -0
  54. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  55. package/lib/utils/ship/localize-metadata.js +126 -0
  56. package/lib/utils/ship/mime.js +1 -9
  57. package/lib/utils/ship/msi.d.ts +126 -0
  58. package/lib/utils/ship/msi.js +417 -0
  59. package/lib/utils/ship/node-runtime.d.ts +71 -0
  60. package/lib/utils/ship/node-runtime.js +122 -0
  61. package/lib/utils/ship/payload.d.ts +156 -5
  62. package/lib/utils/ship/payload.js +580 -14
  63. package/lib/utils/ship/plan.d.ts +21 -0
  64. package/lib/utils/ship/plan.js +20 -7
  65. package/lib/utils/ship/plist.d.ts +58 -0
  66. package/lib/utils/ship/plist.js +198 -0
  67. package/lib/utils/ship/rpm.js +9 -3
  68. package/lib/utils/ship/schemas.d.ts +61 -0
  69. package/lib/utils/ship/schemas.js +118 -0
  70. package/lib/utils/ship/scripts.js +7 -3
  71. package/lib/utils/ship/settings.d.ts +19 -1
  72. package/lib/utils/ship/settings.js +19 -1
  73. package/lib/utils/ship/share-dirs.d.ts +23 -0
  74. package/lib/utils/ship/share-dirs.js +46 -0
  75. package/lib/utils/ship/signing.d.ts +195 -0
  76. package/lib/utils/ship/signing.js +317 -0
  77. package/lib/utils/ship/stage-manifest.d.ts +50 -13
  78. package/lib/utils/ship/stage-manifest.js +137 -14
  79. package/lib/utils/ship/stage-writer.d.ts +48 -0
  80. package/lib/utils/ship/stage-writer.js +71 -1
  81. package/lib/utils/ship/types.d.ts +264 -16
  82. package/lib/utils/ship/xml.d.ts +7 -0
  83. package/lib/utils/ship/xml.js +25 -0
  84. package/lib/utils/ship/zip.d.ts +69 -0
  85. package/lib/utils/ship/zip.js +245 -0
  86. package/lib/utils/trust-registry.js +5 -0
  87. package/package.json +19 -19
@@ -0,0 +1,268 @@
1
+ import type { HostOs, StagedFile } from './types.js';
2
+ /**
3
+ * The positional `gjsify ship <os>` takes, in ADR 0024 § A2's spelling.
4
+ *
5
+ * NOT the same vocabulary as {@link HostOs}, and the difference is deliberate
6
+ * rather than sloppy: § A2 writes `gjsify ship windows --stage`, while the
7
+ * repo-wide target spelling is `${process.platform}-${process.arch}` and
8
+ * therefore `win32-x64` (root AGENTS.md § Runtime & platform model). Both are
9
+ * accepted on the command line and exactly one is ever RECORDED — the stage
10
+ * manifest's `target.os` is a `HostOs`, so `--expect-target win32-x64` keeps
11
+ * meaning what it meant.
12
+ */
13
+ export type LayoutName = 'linux' | 'darwin' | 'windows';
14
+ /** The layout-affecting half of `ShipSettings` — a display name and a binary name, nothing else. */
15
+ export interface LayoutIdentity {
16
+ /** Package name: the launcher's own filename. */
17
+ binaryName: string;
18
+ /** Human-readable display name — the `<App>.app` directory a user sees in Finder. */
19
+ name: string;
20
+ }
21
+ /**
22
+ * What a layout needs to render the metadata it OWNS.
23
+ *
24
+ * `LayoutIdentity` plus the three fields an `Info.plist` names the app by. It
25
+ * EXTENDS the identity rather than sitting beside it so `placeStage` keeps taking
26
+ * one argument and `ShipSettings` satisfies it structurally — no call site has to
27
+ * assemble a second object, and adding a field here is a compile error at exactly
28
+ * the rows that would have to answer for it.
29
+ *
30
+ * Everything here comes from the consumer's own `gjsify.ship` block. Nothing in
31
+ * this file is derived from gjsify's tree and nothing has a gjsify-specific
32
+ * default, which is the same line {@link LayoutDirs} draws for directories.
33
+ */
34
+ export interface LayoutMetadataInput extends LayoutIdentity {
35
+ /** Reverse-DNS application id — `CFBundleIdentifier`, and what the data tree is keyed on. */
36
+ appId: string;
37
+ /** Upstream version — `CFBundleShortVersionString`. */
38
+ version: string;
39
+ /** Packaging revision within one upstream version — the second half of `CFBundleVersion`. */
40
+ release: string;
41
+ }
42
+ /** The four places a payload file can belong, as stage-relative directories. */
43
+ export interface LayoutDirs {
44
+ /** Where the launcher lives. */
45
+ launcher: string;
46
+ /** Where the app's own bundle tree lives. */
47
+ bundle: string;
48
+ /** Where the typelibs and shared libraries the app CARRIES live. */
49
+ native: string;
50
+ /** Where the XDG-shaped data tree lives (icons, schemas, metainfo, locale). */
51
+ data: string;
52
+ /**
53
+ * Where a prefix-relative path that sorts into none of the above goes.
54
+ *
55
+ * Reachable only through `gjsify.ship.extraFiles`, whose destinations are
56
+ * documented as prefix-relative and may name anything. On macOS that has to
57
+ * land inside `Contents/`, or the file sits beside it where nothing in the
58
+ * bundle can address it and `codesign` later refuses the bundle.
59
+ */
60
+ other: string;
61
+ }
62
+ /**
63
+ * One operating system's layout.
64
+ *
65
+ * Data, not code: adding a fourth OS is a row here plus a launcher form, the same
66
+ * way adding a format is a row in `FORMATS`. The one function is {@link place},
67
+ * and it is shared by all three.
68
+ */
69
+ export interface Layout {
70
+ /** The positional that selects this layout. */
71
+ name: LayoutName;
72
+ /** The `process.platform` token recorded in the stage manifest's `target.os`. */
73
+ os: HostOs;
74
+ /**
75
+ * The runtime ADR 0024 § 4 DERIVES for a shipped artifact on this OS.
76
+ *
77
+ * Recorded, not obeyed: it is what #1354 M0's platform packages implement,
78
+ * and it is NOT what the staged launcher execs today. Linux takes GJS from
79
+ * the distribution and the emitted `Depends: gjs` backs it; both other OSes
80
+ * take Node, because Node + `@gjsify/node-gi` + `@gjsify/gtk-runtime-<os>-<arch>`
81
+ * is the only combination either OS's CI proves.
82
+ *
83
+ * Keeping it as DATA rather than as a launcher decision is the whole of what
84
+ * M1 may honestly claim. A layout with no packer produces no installable
85
+ * artifact, so writing `exec node` into a stage assembled from a `--app gjs`
86
+ * bundle would put a runtime that cannot read the payload in front of it —
87
+ * the exact defect `assertShippableTarget` exists to prevent, produced by the
88
+ * code meant to prevent it. Measured before this was data: `gjsify ship
89
+ * darwin --stage` on a project with no `gjsify.app` key exited 0 and staged
90
+ * `exec node "$contents/Resources/lib/gjs.js"` in front of a bundle whose
91
+ * first line is `import Gtk from 'gi://Gtk?version=4.0'`.
92
+ */
93
+ shippedRuntime: 'gjs' | 'node';
94
+ /**
95
+ * Why the staged launcher cannot name {@link shippedRuntime} yet, or absent
96
+ * when it already does.
97
+ *
98
+ * PRINTED at stage time rather than left as a comment: a tree whose launcher
99
+ * execs an interpreter the target OS may not have is a fact its author has to
100
+ * know, and it is the same fact that retires this field when M0 lands.
101
+ */
102
+ runtimeGap?: string;
103
+ /** Suffix the launcher's filename carries, `''` where the OS needs none. */
104
+ launcherExt: string;
105
+ /**
106
+ * The directory inside the STAGE that this layout hangs under, or `''` when
107
+ * the stage root IS the layout root.
108
+ *
109
+ * The asymmetry is the OS's, not ours. A `<App>.app` is the artifact — it is
110
+ * dragged to `/Applications` as one unit, so the stage has to carry the
111
+ * directory itself. A Windows program directory is NOT: the installer chooses
112
+ * `C:\Program Files\<Publisher>\<App>` and lays the stage's contents into it,
113
+ * so a wrapper directory here would become a doubled path there.
114
+ */
115
+ root: (identity: LayoutIdentity) => string;
116
+ /** The four destinations, resolved against one app's names. */
117
+ dirs: (identity: LayoutIdentity) => LayoutDirs;
118
+ /**
119
+ * Files this layout OWNS, already in stage-relative form — `[]` where it owns none.
120
+ *
121
+ * The seam {@link place} structurally cannot provide, and that is why it is a
122
+ * second function rather than another prefix rule. `Contents/Info.plist` has no
123
+ * prefix-relative counterpart to be mapped FROM: `planStage` emits one plan in
124
+ * the Linux/XDG shape, everything unmatched falls to `dirs.other`, and
125
+ * `assertInsidePrefix` forbids a plan entry from escaping upward — so no
126
+ * `gjsify.ship.extraFiles` value and no planner rule can put a file at the
127
+ * bundle root. The layout has to add it.
128
+ *
129
+ * REQUIRED on every row, not optional. An optional field is one a fourth OS can
130
+ * forget; a required one that returns `[]` is a row that has ANSWERED "this
131
+ * layout owns nothing", which is a different statement and the one worth being
132
+ * able to read. Linux and Windows both answer that today — a program directory
133
+ * and a `/usr` prefix have no manifest of their own — so the whole difference
134
+ * between "no metadata yet" and "no metadata ever" stays visible.
135
+ *
136
+ * The result is placed by `placeStage`, which applies the SAME uniqueness check
137
+ * it applies to mapped files: an `extraFiles` destination that lands on
138
+ * `Contents/Info.plist` is refused rather than silently replacing it.
139
+ */
140
+ metadata: (input: LayoutMetadataInput) => StagedFile[];
141
+ /**
142
+ * The `process.arch` values this layout can be assembled for, or `null` when
143
+ * it imposes no limit of its own.
144
+ *
145
+ * REQUIRED on every row, like {@link metadata}, and for the same reason: `null`
146
+ * is an ANSWER ("this layout constrains nothing"), while an optional field is
147
+ * one a fourth OS forgets and then silently inherits Linux's answer. Linux is
148
+ * the `null` row — a `.deb` exists for every architecture Debian has, and
149
+ * `DEBIAN_ARCH`/`RPM_ARCH` in `formats.ts` already refuse the ones this project
150
+ * has no name for.
151
+ *
152
+ * WHY IT IS ON THE LAYOUT AND NOT ONLY ON THE FORMAT, since
153
+ * {@link assertLayoutSupportsArch} runs in `packOne` — the same phase
154
+ * `FormatDescriptor.archName` does, and this field buys neither an earlier
155
+ * refusal nor a different one. It buys the two things a table lookup cannot:
156
+ *
157
+ * * a REASON. `archName` answers "no Windows architecture is known for
158
+ * `process.arch` \"arm64\"", which reads as a gap in our table. The truth is
159
+ * that gvsbuild publishes no arm64 GTK, the blocker is upstream, and #1117
160
+ * is where it is tracked — and the difference between "unsupported" and
161
+ * "here is what would have to change" is the whole value of the message.
162
+ * * a STAGE-TIME warning at the flag that caused it (`commands/ship.ts`), for
163
+ * a phase that deliberately does not refuse — `tests/e2e/ship-layout`
164
+ * assembles all three layouts from ONE payload on purpose, and that
165
+ * payload's native file has an architecture.
166
+ *
167
+ * The constraint is also a fact about the layout's RUNTIME story — which GTK
168
+ * closure and which interpreter exist for it — rather than about any one
169
+ * container, which is why the format rows read it back instead of restating it.
170
+ */
171
+ arches: LayoutArches | null;
172
+ }
173
+ /** The architectures one layout is assemblable for, and why the others are not. */
174
+ export interface LayoutArches {
175
+ /** `process.arch` spellings — the same vocabulary `--arch` takes. */
176
+ only: readonly string[];
177
+ /** What a reader must know to act on the refusal: who blocks it, and where it is tracked. */
178
+ why: string;
179
+ }
180
+ export declare const LAYOUTS: Record<LayoutName, Layout>;
181
+ /** Every layout name, derived from the table so a new row cannot be missed. */
182
+ export declare const LAYOUT_NAMES: LayoutName[];
183
+ /** Stage-relative path of the launcher this layout puts in front of the app. */
184
+ export declare function launcherPath(layout: Layout, identity: LayoutIdentity): string;
185
+ /**
186
+ * The layout for one `process.platform` token, refusing every alias.
187
+ *
188
+ * STRICT, and that is the difference from {@link resolveLayout}, which is for a
189
+ * word a human typed. This one is for the stage manifest's `target.os` — a
190
+ * cross-host WIRE FORMAT with exactly one legal spelling per OS. Accepting
191
+ * `"windows"` there and silently comparing it as `win32` would mean
192
+ * `--expect-target win32-x64` no longer compares against the file's content:
193
+ * two manifests with different bytes in that field would both match, and the
194
+ * flag's whole job is to catch a job that downloaded the wrong artifact.
195
+ */
196
+ export declare function layoutForOs(os: string): Layout;
197
+ /**
198
+ * Resolve the positional to a layout.
199
+ *
200
+ * Accepts BOTH vocabularies (`windows` and `win32`) for the reason
201
+ * {@link LayoutName} records: the ADR writes one and `--expect-target` prints the
202
+ * other, and refusing whichever the user has in front of them is a papercut with
203
+ * no upside. The canonical answer is a single `Layout`, so nothing downstream has
204
+ * two spellings to reconcile. NOT for the stage manifest — see {@link layoutForOs}.
205
+ */
206
+ export declare function resolveLayout(raw: string): Layout;
207
+ /**
208
+ * The layout for the host this process is running on.
209
+ *
210
+ * What `gjsify ship` picks when the positional is absent, so a bare `gjsify ship`
211
+ * on Linux keeps assembling the Linux layout and packing `.deb` + `.rpm` exactly
212
+ * as before. A host whose platform has no layout is refused BY NAME rather than
213
+ * silently treated as Linux — a payload laid out for `/usr` and labelled for a
214
+ * FreeBSD would install and not run.
215
+ */
216
+ export declare function hostLayout(platform?: string): Layout;
217
+ /**
218
+ * Refuse an architecture this layout cannot be assembled for, naming the blocker.
219
+ *
220
+ * AT STAGE TIME, which is the whole point — see {@link Layout.arches}. The
221
+ * message carries the row's own `why` rather than a generic "unsupported", because
222
+ * the two refusals a reader can act on are different jobs: `x64` is what a Windows
223
+ * artifact is built for TODAY, and `win32-arm64` is blocked in a repository we do
224
+ * not own. Telling an author "unknown architecture" for the second one would send
225
+ * them to look for a flag.
226
+ *
227
+ * `process.arch` spelling on both sides, so the value quoted back is the one the
228
+ * user typed after `--arch`.
229
+ */
230
+ export declare function assertLayoutSupportsArch(layout: Layout, arch: string): void;
231
+ /**
232
+ * Map one PREFIX-RELATIVE planned path to where this layout keeps it.
233
+ *
234
+ * The plan is written once, in the Linux/XDG shape, and this is the only place
235
+ * that knows any other. Order matters: `lib/<name>/gi/` is tested before
236
+ * `lib/<name>/`, because on macOS those two go to different directories.
237
+ */
238
+ export declare function place(layout: Layout, identity: LayoutIdentity, rel: string): string;
239
+ /**
240
+ * Apply a layout to a whole planned payload, and add what the layout itself owns.
241
+ *
242
+ * THREE SOURCES, one destination namespace. The mapped files come from
243
+ * `planStage`'s prefix-relative plan; {@link Layout.metadata} adds the files that
244
+ * have no prefix-relative counterpart at all (`Contents/Info.plist`); `carried`
245
+ * adds the runtime the artifact ships inside itself (#1354 M2b — the interpreter,
246
+ * the relocated GTK closure, the node-gi addon). All three go through the same
247
+ * uniqueness check, which is what makes the other two safe: an `extraFiles`
248
+ * destination that maps onto a metadata path is refused by name instead of
249
+ * silently replacing the file that makes the bundle a bundle.
250
+ *
251
+ * `carried` is already stage-relative and deliberately NOT mapped. A relocated
252
+ * closure's every internal relation is relative — `@loader_path/<leaf>` install
253
+ * names, `@loader_path/../../..` in `loaders.cache`, `@loader_path/gtk/lib` on the
254
+ * addon — so the tree survives the copy only if it is copied as a tree. Routing it
255
+ * through the prefix-relative plan would put it through
256
+ * `gjsify.ship.bundledTypelibs`'s flattening (`plan.ts`: `posix.join(libDir, 'gi',
257
+ * basename(file))`), which destroys all three at once.
258
+ *
259
+ * The uniqueness check is not decoration for the mapped half either. `planStage`
260
+ * deduplicates on the PREFIX-RELATIVE path — that is how `gjsify.ship.extraFiles`
261
+ * is documented to override a default — but the MAP can bring two distinct
262
+ * prefix-relative paths together: on Windows the launcher becomes
263
+ * `<binaryName>.cmd` at the program root, which an `extraFiles` destination can
264
+ * name directly. Without this the second one silently wins and which one that is
265
+ * depends on plan order — the "installs cleanly, does nothing" class this command
266
+ * is built against.
267
+ */
268
+ export declare function placeStage(layout: Layout, identity: LayoutMetadataInput, files: readonly StagedFile[], carried?: readonly StagedFile[]): StagedFile[];
@@ -0,0 +1,351 @@
1
+ // WHERE a payload's files land, per operating system (ADR 0024 § 2, § A2).
2
+ //
3
+ // The payload is produced ONCE and is identical everywhere; what differs is the
4
+ // layout. Up to here that difference was a single string — `FormatDescriptor.prefix`,
5
+ // `/usr` for deb and rpm, `/app` for a Flatpak — because every Linux format keeps
6
+ // the same `bin/`, `lib/<name>/`, `share/…` shape underneath it. `Contents/MacOS`
7
+ // is the first layout that is NOT a prefix substitution: the launcher, the bundle,
8
+ // the shared libraries and the data tree each move somewhere different, and on
9
+ // macOS the native files leave the bundle directory altogether for
10
+ // `Contents/Frameworks`.
11
+ //
12
+ // So the layout is a MAP from the prefix-relative plan to a stage-relative path,
13
+ // and `planStage` keeps producing the prefix-relative plan for all three. That
14
+ // split is what makes the claim checkable: one payload, three layouts, and the
15
+ // file SET has to be identical modulo this map (`tests/e2e/ship-layout`).
16
+ //
17
+ // It also draws the line a third-party app author needs. Everything structural
18
+ // here belongs to the OS — `Contents/MacOS` is Apple's, `%~dp0` is cmd.exe's.
19
+ // Everything with a NAME in it comes from the consumer's own `gjsify.ship` block:
20
+ // the `<App>.app` directory is `name`, the launcher inside it is `binaryName`, and
21
+ // the data tree is addressed by `appId`. Nothing here is derived from gjsify's own
22
+ // tree, and nothing here has a gjsify-specific default.
23
+ import { posix } from 'node:path';
24
+ import { BUNDLE_INFO_PLIST, BUNDLE_PKGINFO, renderInfoPlist, renderPkgInfo } from './plist.js';
25
+ /**
26
+ * Characters a `<App>.app` directory name may not contain.
27
+ *
28
+ * `/` is the POSIX separator and `:` is HFS+'s — the Finder shows a stored `:` as
29
+ * a `/` and vice versa, so a name carrying either produces a bundle whose
30
+ * directory is not the one its own metadata names. Refused here rather than at
31
+ * `hdiutil` time, which is three milestones away and on another host.
32
+ */
33
+ const BUNDLE_NAME_FORBIDDEN = /[/:\\]/;
34
+ function appBundleDir(identity) {
35
+ if (BUNDLE_NAME_FORBIDDEN.test(identity.name)) {
36
+ throw new Error(`gjsify ship: the macOS layout would put this app in "${identity.name}.app", and a bundle ` +
37
+ 'directory may contain no "/", "\\" or ":" — HFS+ and the Finder swap the first two, so the ' +
38
+ 'bundle would not be the one its own metadata names. Set `gjsify.ship.name` to the display ' +
39
+ 'name you want on disk.');
40
+ }
41
+ return `${identity.name}.app`;
42
+ }
43
+ export const LAYOUTS = {
44
+ linux: {
45
+ name: 'linux',
46
+ os: 'linux',
47
+ shippedRuntime: 'gjs',
48
+ launcherExt: '',
49
+ root: () => '',
50
+ // The app id in the directory name is what makes `/usr/lib/<pkg>/` a
51
+ // directory the PACKAGE owns — the prefix is shared with every other
52
+ // package on the system. Inside a `.app` or a program directory nothing
53
+ // is shared, which is why the other two rows drop it.
54
+ dirs: (identity) => ({
55
+ launcher: 'bin',
56
+ bundle: `lib/${identity.binaryName}`,
57
+ native: `lib/${identity.binaryName}/gi`,
58
+ data: 'share',
59
+ other: '',
60
+ }),
61
+ // A `/usr` prefix has no manifest of its own — everything a Linux package
62
+ // says about itself is in the `.deb`/`.rpm` header or in the freedesktop
63
+ // files the payload already carries.
64
+ metadata: () => [],
65
+ // Every architecture Debian and rpm have a name for, which is what
66
+ // `DEBIAN_ARCH`/`RPM_ARCH` already enumerate — the layout adds nothing.
67
+ arches: null,
68
+ },
69
+ darwin: {
70
+ name: 'darwin',
71
+ os: 'darwin',
72
+ shippedRuntime: 'node',
73
+ // GJS does exist on macOS — Homebrew ships it and the `macos` CI leg runs
74
+ // `test:gjs` on both arches — so a launcher naming it is true of a
75
+ // developer's machine and NOT of a `.app` a stranger downloads:
76
+ // `packages/node-gi/scripts/build-gtk-runtime-darwin.mjs` records that GJS
77
+ // ships no relocation, which is why § 4 derives Node here.
78
+ runtimeGap: 'the staged launcher execs an interpreter off `PATH`, which a downloaded `.app` cannot assume — ' +
79
+ 'macOS ships no Node, and there is no RELOCATABLE GJS (`build-gtk-runtime-darwin.mjs`: "GJS ' +
80
+ 'ships no relocation"), so ADR 0024 § 4 derives Node here. A self-contained bundle needs `@gjsify/node-runtime-darwin-<arch>` and a ' +
81
+ '`--app node` payload (#1354 M0), or `@gjsify/gjs-runtime-darwin-<arch>` (ADR 0024 stage 7).',
82
+ launcherExt: '',
83
+ root: appBundleDir,
84
+ // Apple's, all four. `Contents/MacOS` holds executables, `Contents/Resources`
85
+ // everything that is not, and `Contents/Frameworks` the dylibs — which is
86
+ // the split that makes this more than a prefix: the `gi/` directory that
87
+ // sits INSIDE `lib/<name>/` on Linux leaves the bundle directory entirely.
88
+ dirs: (identity) => {
89
+ const app = appBundleDir(identity);
90
+ return {
91
+ launcher: `${app}/Contents/MacOS`,
92
+ bundle: `${app}/Contents/Resources/lib`,
93
+ native: `${app}/Contents/Frameworks`,
94
+ data: `${app}/Contents/Resources/share`,
95
+ other: `${app}/Contents/Resources`,
96
+ };
97
+ },
98
+ // The two files that make the directory a BUNDLE. Without `Info.plist`
99
+ // nothing tells LaunchServices which file under `Contents/MacOS` to exec,
100
+ // and a `*.app` with no `Info.plist` is a folder with a suffix — which is
101
+ // exactly what M1 staged. `plist.ts` carries the per-key citations.
102
+ metadata: (input) => [
103
+ {
104
+ path: `${appBundleDir(input)}/${BUNDLE_INFO_PLIST}`,
105
+ mode: 0o644,
106
+ source: { kind: 'text', text: renderInfoPlist(input) },
107
+ },
108
+ {
109
+ path: `${appBundleDir(input)}/${BUNDLE_PKGINFO}`,
110
+ mode: 0o644,
111
+ source: { kind: 'text', text: renderPkgInfo() },
112
+ },
113
+ ],
114
+ // The two architectures macOS runs on, which is also exactly what
115
+ // `@gjsify/gtk-runtime-darwin-*` and `@gjsify/node-runtime-darwin-*` are
116
+ // published for. Nothing upstream blocks a third; there is no third.
117
+ arches: {
118
+ only: ['x64', 'arm64'],
119
+ why: 'macOS runs on x86_64 and Apple silicon and nothing else, so those are the two ' +
120
+ '`@gjsify/gtk-runtime-darwin-*` and `@gjsify/node-runtime-darwin-*` targets that exist',
121
+ },
122
+ },
123
+ windows: {
124
+ name: 'windows',
125
+ os: 'win32',
126
+ shippedRuntime: 'node',
127
+ // The stronger of the two gaps, and not a relocation question: there is
128
+ // no GJS host on Windows AT ALL, so nothing on that OS can run a
129
+ // `--app gjs` payload until M0's bundled Node exists and the payload is
130
+ // built for it. Cited to ADR 0024 § 4, which is where the fact is
131
+ // established — an earlier version of this string cited
132
+ // `docs/ci-selective.md`, which contains no occurrence of it and whose
133
+ // four `gjs` mentions are all about the affected-classifier bundle. The
134
+ // bad citation came in from the ADR's own § 4 table and was promoted into
135
+ // a user-visible string, which is the worst place for one.
136
+ runtimeGap: 'the staged launcher execs an interpreter off `PATH`, which a downloaded program directory ' +
137
+ 'cannot assume — Windows ships neither, and there is NO GJS host on Windows at all ' +
138
+ '(ADR 0024 § 4), so § 4 derives Node here. A self-contained directory needs ' +
139
+ '`@gjsify/node-runtime-win32-x64` and a `--app node` payload (#1354 M3).',
140
+ // `.cmd`, not `.bat`: the two differ in whether a failing built-in (`set`,
141
+ // `path`, `append`) sets ERRORLEVEL, and `.cmd` is the one where it does.
142
+ launcherExt: '.cmd',
143
+ root: () => '',
144
+ // A program directory has no imposed shape, so this one is chosen for the
145
+ // LOADER: `lib/` is what the launcher prepends to `PATH`, because Windows
146
+ // has no rpath — a DLL is found on `PATH`, in the directory of the image
147
+ // that loaded it, or not at all.
148
+ dirs: () => ({ launcher: '', bundle: 'app', native: 'lib', data: 'share', other: '' }),
149
+ // Nothing, and this is the row that shows the field is not a macOS detail
150
+ // wearing a general name: a Windows installer's metadata lives in the
151
+ // `.msi`'s own tables, not in a file inside the program directory. THREE
152
+ // format rows wrap this layout now, and the installer — the one that could
153
+ // have needed a file here — did not: `msi.ts` puts every such fact in the
154
+ // `Product`/`Package` attributes of the document it renders. So the answer
155
+ // held rather than the question going unasked, and if a fourth row ever
156
+ // needs a file, THIS is where it goes — not a branch in the stager.
157
+ metadata: () => [],
158
+ // ONE, and the blocker is a project we do not own. `wingtk/gvsbuild`
159
+ // hardcodes `self.platform = "x64"` in `utils/base_project.py` and its last
160
+ // five releases publish exactly two assets each, both x64 — so there is no
161
+ // arm64 GTK to build `@gjsify/gtk-runtime-win32-arm64` OUT OF, and on
162
+ // Windows that bundle is the only GTK there is. `@gjsify/node-gi` declares
163
+ // `win32-x64` only for the same reason. Tracked in #1117, which also
164
+ // records why an exploratory arm64 leg must NOT be added: its first step
165
+ // downloads a ZIP that does not exist, so it would be red by construction.
166
+ arches: {
167
+ only: ['x64'],
168
+ why: 'gvsbuild publishes no arm64 GTK (it hardcodes `self.platform = "x64"`), so there is nothing ' +
169
+ 'to build `@gjsify/gtk-runtime-win32-arm64` out of and no GTK for a Windows/ARM artifact to ' +
170
+ 'load — the blocker is upstream and is tracked in gjsify/gjsify#1117',
171
+ },
172
+ },
173
+ };
174
+ /** Every layout name, derived from the table so a new row cannot be missed. */
175
+ export const LAYOUT_NAMES = Object.keys(LAYOUTS);
176
+ /**
177
+ * `HostOs` → the layout for it.
178
+ *
179
+ * Derived rather than a second table: `LAYOUTS` is already total over
180
+ * `LayoutName`, so reading its rows back means a fourth OS cannot be added to one
181
+ * list and forgotten in the other.
182
+ */
183
+ const BY_OS = new Map(LAYOUT_NAMES.map((name) => [LAYOUTS[name].os, LAYOUTS[name]]));
184
+ /** Stage-relative path of the launcher this layout puts in front of the app. */
185
+ export function launcherPath(layout, identity) {
186
+ return posix.join(layout.dirs(identity).launcher, `${identity.binaryName}${layout.launcherExt}`);
187
+ }
188
+ /**
189
+ * The layout for one `process.platform` token, refusing every alias.
190
+ *
191
+ * STRICT, and that is the difference from {@link resolveLayout}, which is for a
192
+ * word a human typed. This one is for the stage manifest's `target.os` — a
193
+ * cross-host WIRE FORMAT with exactly one legal spelling per OS. Accepting
194
+ * `"windows"` there and silently comparing it as `win32` would mean
195
+ * `--expect-target win32-x64` no longer compares against the file's content:
196
+ * two manifests with different bytes in that field would both match, and the
197
+ * flag's whole job is to catch a job that downloaded the wrong artifact.
198
+ */
199
+ export function layoutForOs(os) {
200
+ const layout = BY_OS.get(os);
201
+ if (layout === undefined) {
202
+ throw new Error(`"${os}" is not a \`process.platform\` token this gjsify has a layout for. ` +
203
+ `Known: ${LAYOUT_NAMES.map((name) => LAYOUTS[name].os).join(', ')} — the ` +
204
+ '`${process.platform}-${process.arch}` spelling, so `win32` and never `windows`.');
205
+ }
206
+ return layout;
207
+ }
208
+ /**
209
+ * Resolve the positional to a layout.
210
+ *
211
+ * Accepts BOTH vocabularies (`windows` and `win32`) for the reason
212
+ * {@link LayoutName} records: the ADR writes one and `--expect-target` prints the
213
+ * other, and refusing whichever the user has in front of them is a papercut with
214
+ * no upside. The canonical answer is a single `Layout`, so nothing downstream has
215
+ * two spellings to reconcile. NOT for the stage manifest — see {@link layoutForOs}.
216
+ */
217
+ export function resolveLayout(raw) {
218
+ const name = raw.trim().toLowerCase();
219
+ if (Object.hasOwn(LAYOUTS, name))
220
+ return LAYOUTS[name];
221
+ const byOs = BY_OS.get(name);
222
+ if (byOs !== undefined)
223
+ return byOs;
224
+ throw new Error(`gjsify ship: "${raw}" is not an operating system this command has a layout for. ` +
225
+ `Known: ${LAYOUT_NAMES.join(', ')} (\`win32\` is accepted for \`windows\`, because that is the ` +
226
+ 'spelling `--expect-target <os>-<arch>` uses).');
227
+ }
228
+ /**
229
+ * The layout for the host this process is running on.
230
+ *
231
+ * What `gjsify ship` picks when the positional is absent, so a bare `gjsify ship`
232
+ * on Linux keeps assembling the Linux layout and packing `.deb` + `.rpm` exactly
233
+ * as before. A host whose platform has no layout is refused BY NAME rather than
234
+ * silently treated as Linux — a payload laid out for `/usr` and labelled for a
235
+ * FreeBSD would install and not run.
236
+ */
237
+ export function hostLayout(platform = process.platform) {
238
+ const layout = BY_OS.get(platform);
239
+ if (layout === undefined) {
240
+ throw new Error(`gjsify ship: this host is ${platform}, which has no layout. Name the one you want — ` +
241
+ `\`gjsify ship <${LAYOUT_NAMES.join('|')}> --stage\` — assembly is not host-bound ` +
242
+ '(ADR 0024 § A1).');
243
+ }
244
+ return layout;
245
+ }
246
+ /**
247
+ * Refuse an architecture this layout cannot be assembled for, naming the blocker.
248
+ *
249
+ * AT STAGE TIME, which is the whole point — see {@link Layout.arches}. The
250
+ * message carries the row's own `why` rather than a generic "unsupported", because
251
+ * the two refusals a reader can act on are different jobs: `x64` is what a Windows
252
+ * artifact is built for TODAY, and `win32-arm64` is blocked in a repository we do
253
+ * not own. Telling an author "unknown architecture" for the second one would send
254
+ * them to look for a flag.
255
+ *
256
+ * `process.arch` spelling on both sides, so the value quoted back is the one the
257
+ * user typed after `--arch`.
258
+ */
259
+ export function assertLayoutSupportsArch(layout, arch) {
260
+ const arches = layout.arches;
261
+ if (arches === null || arches.only.includes(arch))
262
+ return;
263
+ throw new Error(`gjsify ship: the ${layout.name} layout is not assemblable for \`--arch ${arch}\` — ` +
264
+ `${arches.only.length === 1 ? 'the only architecture it has' : 'the architectures it has'} ` +
265
+ `${arches.only.length === 1 ? 'is' : 'are'} ${arches.only.join(', ')}.\n` +
266
+ ` ${arches.why}.\n` +
267
+ ' `--arch` names the architecture the PAYLOAD was built for and cross-compiles nothing, so ' +
268
+ 'there is no\n' +
269
+ ' flag that makes this work — the artifact would carry a runtime that does not exist.');
270
+ }
271
+ /**
272
+ * Map one PREFIX-RELATIVE planned path to where this layout keeps it.
273
+ *
274
+ * The plan is written once, in the Linux/XDG shape, and this is the only place
275
+ * that knows any other. Order matters: `lib/<name>/gi/` is tested before
276
+ * `lib/<name>/`, because on macOS those two go to different directories.
277
+ */
278
+ export function place(layout, identity, rel) {
279
+ const dirs = layout.dirs(identity);
280
+ if (rel === `bin/${identity.binaryName}`)
281
+ return launcherPath(layout, identity);
282
+ const libPrefix = `lib/${identity.binaryName}/`;
283
+ const giPrefix = `${libPrefix}gi/`;
284
+ if (rel.startsWith(giPrefix))
285
+ return posix.join(dirs.native, rel.slice(giPrefix.length));
286
+ if (rel.startsWith(libPrefix))
287
+ return posix.join(dirs.bundle, rel.slice(libPrefix.length));
288
+ if (rel.startsWith('bin/'))
289
+ return posix.join(dirs.launcher, rel.slice('bin/'.length));
290
+ if (rel.startsWith('share/'))
291
+ return posix.join(dirs.data, rel.slice('share/'.length));
292
+ return posix.join(dirs.other, rel);
293
+ }
294
+ /**
295
+ * Apply a layout to a whole planned payload, and add what the layout itself owns.
296
+ *
297
+ * THREE SOURCES, one destination namespace. The mapped files come from
298
+ * `planStage`'s prefix-relative plan; {@link Layout.metadata} adds the files that
299
+ * have no prefix-relative counterpart at all (`Contents/Info.plist`); `carried`
300
+ * adds the runtime the artifact ships inside itself (#1354 M2b — the interpreter,
301
+ * the relocated GTK closure, the node-gi addon). All three go through the same
302
+ * uniqueness check, which is what makes the other two safe: an `extraFiles`
303
+ * destination that maps onto a metadata path is refused by name instead of
304
+ * silently replacing the file that makes the bundle a bundle.
305
+ *
306
+ * `carried` is already stage-relative and deliberately NOT mapped. A relocated
307
+ * closure's every internal relation is relative — `@loader_path/<leaf>` install
308
+ * names, `@loader_path/../../..` in `loaders.cache`, `@loader_path/gtk/lib` on the
309
+ * addon — so the tree survives the copy only if it is copied as a tree. Routing it
310
+ * through the prefix-relative plan would put it through
311
+ * `gjsify.ship.bundledTypelibs`'s flattening (`plan.ts`: `posix.join(libDir, 'gi',
312
+ * basename(file))`), which destroys all three at once.
313
+ *
314
+ * The uniqueness check is not decoration for the mapped half either. `planStage`
315
+ * deduplicates on the PREFIX-RELATIVE path — that is how `gjsify.ship.extraFiles`
316
+ * is documented to override a default — but the MAP can bring two distinct
317
+ * prefix-relative paths together: on Windows the launcher becomes
318
+ * `<binaryName>.cmd` at the program root, which an `extraFiles` destination can
319
+ * name directly. Without this the second one silently wins and which one that is
320
+ * depends on plan order — the "installs cleanly, does nothing" class this command
321
+ * is built against.
322
+ */
323
+ export function placeStage(layout, identity, files, carried = []) {
324
+ const byPlaced = new Map();
325
+ const out = [];
326
+ const claim = (path, from, file) => {
327
+ const previous = byPlaced.get(path);
328
+ if (previous !== undefined) {
329
+ throw new Error(`gjsify ship: in the ${layout.name} layout both ${previous} and ${from} install as ` +
330
+ `${path}. Two payload files cannot share one destination — one would silently replace the ` +
331
+ 'other. Rename the `gjsify.ship.extraFiles` destination that collides.');
332
+ }
333
+ byPlaced.set(path, from);
334
+ out.push({ ...file, path });
335
+ };
336
+ for (const file of files)
337
+ claim(place(layout, identity, file.path), file.path, file);
338
+ // The layout's own files are ALREADY stage-relative — they were never in the
339
+ // prefix-relative plan, so there is nothing to map. `<layout> metadata` rather
340
+ // than a path as the origin label, because a collision message naming
341
+ // `Contents/Info.plist` as both sides would say nothing.
342
+ for (const file of layout.metadata(identity))
343
+ claim(file.path, `the ${layout.name} layout's own metadata`, file);
344
+ // A THIRD source, and stage-relative like the second: the runtime the bundle
345
+ // CARRIES (`utils/ship/app-runtime.ts`). It goes through `claim` for the reason
346
+ // the metadata does — an `extraFiles` destination landing on a staged dylib
347
+ // would otherwise replace it silently, and the bundle would look complete.
348
+ for (const file of carried)
349
+ claim(file.path, `the runtime staged into the ${layout.name} artifact`, file);
350
+ return out.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
351
+ }
@@ -0,0 +1,22 @@
1
+ /** The generated metadata, before or after translation. */
2
+ export interface LocalizableMetadata {
3
+ /** The rendered AppStream MetaInfo XML. */
4
+ metainfo: string;
5
+ /** The rendered desktop entry — `undefined` for `kind: 'cli'`. */
6
+ desktopEntry?: string;
7
+ }
8
+ /** A staged catalogue, in the `<lang>/LC_MESSAGES/<domain>.mo` shape `discoverLocales` guarantees. */
9
+ export interface StagedCatalogue {
10
+ rel: string;
11
+ abs: string;
12
+ }
13
+ /**
14
+ * Translate the generated metadata with the catalogues the package ships.
15
+ *
16
+ * Returns the input unchanged when there is nothing to fold in, so a project
17
+ * without a `localeDir` never needs the gettext tools. A project WITH catalogues
18
+ * that lacks them is REFUSED rather than shipped in English: an untranslated
19
+ * `.desktop` is indistinguishable from a correct one at install time, and the
20
+ * package would have promised translations it does not deliver.
21
+ */
22
+ export declare function localizeMetadata(metadata: LocalizableMetadata, localeFiles: readonly StagedCatalogue[]): LocalizableMetadata;