@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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Default GNOME-Platform runtime version. Bumped per release window.
3
+ * GNOME 50 = April 2026 stable; tracked in
4
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
5
+ */
6
+ export declare const DEFAULT_GNOME_RUNTIME_VERSION = "50";
7
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
8
+ export declare const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = "24.08";
9
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
10
+ export declare const DEFAULT_GUI_FINISH_ARGS: string[];
11
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
12
+ export declare const DEFAULT_CLI_FINISH_ARGS: string[];
13
+ /**
14
+ * The runtime keys, structurally — not `ConfigDataFlatpak`.
15
+ *
16
+ * Two config blocks answer this question now (`gjsify.flatpak` and
17
+ * `gjsify.ship.flatpak`, § the deprecation window in `utils/ship/flatpak-config.ts`),
18
+ * and a resolver typed to one of them would have to be duplicated for the other.
19
+ */
20
+ export interface RuntimeSelection {
21
+ runtime?: string;
22
+ runtimeVersion?: string;
23
+ }
24
+ export interface ResolvedRuntime {
25
+ runtime: 'gnome' | 'freedesktop';
26
+ runtimeId: string;
27
+ sdk: string;
28
+ runtimeVersion: string;
29
+ }
30
+ /**
31
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
32
+ * `--runtime` and `--runtime-version` flags win over config values.
33
+ */
34
+ export declare function resolveRuntime(config: RuntimeSelection | undefined, overrides: RuntimeSelection): ResolvedRuntime;
35
+ /**
36
+ * `append-path` for the build sandbox, derived from the declared SDK extensions.
37
+ *
38
+ * `/app/bin` is appended last so a build command finds the app's own binaries
39
+ * without knowing where they landed.
40
+ */
41
+ export declare function deriveAppendPath(sdkExtensions: readonly string[]): string[];
@@ -0,0 +1,54 @@
1
+ // Which runtime a Flatpak is built against, and what the finished app may do.
2
+ //
3
+ // Lifted out of `commands/flatpak/` when `gjsify ship --target flatpak` became
4
+ // the second caller (ADR 0024 § 8). Same move, same reason, as
5
+ // `utils/app-metadata.ts`: these are facts about the APP, not about whichever
6
+ // command happens to render them, and the second copy is the one that drifts —
7
+ // a `DEFAULT_GNOME_RUNTIME_VERSION` bumped in one place and not the other
8
+ // produces a `ship` Flatpak and a `flatpak init` Flatpak targeting different
9
+ // GNOME releases from the same `package.json`.
10
+ /**
11
+ * Default GNOME-Platform runtime version. Bumped per release window.
12
+ * GNOME 50 = April 2026 stable; tracked in
13
+ * https://docs.flathub.org/docs/for-app-authors/requirements.
14
+ */
15
+ export const DEFAULT_GNOME_RUNTIME_VERSION = '50';
16
+ /** Default Freedesktop-Platform runtime version (LTS-ish). */
17
+ export const DEFAULT_FREEDESKTOP_RUNTIME_VERSION = '24.08';
18
+ /** Permissive GUI defaults for GTK4 + Adwaita apps. */
19
+ export const DEFAULT_GUI_FINISH_ARGS = ['--device=dri', '--share=ipc', '--socket=fallback-x11', '--socket=wayland'];
20
+ /** Lean defaults for headless CLI tools — no display, no GPU. */
21
+ export const DEFAULT_CLI_FINISH_ARGS = [];
22
+ /**
23
+ * Pick the runtime + sdk + version triple from config + CLI overrides.
24
+ * `--runtime` and `--runtime-version` flags win over config values.
25
+ */
26
+ export function resolveRuntime(config, overrides) {
27
+ const runtime = (overrides.runtime ?? config?.runtime ?? 'gnome');
28
+ if (runtime !== 'gnome' && runtime !== 'freedesktop') {
29
+ throw new Error(`gjsify flatpak: unknown runtime "${runtime}" (expected "gnome" or "freedesktop")`);
30
+ }
31
+ const runtimeVersion = overrides.runtimeVersion ??
32
+ config?.runtimeVersion ??
33
+ (runtime === 'gnome' ? DEFAULT_GNOME_RUNTIME_VERSION : DEFAULT_FREEDESKTOP_RUNTIME_VERSION);
34
+ if (runtime === 'gnome') {
35
+ return { runtime, runtimeId: 'org.gnome.Platform', sdk: 'org.gnome.Sdk', runtimeVersion };
36
+ }
37
+ return { runtime, runtimeId: 'org.freedesktop.Platform', sdk: 'org.freedesktop.Sdk', runtimeVersion };
38
+ }
39
+ /**
40
+ * `append-path` for the build sandbox, derived from the declared SDK extensions.
41
+ *
42
+ * `/app/bin` is appended last so a build command finds the app's own binaries
43
+ * without knowing where they landed.
44
+ */
45
+ export function deriveAppendPath(sdkExtensions) {
46
+ const out = [];
47
+ for (const ext of sdkExtensions) {
48
+ const m = /^org\.freedesktop\.Sdk\.Extension\.([A-Za-z0-9-]+)$/.exec(ext);
49
+ if (m)
50
+ out.push(`/usr/lib/sdk/${m[1]}/bin`);
51
+ }
52
+ out.push('/app/bin');
53
+ return out;
54
+ }
@@ -1,4 +1,4 @@
1
- import type { FormatId } from './types.js';
1
+ import type { DistroFormatId } from './types.js';
2
2
  /**
3
3
  * The GJS the bundles target (root AGENTS.md § Constraints: GJS 1.86 /
4
4
  * SpiderMonkey 140, Rolldown `firefox140`). A `--app gjs` bundle can carry
@@ -14,7 +14,7 @@ import type { FormatId } from './types.js';
14
14
  * is how a project whose bundle really does run on older GJS lowers it.
15
15
  */
