@gjsify/cli 0.27.1 → 0.29.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.
@@ -0,0 +1,221 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // WHERE the host's GObject-Introspection stack keeps its shared libraries, so the
3
+ // dynamic loader can find the ones a typelib names by BARE LEAF
4
+ // (`libgtk-4.1.dylib`, `libgdk_pixbuf-2.0.0.dylib`, …).
5
+ //
6
+ // ── A DELIBERATE MIRROR ──────────────────────────────────────────────────────
7
+ //
8
+ // This is a PINNED PORT of `packages/node-gi/node-gi/system-gi.js`. The two must
9
+ // agree, and `system-gi.spec.ts` is the check: it imports node-gi's copy by
10
+ // relative path and asserts both return identical arrays for a table of injected
11
+ // inputs. That is the same shape `impliedExampleNodeEntry()` in
12
+ // `@gjsify/manifest-conformance` uses for `resolveNodeEntry()` in this package —
13
+ // "this is the check, that is the consumer".
14
+ //
15
+ // Why a mirror and not an import: ADR 0005 Decision 2 forbids any Tier-1/Tier-2
16
+ // package taking a `dependencies`/`optionalDependencies` edge on
17
+ // `@gjsify/node-gi`, and `@gjsify/cli` is Tier 1. The alternative — lifting the
18
+ // module into a new shared npm name — is the documented expensive path (manual
19
+ // first publish + Trusted Publisher bootstrap; the `@gjsify/tls-native` incident
20
+ // stalled 60+ packages), so it is tracked in `status/open-todos.md` instead of
21
+ // taken here. A spec may reach across the repo because it is never bundled into
22
+ // the published `lib/`, which is what lets the agreement be machine-checked
23
+ // without creating the forbidden edge.
24
+ //
25
+ // ── WHY THIS EXISTS ──────────────────────────────────────────────────────────
26
+ //
27
+ // Measured twice, on the same class of host, in two runtimes:
28
+ //
29
+ // 1. Node (macOS 15.7.8 / Homebrew /usr/local / Node 24.18.1 / GTK 4.22.4, the
30
+ // first release that shipped a darwin-x64 addon) — this is node-gi's report:
31
+ //
32
+ // GLib-GIRepository-WARNING: Failed to load shared library 'libgtk-4.1.dylib'
33
+ // referenced by the typelib: dlopen(libgtk-4.1.dylib, 0x0009): tried:
34
+ // 'libgtk-4.1.dylib' (no such file), …, '/usr/lib/libgtk-4.1.dylib' (no such file)
35
+ // TypeError: Gtk.DrawingArea is not a subclassable GObject type
36
+ //
37
+ // 2. GJS (same VM, gjs 1.88.1, `gjs -c "imports.gi.Gtk; Gtk.init()"`, nothing
38
+ // gjsify in the process at all) — the reason this CLI needs the module too:
39
+ //
40
+ // dlopen(libgtk-4.1.dylib, 0x0009): tried: 'libgtk-4.1.dylib' (no such file),
41
+ // '/System/Volumes/Preboot/Cryptexes/OSlibgtk-4.1.dylib' (no such file),
42
+ // '/usr/local/Cellar/gjs/1.88.1/bin/../../../../opt/glib/lib/libgtk-4.1.dylib'
43
+ // (no such file), '/usr/lib/libgtk-4.1.dylib' (no such file, not in dyld cache)
44
+ // JS ERROR: Error: Unsupported type void, deriving from fundamental void
45
+ //
46
+ // Read the third path dyld tried: Homebrew's `gjs` binary has an rpath, and it
47
+ // points into GLIB's keg (`…/opt/glib/lib`) — nowhere else. So Homebrew's gjs
48
+ // resolves the glib-family leaves it links against and NO other keg's. The
49
+ // difference between the two reports is cosmetic, not structural: `node` has no
50
+ // rpath at all, `gjs` has one into the wrong keg, and `/usr/local/lib` — where
51
+ // `libgtk-4.1.dylib` actually is — is in neither, nor in dyld's default fallback
52
+ // path. The gap is the LOADER, not the installation: `gi_repository_require`
53
+ // resolves the typelib, `g_module_open('libgtk-4.1.dylib')` then fails, no
54
+ // `gtk_*_get_type` is ever registered, and the failure surfaces one layer up as
55
+ // something that reads like a type-system bug.
56
+ //
57
+ // The value CANNOT come from `pkg-config --variable=libdir gtk4`. Measured on the
58
+ // same host: that answers `/usr/local/Cellar/gtk4/4.22.4/lib` — a PER-FORMULA
59
+ // directory that fixes GTK and leaves `libgdk_pixbuf-2.0.0.dylib` (a different
60
+ // formula, so a different Cellar libdir) still unresolvable. What works is the
61
+ // directory every keg is symlinked into, `<prefix>/lib`, because it is the UNION.
62
+ // So the question this module answers is "which directories hold the GI stack",
63
+ // never "where is one library".
64
+ //
65
+ // THE RULE — a typelib and the library it names are SIBLINGS by GI's own install
66
+ // layout: `<libdir>/girepository-1.0/Gtk-4.0.typelib` beside
67
+ // `<libdir>/libgtk-4.1.dylib`. A directory holding `girepository-1.0/` therefore
68
+ // IS a GI library directory. That convention is what makes this
69
+ // package-manager-agnostic: it lands on `/usr/local/lib` (Homebrew x64),
70
+ // `/opt/homebrew/lib` (Homebrew arm64), `/opt/local/lib` (MacPorts) or a custom
71
+ // prefix without needing to know that any of them exist.
72
+ //
73
+ // PURE FUNCTION of injected host facts (platform / env / fs / pkg-config), like
74
+ // `resolvePrebuildDirName()` and `buildNativeEnv()` beside it — so the darwin
75
+ // branch is unit-testable from a Linux host and the host values are read only at
76
+ // the outermost call site, as defaults.
77
+ //
78
+ // ── HOW THE CONSUMER DIFFERS FROM NODE-GI'S ──────────────────────────────────
79
+ //
80
+ // node-gi RE-EXECS its own process with the result on `DYLD_FALLBACK_LIBRARY_PATH`,
81
+ // because dyld captures that variable at process LAUNCH and an in-process
82
+ // `process.env` mutation therefore does nothing. This CLI never needs a re-exec:
83
+ // it is the one LAUNCHING the gjs child (`buildNativeEnv()` →
84
+ // `spawn('gjs', …, { env })`), so it sets the variable in the child's launch env,
85
+ // which is the same repair one hop earlier. A launcher could not fix this from
86
+ // inside the bundle either way — GI resolves typelibs and their backers before the
87
+ // bundle's first line runs.
88
+ import { spawnSync } from 'node:child_process';
89
+ import { statSync } from 'node:fs';
90
+ import { basename, dirname, join } from 'node:path';
91
+ /**
92
+ * Prefixes a macOS GI stack is installed under in practice — probed, never
93
+ * assumed: each is accepted only when it actually holds a `girepository-1.0/`.
94
+ *
95
+ * This is the no-pkg-config safety net, and it is load-bearing rather than
96
+ * belt-and-braces: `brew install gtk4` installs RUNTIME dependencies only, so a
97
+ * user running a published showcase routinely has the entire GTK stack and no
98
+ * `pkg-config` at all (it is a BUILD dependency of those formulae). The
99
+ * pkg-config source below is the GENERAL mechanism — it is what finds a jhbuild
100
+ * or otherwise bespoke prefix — but it cannot be the only one.
101
+ */
102
+ const PROBED_GI_LIBDIRS = {
103
+ darwin: ['/opt/homebrew/lib', '/usr/local/lib', '/opt/local/lib'],
104
+ };
105
+ /** The subdir GI installs typelibs into — historically `1.0` even for the girepository-2.0 API. */
106
+ const TYPELIB_SUBDIR = 'girepository-1.0';
107
+ /** Memoized `pkg-config` answer: the spawn happens at most once per process. */
108
+ let pkgConfigDirsCache = null;
109
+ /**
110
+ * Split an OS search-path variable, dropping empty segments.
111
+ */
112
+ export function splitSearchPath(value, separator = ':') {
113
+ return (value ?? '').split(separator).filter(Boolean);
114
+ }
115
+ function isDirectory(dir) {
116
+ try {
117
+ return statSync(dir).isDirectory();
118
+ }
119
+ catch {
120
+ // Any stat failure means the same thing here — ENOENT on an uninstalled
121
+ // prefix, EACCES on a path this process may not read: not a usable libdir.
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * `pkg-config`'s OWN configured `.pc` search path plus `$PKG_CONFIG_PATH`. Every
127
+ * entry is a `<libdir>/pkgconfig` (or `<datadir>/pkgconfig`) directory, so its
128
+ * parent is a candidate libdir — which is how a prefix nobody hardcoded is
129
+ * found. Measured on the reported host: `pc_path` =
130
+ * `/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig:/usr/lib/pkgconfig:…`,
131
+ * whose first parent is exactly the `/usr/local/lib` that resolves every leaf.
132
+ *
133
+ * `spawnSync` reports a missing executable as `result.error`, not by throwing —
134
+ * an absent `pkg-config` is expected on a runtime-only GTK install, so it is a
135
+ * checked return value here and not an exception path. (`@gjsify/child_process`
136
+ * matches Node here deliberately; see its `spawnSync` note.)
137
+ */
138
+ export function pkgConfigSearchDirs(env) {
139
+ if (pkgConfigDirsCache)
140
+ return pkgConfigDirsCache;
141
+ const dirs = splitSearchPath(env['PKG_CONFIG_PATH']);
142
+ const res = spawnSync('pkg-config', ['--variable=pc_path', 'pkg-config'], {
143
+ encoding: 'utf8',
144
+ timeout: 10_000,
145
+ });
146
+ if (!res.error && res.status === 0)
147
+ dirs.push(...splitSearchPath(String(res.stdout).trim()));
148
+ pkgConfigDirsCache = dirs;
149
+ return dirs;
150
+ }
151
+ /**
152
+ * Directories holding the host's system GObject-Introspection shared libraries —
153
+ * most specific first, deduplicated, each verified on disk.
154
+ *
155
+ * Three sources, in precedence order:
156
+ * 1. `GI_TYPELIB_PATH` — the host stating outright where typelibs live; the
157
+ * libraries are the sibling `dirname()`. Accepted on directory existence
158
+ * alone: an explicit statement is not second-guessed.
159
+ * 2. `pkg-config`'s `.pc` search path → each `<dir>/pkgconfig`'s parent. The
160
+ * general mechanism (any prefix, including a custom one).
161
+ * 3. {@link PROBED_GI_LIBDIRS} — the standard macOS prefixes, for the common
162
+ * case where GTK is installed and pkg-config is not.
163
+ * Sources 2 and 3 are GUESSES at a prefix, so each must show the
164
+ * `girepository-1.0/` marker before it is believed.
165
+ *
166
+ * Empty off darwin, and that is a statement about LOADERS, not about GI. Linux
167
+ * resolves these leaves through `ld.so`'s system-wide configured cache
168
+ * (`/etc/ld.so.conf.d`), which a package install populates — nothing per-process
169
+ * is needed. dyld is the one loader that neither consults a system-wide config
170
+ * NOR re-reads its search path after launch, which is why darwin needs the
171
+ * variable set at launch and nobody else does.
172
+ *
173
+ * win32 is deliberately absent rather than proven unnecessary: `PATH` is the DLL
174
+ * search path, a Windows GTK distribution puts its `bin` there itself, and
175
+ * Windows re-reads that path at every `LoadLibrary` — so if a system GTK ever
176
+ * turns out not to be on it, the repair is a `PATH` prepend (which
177
+ * {@link buildNativeEnv} already does for the prebuilds), never a second
178
+ * variable. Untested there.
179
+ *
180
+ * @returns absolute directories, or `[]` when this platform's loader needs none
181
+ */
182
+ export function systemGiLibraryDirs({ platform = process.platform, env = process.env, existsDir = isDirectory, searchDirs = pkgConfigSearchDirs, } = {}) {
183
+ const probed = PROBED_GI_LIBDIRS[platform];
184
+ if (!probed)
185
+ return [];
186
+ const out = [];
187
+ const add = (dir) => {
188
+ if (dir && dir !== '/' && !out.includes(dir))
189
+ out.push(dir);
190
+ };
191
+ // 1. Explicit: the typelib dir's sibling libdir.
192
+ for (const typelibDir of splitSearchPath(env['GI_TYPELIB_PATH'])) {
193
+ const libDir = dirname(typelibDir);
194
+ if (existsDir(libDir))
195
+ add(libDir);
196
+ }
197
+ // 2 + 3. Candidate prefixes, believed only on the girepository-1.0 marker.
198
+ const candidates = [];
199
+ for (const pcDir of searchDirs(env)) {
200
+ if (basename(pcDir) === 'pkgconfig')
201
+ candidates.push(dirname(pcDir));
202
+ }
203
+ candidates.push(...probed);
204
+ for (const libDir of candidates) {
205
+ if (existsDir(join(libDir, TYPELIB_SUBDIR)))
206
+ add(libDir);
207
+ }
208
+ return out;
209
+ }
210
+ /**
211
+ * Whether `current` (a split `DYLD_FALLBACK_LIBRARY_PATH`) already covers every
212
+ * directory in `wanted` — the "nothing to do" test.
213
+ *
214
+ * Kept in the port even though this CLI's consumer does not branch on it: the
215
+ * mirror's value is that the two files are the SAME file, and a surface that
216
+ * drifts by one export is a mirror nobody can check. `system-gi.spec.ts` pins
217
+ * both.
218
+ */
219
+ export function pathCovers(wanted, current) {
220
+ return wanted.every((dir) => current.includes(dir));
221
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/cli",
3
- "version": "0.27.1",
3
+ "version": "0.29.0",
4
4
  "description": "CLI for Gjsify",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -129,19 +129,19 @@
129
129
  "cli"
130
130
  ],
131
131
  "dependencies": {
132
- "@gjsify/buffer": "^0.27.1",
133
- "@gjsify/create-app": "^0.27.1",
134
- "@gjsify/node-globals": "^0.27.1",
135
- "@gjsify/node-polyfills": "^0.27.1",
136
- "@gjsify/npm-registry": "^0.27.1",
137
- "@gjsify/resolve-npm": "^0.27.1",
138
- "@gjsify/rolldown-plugin-gjsify": "^0.27.1",
139
- "@gjsify/rolldown-plugin-pnp": "^0.27.1",
140
- "@gjsify/semver": "^0.27.1",
141
- "@gjsify/tar": "^0.27.1",
142
- "@gjsify/tsc": "^0.27.1",
143
- "@gjsify/web-polyfills": "^0.27.1",
144
- "@gjsify/workspace": "^0.27.1",
132
+ "@gjsify/buffer": "^0.29.0",
133
+ "@gjsify/create-app": "^0.29.0",
134
+ "@gjsify/node-globals": "^0.29.0",
135
+ "@gjsify/node-polyfills": "^0.29.0",
136
+ "@gjsify/npm-registry": "^0.29.0",
137
+ "@gjsify/resolve-npm": "^0.29.0",
138
+ "@gjsify/rolldown-plugin-gjsify": "^0.29.0",
139
+ "@gjsify/rolldown-plugin-pnp": "^0.29.0",
140
+ "@gjsify/semver": "^0.29.0",
141
+ "@gjsify/tar": "^0.29.0",
142
+ "@gjsify/tsc": "^0.29.0",
143
+ "@gjsify/web-polyfills": "^0.29.0",
144
+ "@gjsify/workspace": "^0.29.0",
145
145
  "cosmiconfig": "^9.0.2",
146
146
  "get-tsconfig": "^4.14.0",
147
147
  "pkg-types": "^2.3.1",
@@ -149,13 +149,13 @@
149
149
  "yargs": "^18.0.0"
150
150
  },
151
151
  "devDependencies": {
152
- "@gjsify/unit": "^0.27.1",
152
+ "@gjsify/unit": "^0.29.0",
153
153
  "@types/yargs": "^17.0.35",
154
154
  "typescript": "^6.0.3"
155
155
  },
156
156
  "peerDependencies": {
157
- "@gjsify/oxfmt-native": "^0.27.1",
158
- "@gjsify/rolldown-native": "^0.27.1"
157
+ "@gjsify/oxfmt-native": "^0.29.0",
158
+ "@gjsify/rolldown-native": "^0.29.0"
159
159
  },
160
160
  "peerDependenciesMeta": {
161
161
  "@gjsify/oxfmt-native": {