Skip to content

feat: ship node-gtk apps — bundle (portable) + flatpak (installable)#489

Merged
romgrk merged 6 commits into
masterfrom
bundle-cli
Jul 5, 2026
Merged

feat: ship node-gtk apps — bundle (portable) + flatpak (installable)#489
romgrk merged 6 commits into
masterfrom
bundle-cli

Conversation

@romgrk

@romgrk romgrk commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

One PR for the whole Linux shipping story (consolidated from the former stacked #490, more commits incoming for Flathub release mode + hardening):

npx node-gtk flatpak --install   # a Flatpak users install with one click — Flathub-ready
npx node-gtk bundle --archive    # self-contained portable dir + tar.gz — runs on bare machines

Both read one "bundle" config block in package.json. Full reference: doc/bundling.md.

Verified

  • Flatpak: built, installed and ran real apps locally (smoke app + mariner, a GNOME Files clone) on org.gnome.Platform 49 with the node26 SDK extension. Single-file .flatpak deliverable = 18 MB (GTK shared via the Platform) vs 100 MB portable tar.gz.
  • Bundle: CI-green end-to-end smoke (bundle → run launcher → assert app executed under bundled node); LD_DEBUG-verified library resolution; works from any cwd.
  • Real-app testing surfaced and fixed: gi: imports now work out of the box (launchers pass --import node-gtk/register by default), a module-cache-poisoning bug in gi.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

  • Portable bundle: ldd closure from addon + GI seed libraries + pixbuf loaders; AppImage-excludelist host libs stay on the host; runtime/ vs app/ split keeps the runtime shareable. node-gtk trimmed to lib/ + one ABI binding.
  • Flatpak: GTK from org.gnome.Platform; offline flatpak-builder sources come from the same app-tree stager (no npm-in-sandbox, pnpm-safe); only the addon compiles in-sandbox (--nodedir against the node SDK extension's headers); node binary copied to /app/bin; manifest/desktop/metainfo/launcher generated from config.

🤖 Generated with Claude Code

romgrk and others added 3 commits July 5, 2026 00:20
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>
romgrk and others added 2 commits July 5, 2026 02:17
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>
@romgrk romgrk changed the title feat: node-gtk bundle — ship self-contained Linux app bundles feat: ship node-gtk apps — bundle (portable) + flatpak (installable) Jul 5, 2026
…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>
@romgrk romgrk merged commit e9b9c4e into master Jul 5, 2026
9 checks passed
@romgrk romgrk deleted the bundle-cli branch July 5, 2026 17:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant