diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index bb50a7cd..79a0a688 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
new file mode 100644
index 00000000..043cd10c
--- /dev/null
+++ b/.github/workflows/test-windows-prebuilt.yaml
@@ -0,0 +1,156 @@
+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-cairo
+ 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:
+ 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 GTK4 runtime (DLLs + typelibs + data)
+ 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: 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:
+ 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: 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"
+
+ # 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: |
+ 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/README.md b/README.md
index 3054f896..b0140797 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 95%
rename from doc/installation.md
rename to doc/building.md
index 06c0757b..832c5029 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,12 +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**: no prebuilt binaries
-
### Requirements
- `git`
diff --git a/lib/native.js b/lib/native.js
index 2b16a999..cdddadbf 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
new file mode 100755
index 00000000..147259f0
--- /dev/null
+++ b/scripts/windows-bundle-runtime.sh
@@ -0,0 +1,163 @@
+#!/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 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.
+#
+# ./scripts/windows-bundle-runtime.sh lib/binding/node-v127-win32-x64
+#
+set -euo pipefail
+
+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
+ echo "error: $NODE_FILE not found"
+ 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, 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:
+# libgtk-3-0.dll => C:\msys64\mingw64\bin\libgtk-3-0.dll (0x...)
+# 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
+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
+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)
+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, 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"
+
+# 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
+ 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
new file mode 100644
index 00000000..e7bbf760
--- /dev/null
+++ b/scripts/windows-smoke-test.js
@@ -0,0 +1,104 @@
+/*
+ * windows-smoke-test.js
+ *
+ * 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`.
+ *
+ * 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')
+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}`)
+}
+
+// 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 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.
+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')
+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')
+
+// 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)')
+
+// 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(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC',
+ '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 ===')
diff --git a/windows/runtime-libraries.txt b/windows/runtime-libraries.txt
new file mode 100644
index 00000000..63fcaaef
--- /dev/null
+++ b/windows/runtime-libraries.txt
@@ -0,0 +1,46 @@
+# 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
+# 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