Skip to content

Commit e9b9c4e

Browse files
authored
Merge pull request #489 from romgrk/bundle-cli
feat: ship node-gtk apps — `bundle` (portable) + `flatpak` (installable)
2 parents e0c5da8 + 9276146 commit e9b9c4e

20 files changed

Lines changed: 2032 additions & 12 deletions
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: flatpak-build
2+
#
3+
# Full `node-gtk flatpak` verification: build a smoke app's flatpak for real
4+
# (addon compiled in the sandbox against org.gnome.Platform), install it, and
5+
# run it headless. This downloads the ~1GB GNOME SDK (cached between runs),
6+
# so it is not part of the per-push matrix — main.yaml runs the cheap
7+
# generation-only smoke test instead.
8+
#
9+
on:
10+
workflow_dispatch:
11+
12+
jobs:
13+
full-build:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: 24
20+
21+
- name: Install flatpak tooling
22+
run: |
23+
sudo apt-get update
24+
sudo apt-get install -y flatpak flatpak-builder dbus xvfb
25+
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
26+
27+
# The GNOME SDK + node extension the build pulls via
28+
# --install-deps-from=flathub. Keyed on the runtime/extension pair the
29+
# smoke app resolves to (node 24 -> node24 extension).
30+
- uses: actions/cache@v4
31+
with:
32+
path: ~/.local/share/flatpak
33+
key: flatpak-org.gnome.Sdk-49-node24-v1
34+
35+
# Staging only needs the JS dependency tree (node-gtk ships its compile
36+
# inputs; the addon builds inside the flatpak sandbox), so no host GTK
37+
# and no host addon build.
38+
- run: npm install -g pnpm@11
39+
- run: pnpm install --ignore-scripts
40+
41+
- name: Full flatpak smoke test (build + install + run)
42+
run: node scripts/flatpak-smoke-test.js --full

.github/workflows/main.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,18 @@ jobs:
145145
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
146146
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
147147
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
148+
149+
# `node-gtk bundle` end-to-end: bundle a smoke app against this checkout,
150+
# then run the produced launcher. Linux-only for now; one node version
151+
# suffices (the bundler is ABI-generic JS, tested against the matrix's
152+
# addon build anyway).
153+
- name: Bundle smoke test
154+
if: matrix.os == 'ubuntu-latest' && matrix.node == 24
155+
run: xvfb-run -a node scripts/bundle-smoke-test.js
156+
157+
# `node-gtk flatpak` generation: manifest + offline sources must be
158+
# complete and buildable. The full sandbox build is not run per-CI-run
159+
# (it downloads the ~1GB GNOME SDK); this check needs no flatpak at all.
160+
- name: Flatpak generation smoke test
161+
if: matrix.os == 'ubuntu-latest' && matrix.node == 24
162+
run: node scripts/flatpak-smoke-test.js

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ npx node-gtk create <your-app>
4444
Also see our [hello world](./examples/hello-world.mjs), [web browser](./examples/browser.mjs)
4545
or [system monitor](./examples/system-monitor.mjs) examples.
4646

47+
When it's time to ship (Linux for now — see [doc/bundling.md](./doc/bundling.md)):
48+
49+
```sh
50+
npx node-gtk flatpak # a Flatpak users install with one click (Flathub-ready)
51+
npx node-gtk bundle # a self-contained portable directory / tar.gz
52+
```
53+
4754
## Installing
4855

4956
There are two steps:

bin/node-gtk.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* generate-types Generate TypeScript declarations from the installed typelibs.
77
* create Create a new GTK/Adwaita application.
88
* list List the GObject-Introspection libraries available locally.
9+
* bundle Create a self-contained bundle of a node-gtk application.
10+
* flatpak Package a node-gtk application as a Flatpak.
911
*/
1012

1113
const cmd = process.argv[2]
@@ -21,6 +23,12 @@ switch (cmd) {
2123
case 'list-libraries':
2224
require('../tools/list-libraries.js').run(process.argv.slice(3))
2325
break
26+
case 'bundle':
27+
require('../tools/bundle.js').run(process.argv.slice(3))
28+
break
29+
case 'flatpak':
30+
require('../tools/flatpak.js').run(process.argv.slice(3))
31+
break
2432
case undefined:
2533
case '-h':
2634
case '--help':
@@ -32,6 +40,8 @@ Commands:
3240
create <directory> Create a new GTK/Adwaita app
3341
generate-types <Namespace-Version> [...] Generate TypeScript types (.d.ts)
3442
list [filter] List available libraries & versions
43+
bundle [directory] Create a self-contained app bundle
44+
flatpak [directory] Package the app as a Flatpak
3545
3646
Run \`node-gtk <command> --help\` for details.`)
3747
process.exit(cmd ? 0 : 1)

doc/bundling.md

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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.

doc/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Adwaita is a set of widgets and theming that is built on top of GTK.
99
1. [Widgets and styling](#widgets-and-styling)
1010
1. [Devtools](#devtools)
1111
1. [Typescript](#typescript)
12+
1. [Shipping your app](#shipping-your-app)
1213

1314
## Importing
1415

@@ -72,6 +73,12 @@ npx node-gtk generate-types Gtk-4.0 Pango-1.0 [etc]
7273

7374
See [typescript.md](./typescript.md)
7475

76+
## Shipping your app
77+
78+
`node-gtk flatpak` packages your app as a Flatpak — the format real users
79+
install through GNOME Software, Flathub-ready. `node-gtk bundle` produces a
80+
self-contained portable directory (and `.tar.gz`) that runs on machines with
81+
nothing installed. See [bundling.md](./bundling.md).
7582

7683
## node-gtk low-level API
7784

0 commit comments

Comments
 (0)