@gjsify/cli 0.64.4 → 0.64.6
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 +263 -263
- package/lib/commands/ship.js +20 -2
- package/lib/utils/bin-shim.d.ts +16 -5
- package/lib/utils/bin-shim.js +20 -6
- package/lib/utils/install-backend-native.js +39 -1
- package/lib/utils/install-extraneous.d.ts +25 -0
- package/lib/utils/install-extraneous.js +41 -0
- package/lib/utils/ship/app-runtime.d.ts +22 -4
- package/lib/utils/ship/app-runtime.js +45 -16
- package/lib/utils/ship/gtk-runtime-acquire.d.ts +36 -0
- package/lib/utils/ship/gtk-runtime-acquire.js +59 -0
- package/lib/utils/ship/launcher.js +20 -7
- package/lib/utils/ship/layout.d.ts +23 -0
- package/lib/utils/ship/layout.js +32 -1
- package/lib/utils/ship/node-runtime.js +8 -6
- package/package.json +20 -20
package/lib/commands/ship.js
CHANGED
|
@@ -26,12 +26,16 @@ import { CHANNELS, CHANNEL_ENV, parseDeclaredChannel } from '../utils/channel.js
|
|
|
26
26
|
import { mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
28
28
|
import { Config } from '../config.js';
|
|
29
|
+
import { ensurePkgDir } from './dlx.js';
|
|
29
30
|
import { renderDesktopEntry, renderMetainfoApp, renderMetainfoCli, validateAppMetadata, } from '../utils/app-metadata.js';
|
|
30
31
|
import { readPackageJson } from '../utils/pkg-json.js';
|
|
32
|
+
import { parseSpec } from '../utils/parse-spec.js';
|
|
33
|
+
import { cliVersion } from '../utils/publish-headers.js';
|
|
31
34
|
import { describeExit, spawnToCompletion } from '../utils/spawn.js';
|
|
32
35
|
import { buildDeb } from '../utils/ship/deb.js';
|
|
33
36
|
import { deriveDepends, warnAboutGjsFloor, warnAboutNodeFloor } from '../utils/ship/depends.js';
|
|
34
37
|
import { isGtkRuntimeTarget, stageAppRuntime } from '../utils/ship/app-runtime.js';
|
|
38
|
+
import { acquireGtkRuntime } from '../utils/ship/gtk-runtime-acquire.js';
|
|
35
39
|
import { discoverPayload } from '../utils/ship/discover.js';
|
|
36
40
|
import { appDirFor, appDirPayload, appImageHostRequirements, appImageRuntimeNotice, buildAppImage, findPinnedRuntime, stampAppDirTimes, } from '../utils/ship/appimage.js';
|
|
37
41
|
import { buildDmgImage, dmgVolumeDir, dmgVolumeName } from '../utils/ship/dmg.js';
|
|
@@ -411,7 +415,7 @@ async function assemble(args) {
|
|
|
411
415
|
// `.deb`/`.rpm` `Depends:` is the promise that `node` or `gjs` is on `PATH`, and
|
|
412
416
|
// carrying a second copy inside the payload would be a private interpreter no
|
|
413
417
|
// distribution update ever patches.
|
|
414
|
-
const runtime = resolveCarriedRuntime(layout, settings, projectDir);
|
|
418
|
+
const runtime = await resolveCarriedRuntime(layout, settings, projectDir);
|
|
415
419
|
const stageInputs = {
|
|
416
420
|
bundleFiles: discovered.bundleFiles,
|
|
417
421
|
launcher: renderLauncher(settings, basename(settings.bundlePath), layout, runtime.launcher),
|
|
@@ -761,21 +765,35 @@ function announceSigning(sign, notarize) {
|
|
|
761
765
|
* thing to make non-vacuous on this layout, since a JavaScript bundle has no
|
|
762
766
|
* cputype to disagree with.
|
|
763
767
|
*/
|
|
764
|
-
function resolveCarriedRuntime(layout, settings, projectDir) {
|
|
768
|
+
async function resolveCarriedRuntime(layout, settings, projectDir) {
|
|
765
769
|
const empty = { files: [], launcher: {}, found: [], missing: [] };
|
|
766
770
|
if (layout.os === 'linux' || settings.app !== 'node')
|
|
767
771
|
return empty;
|
|
768
772
|
const target = `${layout.os}-${settings.arch}`;
|
|
769
773
|
if (!isGtkRuntimeTarget(target))
|
|
770
774
|
return empty;
|
|
775
|
+
// The closure is fetched, not installed: a project that lists it puts a second
|
|
776
|
+
// GLib next to Homebrew's in its dev tree (see `gtk-runtime-acquire.ts`).
|
|
777
|
+
const acquired = await acquireGtkRuntime({
|
|
778
|
+
target,
|
|
779
|
+
version: cliVersion(),
|
|
780
|
+
discovery: { cwd: projectDir },
|
|
781
|
+
ensure: async (spec) => (await ensurePkgDir(parseSpec(spec), { verbose: false, cacheMaxAge: DLX_CACHE_MAX_AGE_MIN, frozen: false }))
|
|
782
|
+
.pkgDir,
|
|
783
|
+
log: (message) => console.log(`${LOG} ${message}`),
|
|
784
|
+
});
|
|
771
785
|
return stageAppRuntime({
|
|
772
786
|
layout,
|
|
773
787
|
identity: settings,
|
|
774
788
|
target,
|
|
775
789
|
cwd: projectDir,
|
|
776
790
|
interpreter: isNodeRuntimeTarget(target) ? resolveNodeRuntime(target, { cwd: projectDir }) : null,
|
|
791
|
+
...(acquired.kind === 'fetched' ? { fetchedGtkRuntimeDir: acquired.dir } : {}),
|
|
792
|
+
...(acquired.kind === 'failed' ? { gtkFetchFailure: acquired.reason } : {}),
|
|
777
793
|
});
|
|
778
794
|
}
|
|
795
|
+
/** How long a fetched runtime stays in the dlx cache — the same week `gjsify showcase` uses. */
|
|
796
|
+
const DLX_CACHE_MAX_AGE_MIN = 60 * 24 * 7;
|
|
779
797
|
/**
|
|
780
798
|
* Phase two: pack a stage that was assembled somewhere else.
|
|
781
799
|
*
|
package/lib/utils/bin-shim.d.ts
CHANGED
|
@@ -173,12 +173,23 @@ export declare function buildNativeEnvPreamble(scanRoot: string, bakedDirs?: rea
|
|
|
173
173
|
* cannot go stale the way baking `systemGiLibraryDirs()` in would (a launcher is
|
|
174
174
|
* routinely written before `brew install gtk4`), and it re-derives nothing —
|
|
175
175
|
* four literals from `dyld(1)`, shared with {@link dyldDefaultFallbackDirs}.
|
|
176
|
-
* An inherited value stays AHEAD of it
|
|
177
|
-
* would have searched on its own.
|
|
176
|
+
* An inherited value stays AHEAD of it.
|
|
178
177
|
*
|
|
179
|
-
* NOT the
|
|
180
|
-
*
|
|
181
|
-
* `
|
|
178
|
+
* NOT SUFFICIENT ON ITS OWN, so the `lib/` beside the `gjs` this shim execs is
|
|
179
|
+
* appended: "set to the documented default" is NOT "unset". Measured on macOS 27 / arm64 with Homebrew
|
|
180
|
+
* gjs 1.88.1 and a fresh glib keg: `gjs -c "imports.gi.Gio"` loads with the
|
|
181
|
+
* variable unset and dies with the documented default exported —
|
|
182
|
+
* `Failed to load shared library 'libgjs.0.dylib' referenced by the typelib`,
|
|
183
|
+
* then `Unsupported type void` from Gio's override `_init`. `GjsPrivate-1.0`
|
|
184
|
+
* names GJS's own library by bare leaf, and once the variable is SET dyld only
|
|
185
|
+
* finds it in a listed directory; `gjs`'s rpath names GLib's keg, not its own.
|
|
186
|
+
* That broke every `gjsify` command on an Apple-silicon Mac, `--version`
|
|
187
|
+
* included, until a symlink was planted in GLib's keg by hand (the CI step
|
|
188
|
+
* `Make libgjs resolvable without a DYLD_ variable`) — and a `brew upgrade glib`
|
|
189
|
+
* silently removes it again. `<dir of gjs>/../lib` is where every prefix
|
|
190
|
+
* (Homebrew on either arch, MacPorts) links `libgjs`, resolved at LAUNCH from
|
|
191
|
+
* the same `PATH` lookup the `exec` uses, so it cannot go stale either. It goes
|
|
192
|
+
* LAST: it only has to catch what the defaults miss.
|
|
182
193
|
*/
|
|
183
194
|
export declare function dyldFallbackPreamble(platform: string): string;
|
|
184
195
|
/** POSIX `sh` single-quoting for a launcher's target path. */
|
package/lib/utils/bin-shim.js
CHANGED
|
@@ -444,12 +444,23 @@ export function buildNativeEnvPreamble(scanRoot, bakedDirs = [], target = {}) {
|
|
|
444
444
|
* cannot go stale the way baking `systemGiLibraryDirs()` in would (a launcher is
|
|
445
445
|
* routinely written before `brew install gtk4`), and it re-derives nothing —
|
|
446
446
|
* four literals from `dyld(1)`, shared with {@link dyldDefaultFallbackDirs}.
|
|
447
|
-
* An inherited value stays AHEAD of it
|
|
448
|
-
* would have searched on its own.
|
|
447
|
+
* An inherited value stays AHEAD of it.
|
|
449
448
|
*
|
|
450
|
-
* NOT the
|
|
451
|
-
*
|
|
452
|
-
* `
|
|
449
|
+
* NOT SUFFICIENT ON ITS OWN, so the `lib/` beside the `gjs` this shim execs is
|
|
450
|
+
* appended: "set to the documented default" is NOT "unset". Measured on macOS 27 / arm64 with Homebrew
|
|
451
|
+
* gjs 1.88.1 and a fresh glib keg: `gjs -c "imports.gi.Gio"` loads with the
|
|
452
|
+
* variable unset and dies with the documented default exported —
|
|
453
|
+
* `Failed to load shared library 'libgjs.0.dylib' referenced by the typelib`,
|
|
454
|
+
* then `Unsupported type void` from Gio's override `_init`. `GjsPrivate-1.0`
|
|
455
|
+
* names GJS's own library by bare leaf, and once the variable is SET dyld only
|
|
456
|
+
* finds it in a listed directory; `gjs`'s rpath names GLib's keg, not its own.
|
|
457
|
+
* That broke every `gjsify` command on an Apple-silicon Mac, `--version`
|
|
458
|
+
* included, until a symlink was planted in GLib's keg by hand (the CI step
|
|
459
|
+
* `Make libgjs resolvable without a DYLD_ variable`) — and a `brew upgrade glib`
|
|
460
|
+
* silently removes it again. `<dir of gjs>/../lib` is where every prefix
|
|
461
|
+
* (Homebrew on either arch, MacPorts) links `libgjs`, resolved at LAUNCH from
|
|
462
|
+
* the same `PATH` lookup the `exec` uses, so it cannot go stale either. It goes
|
|
463
|
+
* LAST: it only has to catch what the defaults miss.
|
|
453
464
|
*/
|
|
454
465
|
export function dyldFallbackPreamble(platform) {
|
|
455
466
|
if (platform !== 'darwin')
|
|
@@ -457,7 +468,10 @@ export function dyldFallbackPreamble(platform) {
|
|
|
457
468
|
// `HOME: '$HOME'` on purpose — the shim must expand it at LAUNCH, not bake
|
|
458
469
|
// the writing machine's home directory into every launcher.
|
|
459
470
|
const dirs = dyldDefaultFallbackDirs({ HOME: '$HOME' }).join(':');
|
|
460
|
-
return (`
|
|
471
|
+
return (`gjsify_gjs=$(command -v gjs 2>/dev/null)\n` +
|
|
472
|
+
`DYLD_FALLBACK_LIBRARY_PATH="\${DYLD_FALLBACK_LIBRARY_PATH:+$DYLD_FALLBACK_LIBRARY_PATH:}${dirs}` +
|
|
473
|
+
`\${gjsify_gjs:+:\${gjsify_gjs%/*}/../lib}"\n` +
|
|
474
|
+
`unset gjsify_gjs\n` +
|
|
461
475
|
`export DYLD_FALLBACK_LIBRARY_PATH\n`);
|
|
462
476
|
}
|
|
463
477
|
/** POSIX `sh` single-quoting for a launcher's target path. */
|
|
@@ -72,7 +72,7 @@ import { getCachedPackument, putCachedPackument } from './install-packument-cach
|
|
|
72
72
|
import { assertNativeBackendNodeVersion } from './node-version.js';
|
|
73
73
|
import { buildCmdShim, buildLauncherShims, buildNativeEnvPreamble, buildShLauncher, isGjsBundlePath, parseShebang, pickBinMap, } from './bin-shim.js';
|
|
74
74
|
import { detectNativePackages } from './detect-native-packages.js';
|
|
75
|
-
import { findExtraneous, formatExtraneousError } from './install-extraneous.js';
|
|
75
|
+
import { droppedInstallPaths, findExtraneous, formatExtraneousError } from './install-extraneous.js';
|
|
76
76
|
import { devLinkPath, devLinkRoot } from './dev-link.js';
|
|
77
77
|
import { scanPrefix } from './prune-prefix.js';
|
|
78
78
|
import { badPlatformError, checkPlatform, declaresPlatform, describePlatformTarget, readPlatformForce, resolveHostPlatform, } from './platform-check.js';
|
|
@@ -293,6 +293,12 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
293
293
|
await downloadAndExtractAll(installable, opts.prefix, npmrc, log, opts.signal, progress);
|
|
294
294
|
await linkBins(installable, opts.prefix, log);
|
|
295
295
|
warnMissingNativeBuilds(installable, opts.prefix, log);
|
|
296
|
+
// Only after a resolve that rewrote the lockfile: that is when a placement can
|
|
297
|
+
// be dropped, and the previous lockfile is the record of what this installer
|
|
298
|
+
// put there. After the extract, so a failed download leaves the tree as it was.
|
|
299
|
+
if (resolved && opts.lockfile && existingLock) {
|
|
300
|
+
removeDroppedPackages(opts.prefix, droppedInstallPaths(Object.keys(existingLock.packages), nodes), new Set([...(opts.workspaceNames ?? []), ...(opts.linkedNames ?? [])]), log);
|
|
301
|
+
}
|
|
296
302
|
log('install: done');
|
|
297
303
|
// Top-level requested packages only, so callers can write the resolved version
|
|
298
304
|
// back into package.json (`npm install --save`). From `nodes`, NEVER from
|
|
@@ -300,6 +306,38 @@ async function installPackagesNativeLocked(opts, npmrc, log) {
|
|
|
300
306
|
// downloaded", and a provided package is installed.
|
|
301
307
|
return topLevelResolutions(opts.specs, nodes);
|
|
302
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Remove what {@link droppedInstallPaths} planned, and say so in one line.
|
|
311
|
+
*
|
|
312
|
+
* A symlink is never removed (a workspace or `gjsify link` checkout, i.e. the user's
|
|
313
|
+
* sources), and neither is a name a workspace or a dev link provides, whatever its
|
|
314
|
+
* path: the same two guards `extractOne` applies before it overwrites anything.
|
|
315
|
+
*/
|
|
316
|
+
function removeDroppedPackages(prefix, installPaths, providedNames, log) {
|
|
317
|
+
const removed = [];
|
|
318
|
+
for (const installPath of installPaths) {
|
|
319
|
+
if (providedNames.has(nameFromInstallPath(installPath)))
|
|
320
|
+
continue;
|
|
321
|
+
const dir = path.join(prefix, installPath);
|
|
322
|
+
let stat;
|
|
323
|
+
try {
|
|
324
|
+
stat = fs.lstatSync(dir);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!stat.isDirectory())
|
|
330
|
+
continue;
|
|
331
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
332
|
+
removed.push(installPath);
|
|
333
|
+
}
|
|
334
|
+
if (removed.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
log('install: removed %d dropped placement(s): %s', removed.length, removed.join(', '));
|
|
337
|
+
const names = removed.map(nameFromInstallPath);
|
|
338
|
+
const shown = names.slice(0, 5).join(', ') + (names.length > 5 ? `, … ${names.length - 5} more` : '');
|
|
339
|
+
console.log(`gjsify: removed ${removed.length} package(s) the lockfile no longer names (${shown})`);
|
|
340
|
+
}
|
|
303
341
|
/**
|
|
304
342
|
* `--immutable` only: refuse a `node_modules` holding packages the lockfile does not
|
|
305
343
|
* describe, naming every one of them.
|
|
@@ -91,3 +91,28 @@ export declare const EXTRANEOUS_REPORT_LIMIT = 20;
|
|
|
91
91
|
* hand them a command that changes nothing and a refusal that repeats verbatim.
|
|
92
92
|
*/
|
|
93
93
|
export declare function formatExtraneousError(entries: readonly ExtraneousPackage[], prefix: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Install paths the PREVIOUS lockfile placed and a fresh resolve no longer does —
|
|
96
|
+
* what a plain install DROPPED, and the only thing it removes.
|
|
97
|
+
*
|
|
98
|
+
* THE INCIDENT (2026-10-22). A project removed `@gjsify/gtk-runtime-darwin-*` from its
|
|
99
|
+
* `optionalDependencies`; `gjsify install` rewrote the lockfile without them and left
|
|
100
|
+
* the 0.64.2 copies in `node_modules`. On a Mac every darwin prebuild's rpath prefers
|
|
101
|
+
* that bundle's GLib over Homebrew's, so every build kept dying with `No signal
|
|
102
|
+
* 'context_response' on object 'GVolumeMonitor'` — after the fix, from a dependency
|
|
103
|
+
* the project no longer declared. npm removes such a package; this installer only
|
|
104
|
+
* ever added.
|
|
105
|
+
*
|
|
106
|
+
* WHY THE LOCKFILE DIFF, and not "everything {@link findExtraneous} reports". The tree
|
|
107
|
+
* legitimately holds packages no lockfile names: `ensureProjectGjsEngine` lays the GJS
|
|
108
|
+
* bundler engine down after the install and on purpose keeps it out of the lockfile.
|
|
109
|
+
* Sweeping the undescribed set would delete that engine on every install and fetch it
|
|
110
|
+
* again. A path some earlier lockfile of THIS project placed is this installer's own
|
|
111
|
+
* output, so removing it cannot take anything another writer put there.
|
|
112
|
+
*
|
|
113
|
+
* Nested placements under a dropped root collapse into the root, like
|
|
114
|
+
* {@link findExtraneous}, so the caller removes each directory once.
|
|
115
|
+
*/
|
|
116
|
+
export declare function droppedInstallPaths(previous: Iterable<string>, next: readonly {
|
|
117
|
+
installPath: string;
|
|
118
|
+
}[]): string[];
|
|
@@ -129,3 +129,44 @@ export function formatExtraneousError(entries, prefix) {
|
|
|
129
129
|
'In CI this usually means a cache entry was restored from a DIFFERENT lockfile.',
|
|
130
130
|
].join('\n');
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Install paths the PREVIOUS lockfile placed and a fresh resolve no longer does —
|
|
134
|
+
* what a plain install DROPPED, and the only thing it removes.
|
|
135
|
+
*
|
|
136
|
+
* THE INCIDENT (2026-10-22). A project removed `@gjsify/gtk-runtime-darwin-*` from its
|
|
137
|
+
* `optionalDependencies`; `gjsify install` rewrote the lockfile without them and left
|
|
138
|
+
* the 0.64.2 copies in `node_modules`. On a Mac every darwin prebuild's rpath prefers
|
|
139
|
+
* that bundle's GLib over Homebrew's, so every build kept dying with `No signal
|
|
140
|
+
* 'context_response' on object 'GVolumeMonitor'` — after the fix, from a dependency
|
|
141
|
+
* the project no longer declared. npm removes such a package; this installer only
|
|
142
|
+
* ever added.
|
|
143
|
+
*
|
|
144
|
+
* WHY THE LOCKFILE DIFF, and not "everything {@link findExtraneous} reports". The tree
|
|
145
|
+
* legitimately holds packages no lockfile names: `ensureProjectGjsEngine` lays the GJS
|
|
146
|
+
* bundler engine down after the install and on purpose keeps it out of the lockfile.
|
|
147
|
+
* Sweeping the undescribed set would delete that engine on every install and fetch it
|
|
148
|
+
* again. A path some earlier lockfile of THIS project placed is this installer's own
|
|
149
|
+
* output, so removing it cannot take anything another writer put there.
|
|
150
|
+
*
|
|
151
|
+
* Nested placements under a dropped root collapse into the root, like
|
|
152
|
+
* {@link findExtraneous}, so the caller removes each directory once.
|
|
153
|
+
*/
|
|
154
|
+
export function droppedInstallPaths(previous, next) {
|
|
155
|
+
const kept = new Set(next.map((n) => normalizePath(n.installPath)));
|
|
156
|
+
const dropped = [...new Set([...previous].map(normalizePath))]
|
|
157
|
+
.filter((p) => p.startsWith('node_modules/') && !kept.has(p))
|
|
158
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
159
|
+
const keptList = [...kept];
|
|
160
|
+
const roots = [];
|
|
161
|
+
for (const p of dropped) {
|
|
162
|
+
const parent = roots[roots.length - 1];
|
|
163
|
+
if (parent && p.startsWith(`${parent}/`))
|
|
164
|
+
continue;
|
|
165
|
+
// A directory still holding a kept placement is not removed whole; its own
|
|
166
|
+
// dropped descendants are still listed, one by one.
|
|
167
|
+
if (keptList.some((k) => k.startsWith(`${p}/`)))
|
|
168
|
+
continue;
|
|
169
|
+
roots.push(p);
|
|
170
|
+
}
|
|
171
|
+
return roots;
|
|
172
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Layout, type LayoutIdentity } from './layout.js';
|
|
2
2
|
import { type ResolveInstalledPackageOptions } from '../resolve-npm-package.js';
|
|
3
3
|
import type { StagedFile } from './types.js';
|
|
4
4
|
/**
|
|
@@ -49,6 +49,13 @@ export interface ResolveRuntimeOptions {
|
|
|
49
49
|
* is Bun's global-cache behaviour, which is otherwise unreachable from Node.
|
|
50
50
|
*/
|
|
51
51
|
resolve?: ResolveInstalledPackageOptions['resolve'];
|
|
52
|
+
/**
|
|
53
|
+
* The directory of a `@gjsify/gtk-runtime-<target>` package that
|
|
54
|
+
* {@link acquireGtkRuntime} fetched for THIS run — the last candidate, after
|
|
55
|
+
* everything the author installed or pointed at. See `gtk-runtime-acquire.ts`
|
|
56
|
+
* for why it is fetched rather than installed.
|
|
57
|
+
*/
|
|
58
|
+
fetchedGtkRuntimeDir?: string;
|
|
52
59
|
}
|
|
53
60
|
/**
|
|
54
61
|
* Locate the relocated GTK closure for `target`, or `null`.
|
|
@@ -68,7 +75,11 @@ export interface ResolveRuntimeOptions {
|
|
|
68
75
|
* for a ship run.
|
|
69
76
|
* 3. the sibling monorepo directory `../gtk-runtime-<target>/gtk`, which is what
|
|
70
77
|
* an in-repo `npm run build:bundle` writes.
|
|
71
|
-
* 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk
|
|
78
|
+
* 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk/`, when the
|
|
79
|
+
* author installed it.
|
|
80
|
+
* 5. the same package fetched for this run (`fetchedGtkRuntimeDir`) — NOT in
|
|
81
|
+
* node-gi's probe, because node-gi never fetches: it finds the closure inside
|
|
82
|
+
* the `.app` this function put it in, and the layout there is the same.
|
|
72
83
|
*
|
|
73
84
|
* A candidate counts only when it holds BOTH the loadable-code directory and the
|
|
74
85
|
* typelib directory — node-gi's own probe, restated here rather than weakened,
|
|
@@ -150,8 +161,11 @@ export interface AppRuntimePaths {
|
|
|
150
161
|
/** `<prebuildDir>/node_gi.node` — what `NODE_GI_NATIVE` pins. */
|
|
151
162
|
addonPath: string;
|
|
152
163
|
/**
|
|
153
|
-
* `<App>.app/Contents/
|
|
154
|
-
* launcher
|
|
164
|
+
* `<App>.app/Contents/Helpers/<display name>`, or `<program dir>/node.exe`
|
|
165
|
+
* beside the launcher — darwin names it after the app (`layout.ts`'s
|
|
166
|
+
* `dirs.helpers`) so Activity Monitor and System Events show the app
|
|
167
|
+
* instead of `node`; Windows keeps the interpreter's own release name
|
|
168
|
+
* beside the `.cmd`, where nothing reads a process list by file name.
|
|
155
169
|
*/
|
|
156
170
|
interpreterPath: string;
|
|
157
171
|
/** Node's own LICENSE, under `Contents/Resources` (macOS) or `share/` (Windows). */
|
|
@@ -195,6 +209,10 @@ export interface StageAppRuntimeInput {
|
|
|
195
209
|
nodePath: string;
|
|
196
210
|
licensePath: string;
|
|
197
211
|
} | null;
|
|
212
|
+
/** See {@link ResolveRuntimeOptions.fetchedGtkRuntimeDir}. */
|
|
213
|
+
fetchedGtkRuntimeDir?: string;
|
|
214
|
+
/** Why the fetch produced nothing — named in the `missing` line, so the cause is not guessed at. */
|
|
215
|
+
gtkFetchFailure?: string;
|
|
198
216
|
}
|
|
199
217
|
/**
|
|
200
218
|
* Stage the interpreter, the GTK closure and the addon into one bundle.
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
81
81
|
import { dirname, join, posix, sep } from 'node:path';
|
|
82
82
|
import { listFilesRecursive } from './discover.js';
|
|
83
|
+
import { assertUsableAsHelperName } from './layout.js';
|
|
83
84
|
import { nodeRuntimeBinaryName } from './node-runtime.js';
|
|
84
85
|
import { isExecutableAsset } from './plan.js';
|
|
85
86
|
import { resolveInstalledPackage } from '../resolve-npm-package.js';
|
|
@@ -123,7 +124,11 @@ export const NODE_GI_PACKAGE = '@gjsify/node-gi';
|
|
|
123
124
|
* for a ship run.
|
|
124
125
|
* 3. the sibling monorepo directory `../gtk-runtime-<target>/gtk`, which is what
|
|
125
126
|
* an in-repo `npm run build:bundle` writes.
|
|
126
|
-
* 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk
|
|
127
|
+
* 4. the published `@gjsify/gtk-runtime-<target>` package's `gtk/`, when the
|
|
128
|
+
* author installed it.
|
|
129
|
+
* 5. the same package fetched for this run (`fetchedGtkRuntimeDir`) — NOT in
|
|
130
|
+
* node-gi's probe, because node-gi never fetches: it finds the closure inside
|
|
131
|
+
* the `.app` this function put it in, and the layout there is the same.
|
|
127
132
|
*
|
|
128
133
|
* A candidate counts only when it holds BOTH the loadable-code directory and the
|
|
129
134
|
* typelib directory — node-gi's own probe, restated here rather than weakened,
|
|
@@ -155,6 +160,12 @@ export function resolveGtkRuntime(target, options = {}) {
|
|
|
155
160
|
const entry = resolveStagedPackage(packageName, options);
|
|
156
161
|
if (entry !== null)
|
|
157
162
|
candidates.push({ source: packageName, dir: join(dirname(entry), 'gtk') });
|
|
163
|
+
if (options.fetchedGtkRuntimeDir !== undefined) {
|
|
164
|
+
candidates.push({
|
|
165
|
+
source: `${packageName} (fetched for this ship run)`,
|
|
166
|
+
dir: join(options.fetchedGtkRuntimeDir, 'gtk'),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
158
169
|
for (const candidate of candidates) {
|
|
159
170
|
const libDir = join(candidate.dir, nativeSubdir);
|
|
160
171
|
const typelibDir = join(candidate.dir, 'girepository-1.0');
|
|
@@ -254,20 +265,34 @@ export function appRuntimePaths(layout, identity, target) {
|
|
|
254
265
|
prebuildDir,
|
|
255
266
|
gtkDir: posix.join(prebuildDir, 'gtk'),
|
|
256
267
|
addonPath: posix.join(prebuildDir, NODE_GI_ADDON_FILENAME),
|
|
257
|
-
// `Contents/
|
|
258
|
-
// because
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
268
|
+
// ON DARWIN, `Contents/Helpers/<display name>` — NEVER `Contents/MacOS`,
|
|
269
|
+
// because that directory already holds the launcher at `binaryName`, and
|
|
270
|
+
// APFS is case-insensitive by default: `stamped` and `Stamped` are the
|
|
271
|
+
// same file there, so no capitalisation of `binaryName` can share the
|
|
272
|
+
// directory with it. `Contents/Helpers` is one of Apple's own
|
|
273
|
+
// nested-code locations (`codesign(1)`'s bundle format, beside
|
|
274
|
+
// `Frameworks`, `PlugIns`, `XPCServices`), and naming the leaf after
|
|
275
|
+
// `identity.name` rather than `binaryName` is the whole point: Activity
|
|
276
|
+
// Monitor and System Events read a Mach-O process's name off its own
|
|
277
|
+
// FILE NAME, not off `exec -a` or any argv — measured by hand-editing a
|
|
278
|
+
// released `.app`, moving its interpreter to `Contents/Helpers/<App>`
|
|
279
|
+
// and rewriting the launcher's `exec` line, after which `ps -o comm` and
|
|
280
|
+
// System Events both read the app's name instead of `node`.
|
|
281
|
+
//
|
|
282
|
+
// Derived from the LAYOUT (`dirs.helpers !== ''`) rather than from
|
|
283
|
+
// `layout.name === 'darwin'`: only the darwin row sets `helpers` to a
|
|
284
|
+
// real directory (`layout.ts`), so this branch is "does this layout have
|
|
285
|
+
// a helpers seam", not a second darwin-specific fact typed out here.
|
|
263
286
|
//
|
|
264
|
-
//
|
|
265
|
-
// decided
|
|
266
|
-
// read off the TARGET and never `process.platform` (assembling a
|
|
267
|
-
// program directory on Linux is the supported path).
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
interpreterPath:
|
|
287
|
+
// ON WINDOWS AND LINUX, unchanged: `dirs.launcher` and the leaf
|
|
288
|
+
// `node-runtime.ts` already decided for the SOURCE file — `node.exe` on
|
|
289
|
+
// win32, read off the TARGET and never `process.platform` (assembling a
|
|
290
|
+
// Windows program directory on Linux is the supported path). A Windows
|
|
291
|
+
// program directory has no launcher-collision to dodge in the first
|
|
292
|
+
// place, so its interpreter stays beside the `.cmd`.
|
|
293
|
+
interpreterPath: dirs.helpers === ''
|
|
294
|
+
? posix.join(dirs.launcher, nodeRuntimeBinaryName(target))
|
|
295
|
+
: posix.join(dirs.helpers, assertUsableAsHelperName(identity.name)),
|
|
271
296
|
// rpm's `share/licenses/<name>/LICENSE` shape, which the `macos-app` row
|
|
272
297
|
// already uses for the app's own licence — one layout for a reader to learn,
|
|
273
298
|
// and `node` is a name no `binaryName` can collide with silently: `placeStage`
|
|
@@ -340,8 +365,10 @@ export function stageAppRuntime(input) {
|
|
|
340
365
|
// resolver then refuses -- a hint that is wrong is worse than none,
|
|
341
366
|
// because it costs the reader the time to follow it first.
|
|
342
367
|
const nativeSubdir = input.target.startsWith('win32-') ? 'bin' : 'lib';
|
|
343
|
-
|
|
344
|
-
|
|
368
|
+
const why = input.gtkFetchFailure === undefined ? '' : ` — fetching it failed: ${input.gtkFetchFailure}`;
|
|
369
|
+
missing.push(`no GTK closure${why} — point \`GJSIFY_GTK_RUNTIME\` at a bundle directory holding ` +
|
|
370
|
+
`\`${nativeSubdir}/\` and \`girepository-1.0/\`, or install \`${gtkRuntimePackageName(input.target)}\` ` +
|
|
371
|
+
'where the build can reach it');
|
|
345
372
|
}
|
|
346
373
|
// node-gi's JavaScript, which the bundle `require`s by name. Staged whenever
|
|
347
374
|
// the package is resolvable and reported separately from the addon: a bundle
|
|
@@ -394,6 +421,8 @@ function discoveryOptions(input) {
|
|
|
394
421
|
options.env = input.env;
|
|
395
422
|
if (input.resolve !== undefined)
|
|
396
423
|
options.resolve = input.resolve;
|
|
424
|
+
if (input.fetchedGtkRuntimeDir !== undefined)
|
|
425
|
+
options.fetchedGtkRuntimeDir = input.fetchedGtkRuntimeDir;
|
|
397
426
|
return options;
|
|
398
427
|
}
|
|
399
428
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type GtkRuntimeTarget, type ResolveRuntimeOptions } from './app-runtime.js';
|
|
2
|
+
export type AcquiredGtkRuntime =
|
|
3
|
+
/** Something already resolves — nothing was fetched, and nothing will be. */
|
|
4
|
+
{
|
|
5
|
+
kind: 'present';
|
|
6
|
+
}
|
|
7
|
+
/** Fetched; `dir` is the PACKAGE directory, holding `gtk/`. */
|
|
8
|
+
| {
|
|
9
|
+
kind: 'fetched';
|
|
10
|
+
dir: string;
|
|
11
|
+
}
|
|
12
|
+
/** Could not be had; `reason` is what to tell the person, verbatim. */
|
|
13
|
+
| {
|
|
14
|
+
kind: 'failed';
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
export interface AcquireGtkRuntimeInput {
|
|
18
|
+
target: GtkRuntimeTarget;
|
|
19
|
+
/** The version to fetch: the CLI's own, because addon and closure are a matched pair. */
|
|
20
|
+
version: string;
|
|
21
|
+
/** The same discovery options the staging step will use — so "present" means what it will mean there. */
|
|
22
|
+
discovery?: ResolveRuntimeOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a package spec into a cache and return its directory. Injected: the
|
|
25
|
+
* real one is `ensurePkgDir`, and a test must not touch a registry.
|
|
26
|
+
*/
|
|
27
|
+
ensure: (spec: string) => Promise<string>;
|
|
28
|
+
/** One progress line. The closure is ~100 MB, so a silent first run looks like a hang. */
|
|
29
|
+
log?: (message: string) => void;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Make sure a GTK closure is reachable for `target`, fetching it only if nothing
|
|
33
|
+
* else provides one. Never throws: a ship run with no closure is a PARTIAL result
|
|
34
|
+
* that `stageAppRuntime` reports, not an error (see its doc).
|
|
35
|
+
*/
|
|
36
|
+
export declare function acquireGtkRuntime(input: AcquireGtkRuntimeInput): Promise<AcquiredGtkRuntime>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Getting the GTK closure for a ship run when the project does not carry it.
|
|
2
|
+
//
|
|
3
|
+
// WHY IT IS FETCHED AND NOT INSTALLED. `@gjsify/gtk-runtime-<target>` is 100 MB of
|
|
4
|
+
// GLib, GObject, GTK and friends, and a project that lists it — as
|
|
5
|
+
// `optionalDependencies`, which the `os`/`cpu` gate turns into "installed on every
|
|
6
|
+
// Mac" — gets it into its own `node_modules`. On macOS that breaks every build and
|
|
7
|
+
// every run that goes through a Homebrew GJS. Every darwin prebuild
|
|
8
|
+
// carries the rpath `@loader_path` → `…/gtk-runtime-darwin-<arch>/gtk/lib` →
|
|
9
|
+
// `<brew>/lib` (ADR 0023: bundle before system, `docs/prebuilds.md`), so the first
|
|
10
|
+
// `dlopen` out of `node_modules/@gjsify/*-native-darwin-*` finds the bundle's
|
|
11
|
+
// GLib/GObject beside the one `gjs` already loaded from Homebrew: two GObject type
|
|
12
|
+
// registries in one process, which shows as `No signal 'context_response' on
|
|
13
|
+
// object 'GVolumeMonitor'` and a segfault, or as `GNotificationCenterDelegate is
|
|
14
|
+
// implemented in both …`. The invariant ADR 0023 names — one registry per process
|
|
15
|
+
// — is violated by the dev tree itself.
|
|
16
|
+
//
|
|
17
|
+
// The closure is only ever needed at the moment it is COPIED into a bundle, so it
|
|
18
|
+
// is acquired then: into the dlx cache (`ensurePkgDir`, the same machinery
|
|
19
|
+
// `gjsify showcase` already uses for the same package), never into the project.
|
|
20
|
+
// What this does NOT change: the search order in `resolveGtkRuntime()` keeps its
|
|
21
|
+
// four existing candidates, so a project that still installs the package ships
|
|
22
|
+
// exactly as before — it merely stays exposed to the dev-tree failure above until
|
|
23
|
+
// it drops the dependency.
|
|
24
|
+
//
|
|
25
|
+
// EXACT VERSION, no range and no fallback. The addon's `@rpath` is
|
|
26
|
+
// `@loader_path/gtk/lib` — addon and closure are a matched pair published from the
|
|
27
|
+
// same release — so "the nearest published version" would be a guess about GTK ABI
|
|
28
|
+
// that `assertPayloadMatchesArch` cannot check. A CLI version with no published
|
|
29
|
+
// runtime (a dev or `next` build) fails with the reason named, and
|
|
30
|
+
// `GJSIFY_GTK_RUNTIME` is the way past it.
|
|
31
|
+
import { gtkRuntimePackageName, resolveGtkRuntime, } from './app-runtime.js';
|
|
32
|
+
/**
|
|
33
|
+
* Make sure a GTK closure is reachable for `target`, fetching it only if nothing
|
|
34
|
+
* else provides one. Never throws: a ship run with no closure is a PARTIAL result
|
|
35
|
+
* that `stageAppRuntime` reports, not an error (see its doc).
|
|
36
|
+
*/
|
|
37
|
+
export async function acquireGtkRuntime(input) {
|
|
38
|
+
const discovery = input.discovery ?? {};
|
|
39
|
+
if (resolveGtkRuntime(input.target, discovery) !== null)
|
|
40
|
+
return { kind: 'present' };
|
|
41
|
+
const name = gtkRuntimePackageName(input.target);
|
|
42
|
+
const spec = `${name}@${input.version}`;
|
|
43
|
+
input.log?.(`${name} is not installed here, and is not meant to be — fetching ${spec} into the cache ` +
|
|
44
|
+
'(about 100 MB, once)');
|
|
45
|
+
let dir;
|
|
46
|
+
try {
|
|
47
|
+
dir = await input.ensure(spec);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { kind: 'failed', reason: error instanceof Error ? error.message : String(error) };
|
|
51
|
+
}
|
|
52
|
+
// The same completeness probe the staging step applies, so a package that
|
|
53
|
+
// resolved but holds no `gtk/` (an unpopulated checkout, a truncated cache
|
|
54
|
+
// entry) is reported HERE, with its name, instead of as a bare "no GTK closure".
|
|
55
|
+
if (resolveGtkRuntime(input.target, { ...discovery, fetchedGtkRuntimeDir: dir }) === null) {
|
|
56
|
+
return { kind: 'failed', reason: `${spec} was fetched to ${dir} but holds no complete gtk/ closure` };
|
|
57
|
+
}
|
|
58
|
+
return { kind: 'fetched', dir };
|
|
59
|
+
}
|
|
@@ -181,7 +181,16 @@ function renderPrefixLauncher(settings, bundleRelPath, layout) {
|
|
|
181
181
|
* IT EXECS WHAT THE BUNDLE CARRIES. `runtime` comes from
|
|
182
182
|
* `utils/ship/app-runtime.ts` and names the interpreter, the relocated GTK
|
|
183
183
|
* closure and the node-gi addon staged inside `Contents/` — so the exec line is
|
|
184
|
-
* `"$here
|
|
184
|
+
* `"$here/../Helpers/<display name>"` and the two locators below point into
|
|
185
|
+
* `dirs.native`. The interpreter is staged under `Contents/Helpers`, not
|
|
186
|
+
* `Contents/MacOS` beside this very script, and named after `identity.name`
|
|
187
|
+
* rather than kept as `node`: Activity Monitor and System Events read a
|
|
188
|
+
* process's name off the FILE NAME of the Mach-O that is running, not off argv
|
|
189
|
+
* or any `exec -a`, so a bundle whose carried interpreter is still called `node`
|
|
190
|
+
* shows up as `node` everywhere a user looks for it (`layout.ts`'s `dirs.helpers`,
|
|
191
|
+
* `app-runtime.ts`'s `appRuntimePaths`). `beside()` computes the relative hop
|
|
192
|
+
* from the actual two directories rather than assuming a sibling, so this
|
|
193
|
+
* function needs no special case for the rename.
|
|
185
194
|
* When the stage carries none of them every line here is byte-identical to what
|
|
186
195
|
* M2a wrote, which is what keeps a bundle assembled without the runtime packages
|
|
187
196
|
* exactly as (un)usable as it was rather than differently broken.
|
|
@@ -308,10 +317,14 @@ function renderAppBundleLauncher(settings, bundleRelPath, layout, runtime) {
|
|
|
308
317
|
if (settings.fontFiles.length > 0) {
|
|
309
318
|
lines.push(`GJSIFY_FONT_DIR="$contents/${stagedFontDir(under(dirs.data), settings.appId)}"`, 'export GJSIFY_FONT_DIR');
|
|
310
319
|
}
|
|
311
|
-
// `"$here
|
|
312
|
-
// and false of a `.app` a stranger downloads,
|
|
313
|
-
// degraded experience: macOS ships no Node at
|
|
314
|
-
// would be `exec: node: not found`.
|
|
320
|
+
// `"$here/../Helpers/<display name>"`, not a bare `node`. The bare name is
|
|
321
|
+
// true of a developer's machine and false of a `.app` a stranger downloads,
|
|
322
|
+
// and the difference is not a degraded experience: macOS ships no Node at
|
|
323
|
+
// all, so the launcher's first act would be `exec: node: not found`. The path
|
|
324
|
+
// is relative rather than the literal `../Helpers/…` written out, because
|
|
325
|
+
// `beside()` already carries the relation between `dirs.launcher` and
|
|
326
|
+
// wherever `appRuntimePaths` staged the interpreter — this function does not
|
|
327
|
+
// need to know that the leaf changed from `node` to `identity.name`.
|
|
315
328
|
const interpreter = runtime.interpreter === undefined ? undefined : `"$here/${beside(runtime.interpreter)}"`;
|
|
316
329
|
lines.push(`exec ${execLine(layout, `"$contents/${under(dirs.bundle)}/${bundleRelPath}"`, settings, interpreter)} "$@"`, '');
|
|
317
330
|
return lines.join('\n');
|
|
@@ -462,8 +475,8 @@ function windowsPath(rel) {
|
|
|
462
475
|
* `@gjsify/node-runtime-<target>` exists for; this comment used to say "the day a
|
|
463
476
|
* stage carries one, THIS is the line that names it by a layout-relative path
|
|
464
477
|
* instead", and #1354 M2b is that day — `utils/ship/app-runtime.ts` stages the
|
|
465
|
-
* interpreter into `Contents/
|
|
466
|
-
* `"$here
|
|
478
|
+
* interpreter into `Contents/Helpers/<display name>` and the `.app` launcher
|
|
479
|
+
* execs `"$here/../Helpers/<display name>"`.
|
|
467
480
|
*
|
|
468
481
|
* #1354 M3 is the same day one OS over: the Windows layout stages `node.exe` into
|
|
469
482
|
* the program directory and its `.cmd` runs `"%HERE%node.exe"`.
|
|
@@ -128,6 +128,15 @@ export interface LayoutDirs {
|
|
|
128
128
|
* bundle can address it and `codesign` later refuses the bundle.
|
|
129
129
|
*/
|
|
130
130
|
other: string;
|
|
131
|
+
/**
|
|
132
|
+
* macOS only: where a carried interpreter lives when it must NOT be named
|
|
133
|
+
* `binaryName` inside {@link LayoutDirs.launcher} — see
|
|
134
|
+
* `utils/ship/app-runtime.ts`'s `appRuntimePaths`. `''` on linux and windows,
|
|
135
|
+
* where nothing reads it: Linux takes its interpreter from the distribution,
|
|
136
|
+
* and a Windows program directory has no launcher-collision to dodge in the
|
|
137
|
+
* first place, so its interpreter stays beside the `.cmd`.
|
|
138
|
+
*/
|
|
139
|
+
helpers: string;
|
|
131
140
|
}
|
|
132
141
|
/**
|
|
133
142
|
* One operating system's layout.
|
|
@@ -290,6 +299,20 @@ export interface LayoutArches {
|
|
|
290
299
|
* {@link LAYOUTS.linux.icon} for what asked for each of the three.
|
|
291
300
|
*/
|
|
292
301
|
export declare const HICOLOR_RASTER_SIZES: readonly [64, 128, 256];
|
|
302
|
+
/**
|
|
303
|
+
* Refuse an `identity.name` that cannot be the carried interpreter's own file
|
|
304
|
+
* name under `Contents/Helpers` (`dirs.helpers`) — `appRuntimePaths` in
|
|
305
|
+
* `utils/ship/app-runtime.ts` is the only caller.
|
|
306
|
+
*
|
|
307
|
+
* A SUPERSET of {@link BUNDLE_NAME_FORBIDDEN}, not a restatement: the bundle
|
|
308
|
+
* directory wraps the same string in `.app`, so a name of "." or ".." becomes
|
|
309
|
+
* "..app" or "....app" — both ordinary directory names — while this leaf is
|
|
310
|
+
* `identity.name` taken LITERALLY, where "." and ".." name the directory
|
|
311
|
+
* itself and its parent. An empty name and a NUL byte are refused for the same
|
|
312
|
+
* reason `windowsProgramDirName` refuses them one OS over: neither is a name a
|
|
313
|
+
* filesystem can hold.
|
|
314
|
+
*/
|
|
315
|
+
export declare function assertUsableAsHelperName(name: string): string;
|
|
293
316
|
export declare const LAYOUTS: Record<LayoutName, Layout>;
|
|
294
317
|
/** Every layout name, derived from the table so a new row cannot be missed. */
|
|
295
318
|
export declare const LAYOUT_NAMES: LayoutName[];
|