@gjsify/cli 0.28.0 → 0.30.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 +76 -61
- package/lib/commands/format.js +17 -8
- package/lib/commands/install.d.ts +27 -0
- package/lib/commands/install.js +25 -8
- package/lib/commands/system-check.js +22 -2
- package/lib/config.js +1 -1
- package/lib/utils/bin-shim.d.ts +65 -0
- package/lib/utils/bin-shim.js +115 -13
- package/lib/utils/check-system-deps.d.ts +56 -0
- package/lib/utils/check-system-deps.js +159 -5
- package/lib/utils/detect-native-packages.d.ts +45 -1
- package/lib/utils/detect-native-packages.js +56 -0
- package/lib/utils/dlx-cache.js +1 -1
- package/lib/utils/install-backend-native.d.ts +87 -1
- package/lib/utils/install-backend-native.js +147 -34
- package/lib/utils/install-global.js +13 -1
- package/lib/utils/run-lifecycle-script.js +41 -25
- package/lib/utils/spawn.d.ts +17 -1
- package/lib/utils/spawn.js +23 -6
- package/lib/utils/system-gi.d.ts +71 -0
- package/lib/utils/system-gi.js +221 -0
- package/package.json +17 -17
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// is in the project's dependency tree. A user with only @gjsify/fs in their
|
|
12
12
|
// project never sees a warning about libmanette.
|
|
13
13
|
import { execFileSync } from 'node:child_process';
|
|
14
|
-
import { join, resolve } from 'node:path';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { pathToFileURL } from 'node:url';
|
|
17
17
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
@@ -87,8 +87,8 @@ export function detectPackageManager() {
|
|
|
87
87
|
}
|
|
88
88
|
return 'unknown';
|
|
89
89
|
}
|
|
90
|
-
/** Optional system deps keyed by their DepCheck id. */
|
|
91
|
-
const OPTIONAL_DEPS = {
|
|
90
|
+
/** Optional system deps keyed by their DepCheck id. Exported for the spec's declaration invariant. */
|
|
91
|
+
export const OPTIONAL_DEPS = {
|
|
92
92
|
manette: { id: 'manette', name: 'libmanette', pkgName: 'manette-0.2' },
|
|
93
93
|
gstreamer: { id: 'gstreamer', name: 'GStreamer', pkgName: 'gstreamer-1.0' },
|
|
94
94
|
'gst-app': { id: 'gst-app', name: 'GStreamer App', pkgName: 'gstreamer-app-1.0' },
|
|
@@ -105,6 +105,21 @@ const OPTIONAL_DEPS = {
|
|
|
105
105
|
// `Run-time dependency X found: NO` error mid-build.
|
|
106
106
|
gnutls: { id: 'gnutls', name: 'GnuTLS', pkgName: 'gnutls' },
|
|
107
107
|
nghttp2: { id: 'nghttp2', name: 'libnghttp2', pkgName: 'libnghttp2' },
|
|
108
|
+
// RUNTIME deps of the shipped prebuilds, not build-time ones — the distinction
|
|
109
|
+
// the two entries above make does not hold for these. Measured on fedora:43
|
|
110
|
+
// provisioned like a CONSUMER (gjs + libsoup3 only), `ldd` over every committed
|
|
111
|
+
// linux-x64 prebuild: three sonames do not resolve, and each makes its typelib
|
|
112
|
+
// UNLOADABLE, which surfaces as `Unsupported type void, deriving from
|
|
113
|
+
// fundamental void` at first construction — a message that names nothing.
|
|
114
|
+
// @gjsify/rolldown-native libjson-glib-1.0.so.0
|
|
115
|
+
// @gjsify/webgl libepoxy.so.0 (gdk-pixbuf already listed)
|
|
116
|
+
// @gjsify/webrtc-native libgstwebrtc-1.0.so.0 (gstreamer already listed)
|
|
117
|
+
// json-glib is the severe one: rolldown-native IS the GJS bundler engine, so a
|
|
118
|
+
// host without it cannot `gjsify build` at all, while `gjsify install` printed
|
|
119
|
+
// "System dependencies OK." — a check passing for the wrong reason.
|
|
120
|
+
'json-glib': { id: 'json-glib', name: 'JSON-GLib', pkgName: 'json-glib-1.0' },
|
|
121
|
+
epoxy: { id: 'epoxy', name: 'libepoxy', pkgName: 'epoxy' },
|
|
122
|
+
'gst-webrtc': { id: 'gst-webrtc', name: 'GStreamer WebRTC', pkgName: 'gstreamer-webrtc-1.0' },
|
|
108
123
|
};
|
|
109
124
|
/**
|
|
110
125
|
* Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
|
|
@@ -112,8 +127,11 @@ const OPTIONAL_DEPS = {
|
|
|
112
127
|
* available trio GLib/GObject/Gio).
|
|
113
128
|
*
|
|
114
129
|
* Used to compute the set of optional deps to check for a given project.
|
|
130
|
+
*
|
|
131
|
+
* Exported for the spec's declaration invariant: every id named here must exist in
|
|
132
|
+
* OPTIONAL_DEPS, or the dep is silently never checked.
|
|
115
133
|
*/
|
|
116
|
-
const PACKAGE_DEPS = {
|
|
134
|
+
export const PACKAGE_DEPS = {
|
|
117
135
|
'@gjsify/gamepad': ['manette'],
|
|
118
136
|
'@gjsify/webaudio': ['gstreamer', 'gst-app'],
|
|
119
137
|
'@gjsify/iframe': ['webkitgtk'],
|
|
@@ -124,13 +142,17 @@ const PACKAGE_DEPS = {
|
|
|
124
142
|
// as a special-case checkNpmPackage rather than checkPkgConfig in
|
|
125
143
|
// runOptionalChecks. Mapping it here so its presence in the project's
|
|
126
144
|
// dep tree triggers the check.
|
|
127
|
-
'@gjsify/webgl': ['gwebgl'],
|
|
145
|
+
'@gjsify/webgl': ['gwebgl', 'gdk-pixbuf', 'epoxy'],
|
|
128
146
|
// Native Vala bridges with `dependency('gnutls')` / `dependency('libnghttp2')`
|
|
129
147
|
// in their meson.build. Optional because the shipped prebuild covers the
|
|
130
148
|
// common-arch user path; only contributors rebuilding from source hit the
|
|
131
149
|
// build-time dep.
|
|
132
150
|
'@gjsify/tls-native': ['gnutls'],
|
|
133
151
|
'@gjsify/http2-native': ['nghttp2'],
|
|
152
|
+
// Unlike the two above, these are LOAD-time: the prebuild is dynamically linked
|
|
153
|
+
// and GIRepository cannot open its backing library without them.
|
|
154
|
+
'@gjsify/rolldown-native': ['json-glib'],
|
|
155
|
+
'@gjsify/webrtc-native': ['gstreamer', 'gst-webrtc'],
|
|
134
156
|
// @gjsify/event-bridge only needs gtk4/gdk which are already in the
|
|
135
157
|
// required set, so it doesn't need an optional entry.
|
|
136
158
|
};
|
|
@@ -367,6 +389,8 @@ const PM_PACKAGES = {
|
|
|
367
389
|
cairo: 'libcairo2-dev',
|
|
368
390
|
gnutls: 'libgnutls28-dev',
|
|
369
391
|
nghttp2: 'libnghttp2-dev',
|
|
392
|
+
'json-glib': 'libjson-glib-dev',
|
|
393
|
+
epoxy: 'libepoxy-dev',
|
|
370
394
|
},
|
|
371
395
|
dnf: {
|
|
372
396
|
gjs: 'gjs',
|
|
@@ -390,6 +414,8 @@ const PM_PACKAGES = {
|
|
|
390
414
|
cairo: 'cairo-devel',
|
|
391
415
|
gnutls: 'gnutls-devel',
|
|
392
416
|
nghttp2: 'libnghttp2-devel',
|
|
417
|
+
'json-glib': 'json-glib-devel',
|
|
418
|
+
epoxy: 'libepoxy-devel',
|
|
393
419
|
},
|
|
394
420
|
pacman: {
|
|
395
421
|
gjs: 'gjs',
|
|
@@ -413,6 +439,8 @@ const PM_PACKAGES = {
|
|
|
413
439
|
cairo: 'cairo',
|
|
414
440
|
gnutls: 'gnutls',
|
|
415
441
|
nghttp2: 'libnghttp2',
|
|
442
|
+
'json-glib': 'json-glib',
|
|
443
|
+
epoxy: 'libepoxy',
|
|
416
444
|
},
|
|
417
445
|
zypper: {
|
|
418
446
|
gjs: 'gjs',
|
|
@@ -436,6 +464,8 @@ const PM_PACKAGES = {
|
|
|
436
464
|
cairo: 'cairo-devel',
|
|
437
465
|
gnutls: 'libgnutls-devel',
|
|
438
466
|
nghttp2: 'libnghttp2-devel',
|
|
467
|
+
'json-glib': 'json-glib-devel',
|
|
468
|
+
epoxy: 'libepoxy-devel',
|
|
439
469
|
},
|
|
440
470
|
apk: {
|
|
441
471
|
gjs: 'gjs',
|
|
@@ -459,6 +489,8 @@ const PM_PACKAGES = {
|
|
|
459
489
|
cairo: 'cairo-dev',
|
|
460
490
|
gnutls: 'gnutls-dev',
|
|
461
491
|
nghttp2: 'nghttp2-dev',
|
|
492
|
+
'json-glib': 'json-glib-dev',
|
|
493
|
+
epoxy: 'libepoxy-dev',
|
|
462
494
|
},
|
|
463
495
|
// macOS. Names verified against homebrew/core rather than inferred from the
|
|
464
496
|
// Linux rows — the mapping is NOT mechanical: Homebrew ships no `-dev`/`-devel`
|
|
@@ -502,6 +534,8 @@ const PM_PACKAGES = {
|
|
|
502
534
|
cairo: 'cairo',
|
|
503
535
|
gnutls: 'gnutls',
|
|
504
536
|
nghttp2: 'nghttp2',
|
|
537
|
+
'json-glib': 'json-glib',
|
|
538
|
+
epoxy: 'libepoxy',
|
|
505
539
|
},
|
|
506
540
|
unknown: {},
|
|
507
541
|
};
|
|
@@ -546,3 +580,123 @@ export function buildInstallCommand(pm, missing) {
|
|
|
546
580
|
}
|
|
547
581
|
return lines.length > 0 ? lines.join('\n ') : null;
|
|
548
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Only libraries whose GIR carries a real `<package version>`, verified against
|
|
585
|
+
* the published packages rather than assumed. A binding whose value turns out
|
|
586
|
+
* degenerate is skipped at runtime by `isDegenerate`, so a wrong entry here
|
|
587
|
+
* cannot produce a false alarm — it can only produce silence.
|
|
588
|
+
*/
|
|
589
|
+
const TYPE_PACKAGE_BINDINGS = [
|
|
590
|
+
{ pkgConfig: 'gtk4', typePackage: '@girs/gtk-4.0', namespaceVersion: '4.0' },
|
|
591
|
+
{ pkgConfig: 'libadwaita-1', typePackage: '@girs/adw-1', namespaceVersion: '1' },
|
|
592
|
+
{ pkgConfig: 'libsoup-3.0', typePackage: '@girs/soup-3.0', namespaceVersion: '3.0' },
|
|
593
|
+
{ pkgConfig: 'glib-2.0', typePackage: '@girs/glib-2.0', namespaceVersion: '2.0' },
|
|
594
|
+
{ pkgConfig: 'gobject-2.0', typePackage: '@girs/gobject-2.0', namespaceVersion: '2.0' },
|
|
595
|
+
{ pkgConfig: 'gio-2.0', typePackage: '@girs/gio-2.0', namespaceVersion: '2.0' },
|
|
596
|
+
{ pkgConfig: 'gstreamer-1.0', typePackage: '@girs/gst-1.0', namespaceVersion: '1.0' },
|
|
597
|
+
{ pkgConfig: 'webkitgtk-6.0', typePackage: '@girs/webkit-6.0', namespaceVersion: '6.0' },
|
|
598
|
+
{ pkgConfig: 'manette-0.2', typePackage: '@girs/manette-0.2', namespaceVersion: '0.2' },
|
|
599
|
+
{ pkgConfig: 'pango', typePackage: '@girs/pango-1.0', namespaceVersion: '1.0' },
|
|
600
|
+
{ pkgConfig: 'harfbuzz', typePackage: '@girs/harfbuzz-0.0', namespaceVersion: '0.0' },
|
|
601
|
+
];
|
|
602
|
+
/**
|
|
603
|
+
* Read `libraryVersion` from an installed `@girs/*` package.
|
|
604
|
+
*
|
|
605
|
+
* Resolves the package's MAIN entry and walks up to its root `package.json`
|
|
606
|
+
* rather than resolving the `package.json` subpath: `@girs/*` exports maps do not
|
|
607
|
+
* expose `./package.json` (verified on all 32 installed packages), so
|
|
608
|
+
* `require.resolve('@girs/gtk-4.0/package.json')` throws
|
|
609
|
+
* `ERR_PACKAGE_PATH_NOT_EXPORTED`. The same trap silently disabled the
|
|
610
|
+
* `@nativescript/vite` major-version gate until it was found by measurement.
|
|
611
|
+
*/
|
|
612
|
+
function readGirsLibraryVersion(cwd, typePackage) {
|
|
613
|
+
for (const from of [pathToFileURL(join(cwd, '_girs_.js')).href, import.meta.url]) {
|
|
614
|
+
let dir;
|
|
615
|
+
try {
|
|
616
|
+
dir = dirname(createRequire(from).resolve(typePackage));
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Walk up to the package root — the entry may sit in a subdirectory.
|
|
622
|
+
for (let i = 0; i < 5; i++) {
|
|
623
|
+
const manifest = join(dir, 'package.json');
|
|
624
|
+
if (existsSync(manifest)) {
|
|
625
|
+
try {
|
|
626
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf-8'));
|
|
627
|
+
if (parsed.name === typePackage)
|
|
628
|
+
return parsed.libraryVersion ?? null;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return null; // present but unreadable — nothing to compare against
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const parent = dirname(dir);
|
|
635
|
+
if (parent === dir)
|
|
636
|
+
break;
|
|
637
|
+
dir = parent;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
/** `[major, minor]`, or null when the string is not a comparable version. */
|
|
643
|
+
function majorMinor(version) {
|
|
644
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
645
|
+
if (!match)
|
|
646
|
+
return null;
|
|
647
|
+
return [Number(match[1]), Number(match[2])];
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* True when `libraryVersion` is ts-for-gir's namespace-version fallback rather
|
|
651
|
+
* than a real upstream release — `@girs/gdk-4.0`'s `4.0.0` against namespace `4.0`.
|
|
652
|
+
*
|
|
653
|
+
* Compares against every `.0`-padded spelling of the namespace version, not just
|
|
654
|
+
* the three-component one: `4.0` and `4.0.0` are the same claim, and matching only
|
|
655
|
+
* the padded form let the bare spelling through as a bogus 18-minor "behind"
|
|
656
|
+
* finding. Deliberately NOT a major.minor comparison — `@girs/adw-1` has namespace
|
|
657
|
+
* `1` and a real `libraryVersion` of `1.10.0`, which any major-only rule discards.
|
|
658
|
+
*/
|
|
659
|
+
function isDegenerate(declared, namespaceVersion) {
|
|
660
|
+
const trimmed = declared.trim();
|
|
661
|
+
const parts = namespaceVersion.split('.');
|
|
662
|
+
for (let n = parts.length; n <= 3; n++) {
|
|
663
|
+
if (trimmed === [...parts, ...Array(n - parts.length).fill('0')].join('.'))
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Compare each installed `@girs/*` package's declared upstream version against the
|
|
670
|
+
* library the host actually has. Only reports a `major.minor` difference: a micro
|
|
671
|
+
* difference does not change the API set, and reporting it would train people to
|
|
672
|
+
* ignore the output.
|
|
673
|
+
*/
|
|
674
|
+
export function checkTypeSkew(cwd, readers = {}) {
|
|
675
|
+
const readDeclared = readers.readDeclared ?? readGirsLibraryVersion;
|
|
676
|
+
const readInstalled = readers.readInstalled ?? ((pkgConfig) => tryExecFile('pkg-config', ['--modversion', pkgConfig]));
|
|
677
|
+
const findings = [];
|
|
678
|
+
for (const binding of TYPE_PACKAGE_BINDINGS) {
|
|
679
|
+
const declared = readDeclared(cwd, binding.typePackage);
|
|
680
|
+
if (declared === null)
|
|
681
|
+
continue; // types not installed, or no declaration
|
|
682
|
+
if (isDegenerate(declared, binding.namespaceVersion))
|
|
683
|
+
continue;
|
|
684
|
+
const installed = readInstalled(binding.pkgConfig);
|
|
685
|
+
if (installed === null)
|
|
686
|
+
continue; // library absent — the presence check owns that
|
|
687
|
+
const want = majorMinor(declared);
|
|
688
|
+
const have = majorMinor(installed.split('\n')[0] ?? installed);
|
|
689
|
+
if (!want || !have)
|
|
690
|
+
continue;
|
|
691
|
+
if (want[0] === have[0] && want[1] === have[1])
|
|
692
|
+
continue;
|
|
693
|
+
findings.push({
|
|
694
|
+
typePackage: binding.typePackage,
|
|
695
|
+
pkgConfig: binding.pkgConfig,
|
|
696
|
+
declared,
|
|
697
|
+
installed: installed.split('\n')[0] ?? installed,
|
|
698
|
+
relation: want[0] > have[0] || (want[0] === have[0] && want[1] > have[1]) ? 'ahead' : 'behind',
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return findings;
|
|
702
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SystemGiOptions } from './system-gi.js';
|
|
1
2
|
export interface NativePackage {
|
|
2
3
|
/** npm package name, e.g. "@gjsify/webgl" */
|
|
3
4
|
name: string;
|
|
@@ -42,8 +43,15 @@ export interface NativeEnv {
|
|
|
42
43
|
GI_TYPELIB_PATH: string;
|
|
43
44
|
/** ELF loader (Linux and other ELF platforms). */
|
|
44
45
|
LD_LIBRARY_PATH?: string;
|
|
45
|
-
/** Mach-O loader (macOS). `dyld` ignores `LD_LIBRARY_PATH` entirely. */
|
|
46
|
+
/** Mach-O loader (macOS), OVERRIDING resolution by leaf. `dyld` ignores `LD_LIBRARY_PATH` entirely. */
|
|
46
47
|
DYLD_LIBRARY_PATH?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Mach-O loader (macOS), consulted only AFTER the normal search fails. This
|
|
50
|
+
* is where the HOST's GI libdirs go — never `DYLD_LIBRARY_PATH`, which would
|
|
51
|
+
* shadow correctly-resolved libraries process-wide. See
|
|
52
|
+
* {@link buildNativeEnv}.
|
|
53
|
+
*/
|
|
54
|
+
DYLD_FALLBACK_LIBRARY_PATH?: string;
|
|
47
55
|
[key: string]: string | undefined;
|
|
48
56
|
}
|
|
49
57
|
/**
|
|
@@ -287,8 +295,44 @@ export declare function libraryPathVar(platform: string): {
|
|
|
287
295
|
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
288
296
|
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
289
297
|
* PATH: …}` would hand a child process two competing entries.
|
|
298
|
+
*
|
|
299
|
+
* ── DARWIN ALSO GETS THE *HOST'S* GI LIBDIRS ─────────────────────────────────
|
|
300
|
+
*
|
|
301
|
+
* Everything above is about the prebuilds gjsify itself ships. On macOS there is
|
|
302
|
+
* a second, independent gap that has nothing to do with gjsify: a typelib names
|
|
303
|
+
* its library by BARE LEAF (`libgtk-4.1.dylib`), and dyld carries neither
|
|
304
|
+
* `/usr/local/lib` nor `/opt/homebrew/lib` in its default fallback path.
|
|
305
|
+
* Homebrew's `gjs` has an rpath into GLIB's keg only, so it resolves no other
|
|
306
|
+
* keg's leaves — measured: bare `gjs -c "imports.gi.Gtk; Gtk.init()"`, no gjsify
|
|
307
|
+
* in the process, dies on that dlopen (full trace in `utils/system-gi.ts`). This
|
|
308
|
+
* is the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info` all
|
|
309
|
+
* pass through, so it is where the child's launch env is repaired.
|
|
310
|
+
*
|
|
311
|
+
* Two properties are load-bearing:
|
|
312
|
+
*
|
|
313
|
+
* 1. **`DYLD_FALLBACK_LIBRARY_PATH`, never `DYLD_LIBRARY_PATH`.** The override
|
|
314
|
+
* variable wins by LEAF for every dylib in the process, including ones the
|
|
315
|
+
* host already resolved correctly — pointing it at a whole system libdir is
|
|
316
|
+
* how you replace a linked-against library with a same-named different one.
|
|
317
|
+
* The fallback is consulted only after the normal search fails, which is
|
|
318
|
+
* exactly the "nothing else found it" case. Consequence, and it is the
|
|
319
|
+
* intended precedence: gjsify's own prebuilds keep priority through the
|
|
320
|
+
* override variable, the system stack fills in behind it.
|
|
321
|
+
* 2. **Our dirs go FIRST, an inherited fallback is kept BEHIND them, and
|
|
322
|
+
* `/usr/lib` is appended when there is none.** Setting the variable
|
|
323
|
+
* REPLACES dyld's own default fallback list, so writing only our dirs would
|
|
324
|
+
* silently remove `/usr/lib` from the search — a host that exported the
|
|
325
|
+
* variable itself already carries whatever tail it wants. Same composition
|
|
326
|
+
* as node-gi's re-exec.
|
|
327
|
+
*
|
|
328
|
+
* Emitted even when `packages` is empty: the gap is in the host's loader, not in
|
|
329
|
+
* a gjsify prebuild, so a plain `gjsify run script.gjs.js` that touches GTK needs
|
|
330
|
+
* it just as much. Empty off darwin — `ld.so`'s configured cache already resolves
|
|
331
|
+
* these leaves on Linux, and Windows re-reads `PATH` per `LoadLibrary`.
|
|
290
332
|
*/
|
|
291
333
|
export declare function buildNativeEnv(packages: NativePackage[], opts?: {
|
|
292
334
|
platform?: string;
|
|
293
335
|
env?: Record<string, string | undefined>;
|
|
336
|
+
/** Injectable for tests — the darwin branch is asserted from Linux. */
|
|
337
|
+
systemGiDirs?: (opts: SystemGiOptions) => string[];
|
|
294
338
|
}): NativeEnv;
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { readdirSync, existsSync } from 'node:fs';
|
|
30
30
|
import { join, resolve } from 'node:path';
|
|
31
31
|
import { readPackageJson } from './pkg-json.js';
|
|
32
|
+
import { splitSearchPath, systemGiLibraryDirs } from './system-gi.js';
|
|
32
33
|
/**
|
|
33
34
|
* Map a Node `process.arch` value to the uname-style spelling the Vala/meson
|
|
34
35
|
* bridges used to stage into (`prebuilds/linux-x86_64/`, the output of
|
|
@@ -558,6 +559,18 @@ export function libraryPathVar(platform) {
|
|
|
558
559
|
return { name: 'PATH', separator: ';' };
|
|
559
560
|
return { name: 'LD_LIBRARY_PATH', separator: ':' };
|
|
560
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* dyld's *fallback* search path — consulted only AFTER the normal search
|
|
564
|
+
* (rpath / absolute name / dyld cache) has failed.
|
|
565
|
+
*
|
|
566
|
+
* Deliberately NOT part of {@link libraryPathVar}, which answers a different
|
|
567
|
+
* question: that one names the variable that OVERRIDES resolution for every
|
|
568
|
+
* dylib in the process (the right home for a prebuild we ship and want used),
|
|
569
|
+
* this one names the variable that fills in what nothing else could find (the
|
|
570
|
+
* right home for the host's own libdirs). Mixing the two roles is the mistake
|
|
571
|
+
* `buildNativeEnv` documents at length.
|
|
572
|
+
*/
|
|
573
|
+
const DYLD_FALLBACK_VAR = 'DYLD_FALLBACK_LIBRARY_PATH';
|
|
561
574
|
/**
|
|
562
575
|
* Build the typelib + shared-library search-path environment for the detected
|
|
563
576
|
* native packages. Prepends the new paths to any existing values.
|
|
@@ -569,6 +582,40 @@ export function libraryPathVar(platform) {
|
|
|
569
582
|
* (a stock Windows env block uses `Path`) — Windows env names are
|
|
570
583
|
* case-insensitive, but a plain JS object is not, and `{...process.env,
|
|
571
584
|
* PATH: …}` would hand a child process two competing entries.
|
|
585
|
+
*
|
|
586
|
+
* ── DARWIN ALSO GETS THE *HOST'S* GI LIBDIRS ─────────────────────────────────
|
|
587
|
+
*
|
|
588
|
+
* Everything above is about the prebuilds gjsify itself ships. On macOS there is
|
|
589
|
+
* a second, independent gap that has nothing to do with gjsify: a typelib names
|
|
590
|
+
* its library by BARE LEAF (`libgtk-4.1.dylib`), and dyld carries neither
|
|
591
|
+
* `/usr/local/lib` nor `/opt/homebrew/lib` in its default fallback path.
|
|
592
|
+
* Homebrew's `gjs` has an rpath into GLIB's keg only, so it resolves no other
|
|
593
|
+
* keg's leaves — measured: bare `gjs -c "imports.gi.Gtk; Gtk.init()"`, no gjsify
|
|
594
|
+
* in the process, dies on that dlopen (full trace in `utils/system-gi.ts`). This
|
|
595
|
+
* is the ONE place `gjsify run`, `showcase`, `storybook`, `tsc` and `info` all
|
|
596
|
+
* pass through, so it is where the child's launch env is repaired.
|
|
597
|
+
*
|
|
598
|
+
* Two properties are load-bearing:
|
|
599
|
+
*
|
|
600
|
+
* 1. **`DYLD_FALLBACK_LIBRARY_PATH`, never `DYLD_LIBRARY_PATH`.** The override
|
|
601
|
+
* variable wins by LEAF for every dylib in the process, including ones the
|
|
602
|
+
* host already resolved correctly — pointing it at a whole system libdir is
|
|
603
|
+
* how you replace a linked-against library with a same-named different one.
|
|
604
|
+
* The fallback is consulted only after the normal search fails, which is
|
|
605
|
+
* exactly the "nothing else found it" case. Consequence, and it is the
|
|
606
|
+
* intended precedence: gjsify's own prebuilds keep priority through the
|
|
607
|
+
* override variable, the system stack fills in behind it.
|
|
608
|
+
* 2. **Our dirs go FIRST, an inherited fallback is kept BEHIND them, and
|
|
609
|
+
* `/usr/lib` is appended when there is none.** Setting the variable
|
|
610
|
+
* REPLACES dyld's own default fallback list, so writing only our dirs would
|
|
611
|
+
* silently remove `/usr/lib` from the search — a host that exported the
|
|
612
|
+
* variable itself already carries whatever tail it wants. Same composition
|
|
613
|
+
* as node-gi's re-exec.
|
|
614
|
+
*
|
|
615
|
+
* Emitted even when `packages` is empty: the gap is in the host's loader, not in
|
|
616
|
+
* a gjsify prebuild, so a plain `gjsify run script.gjs.js` that touches GTK needs
|
|
617
|
+
* it just as much. Empty off darwin — `ld.so`'s configured cache already resolves
|
|
618
|
+
* these leaves on Linux, and Windows re-reads `PATH` per `LoadLibrary`.
|
|
572
619
|
*/
|
|
573
620
|
export function buildNativeEnv(packages, opts = {}) {
|
|
574
621
|
const platform = opts.platform ?? process.platform;
|
|
@@ -588,5 +635,14 @@ export function buildNativeEnv(packages, opts = {}) {
|
|
|
588
635
|
GI_TYPELIB_PATH: prepend(env['GI_TYPELIB_PATH'], giSeparator),
|
|
589
636
|
};
|
|
590
637
|
out[libVarKey] = prepend(env[libVarKey], libSeparator);
|
|
638
|
+
// The host's own GI libdirs — see the block comment above. `systemGiLibraryDirs`
|
|
639
|
+
// is itself `[]` off darwin, so this is a no-op elsewhere without a second
|
|
640
|
+
// platform test; the variable is only WRITTEN when there is something to put in
|
|
641
|
+
// it, so a Mac with no GI stack installed is left byte-unchanged.
|
|
642
|
+
const systemDirs = (opts.systemGiDirs ?? systemGiLibraryDirs)({ platform, env });
|
|
643
|
+
if (systemDirs.length > 0) {
|
|
644
|
+
const inherited = splitSearchPath(env[DYLD_FALLBACK_VAR]);
|
|
645
|
+
out[DYLD_FALLBACK_VAR] = [...systemDirs, ...(inherited.length > 0 ? inherited : ['/usr/lib'])].join(':');
|
|
646
|
+
}
|
|
591
647
|
return out;
|
|
592
648
|
}
|
package/lib/utils/dlx-cache.js
CHANGED
|
@@ -158,7 +158,7 @@ export function symlinkSwap(cacheDir, prepareDir, opts = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
/** Clean up `<cacheDir>/<oldPrepareDir>` siblings older than `maxAgeMinutes`. */
|
|
160
160
|
export function cleanupStalePrepareDirs(cacheDir, _maxAgeMinutes = DEFAULT_TTL_MIN) {
|
|
161
|
-
//
|
|
161
|
+
// TODO(open-todos: 10 small API gaps): out of scope for Phase 1 — pnpm has the same stub. Kept so
|
|
162
162
|
// call sites already exist when we do implement it.
|
|
163
163
|
void cacheDir;
|
|
164
164
|
}
|
|
@@ -5,7 +5,13 @@ interface ParsedSpec {
|
|
|
5
5
|
name: string;
|
|
6
6
|
range: string;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* One placed package in the resolved tree. Exported as a TYPE only: it is the
|
|
10
|
+
* graph shape {@link computeOptionalFlags} and {@link applyPlatformFilter} take,
|
|
11
|
+
* so a spec can inject a tree by hand instead of driving a whole install to reach
|
|
12
|
+
* them. Nothing outside this module constructs one for real.
|
|
13
|
+
*/
|
|
14
|
+
export interface ResolvedNode {
|
|
9
15
|
/** Package name (e.g. `@gjsify/cli`, `lodash`). */
|
|
10
16
|
name: string;
|
|
11
17
|
version: string;
|
|
@@ -133,6 +139,78 @@ export declare function applyPlatformFilter(nodes: ResolvedNode[], target: Platf
|
|
|
133
139
|
* detection — no script is run, so node-free-ness is preserved.
|
|
134
140
|
*/
|
|
135
141
|
export declare function warnMissingNativeBuilds(nodes: ResolvedNode[], prefix: string, log: Logger): void;
|
|
142
|
+
/**
|
|
143
|
+
* A node's REQUIRED dependency edges: its `dependencies`, minus every name its
|
|
144
|
+
* `optionalDependencies` also lists. The one place that decides what kind an edge
|
|
145
|
+
* is — both the resolve walk and the optionality fixpoint read it, so the two
|
|
146
|
+
* cannot drift.
|
|
147
|
+
*
|
|
148
|
+
* THE SUBTRACTION IS npm's RULE, not a policy of ours: "entries in
|
|
149
|
+
* optionalDependencies will override entries of the same name in dependencies, so
|
|
150
|
+
* it's usually best to only put in one place" (npm's package.json docs). Measured
|
|
151
|
+
* on `@parcel/rust@2.16.4`, which lists all eight per-platform napi packages in
|
|
152
|
+
* both blocks: `npm install` exits 0, installs `@parcel/rust-linux-x64-gnu` only,
|
|
153
|
+
* and writes `"optional": true` for all eight in its `package-lock.json`.
|
|
154
|
+
*
|
|
155
|
+
* WHY IT IS NOT MERELY A DEDUP: the kind decides what an incompatible
|
|
156
|
+
* `os`/`cpu`/`libc` MEANS (`applyPlatformFilter` — fatal vs. inert) and whether a
|
|
157
|
+
* failed resolve is tolerated. Reading `dependencies` alone therefore does not
|
|
158
|
+
* just visit a name twice, it promotes a foreign-platform binary to a required
|
|
159
|
+
* dependency and fails installs npm completes.
|
|
160
|
+
*
|
|
161
|
+
* The inverse direction is deliberately NOT symmetric: a name in
|
|
162
|
+
* `optionalDependencies` alone is optional (no `dependencies` entry to override),
|
|
163
|
+
* which is what {@link resolveDeps}'s second queue loop covers.
|
|
164
|
+
*/
|
|
165
|
+
export declare function requiredDepEntries(node: Pick<ResolvedNode, 'dependencies' | 'optionalDependencies'>): [string, string][];
|
|
166
|
+
/**
|
|
167
|
+
* Recompute every node's `optional` flag as a FIXPOINT over the placed graph.
|
|
168
|
+
* The only writer of the final flag; whatever the nodes arrive carrying is an
|
|
169
|
+
* input nothing checked (a BFS forward guess on the resolve path, a value read
|
|
170
|
+
* back out of a file on the lockfile path) and is overwritten unconditionally.
|
|
171
|
+
*
|
|
172
|
+
* DEFINITION: a node is REQUIRED iff it is reachable from `requiredNames`
|
|
173
|
+
* through REQUIRED edges alone ({@link requiredDepEntries} — `dependencies` minus
|
|
174
|
+
* the names `optionalDependencies` overrides); every other node is optional.
|
|
175
|
+
* Optionality is therefore INHERITED — a plain dependency OF an optional package
|
|
176
|
+
* is still optional — which is what npm's `optionalSet` computes.
|
|
177
|
+
*
|
|
178
|
+
* WHY A FIXPOINT AND NOT A ONE-SHOT PROMOTION AT THE REUSE SITE (the reviewed
|
|
179
|
+
* defect): the walk queues a node's own dep edges at the moment it is placed,
|
|
180
|
+
* carrying the optionality it had THEN. Promoting the node later leaves its
|
|
181
|
+
* children behind, so the answer depends on which edge BFS happened to traverse
|
|
182
|
+
* first — a genuinely required transitive dep reached first through an optional
|
|
183
|
+
* edge stayed flagged optional, was persisted that way, and was then silently
|
|
184
|
+
* skipped by the platform filter on every machine instead of raising
|
|
185
|
+
* EBADPLATFORM. A monotone worklist over the FINISHED graph cannot have that
|
|
186
|
+
* property: "required" only ever spreads, so the result is independent of visit
|
|
187
|
+
* order. npm reaches for the same shape and says so —
|
|
188
|
+
* refs/npm-cli/workspaces/arborist/lib/calc-dep-flags.js: "If a node is changed,
|
|
189
|
+
* we add to the queue and continue until no more changes."
|
|
190
|
+
*
|
|
191
|
+
* PATH-INDEPENDENCE IS THE OTHER REQUIREMENT, and it is why the lockfile carries
|
|
192
|
+
* an `optionalDependencies` map per entry (v4). The walk USED to read
|
|
193
|
+
* `dependencies` only, for exactly that reason — a lockfile entry recorded nothing
|
|
194
|
+
* else, so consulting `node.optionalDependencies` would have made the two paths
|
|
195
|
+
* compute different flags for the same tree. The premise was fixable and the
|
|
196
|
+
* consequence was not: a publisher listing the same name in BOTH blocks means
|
|
197
|
+
* "optional" to npm, came out REQUIRED here, and the shape is not rare — it is
|
|
198
|
+
* `@parcel/rust@2.16.4` (all eight per-platform napi packages in both blocks), so
|
|
199
|
+
* every Linux install of any tree containing parcel failed with EBADPLATFORM on
|
|
200
|
+
* `@parcel/rust-darwin-x64` while `npm install` thinned the same tree and exited
|
|
201
|
+
* 0. "Erring toward REQUIRED errs toward the loud failure" is true and was the
|
|
202
|
+
* wrong trade: the loud failure was not a real one, and no `--force` lifts it
|
|
203
|
+
* (nor should it). So the declaration is persisted and both paths subtract it.
|
|
204
|
+
* Reading a pre-v4 lockfile leaves the map empty, which reproduces the old
|
|
205
|
+
* behaviour for that file alone — see READABLE_LOCKFILE_VERSIONS.
|
|
206
|
+
*
|
|
207
|
+
* Runs before `writeLockfile` so the persisted flag is the final one, and before
|
|
208
|
+
* `applyPlatformFilter` so the fatal-vs-inert decision reads it rather than the
|
|
209
|
+
* guess. Exported so the unit spec can drive it with INJECTED graphs: the whole
|
|
210
|
+
* fatal-vs-inert question is decided by these two functions in composition, and
|
|
211
|
+
* that is worth pinning without a registry.
|
|
212
|
+
*/
|
|
213
|
+
export declare function computeOptionalFlags(nodes: ResolvedNode[], requiredNames: Set<string>, log: Logger): void;
|
|
136
214
|
export declare function parseSpec(raw: string): ParsedSpec;
|
|
137
215
|
export declare function pickVersion(packument: Packument, range: string, preferred?: string): string | null;
|
|
138
216
|
/**
|
|
@@ -172,6 +250,14 @@ export declare function writeBinEntry(opts: {
|
|
|
172
250
|
envPreamble: string;
|
|
173
251
|
prebuildDirs: readonly string[];
|
|
174
252
|
};
|
|
253
|
+
/**
|
|
254
|
+
* The package's npm `bin` target, when it declares one ALONGSIDE
|
|
255
|
+
* `gjsify.bin`. The launcher then picks a runtime per invocation instead of
|
|
256
|
+
* hard-coding `gjs` — see {@link pickBinMap}. Absent for a package that
|
|
257
|
+
* declares only one of the two, which is every package but `@gjsify/cli`
|
|
258
|
+
* today.
|
|
259
|
+
*/
|
|
260
|
+
nodeFallbackAbs?: string;
|
|
175
261
|
}): void;
|
|
176
262
|
type Logger = (fmt: string, ...args: unknown[]) => void;
|
|
177
263
|
export {};
|