@gjsify/gtk-runtime-win32-x64 0.19.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/README.md ADDED
@@ -0,0 +1,133 @@
1
+ # @gjsify/gtk-runtime-win32-x64
2
+
3
+ Batteries-included GTK / GObject-Introspection runtime bundle for **Windows x64**.
4
+ It lets [`@gjsify/node-gi`](../node-gi) load `gi://` namespaces (GLib · GObject ·
5
+ Gio · cairo · Pango · Graphene · Gdk) with **no gvsbuild / system GTK** installed on
6
+ the host — the Windows sibling of `@gjsify/gtk-runtime-darwin-arm64`.
7
+
8
+ Platform-gated (`os: ["win32"]`, `cpu: ["x64"]`), tier 3 (experimental). On any
9
+ other platform npm skips it and `@gjsify/node-gi` falls back to a system/gvsbuild
10
+ GTK. The heavy `gtk/` payload is **not committed** — it is built on a Windows CI
11
+ runner (`scripts/build-gtk-runtime.mjs`) and shipped via the package tarball /
12
+ staged into node-gi's `prebuilds/win32-x64/gtk/`.
13
+
14
+ ## Layout
15
+
16
+ ```
17
+ gtk/
18
+ bin/ GTK/GLib/cairo/pango/graphene/gdk-pixbuf DLLs (+ deps)
19
+ girepository-1.0/ typelibs (GLib/GObject/Gio/cairo/Pango/Graphene/Gdk/…)
20
+ manifest.json counts + sizes + DLL list
21
+ ```
22
+
23
+ Note the native code lives in **`gtk/bin`** (DLLs), not `gtk/lib` — on Windows the
24
+ loadable images and the import libraries (`.lib`, not shipped) live in different
25
+ dirs, and only the DLLs are needed at runtime.
26
+
27
+ ## How it is built (no relocation — the crux)
28
+
29
+ `node scripts/build-gtk-runtime.mjs` runs on a Windows runner **with** a build-time
30
+ gvsbuild GTK4 stack extracted at `--prefix` (env `GTK_PREFIX`, the closure *source*,
31
+ not shipped):
32
+
33
+ 1. **Collect** — enumerate `<prefix>/bin/*.dll` and, from the display-free seed DLLs
34
+ the conformance loads (glib/gobject/gio/gmodule + girepository + cairo + pango +
35
+ graphene + gdk-pixbuf + gtk4 + harfbuzz), walk the DLL import closure with
36
+ `dumpbin /dependents` (recursively), keeping every dependency that is itself a
37
+ gvsbuild DLL. System DLLs (`KERNEL32`, `msvcrt`, `api-ms-win-*`, …) never resolve
38
+ to a file under `<prefix>/bin`, so the "keep only gvsbuild-resident deps" filter
39
+ leaves them OS-provided. If dumpbin can't be located the script copies the whole
40
+ `bin/*.dll` set (a complete superset).
41
+ 2. **Copy** — the closure DLLs are copied **flat** into `gtk/bin`. **That is the whole
42
+ job.** There is **no** `install_name_tool`/`@rpath`/`codesign` step: Windows
43
+ resolves a DLL's imports by **search path** at `LoadLibrary` time, so a directory
44
+ of DLLs is portable the moment it exists — no per-file rewriting, no re-signing.
45
+ 3. **Typelibs** — copy the typelib set into `gtk/girepository-1.0`.
46
+
47
+ Contrast with macOS, where dyld bakes each dylib's dependency install-names, so the
48
+ darwin bundle must rewrite every reference to `@loader_path/<leaf>` and ad-hoc
49
+ re-sign. Windows needs none of that.
50
+
51
+ ## How node-gi finds + activates it
52
+
53
+ `@gjsify/node-gi`'s loader (`gtk-runtime.js`) resolves the bundle from, in order:
54
+
55
+ 1. `GJSIFY_GTK_RUNTIME` (explicit bundle dir),
56
+ 2. node-gi's own `prebuilds/win32-x64/gtk/` (the CI/dev staging path),
57
+ 3. the sibling monorepo dir `../gtk-runtime-win32-x64/gtk`,
58
+ 4. `require.resolve('@gjsify/gtk-runtime-win32-x64')` (the published dep).
59
+
60
+ On a match (win32 only) node-gi makes the bundle genuinely **env-free**:
61
+
62
+ - **DLLs** — before the native addon is loaded, node-gi **prepends `gtk/bin` to
63
+ `process.env.PATH`** (`maybePrependGtkRuntimeDllPath`, from `index.js`, run BEFORE
64
+ the addon's `require`). Windows re-reads the DLL search path at **every**
65
+ `LoadLibrary` call — unlike dyld, which captures `DYLD_FALLBACK_LIBRARY_PATH` only
66
+ at process launch — so a plain in-process `process.env.PATH` mutation is enough:
67
+ when node-gi's own `require(node_gi.node)` triggers the addon's DLL imports
68
+ (`glib-2.0-0.dll`, `girepository-2.0-0.dll`, `cairo-2.dll`, …), the loader consults
69
+ the just-updated `PATH` and finds them in the bundle. The **same** `PATH` entry also
70
+ satisfies the runtime `g_module_open(<soname>)` lookups a typelib does for its
71
+ non-addon-linked backers (Pango / Gdk / Graphene) — one mechanism covers both, so
72
+ there is no DYLD-style dual path.
73
+ - **typelibs** — node-gi prepends `gtk/girepository-1.0` to the GIRepository search
74
+ path via the native `prependSearchPath` (a runtime API, no env needed).
75
+
76
+ ### Why no re-exec (unlike macOS)
77
+
78
+ The macOS bundle re-execs the process once with `DYLD_FALLBACK_LIBRARY_PATH` set,
79
+ because dyld reads that variable **only at launch** — a JS-time `process.env`
80
+ mutation cannot reach the running dyld. Windows has no such capture: the DLL search
81
+ consults the live `PATH` at each `LoadLibrary`, and the node-gi addon is loaded
82
+ **exclusively** by node-gi's own `require()` (in `loadNative()`), which runs *after*
83
+ the `PATH` prepend. So the addon's static DLL imports resolve against the bundle with
84
+ no re-exec and no native `AddDllDirectory` call (which would be chicken-and-egg —
85
+ the addon must already be loaded to call it). PATH-prepend before the `require` is
86
+ both sufficient and the simplest mechanism.
87
+
88
+ ## Two closures: display-free (default) + full windowing (`--windowing`)
89
+
90
+ `build-gtk-runtime.mjs` builds one of two supersets into the same `gtk/` dir:
91
+
92
+ - **Default (display-free)** — the DLL closure + typelibs the display-free
93
+ conformance needs. `manifest.windowing === false`. Unchanged.
94
+ - **`--windowing` (superset)** — everything above **plus** the runtime DATA a real
95
+ GTK window needs on Windows, so a `Adw.ApplicationWindow` realizes + renders with
96
+ no gvsbuild/system GTK:
97
+
98
+ ```
99
+ gtk/
100
+ bin/ DLLs (+ the SVG/PNG image-loader backers)
101
+ girepository-1.0/ typelibs
102
+ lib/gdk-pixbuf-2.0/2.10.0/loaders/ image decoder DLLs
103
+ lib/gdk-pixbuf-2.0/2.10.0/loaders.cache (rewritten bundle-relative)
104
+ share/glib-2.0/schemas/gschemas.compiled (glib-compile-schemas)
105
+ share/icons/{Adwaita,hicolor}/ icon themes + icon-theme.cache
106
+ etc/fonts/fonts.conf Fontconfig config (+ cache), when present
107
+ manifest.json windowing:true + windowingData counts
108
+ ```
109
+
110
+ Built on the Windows runner:
111
+ ```
112
+ node scripts/build-gtk-runtime.mjs --windowing --prefix C:\gtk-build\gtk\x64\release --out gtk
113
+ ```
114
+ The GdkWin32 backend is compiled **into** `libgtk-4-*.dll` (GTK4 builds every
115
+ backend in), so there is no separate backend DLL — the caches (`loaders.cache`,
116
+ `gschemas.compiled`, `icon-theme.cache`) + the ANGLE/librsvg backers are the
117
+ additions. gvsbuild ships the tools this step runs (`gdk-pixbuf-query-loaders`,
118
+ `glib-compile-schemas`, `gtk4-update-icon-cache`, `fc-cache`) in `<prefix>/bin`.
119
+ Each data step is defensive — a missing tree/tool WARNs and continues (the DLL +
120
+ typelib bundle is always produced).
121
+
122
+ ### How node-gi wires the windowing data
123
+
124
+ `@gjsify/node-gi`'s loader (`gtk-runtime.js` `maybeWireGtkWindowingEnv`, run at
125
+ module load beside the PATH-prepend) detects the windowing data via the
126
+ `gschemas.compiled` marker and sets — only when currently unset — the env vars that
127
+ locate it: `GSETTINGS_SCHEMA_DIR`, `GDK_PIXBUF_MODULEDIR` + `GDK_PIXBUF_MODULE_FILE`,
128
+ `XDG_DATA_DIRS` (prepends `<bundle>/share`) and, when bundled, `FONTCONFIG_PATH` +
129
+ `FONTCONFIG_FILE`. Windows re-reads these at first use (schema / loader / icon-theme
130
+ init runs after the addon loads), so the in-process mutation is sufficient — no
131
+ re-exec, the DLL-search analog. A **display-free** bundle carries no windowing data,
132
+ so the marker is absent and the wiring is a strict no-op: the display-free load is
133
+ byte-unchanged.
package/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /** Absolute path to the bundle root (contains `bin/` + `girepository-1.0/`). */
3
+ export const bundleDir: string;
4
+ /** Absolute path to the bundled DLL dir (prepend to PATH so LoadLibrary finds it). */
5
+ export const binDir: string;
6
+ /** Absolute path to the typelib dir (feed to GIRepository.prepend_search_path). */
7
+ export const typelibDir: string;
8
+ /** Whether the bundle payload is actually present on disk (it is built on CI). */
9
+ export const isPresent: boolean;
10
+ declare const _default: { bundleDir: string; binDir: string; typelibDir: string; isPresent: boolean };
11
+ export default _default;
package/index.js ADDED
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // @gjsify/gtk-runtime-win32-x64 — path helpers for the bundled GTK runtime.
3
+ // @gjsify/node-gi resolves this package (optional, os/cpu-gated) to find the
4
+ // bundled typelib + DLL dirs when no gvsbuild/system GTK is present. Unlike
5
+ // macOS, Windows needs NO relocation: DLLs resolve by SEARCH PATH at LoadLibrary
6
+ // time, so node-gi just prepends `gtk/bin` to PATH before the addon loads. The
7
+ // heavy `gtk/` payload is produced by scripts/build-gtk-runtime.mjs on a Windows
8
+ // runner.
9
+ import { existsSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const here = dirname(fileURLToPath(import.meta.url));
14
+
15
+ /** Absolute path to the bundle root (contains `bin/` + `girepository-1.0/`). */
16
+ export const bundleDir = join(here, 'gtk');
17
+
18
+ /** Absolute path to the bundled DLL dir (prepend to PATH so LoadLibrary finds it). */
19
+ export const binDir = join(bundleDir, 'bin');
20
+
21
+ /** Absolute path to the typelib dir (feed to GIRepository.prepend_search_path). */
22
+ export const typelibDir = join(bundleDir, 'girepository-1.0');
23
+
24
+ /** Whether the bundle payload is actually present on disk (it is built on CI). */
25
+ export const isPresent = existsSync(typelibDir) && existsSync(binDir);
26
+
27
+ export default { bundleDir, binDir, typelibDir, isPresent };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@gjsify/gtk-runtime-win32-x64",
3
+ "version": "0.19.0",
4
+ "description": "Batteries-included GTK/GObject-Introspection runtime bundle for Windows x64 — lets @gjsify/node-gi load gi:// namespaces (GLib/GObject/Gio/cairo/Pango/Graphene/Gdk) with NO gvsbuild/system GTK. Platform-gated (win32/x64 only).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "os": [
8
+ "win32"
9
+ ],
10
+ "cpu": [
11
+ "x64"
12
+ ],
13
+ "main": "./index.js",
14
+ "types": "./index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./index.d.ts",
18
+ "default": "./index.js"
19
+ },
20
+ "./gtk/girepository-1.0": "./gtk/girepository-1.0",
21
+ "./gtk/bin": "./gtk/bin"
22
+ },
23
+ "files": [
24
+ "index.js",
25
+ "index.d.ts",
26
+ "scripts",
27
+ "gtk"
28
+ ],
29
+ "gjsify": {
30
+ "runtimes": {
31
+ "gjs": "none",
32
+ "node": "polyfill",
33
+ "browser": "none",
34
+ "nativescript": "none"
35
+ },
36
+ "tier": 3
37
+ },
38
+ "scripts": {
39
+ "build:bundle": "node scripts/build-gtk-runtime.mjs"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/gjsify/gjsify.git",
44
+ "directory": "packages/node-gi/gtk-runtime-win32-x64"
45
+ }
46
+ }
@@ -0,0 +1,446 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Collect a standalone, batteries-included GTK/GObject-Introspection runtime
3
+ // bundle for Windows x64 — so @gjsify/node-gi loads gi:// namespaces with NO
4
+ // gvsbuild/system GTK on the host (the Windows sibling of the darwin-arm64 bundle).
5
+ //
6
+ // node scripts/build-gtk-runtime.mjs [--out <dir>] [--prefix <gvsbuild GTK prefix>] [--windowing]
7
+ //
8
+ // Two closures, one bundle dir:
9
+ // • DEFAULT (display-free) — the loadable DLLs + typelibs the DISPLAY-FREE
10
+ // conformance needs (GLib/GObject/Gio/cairo/Pango/Graphene/Gdk). Unchanged.
11
+ // • --windowing (SUPERSET) — everything above PLUS the runtime data a REAL GTK
12
+ // WINDOW needs on Windows: the gdk-pixbuf loaders + `loaders.cache`, compiled
13
+ // GSettings schemas (`gschemas.compiled`), the Adwaita/hicolor icon themes +
14
+ // `icon-theme.cache`, and the Fontconfig config/cache. The extra image-loader
15
+ // backing DLLs (librsvg/png/…) are walked into `bin/` too. node-gi's loader
16
+ // (gtk-runtime.js) detects the windowing data (the `gschemas.compiled` marker)
17
+ // and wires the env (GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE /
18
+ // XDG_DATA_DIRS / FONTCONFIG_*) so the SAME loader serves both — the display-free
19
+ // path is byte-unchanged when the windowing data is absent.
20
+ //
21
+ // Runs ONLY on win32/x64 with a build-time gvsbuild GTK4 stack extracted at
22
+ // --prefix (env GTK_PREFIX, the closure *source*, not shipped — the same tree the
23
+ // node-gi.yml windows jobs cache at C:\gtk-build\gtk\x64\release). gvsbuild bundles
24
+ // the tools this needs (gdk-pixbuf-query-loaders / glib-compile-schemas /
25
+ // gtk4-update-icon-cache / fc-cache) in <prefix>/bin.
26
+ //
27
+ // CRUCIAL DIFFERENCE FROM macOS: there is NO relocation step (no install_name_tool,
28
+ // no @rpath, no codesign). Windows resolves a DLL's imports by SEARCH PATH at
29
+ // LoadLibrary time, so a bundle is "portable" the moment its DLLs sit in one dir —
30
+ // node-gi just prepends <out>/bin to PATH before the addon loads. The runtime DATA
31
+ // (loaders/schemas/icons/fonts) is likewise resolved by env vars set at load time,
32
+ // no baked absolute paths (the caches are rewritten to bundle-relative).
33
+ //
34
+ // Reference: GJS ships no relocation; gvsbuild ships the MSVC-ABI GTK4 stack whose
35
+ // DLLs load into stock (MSVC-ABI) Node.
36
+ import { execFileSync } from 'node:child_process';
37
+ import {
38
+ copyFileSync,
39
+ cpSync,
40
+ existsSync,
41
+ mkdirSync,
42
+ readdirSync,
43
+ rmSync,
44
+ statSync,
45
+ writeFileSync,
46
+ } from 'node:fs';
47
+ import { basename, dirname, join } from 'node:path';
48
+ import { fileURLToPath } from 'node:url';
49
+
50
+ const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url)));
51
+
52
+ // --- args ------------------------------------------------------------------
53
+ function argValue(flag) {
54
+ const i = process.argv.indexOf(flag);
55
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : undefined;
56
+ }
57
+ const OUT = argValue('--out') ?? join(pkgRoot, 'gtk');
58
+ const PREFIX = argValue('--prefix') ?? process.env.GTK_PREFIX;
59
+ // The full-windowing SUPERSET: also collect the runtime data a real GTK window needs.
60
+ const WINDOWING = process.argv.includes('--windowing');
61
+
62
+ if (process.platform !== 'win32' || process.arch !== 'x64') {
63
+ console.error(`build-gtk-runtime: only supported on win32/x64, not ${process.platform}/${process.arch}`);
64
+ process.exit(2);
65
+ }
66
+ if (!PREFIX) {
67
+ console.error('build-gtk-runtime: no GTK prefix — pass --prefix or set GTK_PREFIX (e.g. C:\\gtk-build\\gtk\\x64\\release)');
68
+ process.exit(1);
69
+ }
70
+
71
+ const gtkBin = join(PREFIX, 'bin');
72
+ const gtkTypelibs = join(PREFIX, 'lib', 'girepository-1.0');
73
+ if (!existsSync(gtkBin)) {
74
+ console.error(`build-gtk-runtime: ${gtkBin} not found — extract the gvsbuild GTK4 stack first`);
75
+ process.exit(1);
76
+ }
77
+
78
+ const sh = (bin, args, opts) => execFileSync(bin, args, { encoding: 'utf8', ...opts });
79
+
80
+ // The seed DLLs of the DISPLAY-FREE conformance closure. Liberal, version-agnostic
81
+ // patterns matched against <prefix>/bin (gvsbuild's suffixes drift: glib-2.0-0.dll,
82
+ // girepository-2.0-0.dll, cairo-2.dll, pango-1.0-0.dll, gdk_pixbuf-2.0-0.dll,
83
+ // gtk-4-1.dll, harfbuzz.dll, …). The recursive dumpbin walk pulls every transitive
84
+ // gvsbuild dep (harfbuzz, fribidi, fontconfig, freetype, pixman, png, intl, pcre2,
85
+ // ffi, epoxy, zlib, …). gtk-4 is seeded because it backs the Gdk typelib the
86
+ // conformance's struct-construct loads (Gdk/Graphene) and transitively pulls
87
+ // pango/graphene/gdk-pixbuf/cairo/harfbuzz.
88
+ const SEED_PATTERNS = [
89
+ /^glib-2\.0-.*\.dll$/i,
90
+ /^gobject-2\.0-.*\.dll$/i,
91
+ /^gio-2\.0-.*\.dll$/i,
92
+ /^gmodule-2\.0-.*\.dll$/i,
93
+ /^girepository-[12]\.0-.*\.dll$/i, // girepository-2.0 (merged API) on modern gvsbuild
94
+ /^cairo(-gobject|-script-interpreter)?-?\d*\.dll$/i,
95
+ /^pango(cairo|ft2|win32)?-1\.0-.*\.dll$/i,
96
+ /^graphene-1\.0-.*\.dll$/i,
97
+ /^gdk[-_]pixbuf-2\.0-.*\.dll$/i,
98
+ /^gtk-4-.*\.dll$/i, // provides the Gdk typelib's backing library
99
+ /^harfbuzz.*\.dll$/i,
100
+ ];
101
+
102
+ // Extra seeds for the WINDOWING superset: libadwaita (the Adw-1 typelib's backing
103
+ // DLL — a real Adw.Application/ApplicationWindow needs it; the display-free
104
+ // conformance never touches Adwaita, so it is NOT in the base seeds), the GL backing
105
+ // a GSK GL renderer picks (ANGLE libEGL/libGLESv2 + epoxy — the cairo renderer needs
106
+ // none, but a real window may negotiate GL), and librsvg (the SVG gdk-pixbuf loader
107
+ // that renders the Adwaita symbolic icons). Most GL/rsvg deps are pulled transitively
108
+ // by the loaders below; naming them keeps the closure complete even if a loader is
109
+ // missing. (Gdk/Gsk/Gtk are all in gtk-4-*.dll, already a base seed.)
110
+ const WINDOWING_SEED_PATTERNS = [
111
+ /^(lib)?adwaita-1.*\.dll$/i, // gvsbuild leaf: adwaita-1-0.dll (backs the Adw-1 typelib)
112
+ /^libEGL.*\.dll$/i,
113
+ /^libGLESv2.*\.dll$/i,
114
+ /^epoxy.*\.dll$/i,
115
+ /^rsvg.*\.dll$/i,
116
+ /^libxml2.*\.dll$/i,
117
+ ];
118
+
119
+ // Locate an MSVC/gvsbuild tool: env override, then the gvsbuild <prefix>/bin
120
+ // (gvsbuild ships gdk-pixbuf-query-loaders / glib-compile-schemas /
121
+ // gtk4-update-icon-cache / fc-cache there), then PATH. Null when absent.
122
+ function findTool(leaf, envVar) {
123
+ const envd = envVar && process.env[envVar];
124
+ if (envd && existsSync(envd)) return envd;
125
+ const inBin = join(gtkBin, leaf);
126
+ if (existsSync(inBin)) return inBin;
127
+ try {
128
+ const p = sh('where', [leaf.replace(/\.exe$/i, '')]).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
129
+ if (p && existsSync(p)) return p;
130
+ } catch {
131
+ // not on PATH
132
+ }
133
+ return null;
134
+ }
135
+
136
+ // Locate dumpbin (MSVC): env override, then PATH (a dev-cmd/vcvars job), then
137
+ // vswhere's -find glob into the VC toolchain. Null when unavailable → the caller
138
+ // falls back to copying the whole bin/ DLL set.
139
+ function findDumpbin() {
140
+ const envd = process.env.DUMPBIN;
141
+ if (envd && existsSync(envd)) return envd;
142
+ try {
143
+ const p = sh('where', ['dumpbin']).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
144
+ if (p && existsSync(p)) return p;
145
+ } catch {
146
+ // not on PATH
147
+ }
148
+ const vswhere = join(
149
+ process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)',
150
+ 'Microsoft Visual Studio',
151
+ 'Installer',
152
+ 'vswhere.exe',
153
+ );
154
+ if (existsSync(vswhere)) {
155
+ try {
156
+ const out = sh(vswhere, ['-latest', '-products', '*', '-find', '**\\Hostx64\\x64\\dumpbin.exe']);
157
+ const p = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
158
+ if (p && existsSync(p)) return p;
159
+ } catch {
160
+ // vswhere present but no match
161
+ }
162
+ }
163
+ return null;
164
+ }
165
+
166
+ // Parse `dumpbin /dependents <dll>` into the list of imported DLL leaf names (both
167
+ // normal + delay-load sections; stop at the Summary). Exit-tolerant: dumpbin can
168
+ // print the section then exit non-zero on some inputs — capture the attached stdout.
169
+ function dumpbinDeps(dumpbin, dllPath) {
170
+ let out;
171
+ try {
172
+ out = sh(dumpbin, ['/nologo', '/dependents', dllPath]);
173
+ } catch (error) {
174
+ out = typeof error?.stdout === 'string' ? error.stdout : '';
175
+ }
176
+ const deps = [];
177
+ let inDeps = false;
178
+ for (const raw of out.split(/\r?\n/)) {
179
+ const line = raw.replace(/\s+$/, '');
180
+ if (/following (delay load )?dependencies:/i.test(line)) {
181
+ inDeps = true;
182
+ continue;
183
+ }
184
+ if (/^\s*Summary/i.test(line)) break;
185
+ if (inDeps) {
186
+ const m = line.match(/^\s+([A-Za-z0-9_.+-]+\.dll)\s*$/i);
187
+ if (m) deps.push(m[1]);
188
+ }
189
+ }
190
+ return deps;
191
+ }
192
+
193
+ // --- 1. discover the closure ----------------------------------------------
194
+ console.log(`build-gtk-runtime: gvsbuild prefix ${PREFIX}${WINDOWING ? ' (windowing superset)' : ' (display-free)'}`);
195
+ // leaf(lower) -> actual on-disk filename in <prefix>/bin
196
+ const binFiles = new Map();
197
+ for (const f of readdirSync(gtkBin)) {
198
+ if (f.toLowerCase().endsWith('.dll')) binFiles.set(f.toLowerCase(), f);
199
+ }
200
+ if (binFiles.size === 0) {
201
+ console.error(`build-gtk-runtime: no DLLs under ${gtkBin} — is the GTK stack extracted?`);
202
+ process.exit(1);
203
+ }
204
+
205
+ const seedPatterns = WINDOWING ? [...SEED_PATTERNS, ...WINDOWING_SEED_PATTERNS] : SEED_PATTERNS;
206
+ const seeds = [...binFiles.values()].filter((f) => seedPatterns.some((re) => re.test(f)));
207
+ if (seeds.length === 0) {
208
+ console.error('build-gtk-runtime: no seed DLLs matched — check the gvsbuild layout / SEED_PATTERNS');
209
+ process.exit(1);
210
+ }
211
+
212
+ // Windowing: the gdk-pixbuf loaders (SVG/PNG/… image decoders GTK dlopen's for
213
+ // icons) live OUTSIDE bin/ — their backing libs (librsvg, …) must still land in
214
+ // bin/, so seed the closure walk with each loader DLL so its dumpbin walk pulls them.
215
+ const gdkPixbufLoaderDir = join(PREFIX, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders');
216
+ const loaderSeeds = [];
217
+ if (WINDOWING && existsSync(gdkPixbufLoaderDir)) {
218
+ for (const f of readdirSync(gdkPixbufLoaderDir)) {
219
+ if (f.toLowerCase().endsWith('.dll')) loaderSeeds.push(join(gdkPixbufLoaderDir, f));
220
+ }
221
+ }
222
+ console.log(`build-gtk-runtime: ${seeds.length} seed DLLs${loaderSeeds.length ? ` + ${loaderSeeds.length} pixbuf loaders` : ''}`);
223
+
224
+ const dumpbin = findDumpbin();
225
+ // leaf(lower) -> actual filename (source in <prefix>/bin); '\0loader:'-prefixed keys
226
+ // are walked-but-not-bin-copied loader sentinels.
227
+ const bundled = new Map();
228
+ if (dumpbin) {
229
+ console.log(`build-gtk-runtime: walking the DLL closure with ${dumpbin}`);
230
+ // Seed with bin/ seeds (leaf names) + external loader DLLs (full paths) so THEIR
231
+ // deps are walked into bin/ even though the loaders live outside bin/.
232
+ const queue = [...seeds, ...loaderSeeds];
233
+ while (queue.length) {
234
+ const entry = queue.shift();
235
+ const isPath = entry.includes('\\') || entry.includes('/');
236
+ const leaf = isPath ? basename(entry) : entry;
237
+ const lower = leaf.toLowerCase();
238
+ const src = isPath ? entry : binFiles.get(lower);
239
+ if (!src) continue; // system / non-gvsbuild dep — leave as OS-provided
240
+ if (!isPath) {
241
+ if (bundled.has(lower)) continue;
242
+ bundled.set(lower, src);
243
+ } else if (bundled.has('\0loader:' + lower)) {
244
+ continue;
245
+ } else {
246
+ bundled.set('\0loader:' + lower, null); // walked, not bin-copied (copied below)
247
+ }
248
+ // dumpbin needs the ABSOLUTE path: a bin/ entry's `src` is a bare filename
249
+ // (leaf), so resolve it under <prefix>/bin; a loader entry's `src` is already
250
+ // a full path. Passing the bare leaf made dumpbin find nothing → the closure
251
+ // never grew past the seeds → the transitive DLLs (ffi/intl/pcre2/zlib/…)
252
+ // were dropped and the addon failed to dlopen.
253
+ const walkPath = isPath ? src : join(gtkBin, src);
254
+ for (const dep of dumpbinDeps(dumpbin, walkPath)) {
255
+ const dl = dep.toLowerCase();
256
+ if (!bundled.has(dl) && binFiles.has(dl)) queue.push(dep);
257
+ }
258
+ }
259
+ } else {
260
+ console.warn(
261
+ 'build-gtk-runtime: WARNING — dumpbin not found (no MSVC / vswhere); falling back to copying the FULL bin/*.dll set (a complete but larger superset).',
262
+ );
263
+ for (const [lower, src] of binFiles) bundled.set(lower, src);
264
+ }
265
+ // Real bin/ DLLs only (drop the walked-loader sentinels).
266
+ const binDlls = new Map([...bundled].filter(([k, v]) => v !== null && !k.startsWith('\0loader:')));
267
+ console.log(`build-gtk-runtime: closure = ${binDlls.size} DLLs`);
268
+
269
+ // --- 2. copy DLLs (no relocation — Windows resolves by search path) -------
270
+ const binOut = join(OUT, 'bin');
271
+ rmSync(OUT, { recursive: true, force: true });
272
+ mkdirSync(binOut, { recursive: true });
273
+ for (const src of binDlls.values()) {
274
+ copyFileSync(join(gtkBin, src), join(binOut, src));
275
+ }
276
+ console.log(`build-gtk-runtime: copied ${binDlls.size} DLLs -> ${binOut}`);
277
+
278
+ // --- 3. typelibs -----------------------------------------------------------
279
+ const typelibOut = join(OUT, 'girepository-1.0');
280
+ mkdirSync(typelibOut, { recursive: true });
281
+ let typelibCount = 0;
282
+ if (existsSync(gtkTypelibs)) {
283
+ for (const f of readdirSync(gtkTypelibs)) {
284
+ if (f.endsWith('.typelib')) {
285
+ copyFileSync(join(gtkTypelibs, f), join(typelibOut, f));
286
+ typelibCount++;
287
+ }
288
+ }
289
+ }
290
+ console.log(`build-gtk-runtime: copied ${typelibCount} typelibs`);
291
+
292
+ // --- 4. WINDOWING data (loaders / schemas / icons / fonts) ----------------
293
+ // Each step is independent + defensive: a missing tree/tool WARNS + continues so a
294
+ // partial gvsbuild layout still produces the DLL+typelib bundle. The DATA marker
295
+ // node-gi keys on is gschemas.compiled; if that step fails the loader treats the
296
+ // bundle as display-free (no windowing env wired).
297
+ const windowing = { pixbufLoaders: 0, schemas: false, iconThemes: [], fontconfig: false };
298
+ if (WINDOWING) {
299
+ // 4a. gdk-pixbuf loaders + a bundle-relative loaders.cache. The cache maps each
300
+ // decoder DLL to its mime/extensions; gdk-pixbuf resolves the DLL paths in it
301
+ // relative to GDK_PIXBUF_MODULEDIR when set (node-gi sets it), so we rewrite the
302
+ // absolute build paths the query tool emits down to leaf names.
303
+ if (existsSync(gdkPixbufLoaderDir)) {
304
+ const loadersOut = join(OUT, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders');
305
+ mkdirSync(loadersOut, { recursive: true });
306
+ for (const f of readdirSync(gdkPixbufLoaderDir)) {
307
+ if (f.toLowerCase().endsWith('.dll')) {
308
+ copyFileSync(join(gdkPixbufLoaderDir, f), join(loadersOut, f));
309
+ windowing.pixbufLoaders++;
310
+ }
311
+ }
312
+ const queryTool = findTool('gdk-pixbuf-query-loaders.exe', 'GDK_PIXBUF_QUERY_LOADERS');
313
+ const cacheOut = join(OUT, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders.cache');
314
+ if (queryTool && windowing.pixbufLoaders > 0) {
315
+ const loaderPaths = readdirSync(loadersOut)
316
+ .filter((f) => f.toLowerCase().endsWith('.dll'))
317
+ .map((f) => join(loadersOut, f));
318
+ let cache = '';
319
+ try {
320
+ cache = sh(queryTool, loaderPaths);
321
+ } catch (error) {
322
+ cache = typeof error?.stdout === 'string' ? error.stdout : '';
323
+ }
324
+ // A loader header line is a quoted absolute path; strip it to the leaf so
325
+ // GDK_PIXBUF_MODULEDIR resolves it in the consumer's bundle.
326
+ const rel = cache.replace(/^"(.*[\\/])?([^"\\/]+\.dll)"\s*$/gim, '"$2"');
327
+ writeFileSync(cacheOut, rel);
328
+ console.log(`build-gtk-runtime: wrote loaders.cache (${windowing.pixbufLoaders} loaders, bundle-relative)`);
329
+ } else {
330
+ console.warn('build-gtk-runtime: WARNING — gdk-pixbuf-query-loaders not found; loaders.cache NOT generated (SVG/PNG icons may not load)');
331
+ }
332
+ } else {
333
+ console.warn(`build-gtk-runtime: WARNING — ${gdkPixbufLoaderDir} missing; no gdk-pixbuf loaders bundled`);
334
+ }
335
+
336
+ // 4b. Compiled GSettings schemas (GTK/Adwaita read org.gnome.desktop.interface
337
+ // etc. at startup). Copy every schema found (GTK's own + gsettings-desktop-
338
+ // schemas if gvsbuild shipped it) and (re)compile gschemas.compiled.
339
+ const schemasSrc = join(PREFIX, 'share', 'glib-2.0', 'schemas');
340
+ if (existsSync(schemasSrc)) {
341
+ const schemasOut = join(OUT, 'share', 'glib-2.0', 'schemas');
342
+ mkdirSync(schemasOut, { recursive: true });
343
+ for (const f of readdirSync(schemasSrc)) {
344
+ if (f.endsWith('.xml') || f.endsWith('.gschema.override') || f === 'gschemas.compiled') {
345
+ copyFileSync(join(schemasSrc, f), join(schemasOut, f));
346
+ }
347
+ }
348
+ const compileTool = findTool('glib-compile-schemas.exe', 'GLIB_COMPILE_SCHEMAS');
349
+ if (compileTool) {
350
+ try {
351
+ sh(compileTool, [schemasOut]);
352
+ } catch (error) {
353
+ console.warn(`build-gtk-runtime: WARNING — glib-compile-schemas failed: ${error?.message ?? error}`);
354
+ }
355
+ }
356
+ windowing.schemas = existsSync(join(schemasOut, 'gschemas.compiled'));
357
+ console.log(`build-gtk-runtime: GSettings schemas ${windowing.schemas ? 'compiled' : 'copied (no gschemas.compiled!)'}`);
358
+ } else {
359
+ console.warn(`build-gtk-runtime: WARNING — ${schemasSrc} missing; GSettings schemas NOT bundled (GTK settings reads will fail)`);
360
+ }
361
+
362
+ // 4c. Icon themes (Adwaita for symbolic icons + hicolor fallback) + caches. The
363
+ // theme is loaded from XDG_DATA_DIRS/icons/<theme>/ (node-gi prepends <bundle>/share).
364
+ const updateIconCache = findTool('gtk4-update-icon-cache.exe', 'GTK4_UPDATE_ICON_CACHE')
365
+ ?? findTool('gtk-update-icon-cache.exe', 'GTK_UPDATE_ICON_CACHE');
366
+ for (const theme of ['Adwaita', 'hicolor']) {
367
+ const themeSrc = join(PREFIX, 'share', 'icons', theme);
368
+ if (!existsSync(themeSrc)) continue;
369
+ const themeOut = join(OUT, 'share', 'icons', theme);
370
+ cpSync(themeSrc, themeOut, { recursive: true });
371
+ if (updateIconCache) {
372
+ try {
373
+ sh(updateIconCache, ['-q', '-t', '-f', themeOut]);
374
+ } catch {
375
+ // an existing icon-theme.cache from the copy is a usable fallback
376
+ }
377
+ }
378
+ windowing.iconThemes.push(theme);
379
+ }
380
+ console.log(
381
+ windowing.iconThemes.length
382
+ ? `build-gtk-runtime: icon themes ${windowing.iconThemes.join(', ')}${updateIconCache ? ' (cache refreshed)' : ' (no update-icon-cache tool — copied caches)'}`
383
+ : 'build-gtk-runtime: WARNING — no Adwaita/hicolor icon theme found under share/icons (symbolic icons may be blank)',
384
+ );
385
+
386
+ // 4d. Fontconfig config + cache. GTK4-on-Windows text usually goes through the
387
+ // DirectWrite/pangowin32 backend (no fontconfig), but gvsbuild's pango can be
388
+ // fontconfig-backed; ship etc/fonts + a cache when present so either path works.
389
+ const fontsSrc = join(PREFIX, 'etc', 'fonts');
390
+ if (existsSync(fontsSrc)) {
391
+ const fontsOut = join(OUT, 'etc', 'fonts');
392
+ cpSync(fontsSrc, fontsOut, { recursive: true });
393
+ const fcCache = findTool('fc-cache.exe', 'FC_CACHE');
394
+ if (fcCache) {
395
+ try {
396
+ sh(fcCache, ['-f', fontsOut], { env: { ...process.env, FONTCONFIG_PATH: fontsOut } });
397
+ } catch {
398
+ // best-effort — Pango's win32 backend does not need the cache
399
+ }
400
+ }
401
+ windowing.fontconfig = true;
402
+ console.log('build-gtk-runtime: fontconfig config bundled');
403
+ } else {
404
+ console.log('build-gtk-runtime: no etc/fonts (pango uses the win32/DirectWrite backend) — skipping fontconfig');
405
+ }
406
+ }
407
+
408
+ // --- manifest + size -------------------------------------------------------
409
+ function dirSize(dir) {
410
+ if (!existsSync(dir)) return 0;
411
+ let total = 0;
412
+ for (const f of readdirSync(dir, { withFileTypes: true })) {
413
+ const p = join(dir, f.name);
414
+ total += f.isDirectory() ? dirSize(p) : statSync(p).size;
415
+ }
416
+ return total;
417
+ }
418
+ const binBytes = dirSize(binOut);
419
+ const typelibBytes = dirSize(typelibOut);
420
+ const dataBytes = WINDOWING ? dirSize(join(OUT, 'lib')) + dirSize(join(OUT, 'share')) + dirSize(join(OUT, 'etc')) : 0;
421
+ const manifest = {
422
+ platform: 'win32-x64',
423
+ windowing: WINDOWING,
424
+ generatedFrom: PREFIX,
425
+ walkedWith: dumpbin ? 'dumpbin' : 'copy-all-fallback',
426
+ dlls: binDlls.size,
427
+ typelibs: typelibCount,
428
+ binBytes,
429
+ typelibBytes,
430
+ dataBytes,
431
+ totalBytes: binBytes + typelibBytes + dataBytes,
432
+ dllList: [...binDlls.values()].map((f) => basename(f)).sort((a, b) => a.localeCompare(b)),
433
+ ...(WINDOWING ? { windowingData: windowing } : {}),
434
+ };
435
+ writeFileSync(join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2));
436
+
437
+ const mb = (n) => (n / 1024 / 1024).toFixed(1);
438
+ console.log(
439
+ `build-gtk-runtime: DONE -> ${OUT}\n` +
440
+ ` DLLs: ${binDlls.size} (${mb(binBytes)} MiB)\n` +
441
+ ` typelibs: ${typelibCount} (${mb(typelibBytes)} MiB)\n` +
442
+ (WINDOWING
443
+ ? ` data: loaders=${windowing.pixbufLoaders} schemas=${windowing.schemas} icons=[${windowing.iconThemes.join(',')}] fontconfig=${windowing.fontconfig} (${mb(dataBytes)} MiB)\n`
444
+ : '') +
445
+ ` total: ${mb(manifest.totalBytes)} MiB`,
446
+ );