feat: ship node-gtk apps — bundle (portable) + flatpak (installable)#489
Merged
Conversation
Package a node-gtk application into a directory that runs on machines with no node, no GTK and no compiler: app code + production node_modules (pnpm symlinks dereferenced, node-gtk trimmed to runtime files) + stripped node binary + the GTK runtime — ldd shared-library closure walked from the addon, the GI namespace libraries and the gdk-pixbuf loaders (host-tied libraries excluded per the AppImage excludelist), typelibs, compiled GSettings schemas and icon themes. A generated sh launcher wires the environment and execs the bundled node; --archive produces a .tar.gz (~100 MB for a GTK4 app). The runtime/ vs app/ split keeps the runtime content-identical across apps for future sharing; macOS/Windows land later as drop-in platform modules (the Windows DLL closure is already proven by scripts/windows-bundle-runtime.sh). Tested end-to-end by scripts/bundle-smoke-test.js, wired into CI on ubuntu (GTK4-with-GTK3-fallback fixture, since runners only have GTK3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
giRequire inserted the (empty) module object into the cache BEFORE the typelib lookup could throw, so probing for a version and falling back — try Gtk 4.0, catch, require Gtk 3.0 — returned the poisoned empty module: 'Gtk.init is not a function'. Surfaced by the bundle smoke test on CI, whose runners only have GTK3. Cache only after Repository_require succeeds. isLoaded() had the same side effect (and its version check read a key that is never written, so it always returned false); route it through g_irepository_is_registered instead, with no cache mutation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The format real users install: one-click via GNOME Software, Flathub-
ready, sandboxed, with the GTK runtime shared across apps (the smoke
app's deliverable is an 18MB .flatpak vs its 100MB portable tar.gz).
GTK comes from org.gnome.Platform — nothing GTK-related is bundled.
flatpak-builder builds offline; instead of the usual lockfile→sources
generators, the same app-tree step `node-gtk bundle` uses stages the
app + production node_modules as a plain dir source (new rebuildAddon
mode: node-gtk ships its compile inputs — src/, binding.gyp, nan — and
no host binding). The addon is compiled in-sandbox against the
runtime's GTK with npm's internal node-gyp: --nodedir points at the
node SDK extension's bundled headers (offline), and module_name/
module_path are passed explicitly since binding.gyp is node-pre-gyp
style. The node binary is copied from the SDK extension into /app/bin.
Generates manifest + launcher + .desktop + AppStream metainfo + icon
install from the shared "bundle" config (new: summary, icon, license,
categories, flatpak.{runtimeVersion,node,finishArgs}), builds with
flatpak-builder or org.flatpak.Builder (granting the output dir to its
sandbox — it cannot see /tmp), and emits a single-file .flatpak via
build-bundle --runtime-repo so end-user installs fetch the Platform
from Flathub automatically.
Verified end-to-end locally: built, installed and ran the smoke app
under GNOME Platform 49 / node26 extension. CI checks generation only
(the sandbox build needs the ~1GB GNOME SDK).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apps written with the `gi:` import scheme (the `node-gtk create` default) crashed at startup in bundles and flatpaks with ERR_UNSUPPORTED_ESM_URL_SCHEME: the loader hooks are installed by `node --import node-gtk/register`, which the generated launchers only passed when the app configured nodeArgs by hand. Launchers now pass --import node-gtk/register by default; the new `register: false` config key opts out. Harmless for CJS apps — register.mjs only installs module hooks. Verified: an ESM `import Gtk from 'gi:Gtk-4.0'` app built, installed and ran as a flatpak; the CJS bundle smoke test still passes with the flag present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A rolling-release host can expose introspected names an older GNOME runtime doesn't (glib 2.88 introspects g_unix_signal_add_full as GLibUnix.signalAdd; the GNOME 49 runtime's glib 2.86 exposes signalAddFull). Document the version-tolerant lookup pattern and the one-liner to get a node REPL inside the sandbox. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
node-gtk bundle — ship self-contained Linux app bundlesbundle (portable) + flatpak (installable)
…ild CI Everything between 'builds locally' and 'on Flathub': - --release: Flathub builds from fetchable sources, so tarball the staged tree (top-level dir matching archive strip-components) and emit <id>.flathub.yml referencing it by url+sha256; the URL derives from package.json "repository" (--release-url overrides). Prints the upload-and-submit steps. - --lint: flatpak-builder-lint (native or via org.flatpak.Builder with output-dir filesystem access) on the manifest + metainfo — surfaces the exact Flathub-review checklist. Metainfo now auto-fills the homepage <url> from "repository". - --run: install + run in one step. - Warn when staged sources set a GApplication applicationId different from the flatpak id (D-Bus registration fails inside the sandbox — hit this for real with the first app). - bundle.json marker written FIRST so a failed run leaves a directory the next run may overwrite. - flatpak-build.yaml (workflow_dispatch): full sandbox build + install + headless run (dbus-run-session + xvfb) with the GNOME SDK cached; main.yaml keeps the cheap generation-only check. Verified locally: the exact --full path passes end-to-end. - `node-gtk create` scaffolds Flathub-ready: bundle block whose id matches the app's APP_ID by construction, placeholder data/icon.svg, flatpak/bundle npm scripts, README shipping section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
One PR for the whole Linux shipping story (consolidated from the former stacked #490, more commits incoming for Flathub release mode + hardening):
Both read one
"bundle"config block in package.json. Full reference:doc/bundling.md.Verified
.flatpakdeliverable = 18 MB (GTK shared via the Platform) vs 100 MB portable tar.gz.gi:imports now work out of the box (launchers pass--import node-gtk/registerby default), a module-cache-poisoning bug ingi.require(failed version probe broke the fallback require — regression-tested), and documented gotchas (flatpak id must equal GApplication id; runtime API drift vs rolling-release dev machines).Design notes
--nodediragainst the node SDK extension's headers); node binary copied to /app/bin; manifest/desktop/metainfo/launcher generated from config.🤖 Generated with Claude Code