|
| 1 | +# Bundling — ship your app to users |
| 2 | + |
| 3 | +Two commands cover shipping, for different audiences: |
| 4 | + |
| 5 | +- **[`node-gtk flatpak`](#flatpak)** — the way **real users install apps**: |
| 6 | + a Flatpak, one-click installable via GNOME Software, distributable on |
| 7 | + Flathub (updates, sandboxing, shared GTK runtime). Start here for desktop |
| 8 | + distribution. |
| 9 | +- **[`node-gtk bundle`](#portable-bundles)** — a **self-contained portable |
| 10 | + directory** (plus `.tar.gz`): your code, a node runtime, the compiled |
| 11 | + addon, and the entire GTK runtime. Runs on machines with nothing |
| 12 | + installed; right for GitHub release artifacts, kiosks, fleets, and |
| 13 | + "try my app" links. |
| 14 | + |
| 15 | +Both read the same `"bundle"` configuration from your package.json. |
| 16 | + |
| 17 | +> **Platform support: Linux today.** macOS and Windows are planned; the |
| 18 | +> platform-specific work is isolated behind one module interface |
| 19 | +> (`tools/bundle/platform-*.js`), and the Windows DLL-closure logic is already |
| 20 | +> proven by the self-contained npm prebuilt (`scripts/windows-bundle-runtime.sh`). |
| 21 | +
|
| 22 | +# Portable bundles |
| 23 | + |
| 24 | +## Usage |
| 25 | + |
| 26 | +```sh |
| 27 | +cd my-app |
| 28 | +npx node-gtk bundle # → dist/MyApp-linux-x64/ |
| 29 | +npx node-gtk bundle --archive # → dist/MyApp-linux-x64.tar.gz too |
| 30 | +``` |
| 31 | + |
| 32 | +Requirements at bundle time: the app is installed (`node_modules` present, |
| 33 | +any package manager — pnpm symlink layouts are handled), node-gtk has a |
| 34 | +compiled addon for the running node, and the GTK stack you target is |
| 35 | +installed. **The node that runs the bundler is the node that ships.** |
| 36 | + |
| 37 | +Users run the launcher — no installation: |
| 38 | + |
| 39 | +```sh |
| 40 | +tar xzf MyApp-linux-x64.tar.gz |
| 41 | +./MyApp-linux-x64/MyApp |
| 42 | +``` |
| 43 | + |
| 44 | +## Output layout |
| 45 | + |
| 46 | +``` |
| 47 | +MyApp-linux-x64/ |
| 48 | +├── MyApp launcher (sh) — wires the runtime env, execs bundled node |
| 49 | +├── bundle.json manifest: name, id, versions, launcher path |
| 50 | +├── runtime/ ← identical across apps sharing a node-gtk/GTK version |
| 51 | +│ ├── node node binary (stripped copy of the bundling node) |
| 52 | +│ ├── lib/ shared-library closure + girepository-1.0/ typelibs |
| 53 | +│ │ └── gdk-pixbuf-2.0/…/loaders + relocatable loaders.cache.in |
| 54 | +│ └── share/ compiled GSettings schemas, Adwaita/hicolor icon themes |
| 55 | +└── app/ ← yours: files from `include` + production node_modules |
| 56 | +``` |
| 57 | + |
| 58 | +The `runtime/` vs `app/` split is deliberate: `runtime/` is content-identical |
| 59 | +for every app built with the same node-gtk/GTK/node versions, so a future |
| 60 | +shared-runtime install (or content-addressed stores like Flatpak's) can |
| 61 | +deduplicate it without a layout change. `app/` is typically a few MB. |
| 62 | + |
| 63 | +The launcher sets `LD_LIBRARY_PATH`, `GI_TYPELIB_PATH` and `XDG_DATA_DIRS` to |
| 64 | +prefer `runtime/`, generates a per-install gdk-pixbuf loader cache (the cache |
| 65 | +format needs absolute paths), `cd`s into `app/` and execs `runtime/node` on |
| 66 | +your entry. |
| 67 | + |
| 68 | +## Configuration |
| 69 | + |
| 70 | +Everything lives under the `"bundle"` key of your `package.json`; every field |
| 71 | +is optional: |
| 72 | + |
| 73 | +```jsonc |
| 74 | +{ |
| 75 | + "main": "src/main.js", |
| 76 | + "bundle": { |
| 77 | + "name": "MyApp", // launcher/file name; default: PascalCase package name |
| 78 | + "id": "com.example.MyApp", // reverse-DNS id (cache dirs); default derived |
| 79 | + "entry": "src/main.js", // default: "main" |
| 80 | + "gtk": 4, // bundle only this GTK major (default: all installed) |
| 81 | + "include": ["src/**", "assets/**"], // app files; default: "**/*" minus |
| 82 | + // node_modules/.git/dist/out/build |
| 83 | + "nodeArgs": ["--max-old-space-size=512"], // extra node flags, if any |
| 84 | + "register": true, // default: launchers pass --import node-gtk/register |
| 85 | + // so `gi:` imports work; set false to opt out |
| 86 | + "libraries": ["libgstreamer-1.0.so.0"], // extra closure seeds (GStreamer, libsoup, …) |
| 87 | + "omitPackages": ["some-dev-helper"], // npm packages to leave out |
| 88 | + "icons": false, // skip Adwaita/hicolor themes (default: true) |
| 89 | + "node": "vendor/node", // ship this node binary instead (same ABI!) |
| 90 | + "out": "release" // default: dist/<name>-<platform>-<arch> |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +CLI flags `--out`, `--name`, `--entry`, `--archive` override the config. |
| 96 | + |
| 97 | +### What gets bundled, what stays on the host |
| 98 | + |
| 99 | +The shared-library closure is walked with `ldd`, seeded from the addon, the |
| 100 | +GI namespace libraries (GTK, Adwaita, Pango, GdkPixbuf, …— see |
| 101 | +`tools/bundle/seeds.js`; missing ones are skipped, `libraries` adds more) and |
| 102 | +the gdk-pixbuf loaders. Host-tied libraries are **excluded**, following the |
| 103 | +[AppImage community excludelist](https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist): |
| 104 | +glibc, the GPU/OpenGL stack, X11/Wayland client libraries, and the font stack |
| 105 | +(fontconfig/freetype/harfbuzz), which every desktop provides. |
| 106 | + |
| 107 | +Node packages: the production dependency closure of your app, symlinks |
| 108 | +dereferenced. node-gtk itself is trimmed to `package.json` + `lib/` with only |
| 109 | +the target ABI's compiled addon; its build-time deps (node-pre-gyp, node-gyp, |
| 110 | +nan) never ship. |
| 111 | + |
| 112 | +### Compatibility baseline |
| 113 | + |
| 114 | +A bundle runs on distros whose **glibc is at least as new** as the build |
| 115 | +machine's (the classic portable-Linux constraint — the excluded libraries |
| 116 | +resolve against the host). Build on the oldest distribution you want to |
| 117 | +support; a GitHub Actions `ubuntu-latest` runner is a reasonable baseline. |
| 118 | +`bundle.node` exists so you can ship a node built against an older glibc than |
| 119 | +your machine's. |
| 120 | + |
| 121 | +### Size expectations |
| 122 | + |
| 123 | +GTK4 + Adwaita + typelibs + icons ≈ 135 MB, node ≈ 110 MB, ~100 MB as a |
| 124 | +`.tar.gz` — Electron-class. `"gtk": 4` avoids also shipping GTK3 from a |
| 125 | +machine that has both; `"icons": false` saves ~15 MB if you rely on host |
| 126 | +themes. |
| 127 | + |
| 128 | +## Verifying a bundle |
| 129 | + |
| 130 | +CI runs `scripts/bundle-smoke-test.js` (bundle a minimal app, run its |
| 131 | +launcher, assert the app executed under the bundled node). To check where |
| 132 | +libraries resolve from on your machine: |
| 133 | + |
| 134 | +```sh |
| 135 | +LD_DEBUG=libs ./dist/MyApp-linux-x64/MyApp 2>&1 | grep 'trying file.*libgtk' |
| 136 | +``` |
| 137 | + |
| 138 | +# Flatpak |
| 139 | + |
| 140 | +```sh |
| 141 | +cd my-app |
| 142 | +npx node-gtk flatpak # generate + build + MyApp.flatpak |
| 143 | +npx node-gtk flatpak --install # …and install it (user), then: |
| 144 | +flatpak run com.example.MyApp |
| 145 | +``` |
| 146 | + |
| 147 | +The output `dist/flatpak/MyApp.flatpak` is a **single file users double-click |
| 148 | +to install** through GNOME Software on any distro — the file embeds the |
| 149 | +Flathub repo reference, so the GNOME runtime is fetched automatically. For |
| 150 | +real distribution, [submit the generated manifest to Flathub](#shipping-on-flathub). |
| 151 | + |
| 152 | +Requirements: `flatpak` plus a builder (`flatpak install flathub |
| 153 | +org.flatpak.Builder`, or your distro's `flatpak-builder` package). The first |
| 154 | +build downloads the GNOME SDK (~1 GB, cached). `--no-build` generates |
| 155 | +everything without building. |
| 156 | + |
| 157 | +## How it works |
| 158 | + |
| 159 | +Unlike `node-gtk bundle`, **nothing GTK-related is bundled**: the app runs on |
| 160 | +`org.gnome.Platform`, shared across all Flatpak apps and updated |
| 161 | +independently. What's inside `/app` is only your code, node (from the |
| 162 | +`org.freedesktop.Sdk.Extension.node<N>` SDK extension) and the node-gtk |
| 163 | +addon. |
| 164 | + |
| 165 | +`flatpak-builder` builds offline, which is normally painful for Node apps |
| 166 | +(lockfile→sources generators). We sidestep it: the same app-tree step |
| 167 | +`node-gtk bundle` uses stages your files + production node_modules |
| 168 | +(pnpm-safe, symlinks dereferenced) as a plain `dir` source. The only thing |
| 169 | +compiled in the sandbox is the node-gtk addon, against the runtime's GTK, |
| 170 | +using the SDK extension's bundled node headers (`--nodedir`) — still no |
| 171 | +network. |
| 172 | + |
| 173 | +## Flatpak configuration |
| 174 | + |
| 175 | +The shared `"bundle"` key, plus a `"flatpak"` sub-key: |
| 176 | + |
| 177 | +```jsonc |
| 178 | +"bundle": { |
| 179 | + "name": "MyApp", |
| 180 | + "id": "com.example.MyApp", // MUST be your GApplication application-id |
| 181 | + "summary": "Does the thing", // .desktop comment + AppStream summary |
| 182 | + "icon": "assets/icon.svg", // svg or png; required by Flathub |
| 183 | + "license": "MIT", // SPDX, defaults to package.json "license" |
| 184 | + "categories": ["GTK", "Utility"], |
| 185 | + "flatpak": { |
| 186 | + "runtimeVersion": "49", // org.gnome.Platform version (50 is also current; |
| 187 | + // pick one and test against it) |
| 188 | + "node": 26, // SDK extension major (20/22/24/26) |
| 189 | + "finishArgs": [ // sandbox permissions beyond the GUI defaults |
| 190 | + "--share=network", |
| 191 | + "--filesystem=home" |
| 192 | + ] |
| 193 | + } |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +Flags: `--install` (install user-level), `--run` (install + run), `--no-build` |
| 198 | +(generate only), `--release` / `--release-url` (Flathub submission sources, |
| 199 | +below), `--lint` (run `flatpak-builder-lint` on the manifest + metainfo — do |
| 200 | +this before submitting anywhere). |
| 201 | + |
| 202 | +Defaults grant only GUI access (`wayland`, `fallback-x11`, `ipc`, `dri`) — |
| 203 | +network and filesystem are deliberately opt-in; request the minimum, Flathub |
| 204 | +reviews it. |
| 205 | + |
| 206 | +Three things that bite: |
| 207 | + |
| 208 | +- **The flatpak id must equal your `Gtk.Application` `applicationId`** — |
| 209 | + otherwise GNOME Shell can't associate windows with the app (generic icon, |
| 210 | + wrong dock entry). |
| 211 | +- The sandbox has no host filesystem by default: `fs` reads outside the |
| 212 | + sandbox need `--filesystem=` permissions, or better, the XDG portals. |
| 213 | +- **The API surface follows the runtime's libraries, not your dev |
| 214 | + machine's.** A rolling-release host can expose introspected names a |
| 215 | + slightly older GNOME runtime doesn't (e.g. glib ≥2.88 introspects |
| 216 | + `g_unix_signal_add_full` as `GLibUnix.signalAdd`; the GNOME 49 runtime's |
| 217 | + glib 2.86 calls it `signalAddFull`). Write version-tolerant lookups |
| 218 | + (`GLibUnix.signalAdd ?? GLibUnix.signalAddFull`) and test inside the |
| 219 | + sandbox — a node REPL against the runtime is one command: |
| 220 | + `flatpak run --command=/app/bin/node <your.app.Id>`. |
| 221 | + |
| 222 | +## Shipping on Flathub |
| 223 | + |
| 224 | +Flathub builds on its own infrastructure from *fetchable* sources — the |
| 225 | +locally staged tree can't be submitted directly. `--release` produces exactly |
| 226 | +what a submission needs: |
| 227 | + |
| 228 | +```sh |
| 229 | +npx node-gtk flatpak --release --lint |
| 230 | +``` |
| 231 | + |
| 232 | +- `<Name>-<version>-flatpak-src.tar.gz` — the staged sources (app + |
| 233 | + production node_modules + desktop files) as one tarball |
| 234 | +- `<id>.flathub.yml` — the manifest referencing that tarball by URL + sha256 |
| 235 | + (URL derived from package.json `"repository"`: |
| 236 | + `https://github.com/<you>/<app>/releases/download/v<version>/<tarball>`; |
| 237 | + override with `--release-url`) |
| 238 | + |
| 239 | +The flow: **1.** fix everything `--lint` reports (the metainfo TODOs — |
| 240 | +description, screenshots, releases, content rating — and a real icon); |
| 241 | +**2.** create the GitHub release `v<version>` and upload the tarball; |
| 242 | +**3.** submit `<id>.flathub.yml` via PR to |
| 243 | +[flathub/flathub](https://github.com/flathub/flathub). After acceptance, |
| 244 | +users find the app in GNOME Software and every update ships automatically — |
| 245 | +new releases are a version bump + new tarball + manifest update in your |
| 246 | +Flathub repo. |
| 247 | + |
| 248 | +Alternative without Flathub: host your own flatpak repository (an ostree |
| 249 | +repo is static files — GitHub Pages works) and point users at a `.flatpakref`; |
| 250 | +you keep update delivery, minus the store discoverability. |
| 251 | + |
| 252 | +# Roadmap |
| 253 | + |
| 254 | +- **Windows**: same `bundle` architecture; DLL closure via `ntldd` (already |
| 255 | + CI-proven for the npm prebuilt), `.cmd` launcher, `.zip` archive. Needs an |
| 256 | + MSYS2 MINGW64 environment at bundle time. |
| 257 | +- **macOS**: dylib closure via `otool -L` from Homebrew, |
| 258 | + `install_name_tool` relocation + ad-hoc re-signing, `.app`/`.dmg` output, |
| 259 | + `DYLD_FALLBACK_LIBRARY_PATH` launcher. Distribution additionally requires |
| 260 | + codesigning + notarization (Apple developer account). |
| 261 | +- **AppImage**: single-file wrapper around exactly the portable tree. |
| 262 | +- **Shared runtime** (portable bundles): install `runtime/` once per machine |
| 263 | + (`~/.local/share/node-gtk/runtime/<version>`), apps resolve it |
| 264 | + relative-first — the layout already supports this. |
0 commit comments