@gjsify/cli 0.43.0 → 0.44.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.
@@ -1,7 +1,13 @@
1
- // The per-format table. Everything that differs between `.deb` and `.rpm`
2
- // outside the archive container itself lives here as DATA, so a new format is
3
- // a row rather than an edit across the stager, the overlay builder and the
4
- // artifact namer.
1
+ // The per-format table. Everything that differs between the formats outside the
2
+ // archive container itself lives here as DATA, so a new format is a row rather
3
+ // than an edit across the stager, the overlay builder and the artifact namer.
4
+ //
5
+ // Flatpak is the row that proves the claim: ADR 0024 § 2 predicted "the whole
6
+ // difference between a Flatpak and an `.rpm` is a four-line prefix map", and
7
+ // this file is where that turned out to be one `prefix: '/app'`, one arch table
8
+ // and one filename. What it also proved is what the design did NOT predict —
9
+ // a format can be host-bound (§ A3), so `host` is a descriptor field now.
10
+ import { isOnPath } from '../check-system-deps.js';
5
11
  // `process.arch` → the format's architecture name. Taken from dpkg's own
6
12
  // `data/cputable` and rpm's arch table. `arm` cannot be told apart from
7
13
  // `armel` by a running process, and Debian's hard-float port is what anything
@@ -32,16 +38,49 @@ function lookupArch(table, arch, label) {
32
38
  }
33
39
  return mapped;
34
40
  }
41
+ // Flatpak's own vocabulary, which is ostree's: `flatpak --supported-arches` on
42
+ // x86_64 answers `x86_64` then `i386`, and a ref is `app/<id>/<arch>/<branch>`.
43
+ // Deliberately short — a name this table does not know is refused rather than
44
+ // guessed, because the arch is not a label here, it is part of the ref the
45
+ // bundle is exported under and installed from.
46
+ const FLATPAK_ARCH = {
47
+ x64: 'x86_64',
48
+ arm64: 'aarch64',
49
+ ia32: 'i386',
50
+ arm: 'arm',
51
+ };
35
52
  function debArch(arch, archIndependent) {
36
53
  return archIndependent ? 'all' : lookupArch(DEBIAN_ARCH, arch, 'Debian');
37
54
  }
38
55
  function rpmArch(arch, archIndependent) {
39
56
  return archIndependent ? 'noarch' : lookupArch(RPM_ARCH, arch, 'RPM');
40
57
  }
