@gjsify/cli 0.44.0 → 0.45.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.
- package/dist/cli.gjs.mjs +192 -165
- package/lib/commands/gettext.d.ts +2 -1
- package/lib/commands/gettext.js +105 -47
- package/lib/commands/onboard.d.ts +7 -0
- package/lib/commands/onboard.js +349 -96
- package/lib/commands/ship.d.ts +3 -0
- package/lib/commands/ship.js +833 -54
- package/lib/commands/trust.d.ts +6 -0
- package/lib/commands/trust.js +1 -1
- package/lib/types/config-data.d.ts +63 -1
- package/lib/utils/msgfmt-merge.d.ts +35 -0
- package/lib/utils/msgfmt-merge.js +102 -0
- package/lib/utils/npm-otp.d.ts +27 -2
- package/lib/utils/npm-otp.js +82 -7
- package/lib/utils/onboard-discovery.d.ts +68 -0
- package/lib/utils/onboard-discovery.js +178 -0
- package/lib/utils/onboard-probe.d.ts +118 -1
- package/lib/utils/onboard-probe.js +153 -4
- package/lib/utils/prompt-output.d.ts +12 -0
- package/lib/utils/prompt-output.js +54 -0
- package/lib/utils/prompt.js +22 -2
- package/lib/utils/publishable-packages.d.ts +1 -1
- package/lib/utils/publishable-packages.js +1 -0
- package/lib/utils/resolve-npm-package.d.ts +56 -0
- package/lib/utils/resolve-npm-package.js +69 -0
- package/lib/utils/ship/app-runtime.d.ts +214 -0
- package/lib/utils/ship/app-runtime.js +429 -0
- package/lib/utils/ship/depends.d.ts +50 -0
- package/lib/utils/ship/depends.js +117 -3
- package/lib/utils/ship/discover.js +18 -1
- package/lib/utils/ship/dmg.d.ts +71 -0
- package/lib/utils/ship/dmg.js +181 -0
- package/lib/utils/ship/flatpak.js +1 -1
- package/lib/utils/ship/formats.d.ts +85 -7
- package/lib/utils/ship/formats.js +734 -15
- package/lib/utils/ship/launcher.d.ts +13 -3
- package/lib/utils/ship/launcher.js +362 -17
- package/lib/utils/ship/layout.d.ts +268 -0
- package/lib/utils/ship/layout.js +351 -0
- package/lib/utils/ship/localize-metadata.d.ts +22 -0
- package/lib/utils/ship/localize-metadata.js +126 -0
- package/lib/utils/ship/mime.js +1 -9
- package/lib/utils/ship/msi.d.ts +126 -0
- package/lib/utils/ship/msi.js +417 -0
- package/lib/utils/ship/node-runtime.d.ts +71 -0
- package/lib/utils/ship/node-runtime.js +122 -0
- package/lib/utils/ship/payload.d.ts +156 -5
- package/lib/utils/ship/payload.js +580 -14
- package/lib/utils/ship/plan.d.ts +21 -0
- package/lib/utils/ship/plan.js +20 -7
- package/lib/utils/ship/plist.d.ts +58 -0
- package/lib/utils/ship/plist.js +198 -0
- package/lib/utils/ship/rpm.js +9 -3
- package/lib/utils/ship/schemas.d.ts +61 -0
- package/lib/utils/ship/schemas.js +118 -0
- package/lib/utils/ship/scripts.js +7 -3
- package/lib/utils/ship/settings.d.ts +19 -1
- package/lib/utils/ship/settings.js +12 -1
- package/lib/utils/ship/share-dirs.d.ts +23 -0
- package/lib/utils/ship/share-dirs.js +46 -0
- package/lib/utils/ship/signing.d.ts +195 -0
- package/lib/utils/ship/signing.js +317 -0
- package/lib/utils/ship/stage-manifest.d.ts +40 -12
- package/lib/utils/ship/stage-manifest.js +99 -13
- package/lib/utils/ship/stage-writer.d.ts +48 -0
- package/lib/utils/ship/stage-writer.js +71 -1
- package/lib/utils/ship/types.d.ts +158 -13
- package/lib/utils/ship/xml.d.ts +7 -0
- package/lib/utils/ship/xml.js +25 -0
- package/lib/utils/ship/zip.d.ts +69 -0
- package/lib/utils/ship/zip.js +245 -0
- package/lib/utils/trust-registry.js +5 -0
- package/package.json +19 -19
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import type { LauncherRuntime } from './app-runtime.js';
|
|
2
|
+
import type { Layout } from './layout.js';
|
|
1
3
|
import type { ShipSettings } from './types.js';
|
|
2
4
|
/**
|
|
3
|
-
* Render the launcher for
|
|
5
|
+
* Render the launcher for one layout.
|
|
4
6
|
*
|
|
5
|
-
* `bundleRelPath` is the bundle's path inside
|
|
7
|
+
* `bundleRelPath` is the bundle's path inside the layout's BUNDLE directory —
|
|
8
|
+
* `lib/<binaryName>/` on Linux, `Contents/Resources/lib/` in a `.app`, `app\` in
|
|
9
|
+
* a Windows program directory.
|
|
10
|
+
*
|
|
11
|
+
* `runtime` names what the stage CARRIES (`utils/ship/app-runtime.ts`), as
|
|
12
|
+
* stage-relative paths, and is empty when it carries nothing. The macOS and
|
|
13
|
+
* Windows forms read it; the Linux one does not and never will, because its
|
|
14
|
+
* interpreter is a package dependency. The default is what keeps a stage that
|
|
15
|
+
* carries nothing byte-identical to what M1 wrote.
|
|
6
16
|
*/
|
|
7
|
-
export declare function renderLauncher(settings: ShipSettings, bundleRelPath: string): string;
|
|
17
|
+
export declare function renderLauncher(settings: ShipSettings, bundleRelPath: string, layout: Layout, runtime?: LauncherRuntime): string;
|
|
@@ -1,35 +1,108 @@
|
|
|
1
|
-
// The
|
|
1
|
+
// The launcher a package installs in front of the app — one form per OS layout.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Every form execs ONE interpreter, named by `settings.app`, and `deriveDepends`
|
|
4
|
+
// seeds the dependency from that same field. That coupling is the point rather
|
|
5
|
+
// than a convenience: the first cut of the Node half derived "this payload needs
|
|
6
|
+
// Node" from a FILENAME (`*.node.mjs` anywhere in the staged tree) while this
|
|
7
|
+
// function still execed gjs unconditionally, and the two disagreed in both
|
|
8
|
+
// directions — a `--app gjs` project that merely built a Node bundle beside its
|
|
9
|
+
// GJS one got `Depends: gjs (>= 1.86), nodejs (>= 24)` and a launcher running the
|
|
10
|
+
// GJS bundle, and `>= 24` is unsatisfiable on every current DEB stable, so a
|
|
11
|
+
// working package became uninstallable for a reason its author never opted into.
|
|
12
|
+
// `assertLauncherMatchesInterpreter` in `utils/ship/payload.ts` re-reads this
|
|
13
|
+
// line off the STAGED file and is the gate that keeps the two from drifting
|
|
14
|
+
// apart again. It reads the LAYOUT's launcher, so the three forms below share one
|
|
15
|
+
// `execLine` rather than each answering the question themselves.
|
|
16
|
+
//
|
|
17
|
+
// It derives its own location at runtime instead of having one baked in. That is
|
|
4
18
|
// what lets ONE staged payload become a `.deb` under `/usr`, an `.rpm` under
|
|
5
19
|
// `/usr` and a Flatpak under `/app` (ADR 0024 § 3) — a baked path would force a
|
|
6
20
|
// payload per format and collapse the whole design back into N packagers.
|
|
7
21
|
//
|
|
8
|
-
// `XDG_DATA_DIRS` is prepended for the same reason
|
|
9
|
-
// on
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
22
|
+
// `XDG_DATA_DIRS` is prepended for the same reason, and on all three OSes: GTK
|
|
23
|
+
// and GLib read it on macOS and Windows too, and it is what makes the staged
|
|
24
|
+
// icons, the desktop entry and the compiled GSettings schemas findable wherever
|
|
25
|
+
// the tree was installed. Getting that from the launcher rather than from the app
|
|
26
|
+
// keeps the app free of install-layout knowledge.
|
|
27
|
+
//
|
|
28
|
+
// THREE FORMS, because neither other OS runs this file. macOS runs a `/bin/sh`
|
|
29
|
+
// script too but cannot use `readlink -f` and must not be handed a `DYLD_*`;
|
|
30
|
+
// Windows runs `cmd.exe`, which shares no syntax with either. What they DO share
|
|
31
|
+
// is the set of decisions — where the prefix comes from, which variables are
|
|
32
|
+
// exported, what is exec'd — so the three renderers sit here side by side and the
|
|
33
|
+
// dispatch is a `switch` with a `never` guard, the same closed-vocabulary
|
|
34
|
+
// discipline `packOne` uses for the packers.
|
|
35
|
+
import { posix } from 'node:path';
|
|
14
36
|
/** POSIX-shell single-quote: the only escape inside `'…'` is `'\''`. */
|
|
15
37
|
function shellQuote(value) {
|
|
16
38
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
17
39
|
}
|
|
18
40
|
/**
|
|
19
|
-
*
|
|
41
|
+
* Quote one argument for `cmd.exe`, or refuse it.
|
|
42
|
+
*
|
|
43
|
+
* `cmd.exe` has no escape that survives its own parser for `"` or `%` inside a
|
|
44
|
+
* quoted string: `%` starts an expansion at PARSE time (so `%foo%` silently
|
|
45
|
+
* becomes empty rather than staying literal), and `"` ends the quoted run with
|
|
46
|
+
* nothing that reliably re-opens it. `!` is only special under delayed expansion,
|
|
47
|
+
* which this launcher does not enable, but a consumer copying the line into one
|
|
48
|
+
* that does gets the same silent substitution — so all three are refused with the
|
|
49
|
+
* config key named, rather than emitted into a launcher that runs with the wrong
|
|
50
|
+
* argv.
|
|
51
|
+
*/
|
|
52
|
+
function cmdQuote(value) {
|
|
53
|
+
const bad = /["%!\r\n]/.exec(value);
|
|
54
|
+
if (bad !== null) {
|
|
55
|
+
throw new Error(`gjsify ship: \`gjsify.ship.execArgs\` contains ${JSON.stringify(value)}, and cmd.exe has no way ` +
|
|
56
|
+
`to pass ${JSON.stringify(bad[0])} through a Windows launcher literally — \`%\` expands at ` +
|
|
57
|
+
'parse time and `"` ends the quoted run. Move the value into the app (it can read it from its ' +
|
|
58
|
+
'own config) or drop the character.');
|
|
59
|
+
}
|
|
60
|
+
return `"${value}"`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Render the launcher for one layout.
|
|
64
|
+
*
|
|
65
|
+
* `bundleRelPath` is the bundle's path inside the layout's BUNDLE directory —
|
|
66
|
+
* `lib/<binaryName>/` on Linux, `Contents/Resources/lib/` in a `.app`, `app\` in
|
|
67
|
+
* a Windows program directory.
|
|
68
|
+
*
|
|
69
|
+
* `runtime` names what the stage CARRIES (`utils/ship/app-runtime.ts`), as
|
|
70
|
+
* stage-relative paths, and is empty when it carries nothing. The macOS and
|
|
71
|
+
* Windows forms read it; the Linux one does not and never will, because its
|
|
72
|
+
* interpreter is a package dependency. The default is what keeps a stage that
|
|
73
|
+
* carries nothing byte-identical to what M1 wrote.
|
|
74
|
+
*/
|
|
75
|
+
export function renderLauncher(settings, bundleRelPath, layout, runtime = {}) {
|
|
76
|
+
switch (layout.name) {
|
|
77
|
+
case 'linux':
|
|
78
|
+
return renderPrefixLauncher(settings, bundleRelPath, layout);
|
|
79
|
+
case 'darwin':
|
|
80
|
+
return renderAppBundleLauncher(settings, bundleRelPath, layout, runtime);
|
|
81
|
+
case 'windows':
|
|
82
|
+
return renderWindowsLauncher(settings, bundleRelPath, layout, runtime);
|
|
83
|
+
default: {
|
|
84
|
+
const unhandled = layout.name;
|
|
85
|
+
throw new Error(`gjsify ship: no launcher is wired for the "${String(unhandled)}" layout.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Linux: a `/bin/sh` script that walks up from its own RESOLVED path to the prefix.
|
|
20
91
|
*
|
|
21
|
-
* `
|
|
92
|
+
* `readlink -f` is what makes this correct when the package's `bin/` entry has
|
|
93
|
+
* been symlinked — `/usr/local/bin/<name>` → `/usr/lib/<pkg>/…` is a layout
|
|
94
|
+
* distributions produce — and it is GNU coreutils', which every Linux this
|
|
95
|
+
* packages for has.
|
|
22
96
|
*/
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const args = settings.execArgs.map(shellQuote).join(' ');
|
|
97
|
+
function renderPrefixLauncher(settings, bundleRelPath, layout) {
|
|
98
|
+
const dirs = layout.dirs(settings);
|
|
26
99
|
const lines = [
|
|
27
100
|
'#!/bin/sh',
|
|
28
101
|
'# Generated by `gjsify ship` — do not edit.',
|
|
29
102
|
'set -e',
|
|
30
103
|
'self=$(readlink -f "$0")',
|
|
31
104
|
'prefix=$(dirname "$(dirname "$self")")',
|
|
32
|
-
|
|
105
|
+
`XDG_DATA_DIRS="$prefix/${dirs.data}:\${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"`,
|
|
33
106
|
'export XDG_DATA_DIRS',
|
|
34
107
|
];
|
|
35
108
|
// Typelibs the package carries itself (see `gjsify.ship.bundledTypelibs`). PREPENDED, not
|
|
@@ -38,7 +111,7 @@ export function renderLauncher(settings, bundleRelPath) {
|
|
|
38
111
|
// actually staged — an empty `GI_TYPELIB_PATH` entry makes GI search a directory that is not
|
|
39
112
|
// there, on every import, forever.
|
|
40
113
|
if (settings.typelibFiles.length > 0) {
|
|
41
|
-
const giDir =
|
|
114
|
+
const giDir = `"$prefix"/${dirs.native}`;
|
|
42
115
|
for (const variable of ['GI_TYPELIB_PATH', 'LD_LIBRARY_PATH']) {
|
|
43
116
|
// `${VAR:+:$VAR}` appends the existing value with a separator ONLY when it is set —
|
|
44
117
|
// a plain `:$VAR` leaves a trailing colon, which both loaders read as "and the current
|
|
@@ -53,8 +126,280 @@ export function renderLauncher(settings, bundleRelPath) {
|
|
|
53
126
|
// above does for icons and schemas — a baked `/usr/share/locale` would be wrong in a Flatpak
|
|
54
127
|
// and in any `--prefix` tree.
|
|
55
128
|
if (settings.localeFiles.length > 0) {
|
|
56
|
-
lines.push(
|
|
129
|
+
lines.push(`GJSIFY_LOCALE_DIR="$prefix"/${dirs.data}/locale`, 'export GJSIFY_LOCALE_DIR');
|
|
130
|
+
}
|
|
131
|
+
lines.push(`exec ${execLine(layout, `"$prefix"/${dirs.bundle}/${bundleRelPath}`, settings)} "$@"`, '');
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* macOS: `Contents/MacOS/<name>` — what a self-contained bundle's launcher says,
|
|
136
|
+
* and two things it must still NOT do.
|
|
137
|
+
*
|
|
138
|
+
* IT EXECS WHAT THE BUNDLE CARRIES. `runtime` comes from
|
|
139
|
+
* `utils/ship/app-runtime.ts` and names the interpreter, the relocated GTK
|
|
140
|
+
* closure and the node-gi addon staged inside `Contents/` — so the exec line is
|
|
141
|
+
* `"$here/node"` and the two locators below point into `Contents/Frameworks`.
|
|
142
|
+
* When the stage carries none of them every line here is byte-identical to what
|
|
143
|
+
* M2a wrote, which is what keeps a bundle assembled without the runtime packages
|
|
144
|
+
* exactly as (un)usable as it was rather than differently broken.
|
|
145
|
+
*
|
|
146
|
+
* 1. **No `readlink -f`.** That flag is GNU coreutils'; the BSD `readlink` macOS
|
|
147
|
+
* ships does not have it, and a launcher whose first command fails under
|
|
148
|
+
* `set -e` exits before it does anything. It needs none either: LaunchServices,
|
|
149
|
+
* `open(1)` and Finder all exec `Contents/MacOS/<name>` at its real path
|
|
150
|
+
* inside the bundle, and the staged tree holds no symlink to resolve.
|
|
151
|
+
* 2. **No `DYLD_*`.** THE RULE STANDS AND ITS OLD REASON DID NOT, so the reason
|
|
152
|
+
* is replaced rather than the rule. This comment used to say a wrapper
|
|
153
|
+
* "structurally cannot hand the loader a library path" because SIP strips an
|
|
154
|
+
* inherited `DYLD_*` at the `/bin/sh` exec — which is stronger than what was
|
|
155
|
+
* measured, and this repository depends on the difference in two places that
|
|
156
|
+
* are green on the darwin legs today (`utils/bin-shim.ts`'s
|
|
157
|
+
* `dyldFallbackPreamble`, `packages/node-gi`'s `maybeReexecForGtkRuntime`).
|
|
158
|
+
* The narrow fact is that an INHERITED `DYLD_*` is stripped when a PROTECTED
|
|
159
|
+
* binary is exec'd; what a shim exports ITSELF survives into an unprotected
|
|
160
|
+
* child.
|
|
161
|
+
*
|
|
162
|
+
* The reason that holds is one milestone away and points the same way: a
|
|
163
|
+
* hardened-runtime, Developer-ID-signed main executable IS restricted, so the
|
|
164
|
+
* variable is stripped there. A launcher depending on `DYLD_*` therefore
|
|
165
|
+
* works unsigned and breaks on the day the bundle is signed (#1354 M6,
|
|
166
|
+
* ADR 0024 § A4) — the worst possible day to find out. `GI_TYPELIB_PATH` is
|
|
167
|
+
* not a `DYLD_` variable and is stripped under neither rule, so the half that
|
|
168
|
+
* CAN be handed over is; the other half is
|
|
169
|
+
* `GIRepository.Repository.prepend_library_path` from inside the process,
|
|
170
|
+
* which is ADR 0021's decision for prebuilds applied to the app's own runtime.
|
|
171
|
+
*
|
|
172
|
+
* Corrected rather than deleted, because a rule whose stated reason is wrong
|
|
173
|
+
* gets "simplified" back into the bug the first time somebody checks it.
|
|
174
|
+
*
|
|
175
|
+
* What this function exports INSTEAD is the point of the distinction, not an
|
|
176
|
+
* exception to it: `GJSIFY_GTK_RUNTIME`, `NODE_GI_NATIVE`,
|
|
177
|
+
* `GJSIFY_GI_LIBRARY_PATH`, `GI_TYPELIB_PATH`. Every one is read by node-gi
|
|
178
|
+
* or by GLib in JS or in the process, none by dyld, so none is stripped from
|
|
179
|
+
* a restricted process and the bundle behaves the same signed and unsigned.
|
|
180
|
+
* The dylibs themselves need no variable at all: `install_name_tool` rewrote
|
|
181
|
+
* every install name to `@loader_path/<leaf>` when the closure was built, and
|
|
182
|
+
* the addon's `@rpath` is `@loader_path/gtk/lib` — which is exactly why the
|
|
183
|
+
* closure has to be staged as a TREE and not flattened.
|
|
184
|
+
*/
|
|
185
|
+
function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
186
|
+
const root = layout.root(settings);
|
|
187
|
+
const contents = `${root}/Contents`;
|
|
188
|
+
const dirs = layout.dirs(settings);
|
|
189
|
+
// Every path is expressed relative to `Contents/`, which the launcher finds by
|
|
190
|
+
// going one directory up from `Contents/MacOS`. Deriving them from the
|
|
191
|
+
// layout's own directories is what keeps the two from drifting: they are the
|
|
192
|
+
// same strings the planner placed the files with.
|
|
193
|
+
const under = (dir) => posix.relative(contents, dir);
|
|
194
|
+
// …and the interpreter relative to `$here`, which is `Contents/MacOS` itself.
|
|
195
|
+
// Two anchors rather than one because the launcher already computes both, and
|
|
196
|
+
// `"$here/node"` is the shortest true expression for a file beside it.
|
|
197
|
+
const beside = (path) => posix.relative(dirs.launcher, path);
|
|
198
|
+
const lines = [
|
|
199
|
+
'#!/bin/sh',
|
|
200
|
+
'# Generated by `gjsify ship` — do not edit.',
|
|
201
|
+
'set -e',
|
|
202
|
+
'here=$(cd -- "$(dirname -- "$0")" && pwd)',
|
|
203
|
+
'contents=$(dirname -- "$here")',
|
|
204
|
+
`XDG_DATA_DIRS="$contents/${under(dirs.data)}:\${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"`,
|
|
205
|
+
'export XDG_DATA_DIRS',
|
|
206
|
+
];
|
|
207
|
+
// The two locators a carried runtime needs, and both are read by node-gi ITSELF
|
|
208
|
+
// rather than by a loader — which is the whole reason they are allowed here at
|
|
209
|
+
// all (see this function's second numbered note).
|
|
210
|
+
//
|
|
211
|
+
// `GJSIFY_GTK_RUNTIME` is candidate 1 of `resolveGtkRuntimeBundle()`'s four.
|
|
212
|
+
// Candidates 2–4 walk from `@gjsify/node-gi`'s own package directory or through
|
|
213
|
+
// `node_modules`, and a shipped `.app` has neither: its JavaScript is one
|
|
214
|
+
// bundled file under `Contents/Resources/lib`, so `import.meta.url` there
|
|
215
|
+
// resolves node-gi's "package root" to the bundle's directory and every probed
|
|
216
|
+
// path lands beside the bundle instead of in `Contents/Frameworks`.
|
|
217
|
+
//
|
|
218
|
+
// `NODE_GI_NATIVE` is the same problem for the addon: `prebuildAddonPath()`
|
|
219
|
+
// joins `prebuilds/<target>/node_gi.node` onto that same wrong root
|
|
220
|
+
// (`packages/node-gi/node-gi/native-paths.js`). An absolute path is one of the
|
|
221
|
+
// three values that variable takes (`'build'`, `'prebuild'`, or a path), and
|
|
222
|
+
// `nativeCandidates()` resolves it and returns it ALONE — so the bundle's addon
|
|
223
|
+
// is not merely preferred, nothing else is tried.
|
|
224
|
+
if (runtime.gtkRuntimeDir !== undefined) {
|
|
225
|
+
lines.push(`GJSIFY_GTK_RUNTIME="$contents/${under(runtime.gtkRuntimeDir)}"`, 'export GJSIFY_GTK_RUNTIME');
|
|
226
|
+
}
|
|
227
|
+
if (runtime.nodeGiAddon !== undefined) {
|
|
228
|
+
lines.push(`NODE_GI_NATIVE="$contents/${under(runtime.nodeGiAddon)}"`, 'export NODE_GI_NATIVE');
|
|
229
|
+
}
|
|
230
|
+
if (settings.typelibFiles.length > 0) {
|
|
231
|
+
lines.push(`GI_TYPELIB_PATH="$contents/${under(dirs.native)}"\${GI_TYPELIB_PATH:+:$GI_TYPELIB_PATH}`, 'export GI_TYPELIB_PATH');
|
|
232
|
+
// The other half of the same pair, and the half no loader variable can
|
|
233
|
+
// carry here. GI resolves the typelib and then `g_module_open`s the bare
|
|
234
|
+
// leaf it records; `LD_LIBRARY_PATH` answers that on Linux and `DYLD_*`
|
|
235
|
+
// cannot answer it in a signed bundle. `GJSIFY_GI_LIBRARY_PATH` is read by
|
|
236
|
+
// node-gi in JS and handed to `gi_repository_prepend_library_path()`, so
|
|
237
|
+
// dyld never sees it (#1410). Written here for the first time: that fix
|
|
238
|
+
// shipped the READER with no writer anywhere in this pipeline.
|
|
239
|
+
lines.push(`GJSIFY_GI_LIBRARY_PATH="$contents/${under(dirs.native)}"\${GJSIFY_GI_LIBRARY_PATH:+:$GJSIFY_GI_LIBRARY_PATH}`, 'export GJSIFY_GI_LIBRARY_PATH');
|
|
240
|
+
}
|
|
241
|
+
if (settings.localeFiles.length > 0) {
|
|
242
|
+
lines.push(`GJSIFY_LOCALE_DIR="$contents/${under(dirs.data)}/locale"`, 'export GJSIFY_LOCALE_DIR');
|
|
57
243
|
}
|
|
58
|
-
|
|
244
|
+
// `"$here/node"`, not `node`. The bare name is true of a developer's machine
|
|
245
|
+
// and false of a `.app` a stranger downloads, and the difference is not a
|
|
246
|
+
// degraded experience: macOS ships no Node at all, so the launcher's first act
|
|
247
|
+
// would be `exec: node: not found`.
|
|
248
|
+
const interpreter = runtime.interpreter === undefined ? undefined : `"$here/${beside(runtime.interpreter)}"`;
|
|
249
|
+
lines.push(`exec ${execLine(layout, `"$contents/${under(dirs.bundle)}/${bundleRelPath}"`, settings, interpreter)} "$@"`, '');
|
|
59
250
|
return lines.join('\n');
|
|
60
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Windows: a `.cmd` that derives the program directory from `%~dp0` — and execs
|
|
254
|
+
* the interpreter that directory carries.
|
|
255
|
+
*
|
|
256
|
+
* CRLF, and that is not cosmetic — `cmd.exe` reads a batch file in chunks and
|
|
257
|
+
* re-seeks by byte OFFSET while it runs, which is where the documented `goto` and
|
|
258
|
+
* block-parsing failures on LF-only files come from. It costs nothing to be right
|
|
259
|
+
* about, and the file is ASCII for the same reason: a batch file is read in the
|
|
260
|
+
* console's active code page, not as UTF-8.
|
|
261
|
+
*
|
|
262
|
+
* `PATH` carries the native directory because Windows has no rpath: a DLL is
|
|
263
|
+
* found on `PATH`, in the directory of the image that loaded it, or not at all.
|
|
264
|
+
* That makes `PATH` the load-bearing variable here — where `LD_LIBRARY_PATH` is
|
|
265
|
+
* on Linux, and where nothing can be on macOS. It is also the answer to the
|
|
266
|
+
* question `GJSIFY_GI_LIBRARY_PATH` answers in the `.app` form: GI resolves a
|
|
267
|
+
* typelib and then `g_module_open`s the bare leaf it records, and on Windows
|
|
268
|
+
* `PATH` reaches that leaf, so no second variable is needed.
|
|
269
|
+
*
|
|
270
|
+
* WHAT THIS DELIBERATELY DOES NOT PUT ON `PATH` is the carried GTK closure's own
|
|
271
|
+
* `bin/`, and the omission is the Windows counterpart of the `.app` form's "no
|
|
272
|
+
* `DYLD_*`" rule — with the opposite reason, so it is written down rather than
|
|
273
|
+
* inferred. node-gi does it ITSELF, in-process, before it `require`s the addon:
|
|
274
|
+
* `maybePrependGtkRuntimeDllPath()` runs at `packages/node-gi/node-gi/index.js`
|
|
275
|
+
* top level, above `loadNative()`, because "Windows re-reads the DLL search path
|
|
276
|
+
* at every LoadLibrary (unlike dyld's launch-time capture), so mutating
|
|
277
|
+
* process.env.PATH in-process before the require() below covers both the static
|
|
278
|
+
* DLL imports and the runtime g_module_open of typelib backers". What that
|
|
279
|
+
* function needs from the launcher is the LOCATOR — `GJSIFY_GTK_RUNTIME`,
|
|
280
|
+
* candidate 1 of `resolveGtkRuntimeBundle()`'s four — and a launcher-set `PATH`
|
|
281
|
+
* would be a second, silently diverging copy of a directory node-gi already
|
|
282
|
+
* derives from it.
|
|
283
|
+
*
|
|
284
|
+
* `runtime` comes from `utils/ship/app-runtime.ts` and names what the program
|
|
285
|
+
* directory CARRIES, as stage-relative paths. When it carries nothing every line
|
|
286
|
+
* here is byte-identical to what M1 wrote (#1354 M3).
|
|
287
|
+
*/
|
|
288
|
+
function renderWindowsLauncher(settings, bundleRelPath, layout, runtime) {
|
|
289
|
+
const dirs = layout.dirs(settings);
|
|
290
|
+
// `%~dp0` already ends in a backslash, so every path below concatenates
|
|
291
|
+
// directly — which is the whole dialect difference from the `.app` form's
|
|
292
|
+
// `"$here/node"`, where the separator is written out. `utils/ship/payload.ts`
|
|
293
|
+
// reads this token back and has to know it (`readLauncherInterpreters`).
|
|
294
|
+
const at = (rel) => `%HERE%${windowsPath(rel)}`;
|
|
295
|
+
const lines = [
|
|
296
|
+
'@echo off',
|
|
297
|
+
'rem Generated by "gjsify ship" - do not edit.',
|
|
298
|
+
'setlocal',
|
|
299
|
+
'set "HERE=%~dp0"',
|
|
300
|
+
// `if defined` rather than a bare `;%VAR%`, for the reason the sh form
|
|
301
|
+
// gives: an unset variable leaves a trailing separator, and an empty entry
|
|
302
|
+
// in a Windows search path is read as the current directory.
|
|
303
|
+
prependVar('XDG_DATA_DIRS', at(dirs.data)),
|
|
304
|
+
];
|
|
305
|
+
// The two locators a carried runtime needs, identical in role to the `.app`
|
|
306
|
+
// form's and read by node-gi ITSELF rather than by a loader. `GJSIFY_GTK_RUNTIME`
|
|
307
|
+
// is candidate 1 of `resolveGtkRuntimeBundle()`'s four, and candidates 2-4 walk
|
|
308
|
+
// from `@gjsify/node-gi`'s own package directory — which, in a shipped program
|
|
309
|
+
// directory, resolves to wherever the staged JavaScript sits and not to `lib/`.
|
|
310
|
+
// `NODE_GI_NATIVE` pins the addon the same way, absolutely, so
|
|
311
|
+
// `nativeCandidates()` returns it ALONE.
|
|
312
|
+
//
|
|
313
|
+
// `set "NAME=value"`, quoted, because an unquoted `set` keeps trailing spaces
|
|
314
|
+
// and a program directory under `C:\Program Files\…` has spaces in it.
|
|
315
|
+
if (runtime.gtkRuntimeDir !== undefined) {
|
|
316
|
+
lines.push(`set "GJSIFY_GTK_RUNTIME=${at(runtime.gtkRuntimeDir)}"`);
|
|
317
|
+
}
|
|
318
|
+
if (runtime.nodeGiAddon !== undefined) {
|
|
319
|
+
lines.push(`set "NODE_GI_NATIVE=${at(runtime.nodeGiAddon)}"`);
|
|
320
|
+
}
|
|
321
|
+
if (settings.typelibFiles.length > 0) {
|
|
322
|
+
lines.push(prependVar('GI_TYPELIB_PATH', at(dirs.native)), prependVar('PATH', at(dirs.native)));
|
|
323
|
+
}
|
|
324
|
+
if (settings.localeFiles.length > 0) {
|
|
325
|
+
lines.push(`set "GJSIFY_LOCALE_DIR=${at(dirs.data)}\\locale"`);
|
|
326
|
+
}
|
|
327
|
+
// `"%HERE%node.exe"`, not `node`. The bare name is true of a developer's
|
|
328
|
+
// machine and false of a program directory a stranger unzips: Windows ships no
|
|
329
|
+
// Node at all, so the launcher's first act would be `'node' is not recognized
|
|
330
|
+
// as an internal or external command`.
|
|
331
|
+
const interpreter = runtime.interpreter === undefined ? undefined : `"${at(runtime.interpreter)}"`;
|
|
332
|
+
// No `exec` in batch. `cmd.exe` reports the last command's exit code when the
|
|
333
|
+
// script ends, so the interpreter's status is the launcher's status.
|
|
334
|
+
lines.push(`${execLine(layout, `"${at(dirs.bundle)}\\${windowsPath(bundleRelPath)}"`, settings, interpreter)} %*`, '');
|
|
335
|
+
return lines.join('\r\n');
|
|
336
|
+
}
|
|
337
|
+
/** `set "VAR=<value>;%VAR%"`, and just `<value>` when VAR is not set. */
|
|
338
|
+
function prependVar(variable, value) {
|
|
339
|
+
return `if defined ${variable} (set "${variable}=${value};%${variable}%") else (set "${variable}=${value}")`;
|
|
340
|
+
}
|
|
341
|
+
/** POSIX-separated → Windows-separated. An empty segment would produce `\\`, which is a UNC root. */
|
|
342
|
+
function windowsPath(rel) {
|
|
343
|
+
return rel
|
|
344
|
+
.split('/')
|
|
345
|
+
.filter((part) => part.length > 0)
|
|
346
|
+
.join('\\');
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The interpreter, the bundle and the project's own `execArgs` — for every layout.
|
|
350
|
+
*
|
|
351
|
+
* ONE function, called by all three forms, because the answer is the same
|
|
352
|
+
* question everywhere and the launcher is where it is checkable:
|
|
353
|
+
* `assertLauncherMatchesInterpreter` reads this line back off the STAGED file
|
|
354
|
+
* and compares it with the dependency about to be declared.
|
|
355
|
+
*
|
|
356
|
+
* `settings.app`, NOT the layout. ADR 0024 § 4's runtime-per-OS table says what
|
|
357
|
+
* a shipped artifact CARRIES — that is `Layout.shippedRuntime` — and reading it
|
|
358
|
+
* as a per-layout launcher decision breaks both directions at once: it would
|
|
359
|
+
* refuse `gjsify.app: "gjs"` for the macOS layout, and it would put `exec node`
|
|
360
|
+
* in front of a bundle whose first line is
|
|
361
|
+
* `import Gtk from 'gi://Gtk?version=4.0'`. Measured before it was fixed: a
|
|
362
|
+
* project with no `gjsify.app` key staged
|
|
363
|
+
* `exec node "$contents/Resources/lib/gjs.js"` for macOS at exit 0. The only
|
|
364
|
+
* interpreter that can read the payload is the one the payload was BUILT for,
|
|
365
|
+
* and that is `settings.app` on all three.
|
|
366
|
+
*
|
|
367
|
+
* `Layout.runtimeGap` is where the honest half of that lives: it says, per OS,
|
|
368
|
+
* why the staged launcher does not yet name what § 4 derives, and `gjsify ship`
|
|
369
|
+
* prints it.
|
|
370
|
+
*
|
|
371
|
+
* `gjs` needs `-m` to treat the bundle as an ES module; `node` decides from the
|
|
372
|
+
* extension and rejects the flag.
|
|
373
|
+
*
|
|
374
|
+
* THE NAME OR A PATH, and which one is a fact about the STAGE. On Linux it stays
|
|
375
|
+
* the bare name, because the emitted dependency is what guarantees the
|
|
376
|
+
* interpreter is on `PATH` — and on Fedora `/usr/bin/node` is an alternatives
|
|
377
|
+
* symlink whose target is whichever stream package won, so hardcoding a path
|
|
378
|
+
* there would pin the launcher to a layout the dependency does not promise. macOS
|
|
379
|
+
* and Windows have no system Node and no dependency to declare, which is what
|
|
380
|
+
* `@gjsify/node-runtime-<target>` exists for; this comment used to say "the day a
|
|
381
|
+
* stage carries one, THIS is the line that names it by a layout-relative path
|
|
382
|
+
* instead", and #1354 M2b is that day — `utils/ship/app-runtime.ts` stages the
|
|
383
|
+
* interpreter into `Contents/MacOS/node` and the `.app` launcher execs
|
|
384
|
+
* `"$here/node"`.
|
|
385
|
+
*
|
|
386
|
+
* #1354 M3 is the same day one OS over: the Windows layout stages `node.exe` into
|
|
387
|
+
* the program directory and its `.cmd` runs `"%HERE%node.exe"`.
|
|
388
|
+
*
|
|
389
|
+
* `utils/ship/payload.ts`'s `readLauncherInterpreters` reads those quoted,
|
|
390
|
+
* variable-bearing tokens back to the bare name `node`, which is what keeps
|
|
391
|
+
* `assertLauncherMatchesInterpreter` a check rather than a vacuous pass. The two
|
|
392
|
+
* dialects differ in a way that reader has to know: `$here/` writes its separator
|
|
393
|
+
* and `%~dp0` already ends in one, so the macOS token splits on a `/` that is
|
|
394
|
+
* there and the Windows token has no separator to split on at all.
|
|
395
|
+
*/
|
|
396
|
+
function execLine(layout, bundle, settings, interpreter) {
|
|
397
|
+
const quote = layout.name === 'windows' ? cmdQuote : shellQuote;
|
|
398
|
+
const args = settings.execArgs.map(quote).join(' ');
|
|
399
|
+
// The NAME when the stage carries no interpreter, the layout-relative PATH
|
|
400
|
+
// when it does. `-m` stays attached to `gjs` in both branches: it tells the
|
|
401
|
+
// interpreter to read the bundle as a module and has nothing to do with how
|
|
402
|
+
// the interpreter was found.
|
|
403
|
+
const program = interpreter ?? (settings.app === 'node' ? 'node' : 'gjs');
|
|
404
|
+
return `${settings.app === 'node' ? program : `${program} -m`} ${bundle}${args ? ` ${args}` : ''}`;
|
|
405
|
+
}
|