@gjsify/cli 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/cli.gjs.mjs +266 -249
  2. package/lib/bundler-pick.d.ts +35 -0
  3. package/lib/bundler-pick.js +74 -7
  4. package/lib/cli-app.js +3 -1
  5. package/lib/commands/blueprint/index.d.ts +4 -0
  6. package/lib/commands/blueprint/index.js +15 -0
  7. package/lib/commands/blueprint/types.d.ts +8 -0
  8. package/lib/commands/blueprint/types.js +148 -0
  9. package/lib/commands/devtools.d.ts +21 -0
  10. package/lib/commands/devtools.js +146 -0
  11. package/lib/commands/index.d.ts +2 -0
  12. package/lib/commands/index.js +2 -0
  13. package/lib/commands/ship.js +56 -8
  14. package/lib/commands/webext/index.js +1 -1
  15. package/lib/types/config-data.d.ts +5 -0
  16. package/lib/utils/oxc-resolve.js +9 -7
  17. package/lib/utils/ship/app-runtime.d.ts +7 -5
  18. package/lib/utils/ship/app-runtime.js +11 -8
  19. package/lib/utils/ship/appimage.d.ts +9 -0
  20. package/lib/utils/ship/appimage.js +2 -1
  21. package/lib/utils/ship/deb.d.ts +10 -0
  22. package/lib/utils/ship/deb.js +7 -0
  23. package/lib/utils/ship/depends.d.ts +37 -3
  24. package/lib/utils/ship/depends.js +21 -9
  25. package/lib/utils/ship/flatpak-config.d.ts +16 -0
  26. package/lib/utils/ship/flatpak-config.js +26 -0
  27. package/lib/utils/ship/formats.js +10 -7
  28. package/lib/utils/ship/gi-namespaces.d.ts +16 -0
  29. package/lib/utils/ship/gi-namespaces.js +64 -1
  30. package/lib/utils/ship/launcher.js +2 -2
  31. package/lib/utils/ship/layout.d.ts +7 -6
  32. package/lib/utils/ship/layout.js +42 -16
  33. package/lib/utils/ship/rpm.d.ts +5 -0
  34. package/lib/utils/ship/rpm.js +42 -0
  35. package/lib/utils/ship/settings.js +6 -0
  36. package/lib/utils/ship/signing.d.ts +7 -2
  37. package/lib/utils/ship/signing.js +59 -32
  38. package/lib/utils/ship/stage-manifest.d.ts +14 -0
  39. package/lib/utils/ship/stage-manifest.js +7 -0
  40. package/lib/utils/ship/types.d.ts +2 -0
  41. package/lib/utils/webext/targets.d.ts +1 -1
  42. package/lib/utils/webext/targets.js +20 -8
  43. package/package.json +20 -19
@@ -34,6 +34,7 @@ import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.j
34
34
  import { discoverPayload } from '../utils/ship/discover.js';
35
35
  import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
36
36
  import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
37
+ import { assertNoDroppedFlatpakModules } from '../utils/ship/flatpak-config.js';
37
38
  import { buildFlatpakBundle } from '../utils/ship/flatpak.js';
38
39
  import { iconSizes, resolveAppIcon } from '../utils/ship/icons.js';
39
40
  import { localizeMetadata } from '../utils/ship/localize-metadata.js';
@@ -45,7 +46,7 @@ import { compileSchemasForPayload, compileSchemasForStage } from '../utils/ship/
45
46
  import { buildMsi, MSI_PAYLOAD_DIR } from '../utils/ship/msi.js';
46
47
  import { buildZip, zipEntriesFromPayload } from '../utils/ship/zip.js';
47
48
  import { assertEntryRunsUnder } from '../utils/ship/entry-interpreter.js';
48
- import { scanGiNamespaces } from '../utils/ship/gi-namespaces.js';
49
+ import { scanGiRequirements } from '../utils/ship/gi-namespaces.js';
49
50
  import { assertLauncherMatchesInterpreter, assertPayloadMatchesArch, buildTimestamp, isArchIndependent, linuxInstallDependent, readPayloadFacts, } from '../utils/ship/payload.js';
50
51
  import { assertOverlayIsLicensed, planOverlay, planStage } from '../utils/ship/plan.js';
51
52
  import { assertHostCanSign, notarizeArtifact, stapleArtifact, resolveNotaryPlan, resolveSignPlan, signPayload, } from '../utils/ship/signing.js';
@@ -252,6 +253,10 @@ async function assemble(args) {
252
253
  'thing as an error.');
253
254
  }
254
255
  }
256
+ // Under `--stage` too: the stage records the Flatpak settings, and a stage
257
+ // that quietly omits the project's modules packs without them later.
258
+ if (formats.some((format) => format.id === 'flatpak'))
259
+ assertNoDroppedFlatpakModules(flatpak);
255
260
  if (!args.stage)
256
261
  assertPackable(formats, layout, args.os === undefined ? 'host' : 'positional', unusable);
