@gjsify/cli 0.50.0 → 0.51.1

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.
@@ -32,14 +32,15 @@ import { buildDeb } from '../utils/ship/deb.js';
32
32
  import { deriveDepends, warnAboutGjsFloor, warnAboutNodeFloor } from '../utils/ship/depends.js';
33
33
  import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.js';
34
34
  import { discoverPayload } from '../utils/ship/discover.js';
35
+ import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
35
36
  import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
36
37
  import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
37
38
  import { localizeMetadata } from '../utils/ship/localize-metadata.js';
38
- import { assertHostCanFinish, assertToolsInstalled, canCarryTicket, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
39
+ import { assertHostCanFinish, assertKindCanPack, assertToolsInstalled, canCarryTicket, configuredFormats, defaultFormatIds, formatIdsFor, resolveFormats, windowsProgramDirName, FORMAT_IDS, FORMATS, } from '../utils/ship/formats.js';
39
40
  import { assertLayoutSupportsArch, hostLayout, layoutForOs, place, placeStage, resolveLayout, LAYOUT_NAMES, } from '../utils/ship/layout.js';
40
41
  import { isNodeRuntimeTarget, resolveNodeRuntime } from '../utils/ship/node-runtime.js';
41
42
  import { isUnder, SHARE } from '../utils/ship/share-dirs.js';
42
- import { compileSchemasForStage } from '../utils/ship/schemas.js';
43
+ import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/schemas.js';
43
44
  import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
44
45
  import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
45
46
  import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
@@ -49,7 +50,7 @@ import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/p
49
50
  import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
50
51
  import { renderLauncher } from '../utils/ship/launcher.js';
51
52
  import { buildRpm } from '../utils/ship/rpm.js';
52
- import { resolveShipApp, resolveShipBundle, resolveShipSettings, } from '../utils/ship/settings.js';
53
+ import { resolveShipApp, resolveShipBundle, resolveShipKind, resolveShipSettings, } from '../utils/ship/settings.js';
53
54
  import { assertExpectedTarget, assertFormatsStaged, formatTarget, overlayFiles, readStageManifest, resolveStageMtime, warnOnToolSkew, writeStageManifest, STAGE_MANIFEST_FILE, } from '../utils/ship/stage-manifest.js';
54
55
  import { directorySize, readStage, writePayload, writeStage } from '../utils/ship/stage-writer.js';
55
56
  const LOG = '[gjsify ship]';
@@ -276,6 +277,23 @@ async function assemble(args) {
276
277
  // afterwards costs the whole build for a refusal that was knowable up
277
278
  // front. Skipped under `--stage`, which is precisely the phase that does
278
279
  // NOT need the format's tooling — that asymmetry is the point of the split.
280
+ // THE THIRD REFUSAL THAT IS KNOWABLE UP FRONT, and it was the one paid for
281
+ // with a whole build: `appimage` needs a desktop entry at its AppDir root,
282
+ // `kind: 'cli'` stages none by design, and both facts are configuration. It
283
+ // used to surface inside the packer — after `runProjectBuild` — so a CLI
284
+ // project asking for an AppImage built everything first and was then told its
285
+ // payload could never have become one. The ICON half stays in the packer,
286
+ // because an icon is discovered and discovery legitimately reads build output.
287
+ //
288
+ // BEFORE `assertCanPack` AND NOT AFTER IT, which is an ordering between two
289
+ // refusals rather than a detail. `assertCanPack` says "not on THIS machine" and
290
+ // is answered by installing something; this one says "not for THIS PROJECT",
291
+ // which installing appimagetool would not fix. Reporting the machine first
292
+ // sends a CLI author to a GitHub release for a tool that was never going to
293
+ // help them — measured, because the e2e below asserted the message it wanted
294
+ // and only ever ran on a host that had the tool.
295
+ if (!args.stage)
296
+ assertKindCanPack(formats, resolveShipKind(ship, flatpak));
279
297
  if (!args.stage)
280
298
  for (const format of formats)
281
299
  assertCanPack(format);
@@ -398,7 +416,14 @@ async function assemble(args) {
398
416
  // `.deb`/`.rpm` postinst compiles the SYSTEM directory at install time
399
417
  // (`utils/ship/scripts.ts`), where our schemas merge with every other
400
418
  // package's — shipping a prebuilt cache there would be a file the install step
401
- // overwrites. So the compile is conditional on the layout and nothing else.
419
+ // overwrites, and one `rpm -e` takes away with it.
420
+ //
421
+ // WHICH IS WHY THIS CONDITION IS ABOUT THE STAGE AND NOT ABOUT INSTALL STEPS,
422
+ // a distinction that used to be invisible because the two agreed. They stopped
423
+ // agreeing when the AppImage landed: it takes the Linux layout and has no
424
+ // install step at all. It does not get its cache from here — a Linux stage
425
+ // serves three formats and only one of them wants one — it compiles its own
426
+ // into the AppDir, on its own pack path (`packOne`, `case 'appimage'`).
402
427
  const planned = [
403
428
  ...planStage(settings, stageInputs),
404
429
  ...(layout.os === 'linux'
@@ -949,6 +974,80 @@ async function packOne(input) {
949
974
  verbose: input.verbose,
950
975
  });
951
976
  break;
977
+ case 'appimage': {
978
+ // FROM THE PAYLOAD, like every other container here and NOT from the
979
+ // stage directory, so the three properties `writePayload` guarantees
980
+ // hold: the modes are the plan's, the stage's own sidecar stays out
981
+ // because it was never payload, and `--target appimage` alone works
982
+ // without another row having run first.
983
+ //
984
+ // The host requirement list is derived HERE rather than inside the
985
+ // packer, because it is the same pair of inputs `deriveDepends` takes
986
+ // one branch up — `input.namespaces` from the stage manifest and
987
+ // `facts.bundledTypelibs` from the payload. Two derivations would be
988
+ // two answers to "what does this artifact need from the machine", from
989
+ // one build, in two files.
990
+ const hostRequirements = appImageHostRequirements({
991
+ app: settings.app,
992
+ minGjsVersion: settings.minGjsVersion,
993
+ minNodeVersion: settings.minNodeVersion,
994
+ namespaces: input.namespaces,
995
+ bundledTypelibs: facts.bundledTypelibs,
996
+ });
997
+ const appDir = appDirFor(outRoot);
998
+ // THE ONE FILE THIS FORMAT ADDS TO THE PAYLOAD, and the reason it is
999
+ // added HERE and not where every other layout's is (the stage, above).
1000
+ // An AppImage has no install step; the Linux layout it inherits assumes
1001
+ // one, because until this format existed every Linux artifact had a
1002
+ // `postinst`. The compile cannot move up into the stage: the Linux stage
1003
+ // is shared with the `.deb` and the `.rpm`, whose `share/glib-2.0/schemas`
1004
+ // is `/usr/share/glib-2.0/schemas` — the SYSTEM directory — and a cache
1005
+ // staged there is this app's 700 bytes installed over every other
1006
+ // package's schemas, then taken away with `rpm -e`.
1007
+ //
1008
+ // FROM THE PAYLOAD, like the host requirements two lines up and for the
1009
+ // same reason: `--from-stage` has the schema BYTES and not the project
1010
+ // directory they were read from.
1011
+ const schemaCache = await compileSchemasForPayload({
1012
+ payload,
1013
+ workDir: join(outRoot, 'appimage-schemas'),
1014
+ });
1015
+ const tree = appDirPayload(settings, payload, hostRequirements, schemaCache);
1016
+ // ONE `writePayload` FOR THE WHOLE AppDir, prefix and root files
1017
+ // together, because that call WIPES: two of them would leave the
1018
+ // previous run's `<appId>.desktop` and icon at a root the second call
1019
+ // never cleaned, and appimagetool packs an AppDir with two desktop
1020
+ // files without complaining about either.
1021
+ writePayload(appDir, tree, '');
1022
+ // AND THEN THE TIMES, which are the single difference between two packs
1023
+ // of one build (measured, sha256): mksquashfs stores per-file mtimes
1024
+ // and `writePayload`'s own `mkdir`/`writeFileSync` leave the wall clock.
1025
+ stampAppDirTimes(appDir, tree, mtime);
1026
+ // PRINTED, not logged at `--verbose`. ADR 0024 § 9's objection to this
1027
+ // format is that "runs anywhere" is a claim the file cannot keep, and
1028
+ // the answer is that the file says what it does not carry — to the
1029
+ // person building it, every time, and not only in a document.
1030
+ console.log(`${LOG} the AppImage takes these from the host: ${hostRequirements.join(', ')}`);
1031
+ // AND WHAT IT EMBEDS, on the same terms and for the same reason. The
1032
+ // requirement list above says what the file does NOT carry; this says
1033
+ // where the one thing it DOES carry that this tree did not write came
1034
+ // from. With a pin that is a checkable claim (no network, and two packs
1035
+ // of one build embed identical bytes); without one appimagetool fetches
1036
+ // from a rolling tag, which still works and must still be said —
1037
+ // ADR 0024 § A26.1. Unconditional, never behind `--verbose`: the person
1038
+ // who can pin a runtime is the one reading a pack log.
1039
+ const runtimeFile = findPinnedRuntime(archLabel);
1040
+ console.log(`${LOG} ${appImageRuntimeNotice(archLabel, runtimeFile)}`);
1041
+ await buildAppImage({
1042
+ appDir,
1043
+ target,
1044
+ archLabel,
1045
+ runtimeFile,
1046
+ workDir: dirname(appDir),
1047
+ verbose: input.verbose,
1048
+ });
1049
+ break;
1050
+ }
952
1051
  case 'macos-app':
953
1052
  case 'windows-dir':
954
1053
  // ONE STATEMENT FOR TWO ROWS, folded because the code really is
@@ -0,0 +1,372 @@
1
+ import type { PayloadEntry } from './payload.js';
2
+ import type { PackSettings } from './types.js';
3
+ /** The tool this packer execs. One spelling, read by this module AND by the format row. */
4
+ export declare const APPIMAGE_TOOL = "appimagetool";
5
+ /**
6
+ * The environment variable the AppImage runtime reads to extract instead of mount.
7
+ *
8
+ * Named rather than inlined because two different programs read it for two
9
+ * different reasons — `appimagetool` when this packer runs it, and the artifact
10
+ * when a user runs that — and the spec asserts on the pair.
11
+ */
12
+ export declare const EXTRACT_AND_RUN = "APPIMAGE_EXTRACT_AND_RUN";
13
+ /**
14
+ * Where a PINNED AppImage runtime is looked for, and the variable that moves it.
15
+ *
16
+ * THE RUNTIME IS THE HALF OF THE ARTIFACT THIS TREE DOES NOT WRITE, and until it
17
+ * was pinned nothing pinned it: appimagetool fetches `runtime-<arch>` from
18
+ * `type2-runtime`'s ROLLING `continuous` tag on every pack (module header), so
19
+ * the ~940 KB of ELF a user downloads and executes arrived with no digest behind
20
+ * it, while `.docker/ci-fedora.Dockerfile` carefully pinned the tool that fetched
21
+ * it. Measured, and not a theoretical drift: `continuous`'s `runtime-x86_64` and
22
+ * the dated `20251108` release's differ in content at the same 944632 bytes.
23
+ *
24
+ * A DIRECTORY AND AN ENVIRONMENT VARIABLE, rather than a download this packer
25
+ * performs, because that is ADR 0024 § A25's rule applied one level down — a
26
+ * pack step that fetches is a release step that fails when GitHub does. The CI
27
+ * image puts a digest-checked file here; a workstation either does the same or
28
+ * takes the announced path below. Arch-named, `runtime-<arch>`, which is
29
+ * type2-runtime's own asset name, so pinning one is a copy and not a rename.
30
+ */
31
+ export declare const APPIMAGE_RUNTIME_DIR_ENV = "GJSIFY_APPIMAGE_RUNTIME_DIR";
32
+ /** The default home for {@link APPIMAGE_RUNTIME_DIR_ENV}, which the CI image fills. */
33
+ export declare const APPIMAGE_RUNTIME_DIR = "/usr/local/share/gjsify/appimage-runtime";
34
+ /**
35
+ * The AppDir's thumbnail, and the reason this packer writes it rather than
36
+ * letting appimagetool do it.
37
+ *
38
+ * MEASURED, AS A REPRODUCIBILITY DEFECT, and it is the kind only a byte
39
+ * comparison finds: appimagetool creates `.DirIcon` as a SYMLINK to the root icon
40
+ * when the AppDir does not already have one, and it does that AFTER
41
+ * {@link stampAppDirTimes} has run — so the link carries the wall clock, the
42
+ * AppDir root directory's own mtime moves with it, and mksquashfs stores both.
43
+ * Two `gjsify ship --target appimage` runs over ONE build then differ in sha256
44
+ * while every listing, mode, size and byte of content is identical (measured: 965
45
+ * differing bytes, the first inside `.digest_md5`, because the embedded digest
46
+ * follows the filesystem it covers). Four packs over the SAME AppDir are
47
+ * byte-identical, which is what makes this a mutation of the tree rather than a
48
+ * nondeterministic packer.
49
+ *
50
+ * A REGULAR FILE and not a symlink, for `dmg.ts`'s reason: `writePayload` writes
51
+ * regular files and nothing else — a property `tests/e2e/ship` asserts, and one
52
+ * `actions/upload-artifact` would silently dereference anyway. squashfs
53
+ * deduplicates the second copy, so it costs an inode and no data.
54
+ */
55
+ export declare const DIR_ICON_NAME = ".DirIcon";
56
+ /**
57
+ * The one directory the staged prefix goes under inside the AppDir.
58
+ *
59
+ * Exported because `stampAppDirTimes` and the e2e reader both address paths
60
+ * through it, and a second `'usr'` literal is the kind that stays right until
61
+ * somebody changes the first one.
62
+ */
63
+ export declare const APPDIR_PREFIX_DIR = "usr";
64
+ /**
65
+ * Where the AppDir is assembled, under the run's own output root.
66
+ *
67
+ * A directory of its own, never `out/`, for the reason `dmgVolumeDir` gives one
68
+ * format over: appimagetool packs everything it finds in the AppDir, so a stray
69
+ * sibling would ride into the image and the reader that would catch it is the
70
+ * same listing comparison that would then have to be relaxed to allow it.
71
+ */
72
+ export declare function appDirFor(outRoot: string): string;
73
+ /**
74
+ * What the AppImage does NOT carry, derived from the payload rather than authored.
75
+ *
76
+ * THE HONEST HALF OF ADR 0024 § 9, as data. The same scan that gives the `.deb`
77
+ * its `Depends: gir1.2-gtk-4.0` gives this its `Gtk-4.0`, so the two artifacts
78
+ * built from one payload cannot disagree about what the host has to provide —
79
+ * and the AppImage's version is distro-NEUTRAL on purpose, because there is no
80
+ * distribution to name package names in. A user reading `Gtk-4.0` can find it on
81
+ * any system; a user reading `gir1.2-gtk-4.0` on Fedora cannot.
82
+ *
83
+ * The interpreter is first and always present: it is the requirement that is
84
+ * never satisfied by accident, and the one {@link renderAppRun} can check from a
85
+ * shell script.
86
+ */
87
+ export declare function appImageHostRequirements(input: {
88
+ app: PackSettings['app'];
89
+ minGjsVersion?: string;
90
+ minNodeVersion?: string;
91
+ namespaces: readonly string[];
92
+ bundledTypelibs?: readonly string[];
93
+ }): string[];
94
+ /**
95
+ * The version this payload's interpreter must be at least, as the project declared it.
96
+ *
97
+ * ONE DECISION, TWO READERS, which is why it is a function and not two ternaries:
98
+ * {@link appImageHostRequirements} PRINTS this floor and {@link renderAppRun}
99
+ * CHECKS it, and an artifact whose message names 1.86 while its script compares
100
+ * against something else is worse than one that does neither.
101
+ */
102
+ export declare function interpreterFloor(input: {
103
+ app: PackSettings['app'];
104
+ minGjsVersion?: string;
105
+ minNodeVersion?: string;
106
+ }): string;
107
+ /**
108
+ * The `AppRun` the AppImage runtime execs, and the one check it makes first.
109
+ *
110
+ * IT EXECS THE STAGED LAUNCHER AND ADDS NOTHING TO IT. `bin/<name>` already
111
+ * resolves its own prefix — `readlink -f "$0"` then two `dirname`s
112
+ * (`renderPrefixLauncher`) — and exports `XDG_DATA_DIRS`, `GI_TYPELIB_PATH`,
113
+ * `GJSIFY_LOCALE_DIR` and `GJSIFY_FONT_DIR` relative to it. Inside a mounted
114
+ * image that resolves to `/tmp/.mount_xxxx/usr`, measured, so every locator the
115
+ * `.deb` sets up is already correct here. Re-exporting any of them from AppRun
116
+ * would be a second answer to a question the payload answers, and the copy that
117
+ * drifts is the one nothing tests: `tests/e2e/ship-layout` compares the staged
118
+ * trees, not this script.
119
+ *
120
+ * THAT CLAIM WAS RE-MEASURED AGAINST THE ARTIFACT and it holds — which is worth
121
+ * writing down, because when Learn6502 0.8.0's AppImage died on `GSettings schema
122
+ * … not found` this script was the obvious suspect and adding
123
+ * `GSETTINGS_SCHEMA_DIR` here was the obvious fix. It would have changed nothing.
124
+ * Measured on the real 0.8.0 image, by compiling `gschemas.compiled` into the
125
+ * extracted AppDir and touching NO environment variable anywhere: the application
126
+ * started. The launcher's `XDG_DATA_DIRS` was already pointing GSettings at the
127
+ * right directory; the directory had no cache in it. An extra export would have
128
+ * been a second name for a path that was already correct, and it would have
129
+ * "fixed" the bug by sitting next to the change that did.
130
+ *
131
+ * WHAT IT DOES ADD is the refusal ADR 0024 § 9 asks for. An AppImage whose
132
+ * interpreter is missing exits with the loader's own message — `gjs: not found`
133
+ * from a shell, or nothing at all when a desktop launches it — and the user
134
+ * concludes the download is broken. `command -v` costs one fork and turns that
135
+ * into a sentence naming the runtime, this file's promise and the escape hatch.
136
+ * The typelib half is deliberately NOT checked here: girepository's search path
137
+ * is not a shell-visible thing, and GJS's own "Typelib file for namespace 'Gtk'
138
+ * not found" is already precise. Listing them in the refusal is what this file
139
+ * can honestly do — checking them would be a probe that guesses.
140
+ *
141
+ * AND THE FLOOR IS CHECKED, FAIL-OPEN, which is the one design decision in this
142
+ * script. `command -v gjs` alone accepted GJS 1.70 while the message beside it
143
+ * said `gjs (>= 1.86)` — a floor the `.deb` enforces through `Depends:` and this
144
+ * artifact merely printed, so the failure it exists to prevent came back one
145
+ * import later. The check costs the same fork `command -v` already cost.
146
+ *
147
+ * FAIL-OPEN IS THE POINT AND NOT A HEDGE. A version comparison in `/bin/sh` that
148
+ * gets it wrong REFUSES A WORKING SYSTEM, which is strictly worse than not
149
+ * checking: the user cannot argue with an artifact. So the refusal fires only on
150
+ * two integers that were actually parsed out of the interpreter's own
151
+ * `--version`, and every other outcome — no output, a spelling this `sed` does
152
+ * not match, a distro build that prints something else entirely — RUNS THE
153
+ * APPLICATION. `gjs --version` prints `gjs 1.88.1` and `node --version` prints
154
+ * `v24.19.0`; both reduce to a major and a minor, and a floor with no minor
155
+ * (`24`) compares as `24.0`.
156
+ *
157
+ * EXIT 126 AND NOT 127 for the too-old case, because the shell's own vocabulary
158
+ * already separates them: 127 is "not found", 126 is "found and cannot be run".
159
+ * A wrapper script that wants to tell the two apart can, and the message says
160
+ * which it is either way.
161
+ */
162
+ export declare function renderAppRun(settings: PackSettings, hostRequirements: readonly string[]): string;
163
+ /**
164
+ * The icon appimagetool needs at the AppDir root, chosen from the payload.
165
+ *
166
+ * AN APPIMAGE NEEDS TWO FILES THE PREFIX ALREADY HAS, AT A PLACE IT DOES NOT:
167
+ * appimagetool refuses an AppDir without a `.desktop` at its root, and the file
168
+ * named by that entry's `Icon=` must sit beside it. `plan.ts` stages both — the
169
+ * entry at `share/applications/<appId>.desktop` and every icon renamed to
170
+ * `<appId>` under `share/icons/hicolor/…` — so this function is a SELECTION over
171
+ * the payload rather than anything new in the stage, which is ADR 0024 § 2's
172
+ * "one payload, a handful of layouts" holding for a fourth format.
173
+ *
174
+ * LARGEST RASTER WINS, and the size comes from the hicolor directory rather than
175
+ * from reading the file: `share/icons/hicolor/256x256/apps/<id>.png` says 256 in
176
+ * its path, and a PNG header read would be a decoder this packer would then own.
177
+ * `scalable` sorts above every raster size as the SVG fallback and `symbolic` is
178
+ * excluded outright — its staged name is `<appId>-symbolic`, which is not what
179
+ * the desktop entry's `Icon=` says, so shipping it as the root icon installs an
180
+ * icon nothing ever looks up (`plan.ts` documents the same rule for the theme).
181
+ */
182
+ /**
183
+ * The pinned runtime for this architecture, or `undefined` when none is staged here.
184
+ *
185
+ * PURE APART FROM ONE INJECTED PROBE, so both answers are testable on a host that
186
+ * has neither — the split {@link appImageToolArgs} draws for the flags. `exists`
187
+ * is a parameter for exactly that reason and defaults to the real one.
188
+ */
189
+ export declare function findPinnedRuntime(archLabel: string, env?: NodeJS.ProcessEnv, exists?: (path: string) => boolean): string | undefined;
190
+ /**
191
+ * What this pack is about to do about the runtime, as ONE sentence `ship` prints
192
+ * unconditionally.
193
+ *
194
+ * DECLARE RATHER THAN IMPLY — {@link appImageHostRequirements}'s rule, applied to
195
+ * the other half of the artifact. With a pin the claim is checkable: no network,
196
+ * and two packs of one build agree because the embedded bytes are fixed. Without
197
+ * one the pack still works and still produces a correct-architecture container —
198
+ * so refusing would cost the three architectures nothing here pins, for a
199
+ * property they never had. What it must not do is happen silently: an AppImage
200
+ * built this way carries bytes from a rolling tag that nothing in this tree can
201
+ * name, and the person who can fix that is the one reading this line.
202
+ */
203
+ export declare function appImageRuntimeNotice(archLabel: string, runtimeFile: string | undefined): string;
204
+ export declare function selectAppDirIcon(payload: readonly PayloadEntry[], appId: string): PayloadEntry | undefined;
205
+ /**
206
+ * Refuse a payload that cannot become an AppImage, naming the missing half.
207
+ *
208
+ * BEFORE appimagetool RATHER THAN THROUGH IT, and the two refusals it replaces
209
+ * are why: a `kind: 'cli'` project stages no desktop entry at all, and
210
+ * appimagetool answers that with `Desktop file not found, aborting` — true, and
211
+ * about a file the author never wrote and cannot find. Same for the icon. Both
212
+ * failures are decided by the project's OWN configuration (`gjsify.ship.kind`,
213
+ * `gjsify.ship.icon`), so the message that helps is the one that names the key.
214
+ *
215
+ * Separate from `assertToolsInstalled` and `assertHostCanFinish` for the reason
216
+ * those two are separate from each other (ADR 0024 § A3): three failures, three
217
+ * different fixes — a different machine, a package, or a line in package.json.
218
+ */
219
+ export declare function assertAppImageIsPackable(payload: readonly PayloadEntry[], settings: PackSettings, schemaCache?: PayloadEntry): void;
220
+ /**
221
+ * The whole AppDir as one payload: the prefix under `usr/`, plus the compiled
222
+ * schema cache, plus the three files at its root.
223
+ *
224
+ * THE SCHEMA CACHE IS AN ARGUMENT AND NOT A COMPILE, so this function stays pure
225
+ * and so the one impure step in the AppDir has exactly one home (`packOne`). It
226
+ * is prefix-relative on the way in — `share/glib-2.0/schemas/gschemas.compiled`,
227
+ * the same shape every other entry has — and gets the `usr/` prefix from the same
228
+ * map below, because a second spelling of that prefix is the thing this packer
229
+ * spent a comment explaining it must not have. `undefined` is legal and means the
230
+ * payload has no schemas at all; a payload that HAS them and no cache is refused
231
+ * by {@link assertAppImageIsPackable}, not silently packed.
232
+ *
233
+ * PURE, AND ONE LIST RATHER THAN A WRITER, which is the shape `msi.ts` and
234
+ * `dmg.ts` already have — `packOne` owns the single `writePayload` call for every
235
+ * format, and the packer module owns what goes in it. Two calls here would be two
236
+ * wipes, and the second would not clean the first: an AppDir root still holding
237
+ * the PREVIOUS app id's `.desktop` and icon is an AppDir appimagetool packs
238
+ * without complaint, because it looks for *a* desktop file and finds two.
239
+ *
240
+ * `usr/` AND NOT THE APPDIR ROOT — that prefix is the whole layout difference
241
+ * between this format and the `.deb` beside it, and it is why
242
+ * `FormatDescriptor.prefix` can stay `/usr`. The staged launcher walks up two
243
+ * directories from its own resolved path, so under a mount it computes
244
+ * `<mount>/usr` and every locator it exports is already right. A payload written
245
+ * at the root would make it compute the MOUNTPOINT, and the app would look for
246
+ * its schemas one directory too high — at exit 0, until the first
247
+ * `Gio.Settings.new()`.
248
+ */
249
+ export declare function appDirPayload(settings: PackSettings, payload: readonly PayloadEntry[], hostRequirements: readonly string[], schemaCache?: PayloadEntry): PayloadEntry[];
250
+ /**
251
+ * The argument vector, as data.
252
+ *
253
+ * Pure, so every flag is unit-testable on a host with no `appimagetool` — the
254
+ * same split `hdiutilCreateArgs` draws for `hdiutil` and `renderShipFlatpakManifest`
255
+ * for `flatpak-builder`: what the tool is TOLD is checkable everywhere, what the
256
+ * tool DOES needs the tool.
257
+ *
258
+ * `--no-appstream` IS A DECISION AND NOT A DEFAULT. appimagetool otherwise runs
259
+ * `appstreamcli validate-tree` over the AppDir, which would make the pack succeed
260
+ * or fail depending on whether a package unrelated to packaging is installed —
261
+ * green on a workstation with `appstream` and red in a container without it, over
262
+ * a payload that is byte-identical. A validator whose presence changes the
263
+ * outcome is the class this repository calls green-CI-that-checked-nothing, read
264
+ * backwards. The MetaInfo is still validated, by the tool that exists for it:
265
+ * `gjsify flatpak check --appstream` runs `appstreamcli validate --strict` on
266
+ * demand, where a missing `appstreamcli` is the point of the command rather than
267
+ * a surprise inside another one.
268
+ *
269
+ * `--comp zstd` is pinned for `hdiutilCreateArgs`' reason one format over: it is
270
+ * 1.9.1's current default, and a default is a thing that changes. The reader the
271
+ * oracle leg runs is `unsquashfs` from squashfs-tools, which needs its zstd
272
+ * support compiled in — measured present on Fedora 44 (4.6.1) and on
273
+ * ubuntu-24.04's 4.6.1. Pinning means the day appimagetool switches to something
274
+ * newer, this file changes and the reader is considered, instead of the reader
275
+ * silently losing the ability to open what we ship.
276
+ *
277
+ * `--runtime-file` IS PASSED WHEN THERE IS ONE TO PASS, and its absence is the
278
+ * only thing in this vector that is not a decision — see
279
+ * {@link findPinnedRuntime}. With it, the pack embeds bytes this tree pinned and
280
+ * needs no network (measured: the whole pack succeeds with the network blocked,
281
+ * and two packs are byte-identical). Without it appimagetool fetches its own, the
282
+ * pack is announced as doing so, and the artifact is still correct — just built
283
+ * from something nothing here can name.
284
+ */
285
+ export declare function appImageToolArgs(input: {
286
+ appDir: string;
287
+ target: string;
288
+ runtimeFile?: string;
289
+ }): string[];
290
+ /**
291
+ * The environment appimagetool is run with — both entries REQUIRED, neither a hint.
292
+ *
293
+ * `ARCH` because appimagetool derives the architecture from the ELF binaries it
294
+ * finds in the AppDir, and a `--app gjs` payload has none: it is JavaScript and a
295
+ * `/bin/sh` launcher. Without this the tool exits 1 with "Could not determine
296
+ * architecture automatically", on a payload that is perfectly fine. The value is
297
+ * the format row's `archName`, so the architecture inside the file and the one in
298
+ * its name are one decision.
299
+ *
300
+ * `APPIMAGE_EXTRACT_AND_RUN` because appimagetool is itself an AppImage and
301
+ * mounts itself through libfuse to start — see the module header. Set
302
+ * unconditionally: a native `appimagetool` build ignores it, so there is no host
303
+ * for which this is wrong and no flag for a CI author to discover.
304
+ */
305
+ export declare function appImageToolEnv(archLabel: string): Record<string, string>;
306
+ /**
307
+ * What a non-zero `appimagetool` exit is, in the order a host actually hits them.
308
+ *
309
+ * PURE, so the one message a user reads at the worst moment is assertable without
310
+ * a container — the same split {@link appImageToolArgs} draws. It was inline until
311
+ * the list was found to be missing its FIRST entry, which is the shape a message
312
+ * that nothing reads back tends to have.
313
+ *
314
+ * THE FIRST CAUSE DEPENDS ON WHETHER A RUNTIME WAS PINNED, and getting that
315
+ * wrong is what this function existed to fix in the first place: with no pin the
316
+ * download is where an offline host fails (measured — exit 1, no file, `Failed to
317
+ * download runtime file`), and with one there is no download to fail, so naming
318
+ * it would send a reader with a working network to debug their firewall. So the
319
+ * pinned branch names the pinned FILE instead, which is the thing that is new in
320
+ * that configuration and the only one this tree chose.
321
+ */
322
+ export declare function appImageToolFailureMessage(exit: string, runtimeFile?: string): string;
323
+ export interface AppImagePackInput {
324
+ appDir: string;
325
+ target: string;
326
+ archLabel: string;
327
+ verbose: boolean;
328
+ /** The pinned runtime to embed, or `undefined` to let appimagetool fetch one. */
329
+ runtimeFile?: string;
330
+ /** Working root, so the log can name the directory that became the image. */
331
+ workDir: string;
332
+ }
333
+ /**
334
+ * Run appimagetool over the AppDir.
335
+ *
336
+ * THE FAILURE PATHS ARE THE POINT. A packer that execs a foreign tool inherits
337
+ * that tool's silence, and appimagetool's container failures were MEASURED on
338
+ * `fedora:44` rather than guessed. Without `APPIMAGE_EXTRACT_AND_RUN=1` it exits
339
+ * **127** pointing at the AppImageKit FUSE wiki — which is why
340
+ * {@link appImageToolEnv} sets that variable and this branch should never see it.
341
+ * Without `file(1)` it exits **1** with "file command is missing but required",
342
+ * EVEN with `ARCH` set, which is the failure a minimal image really hits and
343
+ * which says nothing about packaging. And without a network it exits **1** having
344
+ * written nothing, because the runtime is fetched rather than embedded — see the
345
+ * module header. {@link appImageToolFailureMessage} names all four.
346
+ *
347
+ * AND THE ARTIFACT IS VERIFIED TO EXIST. appimagetool prints "Success" and a
348
+ * request to submit to AppImageHub; it does not guarantee the destination was
349
+ * written, and a packer whose contract is "there is a file at `target`" must not
350
+ * hand `statSync` a path that is not there and report the ENOENT as a `ship` bug.
351
+ */
352
+ export declare function buildAppImage(input: AppImagePackInput): Promise<void>;
353
+ /**
354
+ * Every path an AppDir holding this payload has, files and the directories above
355
+ * them, deepest LAST.
356
+ *
357
+ * Pure and separate from the stamping, so the "which paths" half is testable with
358
+ * no filesystem at all — and so the answer comes from the payload rather than
359
+ * from a directory walk, which would read back whatever the writer happened to
360
+ * leave rather than what it was told to write.
361
+ */
362
+ export declare function appDirPaths(payload: readonly PayloadEntry[]): string[];
363
+ /**
364
+ * Give every path in the AppDir one mtime.
365
+ *
366
+ * See the module header: this is the single difference between two packs of one
367
+ * build, measured by sha256. `utimesSync` on a directory is legal and is where a
368
+ * squashfs directory inode's time comes from, so directories are stamped as well
369
+ * — `writePayload`'s `mkdir` gives them the wall clock and nothing else would
370
+ * take it away.
371
+ */
372
+ export declare function stampAppDirTimes(appDir: string, payload: readonly PayloadEntry[], mtime: number): void;