58
+ /**
59
+ * Flatpak has no `noarch`, so `archIndependent` is ignored — and that is a
60
+ * measured difference, not an oversight.
61
+ *
62
+ * `all`/`noarch` exist because apt and dnf REFUSE a package whose architecture
63
+ * is not the machine's. Flatpak asks the opposite question: every ref names an
64
+ * arch, `flatpak install` resolves the one matching the host, and an app with no
65
+ * arch is not installable at all. So a payload of pure JavaScript still ships as
66
+ * `app/<id>/x86_64/stable` — which is also what Flathub does for every
67
+ * interpreted app it hosts.
68
+ */
69
+ function flatpakArch(arch, _archIndependent) {
70
+ return lookupArch(FLATPAK_ARCH, arch, 'Flatpak');
71
+ }
72
+ /** `.deb` and `.rpm` are written by this tree, so they exec nothing and read back with GNU tools. */
73
+ const WRITTEN_HERE = (readWith) => ({
74
+ finishOn: 'any',
75
+ requiredTools: [],
76
+ oracle: { readWith, readOn: ['linux'], selfReading: false },
77
+ });
41
78
  export const FORMATS = {
42
79
  deb: {
43
80
  id: 'deb',
44
81
  prefix: '/usr',
82
+ host: WRITTEN_HERE(['ar', 'tar', 'dpkg-deb', 'lintian']),
83
+ depends: 'deb',
45
84
  // Debian policy § 12.5: every package ships its copyright in
46
85
  // /usr/share/doc/<package>/copyright, and lintian errors without it.
47
86
  licenseDest: (binaryName) => `share/doc/${binaryName}/copyright`,
@@ -52,21 +91,121 @@ export const FORMATS = {
52
91
  rpm: {
53
92
  id: 'rpm',
54
93
  prefix: '/usr',
94
+ host: WRITTEN_HERE(['rpm', 'rpm2cpio', 'cpio']),
95
+ depends: 'rpm',
55
96
  licenseDest: (binaryName) => `share/licenses/${binaryName}/LICENSE`,
56
97
  licenseKind: 'plain',
57
98
  archName: rpmArch,
58
99
  fileName: (s, archLabel) => `${s.binaryName}-${s.version}-${s.release}.${archLabel}.rpm`,
59
100
  },
101
+ flatpak: {
102
+ id: 'flatpak',
103
+ // The one-line difference ADR 0024 § 2 promised. Nothing in the payload
104
+ // changes: the launcher derives its prefix at runtime (§ 3), so the same
105
+ // staged `bin/<name>` works under /usr and under /app.
106
+ prefix: '/app',
107
+ host: {
108
+ // A single-file bundle is an OSTree static delta, and the only writer
109
+ // of one is `flatpak build-bundle`. Unlike the `.dmg` (§ A6) that is
110
+ // not an OS restriction: flatpak runs on Linux, so the format is
111
+ // Linux-bound the way the app itself is.
112
+ finishOn: ['linux'],
113
+ requiredTools: ['flatpak-builder', 'flatpak'],
114
+ installHint: 'Fedora: `sudo dnf install flatpak flatpak-builder`, Debian/Ubuntu: ' +
115
+ '`sudo apt install flatpak flatpak-builder`',
116
+ oracle: {
117
+ // `flatpak build-import-bundle` into a FRESH repo, then
118
+ // `ostree ls -R`: ostree parses the delta this tree never wrote,
119
+ // and prints a path + mode + size per file. Measured on this
120
+ // workstation — that listing is what the e2e suite compares
121
+ // against the staged payload.
122
+ readWith: ['flatpak', 'ostree'],
123
+ readOn: ['linux'],
124
+ selfReading: false,
125
+ },
126
+ },
127
+ // A Flatpak's one dependency is its runtime, named in the manifest. See
128
+ // `DistroFormatId`: there is no `Depends:` field here to under-declare,
129
+ // which also means the unmapped-namespace refusal ADR 0024 § 6 built for
130
+ // deb and rpm has nothing to say about this format.
131
+ depends: null,
132
+ // No policy demands a location, so this follows rpm's — one fewer shape
133
+ // for a reader to learn, and `/app/share/licenses/<name>/LICENSE` is
134
+ // where the equivalent file sits in the `.rpm` built from the same stage.
135
+ licenseDest: (binaryName) => `share/licenses/${binaryName}/LICENSE`,
136
+ licenseKind: 'plain',
137
+ archName: flatpakArch,
138
+ // Named after the APP ID, not the binary: the id is the ref the file
139
+ // installs as, and it is what `flatpak install ./file.flatpak` prints.
140
+ fileName: (s, archLabel) => `${s.appId}-${s.version}-${s.release}.${archLabel}.flatpak`,
141
+ },
60
142
  };
61
143
  // DERIVED, never a second list. `FORMATS` is `Record<FormatId, …>`, so the
62
144
  // compiler already refuses a `FormatId` with no descriptor — reading the keys
63
145
  // back inherits that guarantee for free. Written out by hand this was the one
64
146
  // unbound copy of the vocabulary: adding a format to `FormatId` and `FORMATS`
65
147
  // compiled fine and left this list short, which would have made the new format
66
- // absent from the default targets (`ship.ts` uses it as the `??` fallback),
67
- // missing from `--help`, and REFUSED by `readStage` as an unknown id.
148
+ // missing from `--help` and REFUSED by `readStage` as an unknown id.
68
149
  // Insertion order is stable for string keys, and `resolveFormats` sorts anyway.
69
150
  export const FORMAT_IDS = Object.keys(FORMATS);
151
+ /**
152
+ * What `gjsify ship` builds when nothing said otherwise — every format that
153
+ * needs no tool and no particular host.
154
+ *
155
+ * A SECOND derivation from the same table, and it has to be one: `FORMAT_IDS`
156
+ * used to be both "every format that exists" and "every format to build by
157
+ * default", which was the same list only while every format was `finishOn:
158
+ * 'any'`. Adding Flatpak to `FORMAT_IDS` alone would have made a bare
159
+ * `gjsify ship` demand `flatpak-builder` of every project that ever packaged a
160
+ * `.deb` — and on a host without it, of every `release-cut.yml` run, which
161
+ * packs `@gjsify/cli` itself on a bare ubuntu runner. A host-bound format is
162
+ * opt-in through `--target` or `gjsify.ship.targets`; `--help` still lists all
163
+ * of them.
164
+ */
165
+ export const DEFAULT_FORMAT_IDS = FORMAT_IDS.filter((id) => FORMATS[id].host.finishOn === 'any');
166
+ /**
167
+ * Refuse a format this host cannot finish, BEFORE anything is built or written.
168
+ *
169
+ * `host` is injected so both branches are unit-testable from any machine — the
170
+ * same reason `resolvePrebuildDirName` and `checkTypeSkew`'s readers are pure.
171
+ * The message names the two-phase split rather than just saying no: the answer
172
+ * to "I am on macOS and want a Flatpak" is `--stage` here and `--from-stage` on
173
+ * a Linux runner, and a refusal that does not say so reads as "unsupported".
174
+ */
175
+ export function assertHostCanFinish(format, host = process.platform) {
176
+ const { finishOn } = format.host;
177
+ if (finishOn === 'any' || finishOn.includes(host))
178
+ return;
179
+ throw new Error(`gjsify ship: a ${format.id} artifact is packed on ${finishOn.join(' or ')} and ` +
180
+ `this host is ${host} (ADR 0024 § A1: a container is produced where its format's tool lives). ` +
181
+ 'Assembly is not host-bound, so the way across is the two-phase split: run ' +
182
+ '`gjsify ship --stage` here, move `ship/stage/` to a ' +
183
+ `${finishOn.join('/')} runner, and finish with ` +
184
+ `\`gjsify ship --from-stage <dir> --target ${format.id}\`.`);
185
+ }
186
+ /**
187
+ * Refuse a format whose tools are not installed, with the tool named.
188
+ *
189
+ * Separate from {@link assertHostCanFinish} because the two failures have
190
+ * different fixes — one needs a different machine, the other needs a package —
191
+ * and separate from the packer's own `notFound` handler because it fires BEFORE
192
+ * a build: `gjsify ship` runs the project's `build` script first, and finding
193
+ * out afterwards that `flatpak-builder` is absent costs the whole build.
194
+ */
195
+ export function assertToolsInstalled(format, present = isOnPath) {
196
+ const missing = format.host.requiredTools.filter((tool) => !present(tool));
197
+ if (missing.length === 0)
198
+ return;
199
+ // The install instruction is the DESCRIPTOR's, not this function's: hardcoded
200
+ // here it was `dnf install flatpak flatpak-builder` for every format that
201
+ // will ever need a tool, which is the branch this table exists to avoid.
202
+ const hint = format.host.installHint;
203
+ throw new Error(`gjsify ship: packing a ${format.id} needs ${format.host.requiredTools.join(' and ')}, and ` +
204
+ `${missing.join(', ')} ${missing.length > 1 ? 'are' : 'is'} not on PATH. ` +
205
+ `${hint === undefined ? 'Install it' : `Install it (${hint})`}, or drop this target — the other ` +
206
+ `formats need no tools at all. \`gjsify ship --stage\` also works without ${missing.join('/')}: ` +
207
+ 'the payload is assembled by this CLI and only the container needs them.');
208
+ }
70
209
  /** Parse `--target deb,rpm` into a sorted, deduplicated descriptor list. */
71
210
  export function resolveFormats(raw) {
72
211
  const names = raw
@@ -12,6 +12,7 @@
12
12
  import { validateMimeTypes } from './mime.js';
13
13
  import { basename } from 'node:path';
14
14
  import { DEFAULT_GJS_FLOOR } from './depends.js';
15
+ import { resolveShipFlatpakSettings } from './flatpak-config.js';
15
16
  import { assertRelease, normaliseVersion } from './version.js';
16
17
  /** Freedesktop main category → deb `Section:` / rpm `Group:`. First match wins. */
17
18
  const CATEGORY_SECTIONS = [
@@ -71,6 +72,11 @@ export function resolveShipSettings(input) {
71
72
  warnings.push(`gjsify ship: no icon found — the installed app will show a placeholder in menus and app stores. ` +
72
73
  'Point `gjsify.ship.icon` at an SVG (or a directory of sized PNGs).');
73
74
  }
75
+ // Resolved here rather than in the packer, because the packer may run on a
76
+ // host with no `package.json` at all (ADR 0024 § A2) — and because the
77
+ // finish-args default depends on `kind`, which is a project fact.
78
+ const flatpakSettings = resolveShipFlatpakSettings({ ship, flatpak, kind });
79
+ warnings.push(...flatpakSettings.warnings);
74
80
  const settings = {
75
81
  projectDir: input.projectDir,
76
82
  appId,
@@ -101,6 +107,7 @@ export function resolveShipSettings(input) {
101
107
  outDir: input.cli.outDir ?? ship.outDir ?? 'ship',
102
108
  arch: input.cli.arch ?? process.arch,
103
109
  minGjsVersion: ship.minGjsVersion ?? DEFAULT_GJS_FLOOR,
110
+ flatpak: flatpakSettings.settings,
104
111
  };
105
112
  return { settings, metadata, warnings };
106
113
  }
@@ -10,8 +10,14 @@ export declare const STAGE_MANIFEST_FILE = ".gjsify-ship-stage.json";
10
10
  * Bump it whenever a field's MEANING changes, not when one is added that an
11
11
  * older reader would simply ignore; a reader that ignores a field it does not
12
12
  * know is exactly how a mode plan silently loses an entry.
13
+ *
14
+ * 2 added `settings.appId` and `settings.flatpak` for the Flatpak target. That
15
+ * is the second case and not the first: an older reader ignoring
16
+ * `settings.flatpak` would not skip a field, it would pack a Flatpak against
17
+ * whatever runtime IT defaults to — a different `org.gnome.Platform` version
18
+ * than the project asked for, at exit 0.
13
19
  */
14
- export declare const STAGE_SCHEMA_VERSION = 1;
20
+ export declare const STAGE_SCHEMA_VERSION = 2;
15
21
  /**
16
22
  * The OS whose layout `planStage` produces.
17
23
  *
@@ -72,7 +78,10 @@ export interface StageManifestInput {
72
78
  * `extraFiles` and `licenseFile` — seven absolute paths from the assembling
73
79
  * host — into a file whose whole purpose is to be read somewhere else. The
74
80
  * explicit list is also what makes adding a field a decision: see the comment
75
- * on {@link PackSettings} for what belongs on each side.
81
+ * on {@link PackSettings} for what belongs on each side. `appId` and `flatpak`
82
+ * were that decision for the Flatpak target, and the `--from-stage` e2e is
83
+ * where an omission would have shown: it deletes the project, so a field the
84
+ * packer needs and the manifest lacks has nowhere to be read from.
76
85
  */
77
86
  export declare function toPackSettings(settings: ShipSettings): PackSettings;
78
87
  /** Absolute path of a stage's manifest. */
@@ -49,8 +49,14 @@ export const STAGE_MANIFEST_FILE = '.gjsify-ship-stage.json';
49
49
  * Bump it whenever a field's MEANING changes, not when one is added that an
50
50
  * older reader would simply ignore; a reader that ignores a field it does not
51
51
  * know is exactly how a mode plan silently loses an entry.
52
+ *
53
+ * 2 added `settings.appId` and `settings.flatpak` for the Flatpak target. That
54
+ * is the second case and not the first: an older reader ignoring
55
+ * `settings.flatpak` would not skip a field, it would pack a Flatpak against
56
+ * whatever runtime IT defaults to — a different `org.gnome.Platform` version
57
+ * than the project asked for, at exit 0.
52
58
  */
53
- export const STAGE_SCHEMA_VERSION = 1;
59
+ export const STAGE_SCHEMA_VERSION = 2;
54
60
  /**
55
61
  * The OS whose layout `planStage` produces.
56
62
  *
@@ -69,11 +75,15 @@ export const STAGE_LAYOUT_OS = 'linux';
69
75
  * `extraFiles` and `licenseFile` — seven absolute paths from the assembling
70
76
  * host — into a file whose whole purpose is to be read somewhere else. The
71
77
  * explicit list is also what makes adding a field a decision: see the comment
72
- * on {@link PackSettings} for what belongs on each side.
78
+ * on {@link PackSettings} for what belongs on each side. `appId` and `flatpak`
79
+ * were that decision for the Flatpak target, and the `--from-stage` e2e is
80
+ * where an omission would have shown: it deletes the project, so a field the
81
+ * packer needs and the manifest lacks has nowhere to be read from.
73
82
  */
74
83
  export function toPackSettings(settings) {
75
84
  return {
76
85
  binaryName: settings.binaryName,
86
+ appId: settings.appId,
77
87
  version: settings.version,
78
88
  release: settings.release,
79
89
  maintainer: settings.maintainer,
@@ -86,6 +96,7 @@ export function toPackSettings(settings) {
86
96
  extraDepends: settings.extraDepends,
87
97
  typelibPackages: settings.typelibPackages,
88
98
  minGjsVersion: settings.minGjsVersion,
99
+ flatpak: settings.flatpak,
89
100
  };
90
101
  }
91
102
  /** Absolute path of a stage's manifest. */
@@ -355,6 +366,7 @@ function readPackSettings(data, at) {
355
366
  const typelibPackages = record(data.typelibPackages, field('typelibPackages'));
356
367
  return {
357
368
  binaryName: expectString(data.binaryName, field('binaryName')),
369
+ appId: expectString(data.appId, field('appId')),
358
370
  version: expectString(data.version, field('version')),
359
371
  release: expectString(data.release, field('release')),
360
372
  maintainer: expectString(data.maintainer, field('maintainer')),
@@ -379,6 +391,31 @@ function readPackSettings(data, at) {
379
391
  ];
380
392
  })),
381
393
  minGjsVersion: expectString(data.minGjsVersion, field('minGjsVersion')),
394
+ flatpak: readFlatpakSettings(record(data.flatpak, field('flatpak')), field('flatpak')),
395
+ };
396
+ }
397
+ /**
398
+ * The Flatpak half, validated the same way as everything else here.
399
+ *
400
+ * Every field is required and none is defaulted, deliberately: a `?? []` on
401
+ * `finishArgs` would turn a mutilated manifest into an app with no display
402
+ * socket — it installs, it starts, and it draws nothing. The defaults belong on
403
+ * the assembling host, where `gjsify.ship` and `kind` are in reach; by the time
404
+ * a manifest exists the answer has already been decided and writing it down
405
+ * twice is how the two hosts disagree.
406
+ */
407
+ function readFlatpakSettings(data, at) {
408
+ const field = (name) => `${at}.${name}`;
409
+ const strings = (value, name) => expectArray(value, field(name)).map((entry, index) => expectString(entry, field(`${name}[${index}]`)));
410
+ return {
411
+ runtime: expectString(data.runtime, field('runtime')),
412
+ runtimeVersion: expectString(data.runtimeVersion, field('runtimeVersion')),
413
+ sdk: expectString(data.sdk, field('sdk')),
414
+ branch: expectString(data.branch, field('branch')),
415
+ sdkExtensions: strings(data.sdkExtensions, 'sdkExtensions'),
416
+ appendPath: strings(data.appendPath, 'appendPath'),
417
+ finishArgs: strings(data.finishArgs, 'finishArgs'),
418
+ cleanup: strings(data.cleanup, 'cleanup'),
382
419
  };
383
420
  }
384
421
  function record(value, at) {
@@ -36,7 +36,71 @@ export interface ShipArtifact {
36
36
  /** Size in bytes. */
37
37
  size: number;
38
38
  }
39
- export type FormatId = 'deb' | 'rpm';
39
+ export type FormatId = 'deb' | 'rpm' | 'flatpak';
40
+ /**
41
+ * The formats whose ARTIFACT carries a distro dependency list.
42
+ *
43
+ * Not every `FormatId`, and this is where the difference is written down once
44
+ * instead of being rediscovered per call site. A `.deb` says
45
+ * `Depends: gir1.2-gtk-4.0`, an `.rpm` says `Requires: gtk4`, and both are
46
+ * package names in a distribution's namespace. A Flatpak has no such field: it
47
+ * declares ONE dependency, its runtime (`org.gnome.Platform//50`), in the
48
+ * manifest — and inside that runtime a typelib is either present or nothing on
49
+ * the system provides it, so there is no package to name and no gap a name
50
+ * could close.
51
+ *
52
+ * `Extract<…>` rather than a fresh literal union, so this cannot outlive
53
+ * `FormatId`: dropping a member there shrinks this and reds every table keyed on
54
+ * it. The other direction is covered by {@link FormatDescriptor.depends} being
55
+ * REQUIRED — a new format cannot be added without saying which side it is on.
56
+ */
57
+ export type DistroFormatId = Extract<FormatId, 'deb' | 'rpm'>;
58
+ /** An operating system in the repo-wide `process.platform` spelling (root AGENTS.md § Runtime & platform model). */
59
+ export type HostOs = 'linux' | 'darwin' | 'win32';
60
+ /**
61
+ * Where a format can be FINISHED, and who can read the result back (ADR 0024 § A3).
62
+ *
63
+ * Three fields rather than one `hostOs`, because the three questions have
64
+ * different answers. `finishOn` is not the LAYOUT — a `<App>.app` tree is
65
+ * assembled anywhere while the `.dmg` around it needs macOS. `requiredTools` is
66
+ * not implied by `finishOn` — `.deb` and `.rpm` are written by this tree and
67
+ * exec nothing, which is the whole reason `rpm` could become an independent
68
+ * oracle and catch the dpkg-`$1` defect in the first artifact. And `oracle` is
69
+ * derivable from neither: a format built by the platform's own tool forfeits
70
+ * that independence unless a reader from a DIFFERENT implementation family
71
+ * exists, which is exactly what `selfReading` is the honest confession of.
72
+ */
73
+ export interface HostRequirement {
74
+ /** OSes whose tooling can finish this format. `'any'` = pure JS, under GJS, offline. */
75
+ finishOn: 'any' | readonly HostOs[];
76
+ /** Commands the packer EXECS. Empty iff this tree writes the format itself. */
77
+ requiredTools: readonly string[];
78
+ /**
79
+ * How to install {@link requiredTools}, in this format's own words.
80
+ *
81
+ * DATA, because the refusal that names it is one generic function. The
82
+ * first cut hardcoded `sudo dnf install flatpak flatpak-builder` inside
83
+ * `assertToolsInstalled`, which is correct for exactly one format and would
84
+ * have told the first `.dmg` or `.msi` user to install flatpak — from the
85
+ * same branchless dispatch this whole table exists to keep branchless.
86
+ * Required whenever `requiredTools` is non-empty; `flatpak.spec.ts` holds
87
+ * that, the same way it holds `selfReading`.
88
+ */
89
+ installHint?: string;
90
+ oracle: {
91
+ /** Readers from a DIFFERENT implementation family than the packer. */
92
+ readWith: readonly string[];
93
+ /** Where each reader runs. A Linux-runnable reader is worth more — every CI leg has one. */
94
+ readOn: readonly HostOs[];
95
+ /**
96
+ * No independent discriminator yet. Legal to DECLARE while a format is
97
+ * being built; illegal to release — `flatpak.spec.ts` is what turns that
98
+ * sentence into a red test, so flipping it is a decision somebody makes
99
+ * rather than a field nobody reads.
100
+ */
101
+ selfReading: boolean;
102
+ };
103
+ }
40
104
  /**
41
105
  * Everything that is per-FORMAT, as data.
42
106
  *
@@ -48,6 +112,13 @@ export interface FormatDescriptor {
48
112
  id: FormatId;
49
113
  /** Install prefix the format's contents hang under. */
50
114
  prefix: string;
115
+ /** Where this format can be packed, what it execs, and who reads it back. */
116
+ host: HostRequirement;
117
+ /**
118
+ * The key this format's dependency list is spelled under, or `null` when its
119
+ * dependencies are not a package list at all — see {@link DistroFormatId}.
120
+ */
121
+ depends: DistroFormatId | null;
51
122
  /** Where the project's licence text goes in this format's overlay. */
52
123
  licenseDest: (binaryName: string) => string;
53
124
  /**
@@ -109,8 +180,12 @@ export interface ShipMimeType {
109
180
  * `share/locale/<rel>`, and no packer reads the field. So translations
110
181
  * cross in the payload, where they belong, and the `abs` path does not
111
182
  * cross at all. This list being explicit is what caught that.
112
- * - `appId`, `name`, `kind`, `execArgs` — read by the planner and the metadata
113
- * renderers, all of which have run by the time a stage exists.
183
+ * - `name`, `kind`, `execArgs` — read by the planner and the metadata
184
+ * renderers, all of which have run by the time a stage exists. `appId` was
185
+ * on this line and MOVED: the Flatpak packer needs it at pack time twice
186
+ * over — it is the manifest's `id` and the ref `flatpak build-bundle`
187
+ * exports — and neither is derivable from the payload, because the desktop
188
+ * entry that carries the id is only staged for `kind: 'app'`.
114
189
  * - `arch` — carried by the manifest's `target`, which is also the value a
115
190
  * mismatched stage is refused on, so it lives in exactly one place.
116
191
  * - `mimeTypes` — phase 1 renders the shared-mime-info document into the
@@ -121,6 +196,8 @@ export interface ShipMimeType {
121
196
  export interface PackSettings {
122
197
  /** Package name: the `bin/` entry, the deb `Package:`, the rpm `Name:`. */
123
198
  binaryName: string;
199
+ /** Reverse-DNS application id — the Flatpak manifest's `id` and the exported ref. */
200
+ appId: string;
124
201
  /** Upstream version, normalised for package managers. */
125
202
  version: string;
126
203
  /** Package revision within one upstream version (deb revision / rpm release). */
@@ -139,24 +216,50 @@ export interface PackSettings {
139
216
  /** rpm `Group:`. */
140
217
  group: string;
141
218
  /** Extra runtime dependencies per format, appended to the derived set. */
142
- extraDepends: {
143
- deb: string[];
144
- rpm: string[];
145
- };
219
+ extraDepends: Record<DistroFormatId, string[]>;
146
220
  /** Project-supplied GI-namespace → package rows, filling gaps in the built-in table. */
147
- typelibPackages: Record<string, {
148
- deb: string;
149
- rpm: string;
150
- }>;
221
+ typelibPackages: Record<string, Record<DistroFormatId, string>>;
151
222
  /** Minimum GJS the emitted dependency asks for. */
152
223
  minGjsVersion: string;
224
+ /** The Flatpak manifest's non-payload half, fully defaulted at stage time. */
225
+ flatpak: ShipFlatpakSettings;
226
+ }
227
+ /**
228
+ * What the generated Flatpak manifest says about everything that is not the payload.
229
+ *
230
+ * Resolved — defaults applied — while the project is still in reach, because
231
+ * the defaults depend on `kind` and on `gjsify.flatpak`/`gjsify.ship.flatpak`,
232
+ * and the finishing host has neither.
233
+ *
234
+ * There is deliberately no `modules` / `extraModules` here. Under `ship` the
235
+ * module list IS the staged payload (ADR 0024 § 2: "a target that needs an
236
+ * extra file gets an overlay, never a branch in the staging code"), and an
237
+ * escape hatch that injects arbitrary build modules would put a second staging
238
+ * model back in the tree — which is the one thing § 8 gates the whole migration
239
+ * on. A project that genuinely needs to BUILD something inside the sandbox
240
+ * still has `gjsify flatpak init` + `gjsify flatpak build`, unchanged.
241
+ */
242
+ export interface ShipFlatpakSettings {
243
+ /** Runtime id, e.g. `org.gnome.Platform`. */
244
+ runtime: string;
245
+ /** Runtime/SDK version, e.g. `50`. */
246
+ runtimeVersion: string;
247
+ /** SDK id, e.g. `org.gnome.Sdk`. */
248
+ sdk: string;
249
+ /** The branch the app is exported under — the last segment of `app/<id>/<arch>/<branch>`. */
250
+ branch: string;
251
+ sdkExtensions: string[];
252
+ /** Path components prepended to `PATH` inside the build sandbox. */
253
+ appendPath: string[];
254
+ /** Capabilities the finished app is granted. */
255
+ finishArgs: string[];
256
+ /** Cleanup globs applied to `/app` after the module has run. */
257
+ cleanup: string[];
153
258
  }
154
259
  /** The resolved, fully defaulted configuration for one `gjsify ship` run. */
155
260
  export interface ShipSettings extends PackSettings {
156
261
  /** Absolute path to the project being shipped. */
157
262
  projectDir: string;
158
- /** Reverse-DNS application id — desktop entry, metainfo and icon basename. */
159
- appId: string;
160
263
  /** Human-readable display name. */
161
264
  name: string;
162
265
  /** Absolute path to the licence file, when one was found. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -142,21 +142,21 @@
142
142
  "cli"
143
143
  ],
144
144
  "dependencies": {
145
- "@gjsify/buffer": "^0.43.0",
146
- "@gjsify/create-app": "^0.43.0",
147
- "@gjsify/node-globals": "^0.43.0",
148
- "@gjsify/node-polyfills": "^0.43.0",
149
- "@gjsify/npm-registry": "^0.43.0",
150
- "@gjsify/resolve-npm": "^0.43.0",
151
- "@gjsify/rolldown-plugin-gjsify": "^0.43.0",
152
- "@gjsify/vite-plugin-blueprint": "^0.43.0",
153
- "@gjsify/rolldown-plugin-pnp": "^0.43.0",
154
- "@gjsify/semver": "^0.43.0",
155
- "@gjsify/tar": "^0.43.0",
156
- "@gjsify/tsc": "^0.43.0",
157
- "@gjsify/utils": "^0.43.0",
158
- "@gjsify/web-polyfills": "^0.43.0",
159
- "@gjsify/workspace": "^0.43.0",
145
+ "@gjsify/buffer": "^0.44.0",
146
+ "@gjsify/create-app": "^0.44.0",
147
+ "@gjsify/node-globals": "^0.44.0",
148
+ "@gjsify/node-polyfills": "^0.44.0",
149
+ "@gjsify/npm-registry": "^0.44.0",
150
+ "@gjsify/resolve-npm": "^0.44.0",
151
+ "@gjsify/rolldown-plugin-gjsify": "^0.44.0",
152
+ "@gjsify/vite-plugin-blueprint": "^0.44.0",
153
+ "@gjsify/rolldown-plugin-pnp": "^0.44.0",
154
+ "@gjsify/semver": "^0.44.0",
155
+ "@gjsify/tar": "^0.44.0",
156
+ "@gjsify/tsc": "^0.44.0",
157
+ "@gjsify/utils": "^0.44.0",
158
+ "@gjsify/web-polyfills": "^0.44.0",
159
+ "@gjsify/workspace": "^0.44.0",
160
160
  "acorn": "^8.17.0",
161
161
  "cosmiconfig": "^9.0.2",
162
162
  "get-tsconfig": "^4.14.0",
@@ -165,13 +165,13 @@
165
165
  "yargs": "^18.0.0"
166
166
  },
167
167
  "devDependencies": {
168
- "@gjsify/unit": "^0.43.0",
168
+ "@gjsify/unit": "^0.44.0",
169
169
  "@types/yargs": "^17.0.35",
170
170
  "typescript": "^6.0.3"
171
171
  },
172
172
  "peerDependencies": {
173
- "@gjsify/oxfmt-native": "^0.43.0",
174
- "@gjsify/rolldown-native": "^0.43.0"
173
+ "@gjsify/oxfmt-native": "^0.44.0",
174
+ "@gjsify/rolldown-native": "^0.44.0"
175
175
  },
176
176
  "peerDependenciesMeta": {
177
177
  "@gjsify/oxfmt-native": {