@gjsify/cli 0.44.0 → 0.46.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 (81) hide show
  1. package/dist/cli.gjs.mjs +202 -167
  2. package/lib/commands/gettext.d.ts +2 -1
  3. package/lib/commands/gettext.js +105 -47
  4. package/lib/commands/onboard.d.ts +7 -0
  5. package/lib/commands/onboard.js +349 -96
  6. package/lib/commands/ship.d.ts +3 -0
  7. package/lib/commands/ship.js +850 -58
  8. package/lib/commands/trust.d.ts +6 -0
  9. package/lib/commands/trust.js +1 -1
  10. package/lib/commands/upgrade.d.ts +23 -0
  11. package/lib/commands/upgrade.js +95 -12
  12. package/lib/templates/install.mjs.tmpl +2 -2
  13. package/lib/types/config-data.d.ts +127 -1
  14. package/lib/utils/gi-search-path.js +11 -3
  15. package/lib/utils/msgfmt-merge.d.ts +35 -0
  16. package/lib/utils/msgfmt-merge.js +102 -0
  17. package/lib/utils/npm-otp.d.ts +27 -2
  18. package/lib/utils/npm-otp.js +82 -7
  19. package/lib/utils/onboard-discovery.d.ts +68 -0
  20. package/lib/utils/onboard-discovery.js +178 -0
  21. package/lib/utils/onboard-probe.d.ts +118 -1
  22. package/lib/utils/onboard-probe.js +153 -4
  23. package/lib/utils/prompt-output.d.ts +12 -0
  24. package/lib/utils/prompt-output.js +54 -0
  25. package/lib/utils/prompt.js +22 -2
  26. package/lib/utils/publishable-packages.d.ts +1 -1
  27. package/lib/utils/publishable-packages.js +1 -0
  28. package/lib/utils/resolve-npm-package.d.ts +56 -0
  29. package/lib/utils/resolve-npm-package.js +69 -0
  30. package/lib/utils/ship/app-runtime.d.ts +214 -0
  31. package/lib/utils/ship/app-runtime.js +429 -0
  32. package/lib/utils/ship/depends.d.ts +50 -0
  33. package/lib/utils/ship/depends.js +117 -3
  34. package/lib/utils/ship/discover.js +103 -1
  35. package/lib/utils/ship/dmg.d.ts +71 -0
  36. package/lib/utils/ship/dmg.js +181 -0
  37. package/lib/utils/ship/flatpak.js +1 -1
  38. package/lib/utils/ship/formats.d.ts +108 -7
  39. package/lib/utils/ship/formats.js +778 -15
  40. package/lib/utils/ship/launcher.d.ts +13 -3
  41. package/lib/utils/ship/launcher.js +419 -17
  42. package/lib/utils/ship/layout.d.ts +330 -0
  43. package/lib/utils/ship/layout.js +504 -0
  44. package/lib/utils/ship/localize-metadata.d.ts +22 -0
  45. package/lib/utils/ship/localize-metadata.js +126 -0
  46. package/lib/utils/ship/mime.js +1 -9
  47. package/lib/utils/ship/msi.d.ts +126 -0
  48. package/lib/utils/ship/msi.js +434 -0
  49. package/lib/utils/ship/node-runtime.d.ts +71 -0
  50. package/lib/utils/ship/node-runtime.js +122 -0
  51. package/lib/utils/ship/payload.d.ts +156 -5
  52. package/lib/utils/ship/payload.js +599 -14
  53. package/lib/utils/ship/pe-launcher.d.ts +29 -0
  54. package/lib/utils/ship/pe-launcher.js +663 -0
  55. package/lib/utils/ship/plan.d.ts +21 -0
  56. package/lib/utils/ship/plan.js +55 -7
  57. package/lib/utils/ship/plist.d.ts +98 -0
  58. package/lib/utils/ship/plist.js +256 -0
  59. package/lib/utils/ship/rpm.js +16 -3
  60. package/lib/utils/ship/schemas.d.ts +61 -0
  61. package/lib/utils/ship/schemas.js +118 -0
  62. package/lib/utils/ship/scripts.js +7 -3
  63. package/lib/utils/ship/settings.d.ts +91 -2
  64. package/lib/utils/ship/settings.js +93 -1
  65. package/lib/utils/ship/share-dirs.d.ts +53 -0
  66. package/lib/utils/ship/share-dirs.js +76 -0
  67. package/lib/utils/ship/signing.d.ts +311 -0
  68. package/lib/utils/ship/signing.js +506 -0
  69. package/lib/utils/ship/stage-manifest.d.ts +40 -12
  70. package/lib/utils/ship/stage-manifest.js +99 -13
  71. package/lib/utils/ship/stage-writer.d.ts +48 -0
  72. package/lib/utils/ship/stage-writer.js +91 -2
  73. package/lib/utils/ship/types.d.ts +191 -14
  74. package/lib/utils/ship/xml.d.ts +7 -0
  75. package/lib/utils/ship/xml.js +25 -0
  76. package/lib/utils/ship/zip.d.ts +69 -0
  77. package/lib/utils/ship/zip.js +245 -0
  78. package/lib/utils/system-gi.d.ts +20 -5
  79. package/lib/utils/system-gi.js +36 -13
  80. package/lib/utils/trust-registry.js +5 -0
  81. package/package.json +19 -19