257
262
  // BEFORE the build, beside `assertCanPack` and for the same reason: a
@@ -537,9 +542,20 @@ async function assemble(args) {
537
542
  // `gi://` specifiers are what the emitted `Depends:` is derived from
538
543
  // (ADR 0024 § 6), and the packing host has the staged copy but no way to
539
544
  // tell which staged file is the entry.
540
- const namespaces = scanGiNamespaces(bundleSource);
541
- if (args.verbose)
545
+ const gi = scanGiRequirements(bundleSource);
546
+ const namespaces = gi.namespaces;
547
+ const optionalNamespaces = gi.optional;
548
+ if (args.verbose) {
549
+ const hard = namespaces.filter((ns) => !optionalNamespaces.includes(ns));
542
550
  console.log(`${LOG} gi namespaces: ${namespaces.join(', ') || '(none)'}`);
551
+ // The split is worth its own line whenever it is non-empty: an author who
552
+ // wrote `&optional` and sees the namespace under the plain list has learned
553
+ // nothing about whether their package will declare it.
554
+ if (optionalNamespaces.length > 0) {
555
+ console.log(`${LOG} gi optional namespaces: ${optionalNamespaces.join(', ')}`);
556
+ console.log(`${LOG} gi required namespaces: ${hard.join(', ') || '(none)'}`);
557
+ }
558
+ }
543
559
  // PLACED, like the payload, and through the same map. `planOverlay` answers a
544
560
  // question about the FORMAT (`share/doc/<pkg>/copyright` for Debian policy,
545
561
  // `share/licenses/<pkg>/LICENSE` for rpm) in the prefix-relative shape every
@@ -578,8 +594,9 @@ async function assemble(args) {
578
594
  // into a Debian `Depends:`, at exit 0).
579
595
  if (format.depends === null)
580
596
  continue;
581
- deriveDepends(format.depends, {
597
+ const derived = deriveDepends(format.depends, {
582
598
  namespaces,
599
+ optionalNamespaces,
583
600
  hasIcons: facts.hasIcons,
584
601
  hasSchemas: facts.hasSchemas,
585
602
  interpreter: settings.app,
@@ -589,6 +606,17 @@ async function assemble(args) {
589
606
  minGjsVersion: settings.minGjsVersion,
590
607
  minNodeVersion: settings.minNodeVersion,
591
608
  });
609
+ // Reported here and not inside `deriveDepends`, which is also called by
610
+ // `packOne` on a host that has no project — and this is the only place that
611
+ // does. An optional namespace nobody mapped produces NO package field, and
612
+ // the app runs without the typelib either way, so nothing downstream will
613
+ // ever say so again.
614
+ for (const namespace of derived.unmappedOptional) {
615
+ console.warn(`${LOG} gi://${namespace} is imported optionally, but no ${format.depends} package is known ` +
616
+ 'to ship its typelib, so no Recommends entry names it. The package builds and runs ' +
617
+ `without it. Add the row if the namespace should be recommended: \`gjsify.ship.typelibPackages.` +
618
+ `${namespace} = { "${format.depends}": "…" }\`.`);
619
+ }
592
620
  // One warning per package, for the interpreter it actually declares. A
593
621
  // floor warning about a dependency this package does not emit is noise,
594
622
  // and a noisy warning is the one nobody reads when it matters.
@@ -602,7 +630,16 @@ async function assemble(args) {
602
630
  // is sometimes packable elsewhere and sometimes not is a worse contract
603
631
  // than one that always is, and it is what lets the e2e suite compare the
604
632
  // one-process artifact with the two-phase one.
605
- const manifest = writeStageManifest({ stageDir, settings, formats, mtime, namespaces, staged, overlay });
633
+ const manifest = writeStageManifest({
634
+ stageDir,
635
+ settings,
636
+ formats,
637
+ mtime,
638
+ namespaces,
639
+ optionalNamespaces,
640
+ staged,
641
+ overlay,
642
+ });
606
643
  if (args.stage) {
607
644
  // `(none)` spelled out, because an empty list printed as nothing after
608
645
  // "formats " reads as a truncated line rather than as a fact. WHICH fact
@@ -638,6 +675,7 @@ async function assemble(args) {
638
675
  stageDir,
639
676
  outRoot,
640
677
  namespaces,
678
+ optionalNamespaces,
641
679
  mtime,
642
680
  sign,
643
681
  notarize,
@@ -799,6 +837,9 @@ async function finishFromStage(args, fromStage) {
799
837
  for (const file of manifest.staged)
800
838
  console.log(`${LOG} ${file.path}`);
801
839
  console.log(`${LOG} gi namespaces: ${manifest.namespaces.join(', ') || '(none)'}`);
840
+ if ((manifest.optionalNamespaces ?? []).length > 0) {
841
+ console.log(`${LOG} gi optional namespaces: ${manifest.optionalNamespaces?.join(', ')}`);
842
+ }
802
843
  }
803
844
  // The stage's own directory is not written to — `writeStage` wipes what it
804
845
  // is pointed at, and pointing it at an arriving stage would delete the
@@ -832,6 +873,7 @@ async function finishFromStage(args, fromStage) {
832
873
  stageDir,
833
874
  outRoot,
834
875
  namespaces: manifest.namespaces,
876
+ optionalNamespaces: manifest.optionalNamespaces ?? [],
835
877
  mtime,
836
878
  sign,
837
879
  notarize,
@@ -858,10 +900,11 @@ async function packOne(input) {
858
900
  // this answers used to be answered from the project's file lists, which are
859
901
  // absolute paths on the build host and therefore unavailable here.
860
902
  const facts = readPayloadFacts(assembled);
861
- const depends = format.depends === null
862
- ? []
903
+ const derived = format.depends === null
904
+ ? { requires: [], recommends: [], unmappedOptional: [] }
863
905
  : deriveDepends(format.depends, {
864
906
  namespaces: input.namespaces,
907
+ optionalNamespaces: input.optionalNamespaces,
865
908
  hasIcons: facts.hasIcons,
866
909
  hasSchemas: facts.hasSchemas,
867
910
  interpreter: settings.app,
@@ -871,6 +914,8 @@ async function packOne(input) {
871
914
  minGjsVersion: settings.minGjsVersion,
872
915
  minNodeVersion: settings.minNodeVersion,
873
916
  });
917
+ const depends = derived.requires;
918
+ const recommends = derived.recommends;
874
919
  // `--arch` is a CLAIM about the payload and nothing else compared it to one:
875
920
  // an x86-64 `.so` staged with `--arch arm64` packed at exit 0 and `rpm -qp
876
921
  // --qf %{ARCH}` confirmed `aarch64`, because rpm reads the header and the
@@ -956,7 +1001,9 @@ async function packOne(input) {
956
1001
  })
957
1002
  : assembled;
958
1003
  const archLabel = format.archName(input.arch, isArchIndependent(payload));
959
- const common = { settings, payload, prefix: format.prefix, depends, archLabel, mtime };
1004
+ // `recommends` rides the shared input object, so a format that does not write the
1005
+ // field (every non-deb/rpm row) simply ignores an extra property it never reads.
1006
+ const common = { settings, payload, prefix: format.prefix, depends, recommends, archLabel, mtime };
960
1007
  const outDir = join(outRoot, 'out');
961
1008
  mkdirSync(outDir, { recursive: true });
962
1009
  const target = join(outDir, format.fileName(settings, archLabel));
@@ -1010,6 +1057,7 @@ async function packOne(input) {
1010
1057
  minGjsVersion: settings.minGjsVersion,
1011
1058
  minNodeVersion: settings.minNodeVersion,
1012
1059
  namespaces: input.namespaces,
1060
+ optionalNamespaces: input.optionalNamespaces,
1013
1061
  bundledTypelibs: facts.bundledTypelibs,
1014
1062
  });
1015
1063
  const appDir = appDirFor(outRoot);
@@ -5,7 +5,7 @@
5
5
  import { webextBuildCommand, webextDevCommand, webextZipCommand } from './subcommands.js';
6
6
  export const webextCommand = {
7
7
  command: 'webext <subcommand>',
8
- description: 'Browser extensions: build, zip and develop one WebExtension for Chrome, Edge, Firefox and Safari.',
8
+ description: 'Browser extensions: build, zip and develop one WebExtension for Chrome, Edge, Firefox, Opera and Safari.',
9
9
  builder: (yargs) => yargs
10
10
  .command(webextBuildCommand.command, webextBuildCommand.description, webextBuildCommand.builder, webextBuildCommand.handler)
11
11
  .command(webextZipCommand.command, webextZipCommand.description, webextZipCommand.builder, webextZipCommand.handler)
@@ -706,6 +706,11 @@ export interface ShipSignOptions {
706
706
  * the one thing ADR 0024 § 8 gates the whole migration on. A project that has
707
707
  * to BUILD something inside the sandbox still has `gjsify flatpak init` +
708
708
  * `gjsify flatpak build`, unchanged.
709
+ *
710
+ * Setting `gjsify.flatpak.modules` / `extraModules` and shipping the `flatpak`
711
+ * target is REFUSED (`assertNoDroppedFlatpakModules`) rather than ignored:
712
+ * those keys are read by `flatpak init` only, and dropping them at exit 0 hid an
713
+ * `extra-data` module until the app ran without its downloaded binary.
709
714
  */
710
715
  export interface ShipFlatpakOptions {
711
716
  /**
@@ -41,8 +41,8 @@
41
41
  // `GJSIFY_OXFMT=native|npm` is the explicit override (native throws when the
42
42
  // prebuild isn't loadable instead of silently switching engines).
43
43
  //
44
- // oxlint has NO native path yet: its JS-plugin host (the internal
45
- // `gjsify/register-class-order` rule wired via `.oxlintrc.json` jsPlugins)
44
+ // oxlint has NO native path yet: its JS-plugin host (the
45
+ // `@gjsify/oxlint-plugin-gjsify` rules wired via `.oxlintrc.json` jsPlugins)
46
46
  // lives in the Node launcher, so a native oxlint bridge could only run the
47
47
  // Rust rule subset. Lint stays Node-spawned until that trade-off is decided
48
48
  // (tracked in status/open-todos/README.md).
@@ -264,11 +264,13 @@ async function tryLoadNativeOxfmt() {
264
264
  // `bundler-pick.ts`'s `tryLoadNative()` gives — so a library that will
265
265
  // not load names itself instead of failing inside `runOxfmt()`.
266
266
  // Resolved and imported, not statically imported — the reasoning is
267
- // at the top of this file. By the time a formatter asks for the
268
- // native backend, utils' `lib/esm` is built. `./native-library`
269
- // rather than `./core`: `core` re-exports `main-loop`, whose
270
- // module-level singleton would then exist twice in a process that
271
- // already carries it inlined in the GJS bundle.
267
+ // at the top of this file. `./native-library` rather than `./core`:
268
+ // `core` re-exports `main-loop`, whose module-level singleton would
269
+ // then exist twice in a process that already carries it inlined in the
270
+ // GJS bundle. Off disk, so the walk follows the WORKSPACE and
271
+ // `@gjsify/utils build:esm` must precede this call — the same
272
+ // precondition `bundler-pick.ts` carries, held by rule 5 of
273
+ // `scripts/check-build-infra-order.mjs`.
272
274
  //
273
275
  // Its own `try` because the outer one cannot tell this apart from
274
276
  // "there is no native backend", and `_nativeOxfmtLoadError` is the
@@ -102,8 +102,8 @@ export interface ResolvedNodeGiPackage {
102
102
  * NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
103
103
  * `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
104
104
  * `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
105
- * compiles against. And it lists `prebuilds/`, which this module stages into
106
- * `Contents/Frameworks` instead, where a `.app` keeps loadable code.
105
+ * compiles against. And it lists `prebuilds/`, which this module stages under
106
+ * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
107
107
  *
108
108
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
109
109
  * `package.json` (the `exports` map is what resolves every subpath), every `.js`
@@ -120,15 +120,17 @@ export declare function resolveNodeGiPackage(options?: ResolveRuntimeOptions): R
120
120
  * literals, so the paths the launcher exports and the paths the stager writes
121
121
  * cannot drift: they are computed here once and both callers read this result.
122
122
  *
123
- * `Contents/Frameworks/node-gi/prebuilds/<target>/` is node-gi's OWN sibling
124
- * layout reproduced verbatim inside `dirs.native`. It is not a naming preference:
123
+ * `<dirs.native>/node-gi/prebuilds/<target>/` is node-gi's OWN sibling layout
124
+ * reproduced verbatim inside `Layout.dirs().native` (`Contents/Resources/native`
125
+ * on a `.app` — `layout.ts`'s darwin row carries why that is not
126
+ * `Contents/Frameworks`). It is not a naming preference:
125
127
  * `resolveGtkRuntimeBundle()` probes `<dir>/lib` and `<dir>/girepository-1.0`,
126
128
  * the addon's `@rpath` is `@loader_path/gtk/lib`, and `loaders.cache` addresses
127
129
  * each pixbuf loader relative to the bundle toplevel. Reproducing the shape is
128
130
  * what makes every one of those relations still true after the copy.
129
131
  */
130
132
  export interface AppRuntimePaths {
131
- /** `<App>.app/Contents/Frameworks/node-gi` — the package root the layout imitates. */
133
+ /** `<App>.app/Contents/Resources/native/node-gi` — the package root the layout imitates. */
132
134
  nodeGiRoot: string;
133
135
  /**
134
136
  * `<App>.app/Contents/Resources/lib/node_modules/@gjsify/node-gi` — where
@@ -47,11 +47,14 @@
47
47
  // THAT directory — so `prebuildAddonPath()` looks for
48
48
  // `…/node_modules/@gjsify/node-gi/prebuilds/<target>/node_gi.node` and
49
49
  // `resolveGtkRuntimeBundle()`'s candidate 2 for `…/prebuilds/<target>/gtk`, and
50
- // neither is where a `.app` may keep loadable code: `Contents/Frameworks` is, and
51
- // it is what `codesign` now reaches (ADR 0024 § A4) — M6 landed, and
52
- // `utils/ship/signing.ts` re-signs every Mach-O in the payload by magic number,
53
- // which is most of this closure. Putting it under `Resources` to save two
54
- // `export` lines would trade a correct bundle for a shorter launcher.
50
+ // neither is where a `.app` may keep loadable code: `Layout.dirs().native` is, and
51
+ // `utils/ship/signing.ts` re-signs every Mach-O in the payload by magic number
52
+ // (ADR 0024 § A4 — M6 landed), which is most of this closure. What `native` may NOT
53
+ // be is `Contents/Frameworks`: `codesign` reads a version-named DIRECTORY there as
54
+ // a nested bundle and refuses the seal, which is the whole reason this file's
55
+ // output went into `Contents/Resources/native` instead — see `layout.ts`'s darwin
56
+ // row for the measurement and `gjsify ship darwin --sign` for what it used to
57
+ // cost.
55
58
  //
56
59
  // WHY NOT `gjsify.ship.bundledTypelibs`. Because it FLATTENS. `plan.ts` stages
57
60
  // each such file as `posix.join(libDir, 'gi', basename(file))`, and
@@ -210,8 +213,8 @@ function resolveStagedPackage(specifier, options) {
210
213
  * NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
211
214
  * `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
212
215
  * `.app` never runs — and the `.d.ts` declarations, which nothing inside a bundle
213
- * compiles against. And it lists `prebuilds/`, which this module stages into
214
- * `Contents/Frameworks` instead, where a `.app` keeps loadable code.
216
+ * compiles against. And it lists `prebuilds/`, which this module stages under
217
+ * `Layout.dirs().native`, the directory a `.app` keeps loadable code in.
215
218
  *
216
219
  * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
217
220
  * `package.json` (the `exports` map is what resolves every subpath), every `.js`
@@ -252,7 +255,7 @@ export function appRuntimePaths(layout, identity, target) {
252
255
  addonPath: posix.join(prebuildDir, NODE_GI_ADDON_FILENAME),
253
256
  // `Contents/MacOS`, because that is where a `.app` keeps executables and
254
257
  // because `$here` — which the launcher already computes — is then the whole
255
- // path expression. `Contents/Frameworks` would work for dyld and would make
258
+ // path expression. `dirs.native` would work for dyld and would make
256
259
  // the launcher walk back up for the one file it execs. On Windows
257
260
  // `dirs.launcher` is the program directory itself, so the interpreter lands
258
261
  // beside the `.cmd` for the same reason.
@@ -89,6 +89,15 @@ export declare function appImageHostRequirements(input: {
89
89
  minGjsVersion?: string;
90
90
  minNodeVersion?: string;
91
91
  namespaces: readonly string[];
92
+ /**
93
+ * The subset of {@link namespaces} the bundle marks optional (ADR 0087).
94
+ *
95
+ * An AppImage has no `Depends:` field, so "optional" cannot become a package
96
+ * list here — it becomes WORDING instead. That is not cosmetic: an AppRun user
97
+ * told a typelib is required and lacking it goes looking for another distro
98
+ * package, while one told it is optional reads the same sentence correctly.
99
+ */
100
+ optionalNamespaces?: readonly string[];
92
101
  bundledTypelibs?: readonly string[];
93
102
  }): string[];
94
103
  /**
@@ -204,9 +204,10 @@ export function appDirFor(outRoot) {
204
204
  */
205
205
  export function appImageHostRequirements(input) {
206
206
  const floor = interpreterFloor(input);
207
+ const optional = new Set(input.optionalNamespaces ?? []);
207
208
  return [
208
209
  `${input.app} (>= ${floor})`,
209
- ...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => `the ${ns} typelib`),
210
+ ...hostProvidedNamespaces(input.namespaces, input.bundledTypelibs).map((ns) => optional.has(ns) ? `the ${ns} typelib (optional)` : `the ${ns} typelib`),
210
211
  ];
211
212
  }
212
213
  /**
@@ -8,6 +8,16 @@ export interface DebInputs {
8
8
  prefix: string;
9
9
  /** Already-derived dependency list, in `name`/`name >= version` form. */
10
10
  depends: readonly string[];
11
+ /**
12
+ * Soft dependencies, rendered as `Recommends:`.
13
+ *
14
+ * Debian Policy § 7.2: a recommended package is installed by default and
15
+ * `--no-install-recommends` is the user's way out. That is what an `&optional`
16
+ * typelib is (ADR 0087) — the app runs without it and loses a feature — so
17
+ * putting it in `Depends:` would let an optional integration decide whether
18
+ * the package installs at all.
19
+ */
20
+ recommends?: readonly string[];
11
21
  /** Debian architecture (`amd64`, `all`, …). */
12
22
  archLabel: string;
13
23
  /** Unix seconds stamped into every header. */
@@ -104,6 +104,13 @@ function renderControl(inputs, installedSize) {
104
104
  ];
105
105
  if (inputs.depends.length > 0)
106
106
  fields.push(['Depends', inputs.depends.map(formatDebDepend).join(', ')]);
107
+ // After `Depends`, which is the field order dpkg-deb --control writes and the
108
+ // one `grep '^Depends:'` on a control file assumes. Omitted when empty rather
109
+ // than written as `Recommends:` with nothing after it — a field with an empty
110
+ // value is a parse error here, the same reason `formatDebDepend` exists.
111
+ if (inputs.recommends !== undefined && inputs.recommends.length > 0) {
112
+ fields.push(['Recommends', inputs.recommends.map(formatDebDepend).join(', ')]);
113
+ }
107
114
  fields.push(['Section', settings.section], ['Priority', 'optional']);
108
115
  if (settings.homepage)
109
116
  fields.push(['Homepage', settings.homepage]);
@@ -43,6 +43,17 @@ export declare function warnAboutNodeFloor(format: DistroFormatId, floor: string
43
43
  export interface DependsInputs {
44
44
  /** GI namespaces the bundle imports, in `Ns-Version` spelling. */
45
45
  namespaces: readonly string[];
46
+ /**
47
+ * The subset of {@link namespaces} the bundle declares OPTIONAL (ADR 0087 —
48
+ * `gi://Ns?version=X&optional`): a `Recommends:` entry, never a `Depends:`.
49
+ *
50
+ * Kept as its own list rather than filtered out of {@link namespaces}, because
51
+ * the AppImage has no package list at all and still has to SAY which of its host
52
+ * requirements are optional — and because a stage manifest written before the
53
+ * split carries no such field, where answering "everything is hard" is the safe
54
+ * direction.
55
+ */
56
+ optionalNamespaces?: readonly string[];
46
57
  /**
47
58
  * Whether the payload installs into `share/icons/hicolor/`.
48
59
  *
@@ -112,11 +123,34 @@ export interface DependsInputs {
112
123
  */
113
124
  export declare function hostProvidedNamespaces(namespaces: readonly string[], bundledTypelibs?: readonly string[]): string[];
114
125
  /**
115
- * The `Depends:` / `Requires:` list for one format.
126
+ * One format's dependency lists, split by how hard the requirement is.
127
+ *
128
+ * `requires` is what an absent package makes the app fail over; `recommends` is what
129
+ * its absence only costs. A namespace the BUNDLE marked optional (ADR 0087) is the
130
+ * second kind by declaration — the app's own `check()` degrades — and both `.deb`
131
+ * and `.rpm` have a field for it, so there is no reason to declare it hard.
132
+ */
133
+ export interface DerivedDepends {
134
+ /** `Depends:` / `Requires:` — a package the artifact cannot run without. */
135
+ requires: string[];
136
+ /** `Recommends:` — installed by default where the format has such a field. */
137
+ recommends: string[];
138
+ /**
139
+ * Optional namespaces no table row is known for.
140
+ *
141
+ * REPORTED, never fatal: a `Recommends:` line for a namespace nobody mapped is
142
+ * the one case where saying nothing is correct — the app runs without the
143
+ * typelib either way, which is precisely what the flag declared. The HARD
144
+ * unmapped case still throws, below.
145
+ */
146
+ unmappedOptional: string[];
147
+ }
148
+ /**
149
+ * The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
116
150
  *
117
- * @throws when a namespace has no entry in the table — see the module header.
151
+ * @throws when a HARD namespace has no entry in the table — see the module header.
118
152
  */
119
- export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): string[];
153
+ export declare function deriveDepends(format: DistroFormatId, inputs: DependsInputs): DerivedDepends;
120
154
  /** One parsed dependency. `relation`/`version` are absent for an unversioned one. */
121
155
  export interface ParsedDepend {
122
156
  name: string;
@@ -287,9 +287,9 @@ export function hostProvidedNamespaces(namespaces, bundledTypelibs = []) {
287
287
  return [...new Set(namespaces)].sort().filter((ns) => !BUNDLED_TYPELIB.test(ns) && !shipped.has(ns));
288
288
  }
289
289
  /**
290
- * The `Depends:` / `Requires:` list for one format.
290
+ * The `Depends:` / `Requires:` list for one format, plus its `Recommends:`.
291
291
  *
292
- * @throws when a namespace has no entry in the table — see the module header.
292
+ * @throws when a HARD namespace has no entry in the table — see the module header.
293
293
  */
294
294
  export function deriveDepends(format, inputs) {
295
295
  // The interpreter the launcher execs, and nothing else: a package declares ONE.
@@ -297,17 +297,25 @@ export function deriveDepends(format, inputs) {
297
297
  // no system Node, so an artifact for those CARRIES one from
298
298
  // `@gjsify/node-runtime-<target>`, and that is the whole reason those three
299
299
  // packages exist and this one line is their opposite.
300
- const out = inputs.interpreter === 'node'
300
+ const requires = inputs.interpreter === 'node'
301
301
  ? [`${NODE_PACKAGE[format]} >= ${inputs.minNodeVersion ?? DEFAULT_NODE_FLOOR}`]
302
302
  : [`gjs >= ${inputs.minGjsVersion ?? DEFAULT_GJS_FLOOR}`];
303
+ const recommends = [];
304
+ const optional = new Set(inputs.optionalNamespaces ?? []);
303
305
  const unmapped = [];
306
+ const unmappedOptional = [];
304
307
  for (const namespace of hostProvidedNamespaces(inputs.namespaces, inputs.bundledTypelibs)) {
305
308
  const entry = lookupTypelib(namespace, inputs.typelibPackages);
306
309
  if (entry === undefined) {
307
- unmapped.push(namespace);
310
+ (optional.has(namespace) ? unmappedOptional : unmapped).push(namespace);
308
311
  continue;
309
312
  }
310
- out.push(entry[format]);
313
+ // A namespace imported both ways is a HARD dependency — one importer asked
314
+ // for it unguarded — and a package manager field may not name it twice.
315
+ if (optional.has(namespace) && !requires.includes(entry[format]))
316
+ recommends.push(entry[format]);
317
+ else
318
+ requires.push(entry[format]);
311
319
  }
312
320
  if (unmapped.length > 0) {
313
321
  throw new Error(`gjsify ship: the bundle imports ${unmapped.map((n) => `gi://${n.split('-')[0]}`).join(', ')}, ` +
@@ -323,7 +331,7 @@ export function deriveDepends(format, inputs) {
323
331
  // The package that owns `/usr/share/icons/hicolor` — so this follows the icons, not the
324
332
  // app/cli distinction that used to stand in for them.
325
333
  if (inputs.hasIcons)
326
- out.push('hicolor-icon-theme');
334
+ requires.push('hicolor-icon-theme');
327
335
  // The package that ships `glib-compile-schemas`, NOT `gsettings-desktop-schemas`
328
336
  // (which ships GNOME's own `org.gnome.desktop.*` schemas and has nothing to
329
337
  // do with compiling ours). Measured: `rpm -qf /usr/bin/glib-compile-schemas`
@@ -333,11 +341,15 @@ export function deriveDepends(format, inputs) {
333
341
  // `Gio.Settings.new()` aborts the app — an install that succeeds and an app
334
342
  // that does not start.
335
343
  if (inputs.hasSchemas)
336
- out.push(SCHEMA_COMPILER_PACKAGE[format]);
337
- out.push(...inputs.extra);
344
+ requires.push(SCHEMA_COMPILER_PACKAGE[format]);
345
+ requires.push(...inputs.extra);
338
346
  // Set-dedupe keeps first-insertion order, so the interpreter stays first and
339
347
  // the list is stable across runs.
340
- return [...new Set(out)];
348
+ return {
349
+ requires: [...new Set(requires)],
350
+ recommends: [...new Set(recommends)].filter((name) => !requires.includes(name)),
351
+ unmappedOptional,
352
+ };
341
353
  }
342
354
  /**
343
355
  * Resolve a scanned specifier to a table row.
@@ -64,3 +64,19 @@ export interface ResolvedShipFlatpak {
64
64
  * has somewhere else to point at.
65
65
  */
66
66
  export declare function resolveShipFlatpakSettings(input: ShipFlatpakInput): ResolvedShipFlatpak;
67
+ /**
68
+ * Refuse a Flatpak ship that would silently drop the project's own modules.
69
+ *
70
+ * `gjsify.flatpak.modules` / `extraModules` are read by `gjsify flatpak init`
71
+ * only; `ship` renders exactly one module, the staged payload (ADR 0024 § 2),
72
+ * and the keys' absence from {@link ShipFlatpakOptions} is deliberate. The
73
+ * defect was the silence: a project that moved to `ship` kept the keys, got a
74
+ * manifest without its `extra-data` module and exit 0, and found out when the
75
+ * app started without the binary the module downloads. A key `ship` does not
76
+ * read must not be one it ignores quietly, so the run stops BEFORE the build.
77
+ *
78
+ * Called only when `flatpak` is among the formats being made: the same keys
79
+ * still drive `gjsify flatpak init`, and a `ship --target deb` has nothing to
80
+ * drop them from.
81
+ */
82
+ export declare function assertNoDroppedFlatpakModules(flatpak: ConfigDataFlatpak): void;
@@ -120,6 +120,32 @@ export function resolveShipFlatpakSettings(input) {
120
120
  };
121
121
  return { settings, warnings: fromLegacy.length === 0 ? [] : [deprecationWarning(fromLegacy)] };
122
122
  }
123
+ /**
124
+ * Refuse a Flatpak ship that would silently drop the project's own modules.
125
+ *
126
+ * `gjsify.flatpak.modules` / `extraModules` are read by `gjsify flatpak init`
127
+ * only; `ship` renders exactly one module, the staged payload (ADR 0024 § 2),
128
+ * and the keys' absence from {@link ShipFlatpakOptions} is deliberate. The
129
+ * defect was the silence: a project that moved to `ship` kept the keys, got a
130
+ * manifest without its `extra-data` module and exit 0, and found out when the
131
+ * app started without the binary the module downloads. A key `ship` does not
132
+ * read must not be one it ignores quietly, so the run stops BEFORE the build.
133
+ *
134
+ * Called only when `flatpak` is among the formats being made: the same keys
135
+ * still drive `gjsify flatpak init`, and a `ship --target deb` has nothing to
136
+ * drop them from.
137
+ */
138
+ export function assertNoDroppedFlatpakModules(flatpak) {
139
+ const dropped = ['modules', 'extraModules'].filter((key) => (flatpak[key]?.length ?? 0) > 0);
140
+ if (dropped.length === 0)
141
+ return;
142
+ const list = dropped.map((key) => `gjsify.flatpak.${key}`).join(' and ');
143
+ throw new Error(`gjsify ship: ${list} ${dropped.length > 1 ? 'are' : 'is'} set, but the \`flatpak\` target renders ` +
144
+ 'ONE module, the staged payload, and would drop ' +
145
+ `${dropped.length > 1 ? 'them' : 'it'} (ADR 0024 § 2). Build this Flatpak with ` +
146
+ '`gjsify flatpak init` + `gjsify flatpak build`, which honour the key, or leave `flatpak` out of ' +
147
+ '`--target` / `gjsify.ship.targets`.');
148
+ }
123
149
  /** One line, naming every key still read from the old block and where each goes. */
124
150
  function deprecationWarning(keys) {
125
151
  const list = keys.map((key) => `gjsify.flatpak.${key}`).join(', ');
@@ -132,12 +132,14 @@ function macosArch(arch, _archIndependent) {
132
132
  /**
133
133
  * `process.arch` → the name Windows and everything that targets it use.
134
134
  *
135
- * ONE ROW, and it is not caution: `wingtk/gvsbuild` hardcodes
136
- * `self.platform = "x64"` and publishes no arm64 GTK, so there is nothing to build
137
- * `@gjsify/gtk-runtime-win32-arm64` out of and no GTK for a Windows/ARM artifact
138
- * to load (#1117). `Layout.arches` refuses the same value one phase earlier, at
139
- * stage time, with that reason spelled out; this table is the second half — the
140
- * one that stops a stage assembled by an older gjsify from acquiring a label here.
135
+ * ONE ROW, and it is not caution: there is no arm64 GTK CLOSURE to put on it yet.
136
+ * `wingtk/gvsbuild` publishes no arm64 assets, which is why ADR 0089 builds that
137
+ * prefix from our `gjsify/gvsbuild` fork in CI instead — but what a Windows
138
+ * artifact stages is `@gjsify/gtk-runtime-*`, and the arm64 one is not built, so
139
+ * there is nothing for a Windows/ARM artifact to load (#1117). `Layout.arches`
140
+ * refuses the same value one phase earlier, at stage time, with that reason spelled
141
+ * out; this table is the second half — the one that stops a stage assembled by an
142
+ * older gjsify from acquiring a label here.
141
143
  *
142
144
  * `x64` maps to itself, and unlike the macOS table that is not a coincidence to be
143
145
  * caught later: `x64` is the spelling Node's own release archives use
@@ -734,7 +736,8 @@ export const FORMATS = {
734
736
  // * there is no `Info.plist` and no metadata file at all: what a Windows
735
737
  // installer says about an application lives in the `.msi`'s own tables
736
738
  // (#1354 M5), which is why `Layout.metadata` answers `[]` here.
737
- // * `x64` alone, and the blocker is upstream — see `WINDOWS_ARCH`.
739
+ // * `x64` alone, and the blocker is a package of ours we have not built —
740
+ // see `WINDOWS_ARCH`.
738
741
  //
739
742
  // NEITHER IS SIGNED BY DEFAULT, and Windows is the softer of the two asymmetries
740
743
  // ADR 0024 § A5 records: Gatekeeper BLOCKS an unsigned `.app`, while SmartScreen
@@ -1,7 +1,23 @@
1
+ /**
2
+ * What a bundle loads, split by whether the host must provide it.
3
+ *
4
+ * `namespaces` is the union — the same answer {@link scanGiNamespaces} has always
5
+ * given — and `optional` is the subset the artifact itself marks optional. A namespace
6
+ * in both is still a hard dependency as far as anything else is concerned: the
7
+ * importer asked for it both ways, and only one of those edges degrades.
8
+ */
9
+ export interface GiRequirements {
10
+ /** Every GI namespace the bundle loads, `Ns-Version` where a specifier pins one. */
11
+ namespaces: string[];
12
+ /** The subset carrying the bundler's optional marker — absent is not fatal. */
13
+ optional: string[];
14
+ }
1
15
  /**
2
16
  * Extract the GI namespaces a bundle imports, as `Ns-Version` when the
3
17
  * specifier pins one and bare `Ns` when it does not.
4
18
  */
5
19
  export declare function scanGiNamespaces(source: string): string[];
20
+ /** {@link scanGiNamespaces}, plus which of them the artifact marks optional (ADR 0087). */
21
+ export declare function scanGiRequirements(source: string): GiRequirements;
6
22
  /** `gi://Gtk?version=4.0` → `Gtk-4.0`; `gi://Gtk` → `Gtk`; anything else → null. */
7
23
  export declare function parseGiSpecifier(specifier: string): string | null;