From bbda55911f76292522e17f4b9b8d3266f4af8c46 Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 16:23:33 -0400 Subject: [PATCH 1/8] ci: test whether Windows prebuilts work without compiling Builds the addon under MSYS2, bundles its full MinGW DLL closure + GI typelibs next to the .node, then on a separate clean Windows runner (no MSYS2, no compiler) installs deps with --ignore-scripts and runs a smoke test that loads GTK via GObject-Introspection. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test-windows-prebuilt.yaml | 130 +++++++++++++++++++ scripts/windows-bundle-runtime.sh | 63 +++++++++ scripts/windows-smoke-test.js | 67 ++++++++++ 3 files changed, 260 insertions(+) create mode 100644 .github/workflows/test-windows-prebuilt.yaml create mode 100755 scripts/windows-bundle-runtime.sh create mode 100644 scripts/windows-smoke-test.js diff --git a/.github/workflows/test-windows-prebuilt.yaml b/.github/workflows/test-windows-prebuilt.yaml new file mode 100644 index 0000000..6f6d281 --- /dev/null +++ b/.github/workflows/test-windows-prebuilt.yaml @@ -0,0 +1,130 @@ +name: test-windows-prebuilt +# +# Answers: "do prebuilt Windows binaries let users `npm install node-gtk` and use +# it WITHOUT compiling?" +# +# build-prebuilt - build the addon under MSYS2/MinGW (as today), then bundle +# its full DLL closure + GI typelibs next to the .node and +# upload it as an artifact (this is the candidate prebuilt). +# consume-clean - a SEPARATE, clean Windows runner with NO MSYS2 and NO +# compiler. Installs node-gtk's JS deps with --ignore-scripts +# (so nothing is ever built), drops in the prebuilt, and runs +# a smoke test that loads GTK and exercises GObject-Introspection. +# +on: + push: + branches: + - test-windows-prebuilt + workflow_dispatch: + +jobs: + build-prebuilt: + name: build prebuilt - nodejs ${{ matrix.node }} + runs-on: windows-2022 + strategy: + fail-fast: false + matrix: + node: + - 22 + - 20 + defaults: + run: + shell: msys2 {0} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + path-type: inherit + update: true + install: | + git + make + mingw-w64-x86_64-gcc + mingw-w64-x86_64-pkgconf + mingw-w64-x86_64-python + mingw-w64-x86_64-ntldd-git + mingw-w64-x86_64-gobject-introspection + mingw-w64-x86_64-gtk3 + mingw-w64-x86_64-cairo + mingw-w64-x86_64-gstreamer + mingw-w64-x86_64-gst-plugins-good + mingw-w64-x86_64-gst-plugins-bad + mingw-w64-x86_64-libsoup3 + + - name: Build from source + env: + GYP_GENERATORS: make + CC: gcc + CXX: g++ + run: | + ./windows/mingw_include_extra.sh + export MINGW_WINDOWS_PATH=$(./windows/mingw_windows_path.sh) + export PATH="/mingw64/bin:/usr/bin:$PATH" + npm install --build-from-source + + - name: Bundle GTK runtime (DLLs + typelibs) + run: | + ABI=$(node -p "process.versions.modules") + BINDING="lib/binding/node-v${ABI}-win32-x64" + echo "ABI=$ABI BINDING=$BINDING" + ls -la "$BINDING" + ./scripts/windows-bundle-runtime.sh "$BINDING" + + - name: Upload prebuilt artifact + uses: actions/upload-artifact@v4 + with: + name: prebuilt-node-${{ matrix.node }} + path: lib/binding/ + if-no-files-found: error + + consume-clean: + name: consume prebuilt (clean machine) - nodejs ${{ matrix.node }} + needs: build-prebuilt + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + node: + - 22 + - 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + # Install ONLY the JS dependencies, never the native build hook. + # This is what a user gets when a prebuilt is available: no compiler runs. + - name: Install JS deps (no build) + shell: bash + run: npm install --ignore-scripts + + - name: Download prebuilt + uses: actions/download-artifact@v4 + with: + name: prebuilt-node-${{ matrix.node }} + path: lib/binding/ + + - name: Show what we got + shell: bash + run: | + echo "Tools available on this runner (expect NONE of these):" + (which gcc && echo "gcc FOUND") || echo "no gcc (good)" + (where.exe pacman && echo "pacman FOUND") || echo "no pacman (good)" + echo "---- bundled prebuilt ----" + ls -la lib/binding/*/ || true + + # Negative control: without the bundled DLLs on PATH the addon must fail to + # load. Documents WHY bundling is required. Allowed to fail. + - name: Negative control (no bundled runtime — expected to fail) + continue-on-error: true + shell: bash + run: node -e "require('.')" && echo "UNEXPECTED: loaded without runtime" || echo "expected failure: addon needs the bundled GTK runtime" + + - name: Smoke test (bundled runtime — expected to pass) + shell: bash + run: node scripts/windows-smoke-test.js diff --git a/scripts/windows-bundle-runtime.sh b/scripts/windows-bundle-runtime.sh new file mode 100755 index 0000000..1024c6b --- /dev/null +++ b/scripts/windows-bundle-runtime.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# +# windows-bundle-runtime.sh +# +# Make a freshly-built Windows prebuilt self-contained so it can be used WITHOUT +# MSYS2/MinGW or any compiler on the target machine. We copy the addon's entire +# transitive MinGW DLL closure next to the .node, plus the GObject-Introspection +# typelibs it needs at runtime. +# +# Run inside the MINGW64 shell, after building. +# +# ./scripts/windows-bundle-runtime.sh lib/binding/node-v127-win32-x64 +# +set -euo pipefail + +BINDING_DIR="${1:?usage: windows-bundle-runtime.sh }" +NODE_FILE="$BINDING_DIR/node_gtk.node" + +if [ ! -f "$NODE_FILE" ]; then + echo "error: $NODE_FILE not found" + ls -la "$BINDING_DIR" || true + exit 1 +fi + +echo "## Computing recursive DLL closure for $NODE_FILE" +# ntldd -R prints every transitive dependency with its resolved Windows path: +# libgtk-3-0.dll => C:\msys64\mingw64\bin\libgtk-3-0.dll (0x...) +# We keep only the MinGW-provided DLLs (skip C:\Windows\System32 OS DLLs). +copied=0 +ntldd -R "$NODE_FILE" \ + | sed -n 's/.* => \(.*\) (0x.*/\1/p' \ + | while IFS= read -r winpath; do + [ -z "$winpath" ] && continue + u=$(cygpath -u "$winpath" 2>/dev/null || echo "$winpath") + case "$u" in + *mingw64*) + if [ -f "$u" ]; then + cp -f "$u" "$BINDING_DIR/" + echo " + $(basename "$u")" + copied=$((copied + 1)) + fi + ;; + esac + done +echo "## DLLs bundled into $BINDING_DIR" + +echo "## Bundling GObject-Introspection typelibs" +TYPELIB_SRC=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null || true) +if [ -z "$TYPELIB_SRC" ] || [ ! -d "$TYPELIB_SRC" ]; then + TYPELIB_SRC=/mingw64/lib/girepository-1.0 +fi +TYPELIB_DST="$BINDING_DIR/girepository-1.0" +mkdir -p "$TYPELIB_DST" +# Copy the full typelib set; it is small and guarantees every transitive +# namespace dependency (Gdk, Pango, cairo, GdkPixbuf, Atk, HarfBuzz, ...) is present. +cp -f "$TYPELIB_SRC"/*.typelib "$TYPELIB_DST/" +echo "## Typelibs bundled from $TYPELIB_SRC -> $TYPELIB_DST" + +echo +echo "## Bundle contents:" +ls -la "$BINDING_DIR" +echo "## du:" +du -sh "$BINDING_DIR" diff --git a/scripts/windows-smoke-test.js b/scripts/windows-smoke-test.js new file mode 100644 index 0000000..7030143 --- /dev/null +++ b/scripts/windows-smoke-test.js @@ -0,0 +1,67 @@ +/* + * windows-smoke-test.js + * + * Verifies that a Windows prebuilt + bundled GTK runtime (DLLs + typelibs) can + * be loaded and used on a clean machine that has NO MSYS2/MinGW and NO compiler + * — i.e. exactly what a user gets from `npm install node-gtk` if we ship a + * self-contained Windows prebuilt. + * + * It deliberately does NOT depend on anything from MSYS2; the only GTK bits it + * touches are the ones bundled next to the .node by windows-bundle-runtime.sh. + */ + +const path = require('path') +const fs = require('fs') + +const abi = process.versions.modules +const bindingDir = path.join(__dirname, '..', 'lib', 'binding', `node-v${abi}-win32-x64`) +const typelibDir = path.join(bindingDir, 'girepository-1.0') + +console.log('node:', process.version, '| abi:', abi) +console.log('binding dir:', bindingDir, '| exists:', fs.existsSync(bindingDir)) +console.log('.node:', fs.existsSync(path.join(bindingDir, 'node_gtk.node'))) +console.log('typelibs:', fs.existsSync(typelibDir)) +if (fs.existsSync(bindingDir)) { + const dlls = fs.readdirSync(bindingDir).filter(f => f.endsWith('.dll')) + console.log(`bundled DLLs: ${dlls.length}`) +} + +// 1) Make the bundled GTK DLLs discoverable. This covers BOTH: +// - the addon's own static imports (resolved when node loads the .node) +// - GObject-Introspection's g_module_open() of each namespace's shared lib +// Prepending to PATH before the addon is required is enough for both. +process.env.PATH = bindingDir + path.delimiter + (process.env.PATH || '') +// 2) Point GI at the bundled typelibs. +process.env.GI_TYPELIB_PATH = + typelibDir + (process.env.GI_TYPELIB_PATH ? path.delimiter + process.env.GI_TYPELIB_PATH : '') + +// Require the local package (its lib/native.js resolves the prebuilt via +// node-pre-gyp's binary.find, i.e. the same path users hit after install). +const gi = require(path.join(__dirname, '..')) +console.log('OK: require(node-gtk) — prebuilt + bundled DLLs loaded') + +// Belt and suspenders: also register the typelib dir through GI's own API. +try { gi.prependSearchPath(typelibDir) } catch (e) { /* ignore */ } + +function load(ns, version) { + const mod = gi.require(ns, version) + console.log(`OK: gi.require('${ns}', '${version}')`) + return mod +} + +const GLib = load('GLib', '2.0') +if (typeof GLib.getMonotonicTime !== 'function') + throw new Error('GLib.getMonotonicTime missing — typelib not really loaded') +load('GObject', '2.0') +load('Gio', '2.0') +const Gtk = load('Gtk', '3.0') +if (typeof Gtk.Window !== 'function') + throw new Error('Gtk.Window missing — Gtk typelib not really loaded') + +// Gtk.init may fail without a display; that is not a binary-usability problem, +// so we report it but do not fail the smoke test on it. +let initOk = false +try { Gtk.init(); initOk = true } catch (e) { console.log('note: Gtk.init() threw:', e.message) } +console.log('Gtk.init():', initOk ? 'ok' : 'skipped/failed (no display)') + +console.log('\n=== SMOKE TEST PASSED: prebuilt is usable with NO compiler/MSYS2 ===') From 7e872d5637f465a883a2fc1f21bdc1ecd9533ebd Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 16:25:03 -0400 Subject: [PATCH 2/8] ci: fix YAML in negative-control step Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test-windows-prebuilt.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-windows-prebuilt.yaml b/.github/workflows/test-windows-prebuilt.yaml index 6f6d281..da068d8 100644 --- a/.github/workflows/test-windows-prebuilt.yaml +++ b/.github/workflows/test-windows-prebuilt.yaml @@ -123,7 +123,12 @@ jobs: - name: Negative control (no bundled runtime — expected to fail) continue-on-error: true shell: bash - run: node -e "require('.')" && echo "UNEXPECTED: loaded without runtime" || echo "expected failure: addon needs the bundled GTK runtime" + run: | + if node -e "require('.')"; then + echo "UNEXPECTED - loaded without the bundled GTK runtime" + else + echo "expected failure - addon needs the bundled GTK runtime/typelibs" + fi - name: Smoke test (bundled runtime — expected to pass) shell: bash From 144834f1ffa8276e1ac59a2423935ccd565b088a Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 16:32:03 -0400 Subject: [PATCH 3/8] ci: bundle GTK runtime libs (gio/gtk/gdk/...) and isolate DLL search ntldd on the addon alone misses the namespace libraries that GI loads at runtime via g_module_open (libgio, libgtk, libgdk, libpango, ...); seed the closure from those too. Also isolate the smoke test's PATH so the runner's own C:\mingw64 cannot contaminate the result. Co-Authored-By: Claude Opus 4.8 --- scripts/windows-bundle-runtime.sh | 62 ++++++++++++++++++++++--------- scripts/windows-smoke-test.js | 13 ++++++- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/scripts/windows-bundle-runtime.sh b/scripts/windows-bundle-runtime.sh index 1024c6b..037d065 100755 --- a/scripts/windows-bundle-runtime.sh +++ b/scripts/windows-bundle-runtime.sh @@ -22,27 +22,53 @@ if [ ! -f "$NODE_FILE" ]; then exit 1 fi -echo "## Computing recursive DLL closure for $NODE_FILE" +# GObject-Introspection loads each namespace's shared library at runtime via +# g_module_open() when you call gi.require('Gtk', ...). Those libraries +# (libgio, libgtk, libgdk, libpango, libatk, libgdk_pixbuf, ...) are NOT linked +# by node_gtk.node, so ntldd on the addon alone misses them. We therefore seed +# the closure from the addon AND from the GTK runtime libraries themselves. +MB=/mingw64/bin +ENTRY_LIBS=( + "$NODE_FILE" + "$MB/libgirepository-1.0-1.dll" + "$MB/libgio-2.0-0.dll" + "$MB/libgtk-3-0.dll" + "$MB/libgdk-3-0.dll" + "$MB/libgdk_pixbuf-2.0-0.dll" + "$MB/libpango-1.0-0.dll" + "$MB/libpangocairo-1.0-0.dll" + "$MB/libatk-1.0-0.dll" + "$MB/libcairo-gobject-2.dll" +) + +echo "## Computing recursive DLL closure for the addon + GTK runtime" # ntldd -R prints every transitive dependency with its resolved Windows path: # libgtk-3-0.dll => C:\msys64\mingw64\bin\libgtk-3-0.dll (0x...) -# We keep only the MinGW-provided DLLs (skip C:\Windows\System32 OS DLLs). +# Collect the union of every entry's closure, keep only MinGW-provided DLLs +# (skip C:\Windows\System32 OS DLLs), and copy them next to the .node. +: > /tmp/dll-closure.txt +for lib in "${ENTRY_LIBS[@]}"; do + [ -f "$lib" ] || { echo " (skip missing entry $lib)"; continue; } + # the entry library itself (when it is one of the GTK runtime DLLs) + case "$lib" in *mingw64*) echo "$lib" >> /tmp/dll-closure.txt ;; esac + ntldd -R "$lib" \ + | sed -n 's/.* => \(.*\) (0x.*/\1/p' \ + | while IFS= read -r winpath; do + [ -z "$winpath" ] && continue + u=$(cygpath -u "$winpath" 2>/dev/null || echo "$winpath") + case "$u" in *mingw64*) echo "$u" >> /tmp/dll-closure.txt ;; esac + done +done + copied=0 -ntldd -R "$NODE_FILE" \ - | sed -n 's/.* => \(.*\) (0x.*/\1/p' \ - | while IFS= read -r winpath; do - [ -z "$winpath" ] && continue - u=$(cygpath -u "$winpath" 2>/dev/null || echo "$winpath") - case "$u" in - *mingw64*) - if [ -f "$u" ]; then - cp -f "$u" "$BINDING_DIR/" - echo " + $(basename "$u")" - copied=$((copied + 1)) - fi - ;; - esac - done -echo "## DLLs bundled into $BINDING_DIR" +sort -u /tmp/dll-closure.txt | while IFS= read -r u; do + if [ -f "$u" ]; then + cp -f "$u" "$BINDING_DIR/" + echo " + $(basename "$u")" + copied=$((copied + 1)) + fi +done +echo "## DLLs bundled into $BINDING_DIR ($(ls "$BINDING_DIR"/*.dll | wc -l) total)" echo "## Bundling GObject-Introspection typelibs" TYPELIB_SRC=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null || true) diff --git a/scripts/windows-smoke-test.js b/scripts/windows-smoke-test.js index 7030143..2b84d58 100644 --- a/scripts/windows-smoke-test.js +++ b/scripts/windows-smoke-test.js @@ -29,8 +29,17 @@ if (fs.existsSync(bindingDir)) { // 1) Make the bundled GTK DLLs discoverable. This covers BOTH: // - the addon's own static imports (resolved when node loads the .node) // - GObject-Introspection's g_module_open() of each namespace's shared lib -// Prepending to PATH before the addon is required is enough for both. -process.env.PATH = bindingDir + path.delimiter + (process.env.PATH || '') +// We REPLACE the PATH (rather than prepend) with the bundle dir + only the +// Windows system dirs. The GitHub windows-latest runner ships its own +// C:\mingw64; isolating the PATH proves the test uses ONLY the bundled +// runtime, not whatever GTK happens to be on the machine. +const sysRoot = process.env.SystemRoot || 'C:\\Windows' +process.env.PATH = [ + bindingDir, + path.dirname(process.execPath), // node.exe dir + path.join(sysRoot, 'System32'), + sysRoot, +].join(path.delimiter) // 2) Point GI at the bundled typelibs. process.env.GI_TYPELIB_PATH = typelibDir + (process.env.GI_TYPELIB_PATH ? path.delimiter + process.env.GI_TYPELIB_PATH : '') From decf94f65120a766796f1060769301ecd7fabdc2 Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 16:56:16 -0400 Subject: [PATCH 4/8] ci: target GTK4/Adwaita/GtkSource (quilx) and measure real-app bundle size Switch the prebuilt test to the quilx namespace set (Gtk4 + Adw + GtkSource5 + Pango + GdkPixbuf + Graphene; Vte has no Windows port). Bundle runtime data a real app needs (gdk-pixbuf loaders, GSettings schemas, Adwaita/hicolor icons, GtkSourceView data) and print a size breakdown plus the compressed node-pre-gyp tarball size. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/test-windows-prebuilt.yaml | 22 ++++-- scripts/windows-bundle-runtime.sh | 56 ++++++++++++--- scripts/windows-smoke-test.js | 72 +++++++++++++++----- 3 files changed, 118 insertions(+), 32 deletions(-) diff --git a/.github/workflows/test-windows-prebuilt.yaml b/.github/workflows/test-windows-prebuilt.yaml index da068d8..338faca 100644 --- a/.github/workflows/test-windows-prebuilt.yaml +++ b/.github/workflows/test-windows-prebuilt.yaml @@ -48,12 +48,12 @@ jobs: mingw-w64-x86_64-python mingw-w64-x86_64-ntldd-git mingw-w64-x86_64-gobject-introspection - mingw-w64-x86_64-gtk3 mingw-w64-x86_64-cairo - mingw-w64-x86_64-gstreamer - mingw-w64-x86_64-gst-plugins-good - mingw-w64-x86_64-gst-plugins-bad - mingw-w64-x86_64-libsoup3 + mingw-w64-x86_64-gtk4 + mingw-w64-x86_64-libadwaita + mingw-w64-x86_64-gtksourceview5 + mingw-w64-x86_64-graphene + mingw-w64-x86_64-adwaita-icon-theme - name: Build from source env: @@ -66,7 +66,7 @@ jobs: export PATH="/mingw64/bin:/usr/bin:$PATH" npm install --build-from-source - - name: Bundle GTK runtime (DLLs + typelibs) + - name: Bundle GTK4 runtime (DLLs + typelibs + data) run: | ABI=$(node -p "process.versions.modules") BINDING="lib/binding/node-v${ABI}-win32-x64" @@ -74,6 +74,16 @@ jobs: ls -la "$BINDING" ./scripts/windows-bundle-runtime.sh "$BINDING" + - name: Report compressed download size (what users fetch) + run: | + npx node-pre-gyp package >/dev/null 2>&1 || true + TARBALL=$(find build/stage -name '*.tar.gz' 2>/dev/null | head -1) + if [ -n "$TARBALL" ]; then + echo "## node-pre-gyp tarball (gzip, the S3 download): $(du -h "$TARBALL" | cut -f1) -> $TARBALL" + else + echo "## (node-pre-gyp package produced no tarball; uncompressed size is in the bundle step)" + fi + - name: Upload prebuilt artifact uses: actions/upload-artifact@v4 with: diff --git a/scripts/windows-bundle-runtime.sh b/scripts/windows-bundle-runtime.sh index 037d065..0271973 100755 --- a/scripts/windows-bundle-runtime.sh +++ b/scripts/windows-bundle-runtime.sh @@ -28,16 +28,21 @@ fi # by node_gtk.node, so ntldd on the addon alone misses them. We therefore seed # the closure from the addon AND from the GTK runtime libraries themselves. MB=/mingw64/bin +# Seed from the addon AND from every runtime library a real GTK4/Adwaita app +# pulls in (the quilx target: Gtk4 + Adw + GtkSourceView5 + Pango + GdkPixbuf + +# Graphene). Missing entries are skipped, so a few GTK3 names are kept too for +# robustness. ENTRY_LIBS=( "$NODE_FILE" "$MB/libgirepository-1.0-1.dll" "$MB/libgio-2.0-0.dll" - "$MB/libgtk-3-0.dll" - "$MB/libgdk-3-0.dll" - "$MB/libgdk_pixbuf-2.0-0.dll" + "$MB/libgtk-4-1.dll" + "$MB/libadwaita-1-0.dll" + "$MB/libgtksourceview-5-0.dll" "$MB/libpango-1.0-0.dll" "$MB/libpangocairo-1.0-0.dll" - "$MB/libatk-1.0-0.dll" + "$MB/libgdk_pixbuf-2.0-0.dll" + "$MB/libgraphene-1.0-0.dll" "$MB/libcairo-gobject-2.dll" ) @@ -78,12 +83,45 @@ fi TYPELIB_DST="$BINDING_DIR/girepository-1.0" mkdir -p "$TYPELIB_DST" # Copy the full typelib set; it is small and guarantees every transitive -# namespace dependency (Gdk, Pango, cairo, GdkPixbuf, Atk, HarfBuzz, ...) is present. +# namespace dependency (Gdk, Pango, cairo, GdkPixbuf, Graphene, ...) is present. cp -f "$TYPELIB_SRC"/*.typelib "$TYPELIB_DST/" echo "## Typelibs bundled from $TYPELIB_SRC -> $TYPELIB_DST" +# --------------------------------------------------------------------------- +# Runtime DATA a real GTK4/Adwaita app needs at run time (beyond DLLs/typelibs). +# These are copied so we can MEASURE the realistic bundle size and ship a +# self-contained app. cp is best-effort: a missing source is not fatal. +# --------------------------------------------------------------------------- +copy_tree() { # src dst + if [ -d "$1" ]; then mkdir -p "$2"; cp -rf "$1"/. "$2"/ 2>/dev/null || true; fi +} + +echo "## Bundling runtime data (loaders, schemas, icons, gtksourceview)" +# gdk-pixbuf image loaders (PNG/SVG/... — needed for icons/images) +copy_tree /mingw64/lib/gdk-pixbuf-2.0 "$BINDING_DIR/lib/gdk-pixbuf-2.0" +# compiled GSettings schemas (GTK4/Adw/GtkSourceView read settings) +copy_tree /mingw64/share/glib-2.0/schemas "$BINDING_DIR/share/glib-2.0/schemas" +# icon themes (Adwaita + hicolor fallback) — typically the biggest chunk +copy_tree /mingw64/share/icons/Adwaita "$BINDING_DIR/share/icons/Adwaita" +copy_tree /mingw64/share/icons/hicolor "$BINDING_DIR/share/icons/hicolor" +# GtkSourceView language definitions + style schemes +copy_tree /mingw64/share/gtksourceview-5 "$BINDING_DIR/share/gtksourceview-5" + echo -echo "## Bundle contents:" -ls -la "$BINDING_DIR" -echo "## du:" -du -sh "$BINDING_DIR" +echo "## ===== SIZE BREAKDOWN =====" +size() { # label dir-or-glob + local label="$1"; shift + local total + total=$(du -ch "$@" 2>/dev/null | tail -1 | cut -f1) + printf ' %-26s %s\n' "$label" "${total:-0}" +} +size "DLLs (load-critical)" "$BINDING_DIR"/*.dll +size "typelibs (load-critical)" "$TYPELIB_DST" +size " + .node" "$NODE_FILE" +size "gdk-pixbuf loaders" "$BINDING_DIR/lib/gdk-pixbuf-2.0" +size "glib schemas" "$BINDING_DIR/share/glib-2.0/schemas" +size "icons (Adwaita+hicolor)" "$BINDING_DIR/share/icons" +size "gtksourceview data" "$BINDING_DIR/share/gtksourceview-5" +echo " --------------------------------------" +size "TOTAL (uncompressed)" "$BINDING_DIR" +echo "## ==========================" diff --git a/scripts/windows-smoke-test.js b/scripts/windows-smoke-test.js index 2b84d58..0b5594d 100644 --- a/scripts/windows-smoke-test.js +++ b/scripts/windows-smoke-test.js @@ -52,25 +52,63 @@ console.log('OK: require(node-gtk) — prebuilt + bundled DLLs loaded') // Belt and suspenders: also register the typelib dir through GI's own API. try { gi.prependSearchPath(typelibDir) } catch (e) { /* ignore */ } -function load(ns, version) { - const mod = gi.require(ns, version) - console.log(`OK: gi.require('${ns}', '${version}')`) - return mod +// Point GTK4/Adwaita at the bundled runtime data so a real app could run. +const bundledShare = path.join(bindingDir, 'share') +if (fs.existsSync(bundledShare)) { + process.env.XDG_DATA_DIRS = bundledShare + (process.env.XDG_DATA_DIRS ? path.delimiter + process.env.XDG_DATA_DIRS : '') + const schemas = path.join(bundledShare, 'glib-2.0', 'schemas') + if (fs.existsSync(schemas)) process.env.GSETTINGS_SCHEMA_DIR = schemas } -const GLib = load('GLib', '2.0') -if (typeof GLib.getMonotonicTime !== 'function') +// The full quilx namespace set. Vte (3.91) has no Windows port, so it is +// expected to be unavailable; everything else must load. +const REQUIRED = [ + ['GLib', '2.0'], ['GObject', '2.0'], ['Gio', '2.0'], + ['Pango', '1.0'], ['PangoCairo', '1.0'], + ['Gdk', '4.0'], ['GdkPixbuf', '2.0'], ['Graphene', '1.0'], + ['Gtk', '4.0'], ['Adw', '1'], ['GtkSource', '5'], +] +const OPTIONAL = [['Vte', '3.91']] + +const loaded = {} +function load(ns, version, optional) { + try { + const mod = gi.require(ns, version) + console.log(`OK: gi.require('${ns}', '${version}')`) + loaded[ns] = mod + return mod + } catch (e) { + console.log(`${optional ? 'note' : 'FAIL'}: gi.require('${ns}', '${version}') — ${e.message}`) + if (!optional) throw e + return null + } +} + +for (const [ns, v] of REQUIRED) load(ns, v, false) +for (const [ns, v] of OPTIONAL) load(ns, v, true) + +// Sanity: the typelibs really resolved their symbols. +if (typeof loaded.GLib.getMonotonicTime !== 'function') throw new Error('GLib.getMonotonicTime missing — typelib not really loaded') -load('GObject', '2.0') -load('Gio', '2.0') -const Gtk = load('Gtk', '3.0') -if (typeof Gtk.Window !== 'function') - throw new Error('Gtk.Window missing — Gtk typelib not really loaded') +if (typeof loaded.Gtk.Window !== 'function') + throw new Error('Gtk.Window missing — Gtk4 typelib not really loaded') +if (typeof loaded.Adw.ApplicationWindow !== 'function') + throw new Error('Adw.ApplicationWindow missing — libadwaita not really loaded') -// Gtk.init may fail without a display; that is not a binary-usability problem, -// so we report it but do not fail the smoke test on it. -let initOk = false -try { Gtk.init(); initOk = true } catch (e) { console.log('note: Gtk.init() threw:', e.message) } -console.log('Gtk.init():', initOk ? 'ok' : 'skipped/failed (no display)') +// Exercise a real GTK4 + Adwaita object graph (needs a display; the runner has one). +let appOk = false +try { + loaded.Gtk.init() + const win = new loaded.Adw.ApplicationWindow() + const buffer = new loaded.GtkSource.Buffer() + const view = new loaded.GtkSource.View() + view.setBuffer(buffer) + win.setContent(view) + console.log('OK: created Adw.ApplicationWindow + GtkSource.View') + appOk = true +} catch (e) { + console.log('note: live widget creation threw (likely no display):', e.message) +} +console.log('live GTK4/Adwaita widgets:', appOk ? 'ok' : 'skipped (no display)') -console.log('\n=== SMOKE TEST PASSED: prebuilt is usable with NO compiler/MSYS2 ===') +console.log('\n=== SMOKE TEST PASSED: GTK4/Adwaita prebuilt usable with NO compiler/MSYS2 ===') From 45b61a4abb2cfa1a997b3d47a77b845f44a60b33 Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 18:01:37 -0400 Subject: [PATCH 5/8] ci: drive Windows DLL bundle from a curated seed list file Move the bundled-library list out of the bash script into windows/runtime-libraries.txt, which doubles as user-facing reference and the build script's input. Emit the exact resolved closure to /bundled-dlls.txt at build time. Co-Authored-By: Claude Opus 4.8 --- scripts/windows-bundle-runtime.sh | 74 +++++++++++++++++++++---------- windows/runtime-libraries.txt | 43 ++++++++++++++++++ 2 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 windows/runtime-libraries.txt diff --git a/scripts/windows-bundle-runtime.sh b/scripts/windows-bundle-runtime.sh index 0271973..950bd31 100755 --- a/scripts/windows-bundle-runtime.sh +++ b/scripts/windows-bundle-runtime.sh @@ -5,7 +5,11 @@ # Make a freshly-built Windows prebuilt self-contained so it can be used WITHOUT # MSYS2/MinGW or any compiler on the target machine. We copy the addon's entire # transitive MinGW DLL closure next to the .node, plus the GObject-Introspection -# typelibs it needs at runtime. +# typelibs and runtime data it needs at runtime. +# +# The set of bundled libraries is driven by the curated seed list +# windows/runtime-libraries.txt (also a user-facing reference). The exact +# resolved DLL set is written to /bundled-dlls.txt. # # Run inside the MINGW64 shell, after building. # @@ -13,7 +17,15 @@ # set -euo pipefail -BINDING_DIR="${1:?usage: windows-bundle-runtime.sh }" +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) + +BINDING_DIR="${1:?usage: windows-bundle-runtime.sh [runtime-libraries.txt]}" +# The curated seed list lives in a text file so it doubles as user-facing +# reference (windows/runtime-libraries.txt). Override with $2 if needed. +LIBS_FILE="${2:-$SCRIPT_DIR/../windows/runtime-libraries.txt}" +# MinGW bin dir the seed names resolve against. +MB="${MINGW_BIN:-/mingw64/bin}" + NODE_FILE="$BINDING_DIR/node_gtk.node" if [ ! -f "$NODE_FILE" ]; then @@ -21,30 +33,30 @@ if [ ! -f "$NODE_FILE" ]; then ls -la "$BINDING_DIR" || true exit 1 fi +if [ ! -f "$LIBS_FILE" ]; then + echo "error: seed list $LIBS_FILE not found" + exit 1 +fi # GObject-Introspection loads each namespace's shared library at runtime via # g_module_open() when you call gi.require('Gtk', ...). Those libraries -# (libgio, libgtk, libgdk, libpango, libatk, libgdk_pixbuf, ...) are NOT linked -# by node_gtk.node, so ntldd on the addon alone misses them. We therefore seed -# the closure from the addon AND from the GTK runtime libraries themselves. -MB=/mingw64/bin -# Seed from the addon AND from every runtime library a real GTK4/Adwaita app -# pulls in (the quilx target: Gtk4 + Adw + GtkSourceView5 + Pango + GdkPixbuf + -# Graphene). Missing entries are skipped, so a few GTK3 names are kept too for -# robustness. -ENTRY_LIBS=( - "$NODE_FILE" - "$MB/libgirepository-1.0-1.dll" - "$MB/libgio-2.0-0.dll" - "$MB/libgtk-4-1.dll" - "$MB/libadwaita-1-0.dll" - "$MB/libgtksourceview-5-0.dll" - "$MB/libpango-1.0-0.dll" - "$MB/libpangocairo-1.0-0.dll" - "$MB/libgdk_pixbuf-2.0-0.dll" - "$MB/libgraphene-1.0-0.dll" - "$MB/libcairo-gobject-2.dll" -) +# (libgio, libgtk, libgdk, libpango, libgdk_pixbuf, ...) are NOT linked by +# node_gtk.node, so ntldd on the addon alone misses them. We therefore seed the +# closure from the addon AND from the GTK runtime libraries listed in LIBS_FILE. +echo "## Seed libraries from $LIBS_FILE" +ENTRY_LIBS=("$NODE_FILE") +while IFS= read -r line; do + # strip comments and surrounding whitespace; skip blanks + name="${line%%#*}" + name="$(echo "$name" | tr -d '[:space:]')" + [ -z "$name" ] && continue + if [ -f "$MB/$name" ]; then + ENTRY_LIBS+=("$MB/$name") + echo " - $name" + else + echo " (skip missing seed $name)" + fi +done < "$LIBS_FILE" echo "## Computing recursive DLL closure for the addon + GTK runtime" # ntldd -R prints every transitive dependency with its resolved Windows path: @@ -73,7 +85,21 @@ sort -u /tmp/dll-closure.txt | while IFS= read -r u; do copied=$((copied + 1)) fi done -echo "## DLLs bundled into $BINDING_DIR ($(ls "$BINDING_DIR"/*.dll | wc -l) total)" +DLL_COUNT=$(ls "$BINDING_DIR"/*.dll 2>/dev/null | wc -l | tr -d ' ') +echo "## DLLs bundled into $BINDING_DIR ($DLL_COUNT total)" + +# Write the exact set that shipped, as a reference manifest (ships in the +# bundle and the artifact). Generated from the curated seed list above. +MANIFEST="$BINDING_DIR/bundled-dlls.txt" +{ + echo "# bundled-dlls.txt — Windows DLLs shipped with this node-gtk prebuilt." + echo "# GENERATED by scripts/windows-bundle-runtime.sh; do not edit by hand." + echo "# Edit the curated seed list windows/runtime-libraries.txt instead." + echo "# $DLL_COUNT DLLs, expanded from the seed list via 'ntldd -R'." + echo "#" + ( cd "$BINDING_DIR" && ls -1 *.dll 2>/dev/null | sort ) +} > "$MANIFEST" +echo "## Wrote manifest $MANIFEST" echo "## Bundling GObject-Introspection typelibs" TYPELIB_SRC=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null || true) diff --git a/windows/runtime-libraries.txt b/windows/runtime-libraries.txt new file mode 100644 index 0000000..b043e2e --- /dev/null +++ b/windows/runtime-libraries.txt @@ -0,0 +1,43 @@ +# windows/runtime-libraries.txt +# +# Seed libraries bundled with the node-gtk Windows prebuilt. +# +# These are the high-level GObject-Introspection runtime libraries that a +# GTK4 / Adwaita application loads at run time (via g_module_open when you call +# gi.require('Gtk', '4.0') etc.). They are NOT linked by node_gtk.node itself, +# so scanning the addon's own imports does not find them — they must be listed +# here explicitly. +# +# scripts/windows-bundle-runtime.sh reads this file, then uses `ntldd -R` to +# expand each entry into its full transitive DLL closure (~78 DLLs: harfbuzz, +# cairo, pixman, fribidi, freetype, zlib, ...) and copies the whole set next to +# the compiled addon. The exact resolved list that actually ships is written to +# /bundled-dlls.txt at build time — read that file to see every DLL. +# +# Format: +# - one DLL file name per line (no path; resolved against the MinGW bin dir, +# e.g. /mingw64/bin) +# - lines starting with '#' and blank lines are ignored +# - entries that don't exist in the MinGW prefix are skipped with a warning, +# so it is safe to list optional libraries +# +# To support additional namespaces, add the providing DLL here. For example: +# libgstreamer-1.0-0.dll # GStreamer (gi.require('Gst', '1.0')) +# libsoup-3.0-0.dll # libsoup (gi.require('Soup', '3.0')) +# Note: VTE (terminal) has no Windows port and cannot be bundled. + +# --- GObject-Introspection core --- +libgirepository-1.0-1.dll +libgio-2.0-0.dll + +# --- GTK4 stack (quilx target) --- +libgtk-4-1.dll +libadwaita-1-0.dll +libgtksourceview-5-0.dll + +# --- text / graphics --- +libpango-1.0-0.dll +libpangocairo-1.0-0.dll +libgdk_pixbuf-2.0-0.dll +libgraphene-1.0-0.dll +libcairo-gobject-2.dll From 88afb7bc55d2aea908022dbb26f529835329552e Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 18:23:26 -0400 Subject: [PATCH 6/8] feat(windows): auto-wire bundled GTK runtime + publish prebuilts - lib/native.js: on win32, point PATH / GI_TYPELIB_PATH / XDG_DATA_DIRS / GSETTINGS_SCHEMA_DIR / GDK_PIXBUF_MODULE_FILE at the runtime bundled next to the .node, before the addon loads. Plain `npm install node-gtk` now works on Windows with no MSYS2/compiler and no manual env setup. - windows-bundle-runtime.sh: rewrite gdk-pixbuf loaders.cache to portable (bare) names so image loaders work on the user's machine. - main.yaml: install the GTK4 ship-stack alongside the GTK3 test deps, bundle the runtime, and publish the prebuilt to S3 on [publish binary]. - smoke test now sets NO env vars (relies on native.js) and runs with the runner's MinGW stripped from PATH; also decodes a PNG via bundled loaders. - doc: Windows prebuilt binaries are now available. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yaml | 29 ++++++++- .github/workflows/test-windows-prebuilt.yaml | 19 ++++-- doc/installation.md | 5 +- lib/native.js | 51 +++++++++++++++ scripts/windows-bundle-runtime.sh | 10 +++ scripts/windows-smoke-test.js | 66 +++++++++----------- 6 files changed, 136 insertions(+), 44 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index bb50a7c..79a0a68 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -39,13 +39,19 @@ jobs: mingw-w64-x86_64-gcc mingw-w64-x86_64-pkgconf mingw-w64-x86_64-python + mingw-w64-x86_64-ntldd-git mingw-w64-x86_64-gobject-introspection - mingw-w64-x86_64-gtk3 mingw-w64-x86_64-cairo + mingw-w64-x86_64-gtk3 mingw-w64-x86_64-gstreamer mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-libsoup3 + mingw-w64-x86_64-gtk4 + mingw-w64-x86_64-libadwaita + mingw-w64-x86_64-gtksourceview5 + mingw-w64-x86_64-graphene + mingw-w64-x86_64-adwaita-icon-theme - name: Install & Build env: @@ -64,6 +70,27 @@ jobs: --skip=callback \ tests/__run__.js + # Make the freshly-built addon a self-contained prebuilt: bundle the GTK4 + # runtime (DLLs + GI typelibs + data) next to the .node so Windows users + # can `npm install node-gtk` with no MSYS2/compiler. lib/native.js wires + # this bundle up automatically at load time. + - name: Bundle GTK4 runtime + run: | + ABI=$(node -p "process.versions.modules") + ./scripts/windows-bundle-runtime.sh "lib/binding/node-v${ABI}-win32-x64" + + # Publish the prebuilt to S3 on a `[publish binary]` commit, like the + # Linux/macOS jobs do via scripts/ci.sh. + - name: Publish prebuilt + if: contains(github.event.head_commit.message, '[publish binary]') + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: | + npx node-pre-gyp package + npx node-pre-gyp publish + npx node-pre-gyp info + build: name: ${{ matrix.os }} - nodejs ${{ matrix.node }} runs-on: ${{ matrix.os }} diff --git a/.github/workflows/test-windows-prebuilt.yaml b/.github/workflows/test-windows-prebuilt.yaml index 338faca..043cd10 100644 --- a/.github/workflows/test-windows-prebuilt.yaml +++ b/.github/workflows/test-windows-prebuilt.yaml @@ -128,18 +128,29 @@ jobs: echo "---- bundled prebuilt ----" ls -la lib/binding/*/ || true - # Negative control: without the bundled DLLs on PATH the addon must fail to - # load. Documents WHY bundling is required. Allowed to fail. + # Negative control: move the bundled typelibs aside so native.js cannot + # auto-wire; the addon must then fail to load. Proves the pass below comes + # from the bundle (not the runner's GTK), then restores it. Allowed to fail. - name: Negative control (no bundled runtime — expected to fail) continue-on-error: true shell: bash run: | + export PATH=$(echo "$PATH" | tr ':' '\n' | grep -viE 'mingw|msys|chocolatey' | paste -sd ':' -) + DIR=$(echo lib/binding/node-v*-win32-x64) + mv "$DIR/girepository-1.0" "$DIR/girepository-1.0.off" if node -e "require('.')"; then echo "UNEXPECTED - loaded without the bundled GTK runtime" else echo "expected failure - addon needs the bundled GTK runtime/typelibs" fi + mv "$DIR/girepository-1.0.off" "$DIR/girepository-1.0" - - name: Smoke test (bundled runtime — expected to pass) + # The real test: NO env vars set here, and the runner's own MinGW/MSYS is + # stripped from PATH to simulate a clean user machine. A pass proves + # lib/native.js auto-wires the bundled runtime by itself. + - name: Smoke test (auto-wired, simulated clean machine) shell: bash - run: node scripts/windows-smoke-test.js + run: | + export PATH=$(echo "$PATH" | tr ':' '\n' | grep -viE 'mingw|msys|chocolatey' | paste -sd ':' -) + echo "gcc on PATH? $(which gcc || echo no)" + node scripts/windows-smoke-test.js diff --git a/doc/installation.md b/doc/installation.md index 06c0757..5e0ffbd 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -19,7 +19,10 @@ Note that prebuilt binaries are available for common systems, in those cases bui - **Linux**: prebuilt binaries available - **macOS**: prebuilt binaries available -- **Windows**: no prebuilt binaries +- **Windows**: prebuilt binaries available (the GTK4/Adwaita runtime is bundled + with the binary, so no MSYS2/compiler is needed to *use* node-gtk; the terminal + widget Vte is the one exception — it has no Windows port). Building from source + still requires MSYS2 (see below). ### Requirements diff --git a/lib/native.js b/lib/native.js index 2b16a99..cdddadb 100644 --- a/lib/native.js +++ b/lib/native.js @@ -4,10 +4,61 @@ const binary = require('@mapbox/node-pre-gyp') const path = require('path') +const fs = require('fs') const packagePath = path.resolve(path.join(__dirname,'../package.json')) const bindingPath = binary.find(packagePath) +// On Windows, the prebuilt binary ships with its whole GTK runtime bundled in +// the same directory as the .node (DLLs, GObject-Introspection typelibs, and +// runtime data). Wire the process environment to that bundle BEFORE the addon +// is loaded, so a plain `npm install node-gtk` works with no MSYS2, no compiler +// and no manual PATH setup. Done in this file specifically because it must run +// before `require(bindingPath)` (the addon's DLL imports are resolved at load +// time) and before bootstrap.js requires the GIRepository typelib. +if (process.platform === 'win32') + setupBundledRuntime(path.dirname(bindingPath)) + +function setupBundledRuntime(bundleDir) { + // A self-contained prebuilt always bundles its typelibs; if that marker is + // absent the addon was built from source against a system GTK, so leave the + // environment alone. + const typelibDir = path.join(bundleDir, 'girepository-1.0') + if (!fs.existsSync(typelibDir)) + return + + const prepend = (name, value) => { + const cur = process.env[name] + process.env[name] = cur ? value + path.delimiter + cur : value + } + const exists = p => { try { return fs.existsSync(p) } catch (e) { return false } } + + // 1) DLLs — both the addon's own imports and the namespace shared libraries + // GObject-Introspection loads at runtime via g_module_open(). + prepend('PATH', bundleDir) + + // 2) GI typelibs. + prepend('GI_TYPELIB_PATH', typelibDir) + + // 3) Runtime data: icon themes, GtkSourceView languages/styles, schemas. + const share = path.join(bundleDir, 'share') + if (exists(share)) + prepend('XDG_DATA_DIRS', share) + const schemas = path.join(share, 'glib-2.0', 'schemas') + if (exists(schemas) && !process.env.GSETTINGS_SCHEMA_DIR) + process.env.GSETTINGS_SCHEMA_DIR = schemas + + // 4) gdk-pixbuf image loaders (made path-portable at bundle time). The + // loaders dir goes on PATH so g_module_open() of a bare loader name from + // the cache resolves. + const loadersDir = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders') + if (exists(loadersDir)) + prepend('PATH', loadersDir) + const loaderCache = path.join(bundleDir, 'lib', 'gdk-pixbuf-2.0', '2.10.0', 'loaders.cache') + if (exists(loaderCache) && !process.env.GDK_PIXBUF_MODULE_FILE) + process.env.GDK_PIXBUF_MODULE_FILE = loaderCache +} + const binding = require(bindingPath) module.exports = binding diff --git a/scripts/windows-bundle-runtime.sh b/scripts/windows-bundle-runtime.sh index 950bd31..147259f 100755 --- a/scripts/windows-bundle-runtime.sh +++ b/scripts/windows-bundle-runtime.sh @@ -133,6 +133,16 @@ copy_tree /mingw64/share/icons/hicolor "$BINDING_DIR/share/icons/hicolor" # GtkSourceView language definitions + style schemes copy_tree /mingw64/share/gtksourceview-5 "$BINDING_DIR/share/gtksourceview-5" +# Make the gdk-pixbuf loaders cache path-portable: the build machine bakes in +# absolute paths to each loader DLL, which don't exist on the user's machine. +# Rewrite each loader path to its bare file name; lib/native.js puts the loaders +# dir on PATH so g_module_open() resolves the bare name at run time. +LOADER_CACHE="$BINDING_DIR/lib/gdk-pixbuf-2.0/2.10.0/loaders.cache" +if [ -f "$LOADER_CACHE" ]; then + sed -i -E 's#^"[^"]*[\\/]([^"\\/]+\.dll)"#"\1"#' "$LOADER_CACHE" + echo "## Rewrote $LOADER_CACHE to portable (bare) loader names" +fi + echo echo "## ===== SIZE BREAKDOWN =====" size() { # label dir-or-glob diff --git a/scripts/windows-smoke-test.js b/scripts/windows-smoke-test.js index 0b5594d..860d75a 100644 --- a/scripts/windows-smoke-test.js +++ b/scripts/windows-smoke-test.js @@ -1,13 +1,15 @@ /* * windows-smoke-test.js * - * Verifies that a Windows prebuilt + bundled GTK runtime (DLLs + typelibs) can - * be loaded and used on a clean machine that has NO MSYS2/MinGW and NO compiler - * — i.e. exactly what a user gets from `npm install node-gtk` if we ship a - * self-contained Windows prebuilt. + * Verifies that a Windows prebuilt + bundled GTK runtime can be loaded and used + * on a clean machine that has NO MSYS2/MinGW and NO compiler — i.e. exactly what + * a user gets from `npm install node-gtk`. * - * It deliberately does NOT depend on anything from MSYS2; the only GTK bits it - * touches are the ones bundled next to the .node by windows-bundle-runtime.sh. + * CRITICAL: this test sets NO environment variables. Everything (DLL search + * path, GI typelib path, icon/schema/loader data) is wired up automatically by + * lib/native.js when it loads the bundled prebuilt. The workflow runs this with + * the runner's own MinGW stripped from PATH, so a pass proves the bundle is + * fully self-sufficient via the auto-wiring alone. */ const path = require('path') @@ -26,39 +28,11 @@ if (fs.existsSync(bindingDir)) { console.log(`bundled DLLs: ${dlls.length}`) } -// 1) Make the bundled GTK DLLs discoverable. This covers BOTH: -// - the addon's own static imports (resolved when node loads the .node) -// - GObject-Introspection's g_module_open() of each namespace's shared lib -// We REPLACE the PATH (rather than prepend) with the bundle dir + only the -// Windows system dirs. The GitHub windows-latest runner ships its own -// C:\mingw64; isolating the PATH proves the test uses ONLY the bundled -// runtime, not whatever GTK happens to be on the machine. -const sysRoot = process.env.SystemRoot || 'C:\\Windows' -process.env.PATH = [ - bindingDir, - path.dirname(process.execPath), // node.exe dir - path.join(sysRoot, 'System32'), - sysRoot, -].join(path.delimiter) -// 2) Point GI at the bundled typelibs. -process.env.GI_TYPELIB_PATH = - typelibDir + (process.env.GI_TYPELIB_PATH ? path.delimiter + process.env.GI_TYPELIB_PATH : '') - -// Require the local package (its lib/native.js resolves the prebuilt via -// node-pre-gyp's binary.find, i.e. the same path users hit after install). +// Require the local package. lib/native.js resolves the prebuilt via +// node-pre-gyp's binary.find and auto-wires the bundled runtime — NO manual +// PATH / GI_TYPELIB_PATH / XDG_DATA_DIRS setup here on purpose. const gi = require(path.join(__dirname, '..')) -console.log('OK: require(node-gtk) — prebuilt + bundled DLLs loaded') - -// Belt and suspenders: also register the typelib dir through GI's own API. -try { gi.prependSearchPath(typelibDir) } catch (e) { /* ignore */ } - -// Point GTK4/Adwaita at the bundled runtime data so a real app could run. -const bundledShare = path.join(bindingDir, 'share') -if (fs.existsSync(bundledShare)) { - process.env.XDG_DATA_DIRS = bundledShare + (process.env.XDG_DATA_DIRS ? path.delimiter + process.env.XDG_DATA_DIRS : '') - const schemas = path.join(bundledShare, 'glib-2.0', 'schemas') - if (fs.existsSync(schemas)) process.env.GSETTINGS_SCHEMA_DIR = schemas -} +console.log('OK: require(node-gtk) — prebuilt loaded and runtime auto-wired by native.js') // The full quilx namespace set. Vte (3.91) has no Windows port, so it is // expected to be unavailable; everything else must load. @@ -111,4 +85,20 @@ try { } console.log('live GTK4/Adwaita widgets:', appOk ? 'ok' : 'skipped (no display)') +// Exercise the bundled gdk-pixbuf image loaders + (portable) loaders.cache by +// decoding a real PNG. This proves the loader subsystem works from the bundle. +const PNG_1x1 = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64') +const pngPath = path.join(__dirname, '..', 'smoke-test-pixel.png') +fs.writeFileSync(pngPath, PNG_1x1) +try { + const pixbuf = loaded.GdkPixbuf.Pixbuf.newFromFile(pngPath) + if (pixbuf.getWidth() !== 1 || pixbuf.getHeight() !== 1) + throw new Error(`unexpected pixbuf size ${pixbuf.getWidth()}x${pixbuf.getHeight()}`) + console.log('OK: GdkPixbuf decoded a PNG via the bundled loaders') +} finally { + try { fs.unlinkSync(pngPath) } catch (e) { /* ignore */ } +} + console.log('\n=== SMOKE TEST PASSED: GTK4/Adwaita prebuilt usable with NO compiler/MSYS2 ===') From cf5cb9184e9b31c6964992de79cdc9ef2032cd1c Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 18:33:03 -0400 Subject: [PATCH 7/8] ci: fix PNG fixture (valid CRC) and bundle libharfbuzz-gobject Co-Authored-By: Claude Opus 4.8 --- scripts/windows-smoke-test.js | 2 +- windows/runtime-libraries.txt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/windows-smoke-test.js b/scripts/windows-smoke-test.js index 860d75a..e7bbf76 100644 --- a/scripts/windows-smoke-test.js +++ b/scripts/windows-smoke-test.js @@ -88,7 +88,7 @@ console.log('live GTK4/Adwaita widgets:', appOk ? 'ok' : 'skipped (no display)') // Exercise the bundled gdk-pixbuf image loaders + (portable) loaders.cache by // decoding a real PNG. This proves the loader subsystem works from the bundle. const PNG_1x1 = Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC', 'base64') const pngPath = path.join(__dirname, '..', 'smoke-test-pixel.png') fs.writeFileSync(pngPath, PNG_1x1) diff --git a/windows/runtime-libraries.txt b/windows/runtime-libraries.txt index b043e2e..63fcaae 100644 --- a/windows/runtime-libraries.txt +++ b/windows/runtime-libraries.txt @@ -41,3 +41,6 @@ libpangocairo-1.0-0.dll libgdk_pixbuf-2.0-0.dll libgraphene-1.0-0.dll libcairo-gobject-2.dll +# HarfBuzz GObject bindings — referenced by the HarfBuzz typelib but not linked +# by the GTK stack, so it must be listed explicitly. +libharfbuzz-gobject-0.dll From d886e0a39c246daaeb597f49aca937b16dc9e537 Mon Sep 17 00:00:00 2001 From: Rom Grk Date: Fri, 19 Jun 2026 19:05:14 -0400 Subject: [PATCH 8/8] doc: update --- README.md | 47 +++++++++++++++++++++++++--- doc/{installation.md => building.md} | 17 +++------- 2 files changed, 48 insertions(+), 16 deletions(-) rename doc/{installation.md => building.md} (92%) diff --git a/README.md b/README.md index 3054f89..b014079 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,11 @@ or [PyGObject](https://pygobject.readthedocs.io). Please note this project is currently in a _alpha_ state. Supported Node.js versions: **20**, **22**, **24** (other versions may work but are untested)
-Pre-built binaries available for: **Linux**, **macOS** +Supported platforms: +- **Linux** — prebuilt binaries available +- **macOS** — prebuilt binaries available +- **Windows** — prebuilt binaries available (but read [Windows](#windows)) + ### Table of contents @@ -30,7 +34,7 @@ Pre-built binaries available for: **Linux**, **macOS** - [ES modules](#es-modules) - [Documentation](#documentation) - [TypeScript](#typescript) -- [Installing and building](#installing-and-building) +- [Installing](#installing) - [Contributing](#contributing) ## Usage @@ -162,9 +166,44 @@ script so it regenerates on install: Run `npx node-gtk generate-types --help` for options. -## Installing and building +## Installing + +1. Install `node-gtk` itself +2. Install the native libraries you use (see examples per platform below) + +```sh +npm install node-gtk + +# This installs a prebuilt binary when one is available for your platform and +# Node.js version, otherwise it falls back to building from source. +``` + +#### Linux + +```sh +# archlinux +pacman -S gtk4 + +# ubuntu +apt install libgtk-4-1 +``` + +#### macOS + +```sh +brew install gtk4 +``` + +#### Windows + +Windows doesn't have the dependencies we need in a package manager, therefore +`node-gtk` ships prebuilt versions of GTK 4 / Adwaita runtime (DLLs, typelibs, +icons), so `npm install node-gtk` is all you need **if** your dependency is in +our [list of prebuilt libraries](./windows/runtime-libraries.txt). + +### build from source -See [Installing & building](./doc/installation.md) for prebuilt-binary notes, per-platform build instructions (Linux, macOS, Windows), and how to run the tests and examples. +Building from source, or contributing? See [Building from source](./doc/building.md). ## Contributing diff --git a/doc/installation.md b/doc/building.md similarity index 92% rename from doc/installation.md rename to doc/building.md index 5e0ffbd..832c502 100644 --- a/doc/installation.md +++ b/doc/building.md @@ -1,10 +1,12 @@ -# Installing & building +# Building from source -Note that prebuilt binaries are available for common systems, in those cases building is not necessary. +This guide is for **contributors** and anyone building node-gtk from source. +Most users don't need it — `npm install node-gtk` ships prebuilt binaries (see +[Installing](../README.md#installing)). Build from source to hack on node-gtk, +or to target a platform/Node.js version that has no prebuilt. ### Table of contents -- [Target Platforms](#target-platforms) - [Requirements](#requirements) - [How to build on Ubuntu](#how-to-build-on-ubuntu) - [How to build on Fedora](#how-to-build-on-fedora) @@ -15,15 +17,6 @@ Note that prebuilt binaries are available for common systems, in those cases bui - [Unit tests](#unit-tests) - [Browser demo](#browser-demo) -##### Target Platforms - -- **Linux**: prebuilt binaries available -- **macOS**: prebuilt binaries available -- **Windows**: prebuilt binaries available (the GTK4/Adwaita runtime is bundled - with the binary, so no MSYS2/compiler is needed to *use* node-gtk; the terminal - widget Vte is the one exception — it has no Windows port). Building from source - still requires MSYS2 (see below). - ### Requirements - `git`