@@ -0,0 +1,214 @@
1
+ import type { Layout, LayoutIdentity } from './layout.js';
2
+ import { type ResolveInstalledPackageOptions } from '../resolve-npm-package.js';
3
+ import type { StagedFile } from './types.js';
4
+ /**
5
+ * The targets a relocated GTK closure is published for.
6
+ *
7
+ * The same three `@gjsify/gtk-runtime-*` packages node-gi resolves, and
8
+ * deliberately NOT derived from {@link NODE_RUNTIME_TARGETS}: the two lists agree
9
+ * today by coincidence of which platforms lack a dependable system stack, and
10
+ * deriving one from the other would make a future Linux GTK bundle silently imply
11
+ * a bundled Linux interpreter (which `node-runtime.ts` refuses on purpose).
12
+ */
13
+ export declare const GTK_RUNTIME_TARGETS: readonly ["darwin-arm64", "darwin-x64", "win32-x64"];
14
+ export type GtkRuntimeTarget = (typeof GTK_RUNTIME_TARGETS)[number];
15
+ /** Is this `<platform>-<arch>` one of the published GTK-closure targets? */
16
+ export declare function isGtkRuntimeTarget(target: string): target is GtkRuntimeTarget;
17
+ /** The npm name carrying the relocated closure for a target. One spelling, derived. */
18
+ export declare function gtkRuntimePackageName(target: GtkRuntimeTarget): string;
19
+ /** The addon's filename, node-gi's `ADDON_FILENAME`. Identical in every prebuild. */
20
+ export declare const NODE_GI_ADDON_FILENAME = "node_gi.node";
21
+ /** The package whose `prebuilds/<target>/` holds the addon and, in CI, the closure beside it. */
22
+ export declare const NODE_GI_PACKAGE = "@gjsify/node-gi";
23
+ /** Where the relocated GTK closure is, once found. */
24
+ export interface ResolvedGtkRuntime {
25
+ /** What produced the hit — an env var name, or the npm name it was resolved from. */
26
+ source: string;
27
+ /** The bundle directory: the one holding `lib/`, `girepository-1.0/`, `share/`. */
28
+ dir: string;
29
+ /** `<dir>/lib` on darwin, `<dir>/bin` on win32 — the loadable-code directory. */
30
+ libDir: string;
31
+ /** `<dir>/girepository-1.0`. */
32
+ typelibDir: string;
33
+ }
34
+ /** Where the node-gi addon is, once found. */
35
+ export interface ResolvedNodeGiAddon {
36
+ /** The npm name it was resolved from. */
37
+ source: string;
38
+ /** Absolute path to `node_gi.node`. */
39
+ addonPath: string;
40
+ }
41
+ export interface ResolveRuntimeOptions {
42
+ /** Project directory to resolve from — the consumer's, not gjsify's. */
43
+ cwd?: string;
44
+ /** Environment to read the overrides from. Injected so it is testable. */
45
+ env?: Record<string, string | undefined>;
46
+ /**
47
+ * TEST SEAM, passed through to {@link resolveInstalledPackage} — resolve a
48
+ * specifier from one anchor. A fake answering the SAME path for both anchors
49
+ * is Bun's global-cache behaviour, which is otherwise unreachable from Node.
50
+ */
51
+ resolve?: ResolveInstalledPackageOptions['resolve'];
52
+ }
53
+ /**
54
+ * Locate the relocated GTK closure for `target`, or `null`.
55
+ *
56
+ * The search order MIRRORS `resolveGtkRuntimeBundle()` rather than inventing a
57
+ * second one, because the two have to agree: this function decides what gets
58
+ * copied into the bundle, and that function decides what the bundle finds at
59
+ * runtime. Four candidates, first complete hit wins:
60
+ *
61
+ * 1. `GJSIFY_GTK_RUNTIME` — an explicit bundle directory, for a maintainer
62
+ * holding an unpublished or patched closure. The same variable the launcher
63
+ * later EXPORTS, and the same precedence it has there.
64
+ * 2. `@gjsify/node-gi`'s own `prebuilds/<target>/gtk`. This is the layout the
65
+ * macOS CI legs stage by hand (`node-gi.yml`: download the bundle artifact
66
+ * into `prebuilds/darwin-<arch>/gtk`, the addon beside it), so a job that has
67
+ * prepared node-gi for a conformance run has, by that act alone, prepared it
68
+ * for a ship run.
69
+ * 3. the sibling monorepo directory `../gtk-runtime-<target>/gtk`, which is what
70
+ * an in-repo `npm run build:bundle` writes.
71
+ * 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk/`.
72
+ *
73
+ * A candidate counts only when it holds BOTH the loadable-code directory and the
74
+ * typelib directory — node-gi's own probe, restated here rather than weakened,
75
+ * because a directory that passes here and fails there is a `.app` that ships
76
+ * 200 MB and still cannot open a window.
77
+ */
78
+ export declare function resolveGtkRuntime(target: GtkRuntimeTarget, options?: ResolveRuntimeOptions): ResolvedGtkRuntime | null;
79
+ /**
80
+ * Locate the node-gi addon built for `target`, or `null`.
81
+ *
82
+ * ONE source, and no override. The addon and the closure ship as a matched pair —
83
+ * the addon's `@rpath` is `@loader_path/gtk/lib`, so it names the very directory
84
+ * the closure occupies — and `@gjsify/node-gi` publishes `prebuilds/` for all five
85
+ * of its platforms in ONE package, so resolving the package by name is already
86
+ * enough to reach a foreign target's binary from a Linux host. A second
87
+ * environment knob here would be a way to pair an addon with a closure it was not
88
+ * built against, which is #910's failure shape wearing a convenience.
89
+ */
90
+ export declare function resolveNodeGiAddon(target: GtkRuntimeTarget, options?: ResolveRuntimeOptions): ResolvedNodeGiAddon | null;
91
+ /** node-gi's JavaScript, as package-relative paths. */
92
+ export interface ResolvedNodeGiPackage {
93
+ source: string;
94
+ /** The package root the files are relative to. */
95
+ root: string;
96
+ /** POSIX-separated, package-relative — `package.json`, the root `.js`, `overrides/**`. */
97
+ files: string[];
98
+ }
99
+ /**
100
+ * The files a SHIPPED `.app` needs from `@gjsify/node-gi`, or `null`.
101
+ *
102
+ * NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
103
+ * `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
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.
107
+ *
108
+ * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
109
+ * `package.json` (the `exports` map is what resolves every subpath), every `.js`
110
+ * at the package root (every `exports` target is one), and `overrides/`, which
111
+ * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
112
+ * REAL package's `exports` map, so a node-gi release that adds an entry outside
113
+ * this set fails there rather than in a `.app` a stranger downloaded.
114
+ */
115
+ export declare function resolveNodeGiPackage(options?: ResolveRuntimeOptions): ResolvedNodeGiPackage | null;
116
+ /**
117
+ * Where each piece of the runtime lands inside the bundle — STAGE-relative.
118
+ *
119
+ * PURE, and derived from `Layout.dirs` rather than from a second set of string
120
+ * literals, so the paths the launcher exports and the paths the stager writes
121
+ * cannot drift: they are computed here once and both callers read this result.
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:
125
+ * `resolveGtkRuntimeBundle()` probes `<dir>/lib` and `<dir>/girepository-1.0`,
126
+ * the addon's `@rpath` is `@loader_path/gtk/lib`, and `loaders.cache` addresses
127
+ * each pixbuf loader relative to the bundle toplevel. Reproducing the shape is
128
+ * what makes every one of those relations still true after the copy.
129
+ */
130
+ export interface AppRuntimePaths {
131
+ /** `<App>.app/Contents/Frameworks/node-gi` — the package root the layout imitates. */
132
+ nodeGiRoot: string;
133
+ /**
134
+ * `<App>.app/Contents/Resources/lib/node_modules/@gjsify/node-gi` — where
135
+ * node-gi's JAVASCRIPT goes.
136
+ *
137
+ * Not a preference: `require('@gjsify/node-gi/gi')` from the staged bundle at
138
+ * `Contents/Resources/lib/<bundle>.mjs` walks `node_modules` upward from that
139
+ * file's own directory, and this is the FIRST directory that walk visits. Any
140
+ * other location works only by accident of what sits above the bundle.
141
+ */
142
+ nodeGiPackageDir: string;
143
+ /** `<nodeGiRoot>/prebuilds/<target>` — the addon and the closure, siblings. */
144
+ prebuildDir: string;
145
+ /** `<prebuildDir>/gtk` — what `GJSIFY_GTK_RUNTIME` names. */
146
+ gtkDir: string;
147
+ /** `<prebuildDir>/node_gi.node` — what `NODE_GI_NATIVE` pins. */
148
+ addonPath: string;
149
+ /**
150
+ * `<App>.app/Contents/MacOS/node`, or `<program dir>/node.exe` — beside the
151
+ * launcher that execs it, under the name the target's own release uses.
152
+ */
153
+ interpreterPath: string;
154
+ /** Node's own LICENSE, under `Contents/Resources` (macOS) or `share/` (Windows). */
155
+ interpreterLicensePath: string;
156
+ }
157
+ export declare function appRuntimePaths(layout: Layout, identity: LayoutIdentity, target: GtkRuntimeTarget): AppRuntimePaths;
158
+ /** What the launcher must export and exec, once the runtime is staged. Absent member = not staged. */
159
+ export interface LauncherRuntime {
160
+ /** Stage-relative path of the interpreter the bundle carries. */
161
+ interpreter?: string;
162
+ /** Stage-relative path of the GTK closure — `GJSIFY_GTK_RUNTIME`. */
163
+ gtkRuntimeDir?: string;
164
+ /** Stage-relative path of the node-gi addon — `NODE_GI_NATIVE`. */
165
+ nodeGiAddon?: string;
166
+ }
167
+ /** The staged runtime, and an account of what was and was not found. */
168
+ export interface StagedAppRuntime {
169
+ /** Stage-relative files, already placed — they never passed through the prefix-relative plan. */
170
+ files: StagedFile[];
171
+ /** What the launcher can now name. */
172
+ launcher: LauncherRuntime;
173
+ /** One line per piece that WAS found, naming where it came from. */
174
+ found: string[];
175
+ /** One line per piece that was not, naming the package to install. */
176
+ missing: string[];
177
+ }
178
+ export interface StageAppRuntimeInput {
179
+ layout: Layout;
180
+ identity: LayoutIdentity;
181
+ /** `${layout.os}-${settings.arch}` — the target the payload was built for. */
182
+ target: GtkRuntimeTarget;
183
+ /** Project directory the three packages are resolved from. */
184
+ cwd: string;
185
+ /** Injected so the overrides are testable. */
186
+ env?: Record<string, string | undefined>;
187
+ /** Passed to every discovery below — see {@link ResolveRuntimeOptions.resolve}. */
188
+ resolve?: ResolveRuntimeOptions['resolve'];
189
+ /** The interpreter, already resolved — see `node-runtime.ts`. `null` when absent. */
190
+ interpreter: {
191
+ source: string;
192
+ nodePath: string;
193
+ licensePath: string;
194
+ } | null;
195
+ }
196
+ /**
197
+ * Stage the interpreter, the GTK closure and the addon into one bundle.
198
+ *
199
+ * PARTIAL BY DESIGN, and reported. An all-or-nothing rule would drop a 200 MB
200
+ * closure that IS present because an interpreter that is not shares the milestone
201
+ * with it, and would do it at exit 0 — while a bundle carrying the closure and no
202
+ * interpreter is a real, useful intermediate (it runs wherever a Node is on
203
+ * `PATH`, which is every developer's machine and no stranger's). So each piece is
204
+ * staged when found and named when not, and `commands/ship.ts` keeps
205
+ * `Layout.runtimeGap` for precisely the case where the interpreter is still
206
+ * missing.
207
+ *
208
+ * TREE-PRESERVING for the closure and FLAT for the other two, which is the whole
209
+ * distinction this module exists to draw: `listFilesRecursive` walks the bundle
210
+ * and every relative path is carried through unchanged, so
211
+ * `lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so` is still four levels
212
+ * under `gtk/` on the other side of the copy.
213
+ */
214
+ export declare function stageAppRuntime(input: StageAppRuntimeInput): StagedAppRuntime;
@@ -0,0 +1,429 @@
1
+ // The runtime a self-contained `<App>.app` CARRIES — found, then staged with its
2
+ // tree intact (#1354 M2b, ADR 0024 stage 4).
3
+ //
4
+ // WHAT M2a LEFT. `gjsify ship darwin` produced a real bundle — `Info.plist`,
5
+ // `PkgInfo`, a compiled schema cache — whose launcher still `exec`ed the bare
6
+ // name `node` off `PATH` and whose GI namespaces still expected a GTK on the
7
+ // machine. macOS ships neither. So the artifact was an application in shape and
8
+ // not in behaviour, and `formats.ts`'s `macos-app` row says so in as many words:
9
+ // "whatever the bundle does not carry, it finds on the machine or it does not
10
+ // run — which is what makes M2b (staging a runtime) the milestone that matters".
11
+ // This module is that staging.
12
+ //
13
+ // FOUR PIECES, THREE OWNERS — `@gjsify/node-gi` supplies two of them — and none
14
+ // is a dependency of anything here:
15
+ //
16
+ // * the interpreter — `@gjsify/node-runtime-<target>`, resolved by
17
+ // {@link resolveNodeRuntime} in `node-runtime.ts`. This module is the caller
18
+ // that module was written for, and #1354 M3 gave it a second layout to serve.
19
+ // * the relocated GTK closure — `@gjsify/gtk-runtime-<target>`'s `gtk/`.
20
+ // * the node-gi addon built against that closure —
21
+ // `@gjsify/node-gi`'s `prebuilds/<target>/node_gi.node`, whose `@rpath` is
22
+ // `@loader_path/gtk/lib`.
23
+ // * node-gi's own JAVASCRIPT, which is a fourth piece and not an afterthought:
24
+ // `@gjsify/node-gi/*` is EXTERNAL in every `--app node` bundle by design
25
+ // (`rolldown-plugin-gjsify/src/app/node.ts`: the reverse-bridge modules
26
+ // "must resolve at runtime against the consumer's node_modules"), so a
27
+ // `gi://Gtk` import compiles to `require('@gjsify/node-gi/gi')` in the
28
+ // shipped file. Measured on the `.app` fixture's own bundle. A `.app` has no
29
+ // consumer node_modules, so the package is staged into one the bundle's
30
+ // directory owns.
31
+ //
32
+ // THE LAYOUT IS NOT OURS TO CHOOSE. `resolveGtkRuntimeBundle()` in
33
+ // `packages/node-gi/node-gi/gtk-runtime.js` walks four candidates and probes each
34
+ // for `<dir>/lib` + `<dir>/girepository-1.0`; the addon's `@rpath` reaches its
35
+ // dylibs as `@loader_path/gtk/lib`; `loaders.cache` names each gdk-pixbuf loader
36
+ // `@loader_path/../../..`-relative; `GST_PLUGIN_SCANNER` is an executable under
37
+ // `libexec/gstreamer-1.0`. Every one of those relations is RELATIVE, so the whole
38
+ // tree survives a copy — as long as it is copied as a tree. Hence
39
+ // {@link appRuntimePaths}: the staged shape is node-gi's own sibling layout,
40
+ // `prebuilds/<target>/{node_gi.node, gtk/}`, the same one
41
+ // `.github/workflows/node-gi.yml`'s macOS legs assemble by hand before they run
42
+ // the conformance.
43
+ //
44
+ // AND THAT SPLIT IS WHY THE TWO ENVIRONMENT VARIABLES ARE LOAD-BEARING rather
45
+ // than belt-and-braces. Once node-gi's JS sits at
46
+ // `Contents/Resources/lib/node_modules/@gjsify/node-gi`, its `packageRoot` is
47
+ // THAT directory — so `prebuildAddonPath()` looks for
48
+ // `…/node_modules/@gjsify/node-gi/prebuilds/<target>/node_gi.node` and
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.
55
+ //
56
+ // WHY NOT `gjsify.ship.bundledTypelibs`. Because it FLATTENS. `plan.ts` stages
57
+ // each such file as `posix.join(libDir, 'gi', basename(file))`, and
58
+ // `discoverTypelibs` walks the directory recursively — so three inputs at three
59
+ // depths collapse into one directory. Measured on this tree, through the built
60
+ // planner:
61
+ //
62
+ // gi/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so → lib/demo/gi/libpixbufloader-svg.so
63
+ // gi/loaders.cache → lib/demo/gi/loaders.cache
64
+ // gi/Gtk-4.0.typelib → lib/demo/gi/Gtk-4.0.typelib
65
+ //
66
+ // That destroys the loader paths `loaders.cache` records, and it leaves a
67
+ // directory with no `lib/` and no `girepository-1.0/` — so
68
+ // `resolveGtkRuntimeBundle()`'s existence probe rejects it and node-gi falls back
69
+ // to a GTK the machine does not have. `bundledTypelibs` stays what it is (a FLAT
70
+ // pair of a typelib and its backer, which is all it ever claimed to be); a
71
+ // relocated closure needs its own stager, and this is it.
72
+ //
73
+ // `null`-not-throw, like `node-runtime.ts`, and for the same reason: whether a
74
+ // missing piece is fatal depends on what the caller is building. `assemble` in
75
+ // `commands/ship.ts` stages what was found, names what was not, and keeps
76
+ // `Layout.runtimeGap` for exactly the case where the interpreter is still absent.
77
+ import { existsSync, readdirSync, statSync } from 'node:fs';
78
+ import { dirname, join, posix, sep } from 'node:path';
79
+ import { listFilesRecursive } from './discover.js';
80
+ import { nodeRuntimeBinaryName } from './node-runtime.js';
81
+ import { isExecutableAsset } from './plan.js';
82
+ import { resolveInstalledPackage } from '../resolve-npm-package.js';
83
+ /**
84
+ * The targets a relocated GTK closure is published for.
85
+ *
86
+ * The same three `@gjsify/gtk-runtime-*` packages node-gi resolves, and
87
+ * deliberately NOT derived from {@link NODE_RUNTIME_TARGETS}: the two lists agree
88
+ * today by coincidence of which platforms lack a dependable system stack, and
89
+ * deriving one from the other would make a future Linux GTK bundle silently imply
90
+ * a bundled Linux interpreter (which `node-runtime.ts` refuses on purpose).
91
+ */
92
+ export const GTK_RUNTIME_TARGETS = ['darwin-arm64', 'darwin-x64', 'win32-x64'];
93
+ /** Is this `<platform>-<arch>` one of the published GTK-closure targets? */
94
+ export function isGtkRuntimeTarget(target) {
95
+ return GTK_RUNTIME_TARGETS.includes(target);
96
+ }
97
+ /** The npm name carrying the relocated closure for a target. One spelling, derived. */
98
+ export function gtkRuntimePackageName(target) {
99
+ return `@gjsify/gtk-runtime-${target}`;
100
+ }
101
+ /** The addon's filename, node-gi's `ADDON_FILENAME`. Identical in every prebuild. */
102
+ export const NODE_GI_ADDON_FILENAME = 'node_gi.node';
103
+ /** The package whose `prebuilds/<target>/` holds the addon and, in CI, the closure beside it. */
104
+ export const NODE_GI_PACKAGE = '@gjsify/node-gi';
105
+ /**
106
+ * Locate the relocated GTK closure for `target`, or `null`.
107
+ *
108
+ * The search order MIRRORS `resolveGtkRuntimeBundle()` rather than inventing a
109
+ * second one, because the two have to agree: this function decides what gets
110
+ * copied into the bundle, and that function decides what the bundle finds at
111
+ * runtime. Four candidates, first complete hit wins:
112
+ *
113
+ * 1. `GJSIFY_GTK_RUNTIME` — an explicit bundle directory, for a maintainer
114
+ * holding an unpublished or patched closure. The same variable the launcher
115
+ * later EXPORTS, and the same precedence it has there.
116
+ * 2. `@gjsify/node-gi`'s own `prebuilds/<target>/gtk`. This is the layout the
117
+ * macOS CI legs stage by hand (`node-gi.yml`: download the bundle artifact
118
+ * into `prebuilds/darwin-<arch>/gtk`, the addon beside it), so a job that has
119
+ * prepared node-gi for a conformance run has, by that act alone, prepared it
120
+ * for a ship run.
121
+ * 3. the sibling monorepo directory `../gtk-runtime-<target>/gtk`, which is what
122
+ * an in-repo `npm run build:bundle` writes.
123
+ * 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk/`.
124
+ *
125
+ * A candidate counts only when it holds BOTH the loadable-code directory and the
126
+ * typelib directory — node-gi's own probe, restated here rather than weakened,
127
+ * because a directory that passes here and fails there is a `.app` that ships
128
+ * 200 MB and still cannot open a window.
129
+ */
130
+ export function resolveGtkRuntime(target, options = {}) {
131
+ const env = options.env ?? process.env;
132
+ // `bin` on win32, `lib` everywhere else — the NAME differs, the ROLE does not
133
+ // (`gtk-runtime.js`'s `nativeSubdir`). Read off the TARGET and never
134
+ // `process.platform`: assembling a win32 tree from Linux is a supported path.
135
+ const nativeSubdir = target.startsWith('win32-') ? 'bin' : 'lib';
136
+ const candidates = [];
137
+ const override = env['GJSIFY_GTK_RUNTIME'];
138
+ if (override)
139
+ candidates.push({ source: 'GJSIFY_GTK_RUNTIME', dir: override });
140
+ const nodeGiRoot = resolveNodeGiRoot(options);
141
+ if (nodeGiRoot !== null) {
142
+ candidates.push({
143
+ source: `${NODE_GI_PACKAGE}/prebuilds/${target}`,
144
+ dir: join(nodeGiRoot, 'prebuilds', target, 'gtk'),
145
+ });
146
+ candidates.push({
147
+ source: `${NODE_GI_PACKAGE} sibling`,
148
+ dir: join(nodeGiRoot, '..', `gtk-runtime-${target}`, 'gtk'),
149
+ });
150
+ }
151
+ const packageName = gtkRuntimePackageName(target);
152
+ const entry = resolveStagedPackage(packageName, options);
153
+ if (entry !== null)
154
+ candidates.push({ source: packageName, dir: join(dirname(entry), 'gtk') });
155
+ for (const candidate of candidates) {
156
+ const libDir = join(candidate.dir, nativeSubdir);
157
+ const typelibDir = join(candidate.dir, 'girepository-1.0');
158
+ if (!existsSync(libDir) || !existsSync(typelibDir))
159
+ continue;
160
+ return { source: candidate.source, dir: candidate.dir, libDir, typelibDir };
161
+ }
162
+ return null;
163
+ }
164
+ /**
165
+ * Locate the node-gi addon built for `target`, or `null`.
166
+ *
167
+ * ONE source, and no override. The addon and the closure ship as a matched pair —
168
+ * the addon's `@rpath` is `@loader_path/gtk/lib`, so it names the very directory
169
+ * the closure occupies — and `@gjsify/node-gi` publishes `prebuilds/` for all five
170
+ * of its platforms in ONE package, so resolving the package by name is already
171
+ * enough to reach a foreign target's binary from a Linux host. A second
172
+ * environment knob here would be a way to pair an addon with a closure it was not
173
+ * built against, which is #910's failure shape wearing a convenience.
174
+ */
175
+ export function resolveNodeGiAddon(target, options = {}) {
176
+ const root = resolveNodeGiRoot(options);
177
+ if (root === null)
178
+ return null;
179
+ const addonPath = join(root, 'prebuilds', target, NODE_GI_ADDON_FILENAME);
180
+ // A package resolved but not POPULATED is `null`, not a path — `prebuilds/` is
181
+ // gitignored, so an in-repo checkout is exactly what that looks like, and a
182
+ // path to a file that is not there fails later, in a copy, with the target's
183
+ // name nowhere in the message. Same rule as `node-runtime.ts`'s `complete()`.
184
+ return existsSync(addonPath) ? { source: NODE_GI_PACKAGE, addonPath } : null;
185
+ }
186
+ /** `@gjsify/node-gi`'s package root, or `null` when it is not resolvable from here. */
187
+ function resolveNodeGiRoot(options) {
188
+ const entry = resolveStagedPackage(NODE_GI_PACKAGE, options);
189
+ return entry === null ? null : dirname(entry);
190
+ }
191
+ /**
192
+ * Resolve a package whose BYTES this module stages into a redistributed artifact.
193
+ *
194
+ * `resolveInstalledPackage`, never `resolveNpmPackage`: under Bun the latter
195
+ * answers from the runtime's global install cache for a project that merely has a
196
+ * `package.json` and no `node_modules`, which would put files the author never
197
+ * declared inside a `.app`. See that function for the measurement.
198
+ */
199
+ function resolveStagedPackage(specifier, options) {
200
+ const opts = { bundleUrl: import.meta.url };
201
+ if (options.cwd !== undefined)
202
+ opts.cwd = options.cwd;
203
+ if (options.resolve !== undefined)
204
+ opts.resolve = options.resolve;
205
+ return resolveInstalledPackage(specifier, opts);
206
+ }
207
+ /**
208
+ * The files a SHIPPED `.app` needs from `@gjsify/node-gi`, or `null`.
209
+ *
210
+ * NOT the published tarball. `files` in node-gi's manifest also lists `src/`,
211
+ * `binding.gyp` and `scripts/install.mjs` — the inputs to `node-gyp`, which a
212
+ * `.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.
215
+ *
216
+ * So the rule is what `require('@gjsify/node-gi/<subpath>')` can REACH:
217
+ * `package.json` (the `exports` map is what resolves every subpath), every `.js`
218
+ * at the package root (every `exports` target is one), and `overrides/`, which
219
+ * `gi.js` loads at runtime. `tests/e2e/ship-macos` holds that rule against the
220
+ * REAL package's `exports` map, so a node-gi release that adds an entry outside
221
+ * this set fails there rather than in a `.app` a stranger downloaded.
222
+ */
223
+ export function resolveNodeGiPackage(options = {}) {
224
+ const root = resolveNodeGiRoot(options);
225
+ if (root === null)
226
+ return null;
227
+ const files = ['package.json'];
228
+ for (const entry of readdirSync(root, { withFileTypes: true }).sort((a, b) => (a.name < b.name ? -1 : 1))) {
229
+ if (entry.isFile() && entry.name.endsWith('.js'))
230
+ files.push(entry.name);
231
+ }
232
+ const overrides = join(root, 'overrides');
233
+ if (existsSync(overrides)) {
234
+ for (const rel of listFilesRecursive(overrides))
235
+ files.push(posix.join('overrides', rel));
236
+ }
237
+ // One `.js` is the floor: a package whose root holds only `package.json` is a
238
+ // directory that resolved and a runtime that is not there, and staging it would
239
+ // produce a `.app` failing at `require` with node-gi's name in the message and
240
+ // nothing to act on.
241
+ return files.length > 1 ? { source: NODE_GI_PACKAGE, root, files: files.sort() } : null;
242
+ }
243
+ export function appRuntimePaths(layout, identity, target) {
244
+ const dirs = layout.dirs(identity);
245
+ const nodeGiRoot = posix.join(dirs.native, 'node-gi');
246
+ const prebuildDir = posix.join(nodeGiRoot, 'prebuilds', target);
247
+ return {
248
+ nodeGiRoot,
249
+ nodeGiPackageDir: posix.join(dirs.bundle, 'node_modules', NODE_GI_PACKAGE),
250
+ prebuildDir,
251
+ gtkDir: posix.join(prebuildDir, 'gtk'),
252
+ addonPath: posix.join(prebuildDir, NODE_GI_ADDON_FILENAME),
253
+ // `Contents/MacOS`, because that is where a `.app` keeps executables and
254
+ // because `$here` — which the launcher already computes — is then the whole
255
+ // path expression. `Contents/Frameworks` would work for dyld and would make
256
+ // the launcher walk back up for the one file it execs. On Windows
257
+ // `dirs.launcher` is the program directory itself, so the interpreter lands
258
+ // beside the `.cmd` for the same reason.
259
+ //
260
+ // THE LEAF COMES FROM `node-runtime.ts`, which is the module that already
261
+ // decided it for the SOURCE file — `node.exe` on win32, `node` elsewhere,
262
+ // read off the TARGET and never `process.platform` (assembling a Windows
263
+ // program directory on Linux is the supported path). Restating the rule here
264
+ // would let the two drift into a stage that copies `node.exe` to `node` and a
265
+ // launcher that execs neither.
266
+ interpreterPath: posix.join(dirs.launcher, nodeRuntimeBinaryName(target)),
267
+ // rpm's `share/licenses/<name>/LICENSE` shape, which the `macos-app` row
268
+ // already uses for the app's own licence — one layout for a reader to learn,
269
+ // and `node` is a name no `binaryName` can collide with silently: `placeStage`
270
+ // refuses the collision by name.
271
+ interpreterLicensePath: posix.join(dirs.data, 'licenses', 'node', 'LICENSE'),
272
+ };
273
+ }
274
+ /**
275
+ * Stage the interpreter, the GTK closure and the addon into one bundle.
276
+ *
277
+ * PARTIAL BY DESIGN, and reported. An all-or-nothing rule would drop a 200 MB
278
+ * closure that IS present because an interpreter that is not shares the milestone
279
+ * with it, and would do it at exit 0 — while a bundle carrying the closure and no
280
+ * interpreter is a real, useful intermediate (it runs wherever a Node is on
281
+ * `PATH`, which is every developer's machine and no stranger's). So each piece is
282
+ * staged when found and named when not, and `commands/ship.ts` keeps
283
+ * `Layout.runtimeGap` for precisely the case where the interpreter is still
284
+ * missing.
285
+ *
286
+ * TREE-PRESERVING for the closure and FLAT for the other two, which is the whole
287
+ * distinction this module exists to draw: `listFilesRecursive` walks the bundle
288
+ * and every relative path is carried through unchanged, so
289
+ * `lib/gdk-pixbuf-2.0/2.10.0/loaders/libpixbufloader-svg.so` is still four levels
290
+ * under `gtk/` on the other side of the copy.
291
+ */
292
+ export function stageAppRuntime(input) {
293
+ const paths = appRuntimePaths(input.layout, input.identity, input.target);
294
+ const files = [];
295
+ const launcher = {};
296
+ const found = [];
297
+ const missing = [];
298
+ if (input.interpreter !== null) {
299
+ files.push({
300
+ path: paths.interpreterPath,
301
+ mode: 0o755,
302
+ source: { kind: 'file', path: input.interpreter.nodePath },
303
+ });
304
+ // The licence travels with the binary or neither does — `ResolvedNodeRuntime`
305
+ // makes that a property of the RESULT for this reason, and this is the caller
306
+ // that would otherwise have been free to forget it.
307
+ files.push({
308
+ path: paths.interpreterLicensePath,
309
+ mode: 0o644,
310
+ source: { kind: 'file', path: input.interpreter.licensePath },
311
+ });
312
+ launcher.interpreter = paths.interpreterPath;
313
+ found.push(`interpreter from ${input.interpreter.source}`);
314
+ }
315
+ else {
316
+ missing.push(`no bundled interpreter — install \`@gjsify/node-runtime-${input.target}\` (or point ` +
317
+ '`GJSIFY_NODE_RUNTIME` at a `bin/` directory holding `node` and its `LICENSE`)');
318
+ }
319
+ const gtk = resolveGtkRuntime(input.target, discoveryOptions(input));
320
+ if (gtk !== null) {
321
+ for (const rel of listFilesRecursive(gtk.dir)) {
322
+ const abs = join(gtk.dir, rel.split('/').join(sep));
323
+ files.push({
324
+ path: posix.join(paths.gtkDir, rel),
325
+ mode: stagedMode(rel, abs),
326
+ source: { kind: 'file', path: abs },
327
+ });
328
+ }
329
+ launcher.gtkRuntimeDir = paths.gtkDir;
330
+ found.push(`GTK closure from ${gtk.source}`);
331
+ }
332
+ else {
333
+ // The native subdirectory is `bin` on win32 and `lib` everywhere else,
334
+ // exactly as `resolveGtkRuntimeBundle()` probes for it. Naming `lib/`
335
+ // unconditionally sent a Windows reader to build a directory this very
336
+ // resolver then refuses -- a hint that is wrong is worse than none,
337
+ // because it costs the reader the time to follow it first.
338
+ const nativeSubdir = input.target.startsWith('win32-') ? 'bin' : 'lib';
339
+ missing.push(`no GTK closure — install \`${gtkRuntimePackageName(input.target)}\` (or point ` +
340
+ `\`GJSIFY_GTK_RUNTIME\` at a bundle directory holding \`${nativeSubdir}/\` and \`girepository-1.0/\`)`);
341
+ }
342
+ // node-gi's JavaScript, which the bundle `require`s by name. Staged whenever
343
+ // the package is resolvable and reported separately from the addon: a bundle
344
+ // that carries the JS and no addon fails with node-gi's own diagnosis
345
+ // (`load-diagnostics.js`), while one that carries the addon and no JS fails at
346
+ // `require` before anything of node-gi's has run.
347
+ const js = resolveNodeGiPackage(discoveryOptions(input));
348
+ if (js !== null) {
349
+ for (const rel of js.files) {
350
+ files.push({
351
+ path: posix.join(paths.nodeGiPackageDir, rel),
352
+ mode: 0o644,
353
+ source: { kind: 'file', path: join(js.root, rel.split('/').join(sep)) },
354
+ });
355
+ }
356
+ found.push(`node-gi runtime (${js.files.length} file(s)) from ${js.source}`);
357
+ }
358
+ else {
359
+ missing.push(`no node-gi JavaScript — install \`${NODE_GI_PACKAGE}\`; a \`--app node\` bundle keeps ` +
360
+ '`@gjsify/node-gi/*` external, so the staged bundle would `require` a package the artifact ' +
361
+ 'does not carry');
362
+ }
363
+ const addon = resolveNodeGiAddon(input.target, discoveryOptions(input));
364
+ if (addon !== null) {
365
+ files.push({ path: paths.addonPath, mode: 0o755, source: { kind: 'file', path: addon.addonPath } });
366
+ launcher.nodeGiAddon = paths.addonPath;
367
+ found.push(`node-gi addon from ${addon.source}`);
368
+ }
369
+ else {
370
+ missing.push(`no node-gi addon — install \`${NODE_GI_PACKAGE}\`, whose \`prebuilds/${input.target}/` +
371
+ `${NODE_GI_ADDON_FILENAME}\` is the binary built against that closure`);
372
+ }
373
+ return { files, launcher, found, missing };
374
+ }
375
+ /**
376
+ * The discovery options every piece is looked up with.
377
+ *
378
+ * ONE function, so a field added to {@link StageAppRuntimeInput} cannot reach two
379
+ * of the three lookups and miss the third. It missed all three once already:
380
+ * `resolve` was declared on the input, spelled correctly at the call site in the
381
+ * spec, and silently dropped here — `packages/infra/cli/tsconfig.json` excludes
382
+ * `src/**` + `.spec.ts`, so no excess-property check ever ran on it and the test
383
+ * that was supposed to red the global-cache guard passed with the guard removed.
384
+ */
385
+ function discoveryOptions(input) {
386
+ const options = {};
387
+ if (input.cwd !== undefined)
388
+ options.cwd = input.cwd;
389
+ if (input.env !== undefined)
390
+ options.env = input.env;
391
+ if (input.resolve !== undefined)
392
+ options.resolve = input.resolve;
393
+ return options;
394
+ }
395
+ /**
396
+ * The mode one staged closure file gets.
397
+ *
398
+ * THREE tests, and each covers a case the others do not:
399
+ *
400
+ * * the source's own execute bit — the truth when the closure came off a real
401
+ * filesystem, and the ONLY thing that knows `libexec/gstreamer-1.0/gst-plugin-scanner`
402
+ * is a program (it has no extension, and GStreamer forks it out-of-process so a
403
+ * plugin that crashes on load cannot take the app down with it);
404
+ * * the shared-library spelling, shared with `plan.ts` rather than restated —
405
+ * load-bearing because `actions/upload-artifact` stores NO POSIX mode, so a
406
+ * closure that arrived through a CI artifact has 0644 on every file and the
407
+ * source-mode test above is blind on exactly the path this milestone's own CI
408
+ * leg takes;
409
+ * * `libexec/` and `bin/`, for the same reason applied to the extensionless
410
+ * programs the artifact round trip flattened.
411
+ *
412
+ * A dylib does not need the execute bit to be `dlopen`'d, so this is not about
413
+ * making the closure loadable — it is about the two files that are EXEC'd.
414
+ */
415
+ function stagedMode(rel, abs) {
416
+ if (isExecutableAsset(rel))
417
+ return 0o755;
418
+ if (rel.startsWith('libexec/') || rel.startsWith('bin/'))
419
+ return 0o755;
420
+ try {
421
+ if ((statSync(abs).mode & 0o111) !== 0)
422
+ return 0o755;
423
+ }
424
+ catch {
425
+ // Unreadable here means unreadable in the copy too, and `writeStage` is
426
+ // where that gets reported against the path it happened on.
427
+ }
428
+ return 0o644;
429
+ }