16
16
  export declare const DEFAULT_GJS_FLOOR = "1.86";
17
- export declare function warnAboutGjsFloor(format: FormatId, floor: string): string[];
17
+ export declare function warnAboutGjsFloor(format: DistroFormatId, floor: string): string[];
18
18
  export interface DependsInputs {
19
19
  /** GI namespaces the bundle imports, in `Ns-Version` spelling. */
20
20
  namespaces: readonly string[];
@@ -32,10 +32,7 @@ export interface DependsInputs {
32
32
  /** User-supplied additions for this format. */
33
33
  extra: readonly string[];
34
34
  /** Project-supplied table rows, filling gaps in {@link TYPELIB_PACKAGES}. */
35
- typelibPackages?: Record<string, {
36
- deb: string;
37
- rpm: string;
38
- }>;
35
+ typelibPackages?: Record<string, Record<DistroFormatId, string>>;
39
36
  /**
40
37
  * Paths of typelib files the payload carries itself. The namespaces they cover need no distro
41
38
  * dependency — and are derived from these filenames, never declared separately.
@@ -49,7 +46,7 @@ export interface DependsInputs {
49
46
  *
50
47
  * @throws when a namespace has no entry in the table — see the module header.
51
48
  */
52
- export declare function deriveDepends(format: FormatId, inputs: DependsInputs): string[];
49
+ export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): string[];
53
50
  /** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
54
51
  export interface ParsedDepend {
55
52
  name: string;
@@ -41,7 +41,7 @@ export const DEFAULT_GJS_FLOOR = '1.86';
41
41
  * A record, not `format === 'deb' ? … : …`. The ternary reads as a two-way
42
42
  * choice and is really "deb, or ELSE rpm's package name" — measured on a third
43
43
  * format, `dmg` got `glib2`, at exit 0, in the `Depends:` of a package that has
44
- * no such thing. Adding a `FormatId` now fails to build here instead.
44
+ * no such thing. Adding a `DistroFormatId` now fails to build here instead.
45
45
  */
46
46
  const SCHEMA_COMPILER_PACKAGE = { deb: 'libglib2.0-bin', rpm: 'glib2' };
47
47
  /** Debian's `gjs` versions per suite, as measured 2026-08-15 — see {@link warnAboutGjsFloor}. */
@@ -53,10 +53,10 @@ const DEBIAN_GJS = 'trixie ships 1.82.3 and forky 1.88.1; 1.84 and 1.86 were ski
53
53
  /**
54
54
  * Which formats this Debian-suite warning is ABOUT.
55
55
  *
56
- * A `Record<FormatId, …>` and not `format !== 'deb'`: the negative form answers
56
+ * A `Record<DistroFormatId, …>` and not `format !== 'deb'`: the negative form answers
57
57
  * for every format that will ever exist, and answers "stay quiet" — so a third
58
58
  * format would inherit silence about a floor nobody has checked for its distro,
59
- * with no compile error. As a record, adding a `FormatId` fails to build until
59
+ * with no compile error. As a record, adding a `DistroFormatId` fails to build until
60
60
  * someone decides. `rpm` is false because Fedora ships a current GJS, which is a
61
61
  * measured fact about Fedora, not a default.
62
62
  */
@@ -0,0 +1,66 @@
1
+ import type { ConfigDataFlatpak, ConfigDataShip, ShipFlatpakOptions } from '../../types/config-data.js';
2
+ import type { ShipFlatpakSettings } from './types.js';
3
+ /**
4
+ * The release that stops reading `gjsify.flatpak` for the keys below.
5
+ *
6
+ * A major, not the next minor: the block is in published releases and in
7
+ * downstream `package.json`s that this repository does not control, and a
8
+ * window measured in weeks is a window nobody notices before it closes.
9
+ */
10
+ export declare const LEGACY_FLATPAK_KEYS_REMOVED_IN = "1.0.0";
11
+ /** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
12
+ export declare const MIGRATED_FLATPAK_KEYS: readonly ["runtime", "runtimeVersion", "sdkExtensions", "appendPath", "finishArgs", "cleanup"];
13
+ export type MigratedFlatpakKey = (typeof MIGRATED_FLATPAK_KEYS)[number];
14
+ /** The six keys as a reader sees them, before any default is applied. */
15
+ export type MigratedFlatpakValues = {
16
+ [K in MigratedFlatpakKey]: ConfigDataFlatpak[K];
17
+ };
18
+ export interface PickedFlatpakBuildKeys {
19
+ values: MigratedFlatpakValues;
20
+ /** Keys that came from `gjsify.flatpak`, in {@link MIGRATED_FLATPAK_KEYS} order. */
21
+ fromLegacy: MigratedFlatpakKey[];
22
+ }
23
+ /**
24
+ * `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
25
+ * noting which side answered.
26
+ *
27
+ * Per-key and not per-block: a project migrating one key at a time must not
28
+ * lose the five it has not moved yet. That is also what makes the warning
29
+ * actionable — it names the keys still coming from the old block, so the reader
30
+ * knows what to edit rather than being told a block is deprecated.
31
+ *
32
+ * A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
33
+ * out, the list was the one unbound copy of this vocabulary: a key added to it
34
+ * with no `pick` call behind it compiled fine, went missing from the window, and
35
+ * the only test that mentioned the constant compared it to a literal copy of
36
+ * itself. Reading the keys off the constant is what makes "this list IS the
37
+ * window" true rather than asserted.
38
+ *
39
+ * NO default is applied here. The defaults differ per reader — `ship` derives
40
+ * `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
41
+ * and a default applied here would be a third answer neither of them chose.
42
+ */
43
+ export declare function pickFlatpakBuildKeys(preferred: ShipFlatpakOptions | undefined, legacy: ConfigDataFlatpak): PickedFlatpakBuildKeys;
44
+ /** The branch a `ship` Flatpak is exported under when the project names none. */
45
+ export declare const DEFAULT_SHIP_FLATPAK_BRANCH = "stable";
46
+ export interface ShipFlatpakInput {
47
+ ship: ConfigDataShip;
48
+ flatpak: ConfigDataFlatpak;
49
+ /** Decides the finish-args default: a GUI app needs a display, a CLI does not. */
50
+ kind: 'app' | 'cli';
51
+ }
52
+ export interface ResolvedShipFlatpak {
53
+ settings: ShipFlatpakSettings;
54
+ warnings: string[];
55
+ }
56
+ /**
57
+ * Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
58
+ * new one is silent, and applying `ship`'s own defaults.
59
+ *
60
+ * The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
61
+ * `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
62
+ * defaulting — `branch`, the `kind`-dependent finish-args, the derived
63
+ * `appendPath` — plus the warning, which only `ship` prints because only `ship`
64
+ * has somewhere else to point at.
65
+ */
66
+ export declare function resolveShipFlatpakSettings(input: ShipFlatpakInput): ResolvedShipFlatpak;
@@ -0,0 +1,132 @@
1
+ // The deprecation window on the `gjsify.flatpak` build keys (ADR 0024 § 8).
2
+ //
3
+ // § 8 names the cost of the migration and does not let it be skipped:
4
+ // "`gjsify.flatpak` is a published config contract, so the keys move with a
5
+ // deprecation window in which both spellings resolve and the old one warns."
6
+ // This file is that window.
7
+ //
8
+ // WHAT MOVED and what did not — the table is `docs/ship-formats.md`, and the
9
+ // short form is: the six BUILD keys moved and warn from the old spelling; the
10
+ // `AppMetadata` half is a designed ALIAS and is NOT deprecated (both blocks
11
+ // describe the same app, and warning on it would print for every project that
12
+ // has a `gjsify.flatpak` block at all); and the TOOLCHAIN keys are untouched
13
+ // because `gjsify flatpak <sub>` has not moved, so deprecating them would warn
14
+ // on commands with nowhere else to read from.
15
+ //
16
+ // A WINDOW HAS TWO SIDES, and the first cut only built one. The six build keys
17
+ // are read by `gjsify flatpak init` and `flatpak ci` as well, and those
18
+ // commands have NOT moved — so a project that did what this file's warning told
19
+ // it to do, and moved them, silently lost them there: `flatpak init` fell back
20
+ // to its own defaults and wrote a manifest against a different
21
+ // `org.gnome.Platform` version with different finish-args, committed, at exit 0.
22
+ // That is why {@link pickFlatpakBuildKeys} is exported and BOTH command groups
23
+ // go through it. "Both spellings resolve" is now true of every reader of these
24
+ // keys, which is what makes the advice safe to follow.
25
+ //
26
+ // WHY A NAMED VERSION, IN ONE PLACE. The precedent is
27
+ // `utils/normalize-bundler-options.ts`, the `esbuild` → `bundler` shim, and it
28
+ // also carries the precedent's DEFECT: its warning text names 0.5.0, the tree is
29
+ // far past it, and its header now has to say "removing the shim means fixing
30
+ // that string too". So the version here is a constant the message reads, and the
31
+ // spec asserts the message names it.
32
+ import { DEFAULT_CLI_FINISH_ARGS, DEFAULT_GUI_FINISH_ARGS, deriveAppendPath, resolveRuntime, } from '../flatpak-runtime.js';
33
+ /**
34
+ * The release that stops reading `gjsify.flatpak` for the keys below.
35
+ *
36
+ * A major, not the next minor: the block is in published releases and in
37
+ * downstream `package.json`s that this repository does not control, and a
38
+ * window measured in weeks is a window nobody notices before it closes.
39
+ */
40
+ export const LEGACY_FLATPAK_KEYS_REMOVED_IN = '1.0.0';
41
+ /** The keys whose new home is `gjsify.ship.flatpak`, in the order the warning lists them. */
42
+ export const MIGRATED_FLATPAK_KEYS = [
43
+ 'runtime',
44
+ 'runtimeVersion',
45
+ 'sdkExtensions',
46
+ 'appendPath',
47
+ 'finishArgs',
48
+ 'cleanup',
49
+ ];
50
+ /**
51
+ * `gjsify.ship.flatpak.<key>` when set, else `gjsify.flatpak.<key>` — per KEY,
52
+ * noting which side answered.
53
+ *
54
+ * Per-key and not per-block: a project migrating one key at a time must not
55
+ * lose the five it has not moved yet. That is also what makes the warning
56
+ * actionable — it names the keys still coming from the old block, so the reader
57
+ * knows what to edit rather than being told a block is deprecated.
58
+ *
59
+ * A LOOP over {@link MIGRATED_FLATPAK_KEYS}, not six hand-written picks. Written
60
+ * out, the list was the one unbound copy of this vocabulary: a key added to it
61
+ * with no `pick` call behind it compiled fine, went missing from the window, and
62
+ * the only test that mentioned the constant compared it to a literal copy of
63
+ * itself. Reading the keys off the constant is what makes "this list IS the
64
+ * window" true rather than asserted.
65
+ *
66
+ * NO default is applied here. The defaults differ per reader — `ship` derives
67
+ * `finishArgs` from `kind`, `flatpak init` also merges `--sdk-extension` flags —
68
+ * and a default applied here would be a third answer neither of them chose.
69
+ */
70
+ export function pickFlatpakBuildKeys(preferred, legacy) {
71
+ const ship = preferred ?? {};
72
+ // Assembled through an index signature because the value type varies per
73
+ // key; the cast is on the finished object, where every key is present.
74
+ const values = {};
75
+ const fromLegacy = [];
76
+ for (const key of MIGRATED_FLATPAK_KEYS) {
77
+ const own = ship[key];
78
+ if (own !== undefined) {
79
+ values[key] = own;
80
+ continue;
81
+ }
82
+ const inherited = legacy[key];
83
+ if (inherited !== undefined)
84
+ fromLegacy.push(key);
85
+ values[key] = inherited;
86
+ }
87
+ return { values: values, fromLegacy };
88
+ }
89
+ /** The branch a `ship` Flatpak is exported under when the project names none. */
90
+ export const DEFAULT_SHIP_FLATPAK_BRANCH = 'stable';
91
+ /**
92
+ * Resolve the Flatpak half of `gjsify.ship`, reading the legacy block where the
93
+ * new one is silent, and applying `ship`'s own defaults.
94
+ *
95
+ * The per-key fallback itself is {@link pickFlatpakBuildKeys}, shared with
96
+ * `gjsify flatpak init`/`ci`; what is `ship`-specific and stays here is the
97
+ * defaulting — `branch`, the `kind`-dependent finish-args, the derived
98
+ * `appendPath` — plus the warning, which only `ship` prints because only `ship`
99
+ * has somewhere else to point at.
100
+ */
101
+ export function resolveShipFlatpakSettings(input) {
102
+ const shipFlatpak = input.ship.flatpak ?? {};
103
+ const { values, fromLegacy } = pickFlatpakBuildKeys(shipFlatpak, input.flatpak);
104
+ const resolved = resolveRuntime(values, {});
105
+ const extensions = values.sdkExtensions ?? [];
106
+ const { appendPath, finishArgs, cleanup } = values;
107
+ const settings = {
108
+ runtime: resolved.runtimeId,
109
+ runtimeVersion: resolved.runtimeVersion,
110
+ sdk: resolved.sdk,
111
+ branch: shipFlatpak.branch ?? DEFAULT_SHIP_FLATPAK_BRANCH,
112
+ sdkExtensions: [...extensions],
113
+ // Derived from the extensions when nothing was declared, and only then:
114
+ // an extension whose `/usr/lib/sdk/<x>/bin` is not on PATH inside the
115
+ // sandbox is an extension the build cannot use, which looks like the
116
+ // extension not being installed.
117
+ appendPath: appendPath ?? (extensions.length > 0 ? deriveAppendPath(extensions) : []),
118
+ finishArgs: finishArgs ?? (input.kind === 'cli' ? DEFAULT_CLI_FINISH_ARGS : DEFAULT_GUI_FINISH_ARGS),
119
+ cleanup: cleanup ?? [],
120
+ };
121
+ return { settings, warnings: fromLegacy.length === 0 ? [] : [deprecationWarning(fromLegacy)] };
122
+ }
123
+ /** One line, naming every key still read from the old block and where each goes. */
124
+ function deprecationWarning(keys) {
125
+ const list = keys.map((key) => `gjsify.flatpak.${key}`).join(', ');
126
+ const moved = keys.map((key) => `gjsify.ship.flatpak.${key}`).join(', ');
127
+ return (`DEPRECATION: ${list} ${keys.length > 1 ? 'are' : 'is'} read by \`gjsify ship\` for compatibility and ` +
128
+ `will be removed in ${LEGACY_FLATPAK_KEYS_REMOVED_IN}. Move ${keys.length > 1 ? 'them' : 'it'} to ` +
129
+ `${moved} — both spellings resolve until then, and \`gjsify.ship.flatpak\` wins where both are set. ` +
130
+ 'The app METADATA in `gjsify.flatpak` (name, summary, developer, categories, licence) is NOT ' +
131
+ 'deprecated: both blocks describe the same application and either may carry it (ADR 0024 § 8).');
132
+ }
@@ -0,0 +1,45 @@
1
+ import type { PackSettings } from './types.js';
2
+ export interface FlatpakManifestInput {
3
+ settings: PackSettings;
4
+ /** Absolute path of the staged payload — the tree `readStage` validated. */
5
+ stageDir: string;
6
+ /** Absolute path of this format's rendered overlay (the licence file). */
7
+ overlayDir: string;
8
+ /** The stage's own closure file, which is not payload and must not be copied into `/app`. */
9
+ stageManifestFile: string;
10
+ }
11
+ /**
12
+ * The generated Flatpak manifest, as a plain object.
13
+ *
14
+ * Pure, so every field is unit-testable without flatpak tooling — and the
15
+ * suite that needs that is the one running in CI, where there is none.
16
+ *
17
+ * The app id is what makes this legal Flatpak: every file flatpak EXPORTS out
18
+ * of `/app/share` has to be named after it, and `planStage` already enforces
19
+ * exactly that for its own reasons — the desktop entry, the metainfo, the icon,
20
+ * the GSettings schema and the shared-mime-info document are all
21
+ * `<appId>[.…].<ext>`, because `share/glib-2.0/schemas/` and
22
+ * `share/mime/packages/` are directories shared between packages. Two
23
+ * independent constraints, one naming rule.
24
+ */
25
+ export declare function renderShipFlatpakManifest(input: FlatpakManifestInput): Record<string, unknown>;
26
+ export interface FlatpakPackInput extends FlatpakManifestInput {
27
+ /** Working root for the manifest, the build dir, the repo and the builder's state. */
28
+ workDir: string;
29
+ /** Absolute path the finished `.flatpak` is written to. */
30
+ target: string;
31
+ /** Flatpak's spelling of the architecture, e.g. `x86_64`. */
32
+ archLabel: string;
33
+ verbose: boolean;
34
+ }
35
+ /**
36
+ * Build the app and export a single-file bundle at `input.target`.
37
+ *
38
+ * The only packer in this tree that execs anything, and the descriptor says so
39
+ * (`FORMATS.flatpak.host.requiredTools`). ADR 0024 § A3 is explicit that this
40
+ * costs the independent oracle the hand-written deb/rpm writers bought: what
41
+ * pays it back here is that a `.flatpak` is an OSTree static delta, so
42
+ * `flatpak build-import-bundle` + `ostree ls -R` read the artifact back with
43
+ * code this tree did not write.
44
+ */
45
+ export declare function buildFlatpakBundle(input: FlatpakPackInput): Promise<void>;
@@ -0,0 +1,148 @@
1
+ // The Flatpak packer — ADR 0024 § 8, stage 6.
2
+ //
3
+ // This is a MOVE, not a second implementation. `gjsify flatpak` has emitted
4
+ // Flatpak manifests since long before `ship` existed; what it emitted was a
5
+ // `buildsystem: meson` module, which made every app carry meson glue whose only
6
+ // job was to call `gjsify build` and copy the result into a prefix — 158 lines
7
+ // of it in Learn6502, 66 of them `GI_TYPELIB_PATH`/`LD_LIBRARY_PATH` plumbing.
8
+ // `ship` already produces that prefix. So the module becomes:
9
+ //
10
+ // buildsystem: simple
11
+ // build-commands: mkdir -p /app; cp -a stage/. /app/; cp -a overlay/. /app/
12
+ //
13
+ // and meson leaves the sandbox. Nothing else about the payload changes — the
14
+ // launcher derives its own prefix at runtime (§ 3), which is exactly why one
15
+ // staged tree can be `/usr` in a `.deb` and `/app` here.
16
+ //
17
+ // FOUR THINGS ABOUT flatpak-builder 1.4.10 WERE MEASURED before this file was
18
+ // written, because each had a plausible wrong answer and flatpak-builder reports
19
+ // none of them: an absolute `dir` source `path` works, `skip` works and is what
20
+ // keeps the stage's own sidecar out of `/app`, `cp -a` preserves the mode, and
21
+ // `--show-manifest` is NOT a validator (it accepted an unknown source property
22
+ // and `buildsystem: "nonsense"` at exit 0, so it must never be used as a gate).
23
+ // The numbers, the commands and the oracle that replaces `--show-manifest`:
24
+ // `docs/ship-formats.md`.
25
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { describeExit, spawnToCompletion } from '../spawn.js';
28
+ /**
29
+ * The generated Flatpak manifest, as a plain object.
30
+ *
31
+ * Pure, so every field is unit-testable without flatpak tooling — and the
32
+ * suite that needs that is the one running in CI, where there is none.
33
+ *
34
+ * The app id is what makes this legal Flatpak: every file flatpak EXPORTS out
35
+ * of `/app/share` has to be named after it, and `planStage` already enforces
36
+ * exactly that for its own reasons — the desktop entry, the metainfo, the icon,
37
+ * the GSettings schema and the shared-mime-info document are all
38
+ * `<appId>[.…].<ext>`, because `share/glib-2.0/schemas/` and
39
+ * `share/mime/packages/` are directories shared between packages. Two
40
+ * independent constraints, one naming rule.
41
+ */
42
+ export function renderShipFlatpakManifest(input) {
43
+ const { settings, stageDir, overlayDir } = input;
44
+ const flatpak = settings.flatpak;
45
+ const manifest = {
46
+ id: settings.appId,
47
+ runtime: flatpak.runtime,
48
+ 'runtime-version': flatpak.runtimeVersion,
49
+ sdk: flatpak.sdk,
50
+ branch: flatpak.branch,
51
+ // `bin/<binaryName>`, which is the one executable the stage plans. NOT
52
+ // the app id, which is what `gjsify flatpak init` defaults to: under
53
+ // `ship` the launcher's name is decided by `gjsify.ship.binaryName`, and
54
+ // a `command` naming a file that is not in `/app/bin` produces an app
55
+ // that installs and cannot be launched.
56
+ command: settings.binaryName,
57
+ 'finish-args': flatpak.finishArgs,
58
+ };
59
+ if (flatpak.sdkExtensions.length > 0)
60
+ manifest['sdk-extensions'] = flatpak.sdkExtensions;
61
+ if (flatpak.appendPath.length > 0) {
62
+ manifest['build-options'] = { 'append-path': flatpak.appendPath.join(':') };
63
+ }
64
+ if (flatpak.cleanup.length > 0)
65
+ manifest.cleanup = flatpak.cleanup;
66
+ manifest.modules = [
67
+ {
68
+ name: settings.binaryName,
69
+ buildsystem: 'simple',
70
+ // `mkdir -p /app` first: flatpak-builder creates the app directory,
71
+ // but a module whose only command is a `cp` into a path that does
72
+ // not exist yet is one refactor away from failing, and the mkdir is
73
+ // free. `stage/.` and not `stage` — `cp -a stage /app/` would
74
+ // create `/app/stage`, which installs cleanly and puts the whole
75
+ // payload one directory too deep.
76
+ 'build-commands': ['mkdir -p /app', 'cp -a stage/. /app/', 'cp -a overlay/. /app/'],
77
+ sources: [
78
+ { type: 'dir', path: stageDir, dest: 'stage', skip: [input.stageManifestFile] },
79
+ { type: 'dir', path: overlayDir, dest: 'overlay' },
80
+ ],
81
+ },
82
+ ];
83
+ return manifest;
84
+ }
85
+ /**
86
+ * Build the app and export a single-file bundle at `input.target`.
87
+ *
88
+ * The only packer in this tree that execs anything, and the descriptor says so
89
+ * (`FORMATS.flatpak.host.requiredTools`). ADR 0024 § A3 is explicit that this
90
+ * costs the independent oracle the hand-written deb/rpm writers bought: what
91
+ * pays it back here is that a `.flatpak` is an OSTree static delta, so
92
+ * `flatpak build-import-bundle` + `ostree ls -R` read the artifact back with
93
+ * code this tree did not write.
94
+ */
95
+ export async function buildFlatpakBundle(input) {
96
+ const { settings, workDir, archLabel } = input;
97
+ const manifestPath = join(workDir, `${settings.appId}.json`);
98
+ const buildDir = join(workDir, 'build');
99
+ const repoDir = join(workDir, 'repo');
100
+ // Kept between runs: it is flatpak-builder's own ostree cache, and dropping
101
+ // it turns every re-pack into a full rebuild of every module.
102
+ const stateDir = join(workDir, 'state');
103
+ mkdirSync(workDir, { recursive: true });
104
+ // The repo is wiped, the state dir is not. A repo that survives holds the
105
+ // PREVIOUS commit on the same ref, and `build-bundle` would happily export
106
+ // it if this run's export failed for any reason that did not fail the
107
+ // build — a stale artifact wearing the new version's filename.
108
+ rmSync(repoDir, { recursive: true, force: true, maxRetries: 5 });
109
+ writeFileSync(manifestPath, `${JSON.stringify(renderShipFlatpakManifest(input), null, 2)}\n`);
110
+ await run('flatpak-builder', [
111
+ '--force-clean',
112
+ // Needed wherever FUSE is unavailable — containers, most CI runners.
113
+ // It only disables a read-only-bind optimisation, and a build that
114
+ // cannot start is worse than one that copies a few more bytes.
115
+ '--disable-rofiles-fuse',
116
+ '--delete-build-dirs',
117
+ `--state-dir=${stateDir}`,
118
+ `--arch=${archLabel}`,
119
+ `--repo=${repoDir}`,
120
+ buildDir,
121
+ manifestPath,
122
+ ], input, 'flatpak-builder not found. Install it (Fedora: `sudo dnf install flatpak-builder`).');
123
+ // The branch is passed explicitly even though the manifest declares it: the
124
+ // two have to name the same ref, and `build-bundle`'s default is `master`
125
+ // while a manifest that sets `branch` exports somewhere else. Leaving it
126
+ // implicit is how this exports nothing and says "ref not found".
127
+ await run('flatpak', ['build-bundle', `--arch=${archLabel}`, repoDir, input.target, settings.appId, settings.flatpak.branch], input, 'flatpak not found. Install it via your distro: see https://flathub.org/setup.');
128
+ }
129
+ async function run(cmd, args, input, notFoundHint) {
130
+ if (input.verbose)
131
+ console.log(`[gjsify ship] ${cmd} ${args.join(' ')}`);
132
+ // `completion: 'return'` because `ship` packs the remaining formats and
133
+ // prints the artifact list afterwards, so it cannot end in `process.exit()`
134
+ // the way `gjsify flatpak build` does. Under GJS that selects the blocking
135
+ // path, which captures the child's output and re-emits it at the end
136
+ // instead of streaming it — the documented cost of a spawn a caller has to
137
+ // return from (see utils/spawn.ts), and the reason `--verbose` prints the
138
+ // invocation before the silence starts.
139
+ const result = await spawnToCompletion(cmd, args, {
140
+ completion: 'return',
141
+ cwd: input.workDir,
142
+ stdio: 'inherit',
143
+ notFound: () => new Error(`gjsify ship: ${notFoundHint}`),
144
+ });
145
+ if (result.code !== 0) {
146
+ throw new Error(`gjsify ship: ${cmd} failed${describeExit(result)}.`);
147
+ }
148
+ }
@@ -1,5 +1,40 @@
1
1
  import type { FormatDescriptor, FormatId } from './types.js';
2
2
  export declare const FORMATS: Record<FormatId, FormatDescriptor>;
3
3
  export declare const FORMAT_IDS: FormatId[];
4
+ /**
5
+ * What `gjsify ship` builds when nothing said otherwise — every format that
6
+ * needs no tool and no particular host.
7
+ *
8
+ * A SECOND derivation from the same table, and it has to be one: `FORMAT_IDS`
9
+ * used to be both "every format that exists" and "every format to build by
10
+ * default", which was the same list only while every format was `finishOn:
11
+ * 'any'`. Adding Flatpak to `FORMAT_IDS` alone would have made a bare
12
+ * `gjsify ship` demand `flatpak-builder` of every project that ever packaged a
13
+ * `.deb` — and on a host without it, of every `release-cut.yml` run, which
14
+ * packs `@gjsify/cli` itself on a bare ubuntu runner. A host-bound format is
15
+ * opt-in through `--target` or `gjsify.ship.targets`; `--help` still lists all
16
+ * of them.
17
+ */
18
+ export declare const DEFAULT_FORMAT_IDS: FormatId[];
19
+ /**
20
+ * Refuse a format this host cannot finish, BEFORE anything is built or written.
21
+ *
22
+ * `host` is injected so both branches are unit-testable from any machine — the
23
+ * same reason `resolvePrebuildDirName` and `checkTypeSkew`'s readers are pure.
24
+ * The message names the two-phase split rather than just saying no: the answer
25
+ * to "I am on macOS and want a Flatpak" is `--stage` here and `--from-stage` on
26
+ * a Linux runner, and a refusal that does not say so reads as "unsupported".
27
+ */
28
+ export declare function assertHostCanFinish(format: FormatDescriptor, host?: string): void;
29
+ /**
30
+ * Refuse a format whose tools are not installed, with the tool named.
31
+ *
32
+ * Separate from {@link assertHostCanFinish} because the two failures have
33
+ * different fixes — one needs a different machine, the other needs a package —
34
+ * and separate from the packer's own `notFound` handler because it fires BEFORE
35
+ * a build: `gjsify ship` runs the project's `build` script first, and finding
36
+ * out afterwards that `flatpak-builder` is absent costs the whole build.
37
+ */
38
+ export declare function assertToolsInstalled(format: FormatDescriptor, present?: (cmd: string) => boolean): void;
4
39
  /** Parse `--target deb,rpm` into a sorted, deduplicated descriptor list. */
5
40
  export declare function resolveFormats(raw: readonly string[]): FormatDescriptor[];