Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,10 @@ jobs:
- name: Bundle smoke test
if: matrix.os == 'ubuntu-latest' && matrix.node == 24
run: xvfb-run -a node scripts/bundle-smoke-test.js

# `node-gtk flatpak` generation: manifest + offline sources must be
# complete and buildable. The full sandbox build is not run per-CI-run
# (it downloads the ~1GB GNOME SDK); this check needs no flatpak at all.
- name: Flatpak generation smoke test
if: matrix.os == 'ubuntu-latest' && matrix.node == 24
run: node scripts/flatpak-smoke-test.js
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,11 @@ npx node-gtk create <your-app>
Also see our [hello world](./examples/hello-world.mjs), [web browser](./examples/browser.mjs)
or [system monitor](./examples/system-monitor.mjs) examples.

When it's time to ship, the **bundle** tool packages your app, node, node-gtk and
the whole GTK runtime into a self-contained directory that runs on machines with
nothing installed (Linux for now — see [doc/bundling.md](./doc/bundling.md)):
When it's time to ship (Linux for now — see [doc/bundling.md](./doc/bundling.md)):

```sh
npx node-gtk bundle --archive
npx node-gtk flatpak # a Flatpak users install with one click (Flathub-ready)
npx node-gtk bundle # a self-contained portable directory / tar.gz
```

## Installing
Expand Down
5 changes: 5 additions & 0 deletions bin/node-gtk.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* create Create a new GTK/Adwaita application.
* list List the GObject-Introspection libraries available locally.
* bundle Create a self-contained bundle of a node-gtk application.
* flatpak Package a node-gtk application as a Flatpak.
*/

const cmd = process.argv[2]
Expand All @@ -25,6 +26,9 @@ switch (cmd) {
case 'bundle':
require('../tools/bundle.js').run(process.argv.slice(3))
break
case 'flatpak':
require('../tools/flatpak.js').run(process.argv.slice(3))
break
case undefined:
case '-h':
case '--help':
Expand All @@ -37,6 +41,7 @@ Commands:
generate-types <Namespace-Version> [...] Generate TypeScript types (.d.ts)
list [filter] List available libraries & versions
bundle [directory] Create a self-contained app bundle
flatpak [directory] Package the app as a Flatpak

Run \`node-gtk <command> --help\` for details.`)
process.exit(cmd ? 0 : 1)
Expand Down
131 changes: 113 additions & 18 deletions doc/bundling.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
# Bundling — ship your app to users

`node-gtk bundle` creates a **self-contained, double-clickable bundle** of a
node-gtk application: your code, a node runtime, the compiled node-gtk addon,
and the entire GTK runtime it needs. The result runs on machines with no
node, no GTK, and no compiler installed.
Two commands cover shipping, for different audiences:

- **[`node-gtk flatpak`](#flatpak)** — the way **real users install apps**:
a Flatpak, one-click installable via GNOME Software, distributable on
Flathub (updates, sandboxing, shared GTK runtime). Start here for desktop
distribution.
- **[`node-gtk bundle`](#portable-bundles)** — a **self-contained portable
directory** (plus `.tar.gz`): your code, a node runtime, the compiled
addon, and the entire GTK runtime. Runs on machines with nothing
installed; right for GitHub release artifacts, kiosks, fleets, and
"try my app" links.

Both read the same `"bundle"` configuration from your package.json.

> **Platform support: Linux today.** macOS and Windows are planned; the
> platform-specific work is isolated behind one module interface
> (`tools/bundle/platform-*.js`), and the Windows DLL-closure logic is already
> proven by the self-contained npm prebuilt (`scripts/windows-bundle-runtime.sh`).
> For distribution-grade Linux packaging (Flatpak, AppImage), see
> [Roadmap](#roadmap) — the portable tree this command produces is exactly what
> those formats wrap.

# Portable bundles

## Usage

Expand Down Expand Up @@ -72,7 +80,9 @@ is optional:
"gtk": 4, // bundle only this GTK major (default: all installed)
"include": ["src/**", "assets/**"], // app files; default: "**/*" minus
// node_modules/.git/dist/out/build
"nodeArgs": ["--import", "node-gtk/register"], // e.g. for TS entries
"nodeArgs": ["--max-old-space-size=512"], // extra node flags, if any
"register": true, // default: launchers pass --import node-gtk/register
// so `gi:` imports work; set false to opt out
"libraries": ["libgstreamer-1.0.so.0"], // extra closure seeds (GStreamer, libsoup, …)
"omitPackages": ["some-dev-helper"], // npm packages to leave out
"icons": false, // skip Adwaita/hicolor themes (default: true)
Expand Down Expand Up @@ -125,20 +135,105 @@ libraries resolve from on your machine:
LD_DEBUG=libs ./dist/MyApp-linux-x64/MyApp 2>&1 | grep 'trying file.*libgtk'
```

## Roadmap
# Flatpak

```sh
cd my-app
npx node-gtk flatpak # generate + build + MyApp.flatpak
npx node-gtk flatpak --install # …and install it (user), then:
flatpak run com.example.MyApp
```

The output `dist/flatpak/MyApp.flatpak` is a **single file users double-click
to install** through GNOME Software on any distro — the file embeds the
Flathub repo reference, so the GNOME runtime is fetched automatically. For
real distribution, [submit the generated manifest to Flathub](#shipping-on-flathub).

Requirements: `flatpak` plus a builder (`flatpak install flathub
org.flatpak.Builder`, or your distro's `flatpak-builder` package). The first
build downloads the GNOME SDK (~1 GB, cached). `--no-build` generates
everything without building.

## How it works

Unlike `node-gtk bundle`, **nothing GTK-related is bundled**: the app runs on
`org.gnome.Platform`, shared across all Flatpak apps and updated
independently. What's inside `/app` is only your code, node (from the
`org.freedesktop.Sdk.Extension.node<N>` SDK extension) and the node-gtk
addon.

`flatpak-builder` builds offline, which is normally painful for Node apps
(lockfile→sources generators). We sidestep it: the same app-tree step
`node-gtk bundle` uses stages your files + production node_modules
(pnpm-safe, symlinks dereferenced) as a plain `dir` source. The only thing
compiled in the sandbox is the node-gtk addon, against the runtime's GTK,
using the SDK extension's bundled node headers (`--nodedir`) — still no
network.

## Flatpak configuration

The shared `"bundle"` key, plus a `"flatpak"` sub-key:

```jsonc
"bundle": {
"name": "MyApp",
"id": "com.example.MyApp", // MUST be your GApplication application-id
"summary": "Does the thing", // .desktop comment + AppStream summary
"icon": "assets/icon.svg", // svg or png; required by Flathub
"license": "MIT", // SPDX, defaults to package.json "license"
"categories": ["GTK", "Utility"],
"flatpak": {
"runtimeVersion": "49", // org.gnome.Platform version
"node": 26, // SDK extension major (20/22/24/26)
"finishArgs": [ // sandbox permissions beyond the GUI defaults
"--share=network",
"--filesystem=home"
]
}
}
```

- **Windows**: same architecture; DLL closure via `ntldd` (already CI-proven
for the npm prebuilt), `.cmd` launcher, `.zip` archive. Needs an MSYS2
MINGW64 environment at bundle time.
Defaults grant only GUI access (`wayland`, `fallback-x11`, `ipc`, `dri`) —
network and filesystem are deliberately opt-in; request the minimum, Flathub
reviews it.

Three things that bite:

- **The flatpak id must equal your `Gtk.Application` `applicationId`** —
otherwise GNOME Shell can't associate windows with the app (generic icon,
wrong dock entry).
- The sandbox has no host filesystem by default: `fs` reads outside the
sandbox need `--filesystem=` permissions, or better, the XDG portals.
- **The API surface follows the runtime's libraries, not your dev
machine's.** A rolling-release host can expose introspected names a
slightly older GNOME runtime doesn't (e.g. glib ≥2.88 introspects
`g_unix_signal_add_full` as `GLibUnix.signalAdd`; the GNOME 49 runtime's
glib 2.86 calls it `signalAddFull`). Write version-tolerant lookups
(`GLibUnix.signalAdd ?? GLibUnix.signalAddFull`) and test inside the
sandbox — a node REPL against the runtime is one command:
`flatpak run --command=/app/bin/node <your.app.Id>`.

## Shipping on Flathub

Flathub is a manifest repository: you submit the generated
`<id>.yml` (plus your app source — for a node app, typically a release
tarball of the staged `app/` tree) via PR to
[flathub/flathub](https://github.com/flathub/flathub). Before submitting,
fill in the metainfo TODOs (`description`, screenshots, releases,
`content_rating`) and provide a real icon; `flatpak run org.flatpak.Builder
--command=flatpak-builder-lint` checks compliance. After acceptance, users
find the app in GNOME Software and updates ship automatically.

# Roadmap

- **Windows**: same `bundle` architecture; DLL closure via `ntldd` (already
CI-proven for the npm prebuilt), `.cmd` launcher, `.zip` archive. Needs an
MSYS2 MINGW64 environment at bundle time.
- **macOS**: dylib closure via `otool -L` from Homebrew,
`install_name_tool` relocation + ad-hoc re-signing, `.app`/`.dmg` output,
`DYLD_FALLBACK_LIBRARY_PATH` launcher. Distribution additionally requires
codesigning + notarization (Apple developer account).
- **Flatpak**: the best Linux end-state — apps on `org.gnome.Platform` share
one GTK runtime and get Flathub distribution; the per-app payload shrinks
to `app/` + the addon. A manifest generator can build on the same app-tree
step this command uses.
- **AppImage**: single-file wrapper around exactly this portable tree.
- **Shared runtime**: install `runtime/` once per machine
- **AppImage**: single-file wrapper around exactly the portable tree.
- **Shared runtime** (portable bundles): install `runtime/` once per machine
(`~/.local/share/node-gtk/runtime/<version>`), apps resolve it
relative-first — the layout already supports this.
7 changes: 4 additions & 3 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ See [typescript.md](./typescript.md)

## Shipping your app

`node-gtk bundle` packages your app, node, node-gtk and the GTK runtime into a
self-contained directory (and `.tar.gz`) that runs on machines with nothing
installed. See [bundling.md](./bundling.md).
`node-gtk flatpak` packages your app as a Flatpak — the format real users
install through GNOME Software, Flathub-ready. `node-gtk bundle` produces a
self-contained portable directory (and `.tar.gz`) that runs on machines with
nothing installed. See [bundling.md](./bundling.md).

## node-gtk low-level API

Expand Down
85 changes: 85 additions & 0 deletions scripts/flatpak-smoke-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* flatpak-smoke-test.js
*
* Checks `node-gtk flatpak --no-build`: stages a minimal app and asserts the
* generated flatpak sources are complete and buildable — manifest, launcher,
* desktop file, metainfo, and an app tree carrying the node-gtk COMPILE
* inputs (src/ + binding.gyp + nan) instead of a host-compiled binding.
*
* The actual sandbox build is exercised locally / at release time, not on
* every CI run: it downloads the ~1 GB GNOME SDK. This test needs no GTK, no
* display and no flatpak.
*
* Usage: node scripts/flatpak-smoke-test.js
*/

const fs = require('fs')
const os = require('os')
const path = require('path')
const child_process = require('child_process')

if (process.platform !== 'linux') {
console.log(`flatpak-smoke-test: flatpaks are Linux-only, skipping on ${process.platform}`)
process.exit(0)
}

const repoRoot = path.resolve(__dirname, '..')
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'node-gtk-flatpak-smoke-'))
const appDir = path.join(tmp, 'app')
const id = 'org.nodegtk.FlatpakSmoke'
fs.mkdirSync(appDir, { recursive: true })

fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({
name: 'flatpak-smoke',
version: '1.0.0',
main: 'main.js',
license: 'MIT',
dependencies: { 'node-gtk': '*' },
bundle: { name: 'FlatpakSmoke', id, summary: 'Flatpak generation smoke test' },
}, null, 2))
fs.writeFileSync(path.join(appDir, 'main.js'), `require('node-gtk')\n`)
fs.mkdirSync(path.join(appDir, 'node_modules'))
fs.symlinkSync(repoRoot, path.join(appDir, 'node_modules', 'node-gtk'), 'dir')

const outDir = path.join(tmp, 'out')
const result = child_process.spawnSync(
process.execPath,
[path.join(repoRoot, 'bin', 'node-gtk.js'), 'flatpak', appDir, '--out', outDir, '--no-build'],
{ stdio: 'inherit', env: { ...process.env, NODE_GTK_BUNDLE_DEBUG: '1' } })
if (result.status !== 0) {
console.error(`smoke: FAIL — generation exited with ${result.status} (kept: ${tmp})`)
process.exit(1)
}

const checks = [
// generated integration files
[`${id}.yml`, content => content.includes(`app-id: ${id}`) && content.includes('org.freedesktop.Sdk.Extension.node')],
// the gi: import scheme must work out of the box → loader registered
['launcher.sh', content => content.includes('exec /app/bin/node --import node-gtk/register')],
[`${id}.desktop`, content => content.includes('Exec=' + id)],
[`${id}.metainfo.xml`, content => content.includes(`<id>${id}</id>`)],
// the staged tree must be COMPILABLE in the sandbox
['app/node_modules/node-gtk/binding.gyp', () => true],
['app/node_modules/node-gtk/src', () => true],
['app/node_modules/nan', () => true],
]
for (const [rel, check] of checks) {
const p = path.join(outDir, rel)
if (!fs.existsSync(p)) {
console.error(`smoke: FAIL — missing ${rel} (kept: ${tmp})`)
process.exit(1)
}
if (fs.statSync(p).isFile() && !check(fs.readFileSync(p, 'utf8'))) {
console.error(`smoke: FAIL — unexpected content in ${rel} (kept: ${tmp})`)
process.exit(1)
}
}

// No host-compiled binding may leak into the offline sources.
if (fs.existsSync(path.join(outDir, 'app/node_modules/node-gtk/lib/binding'))) {
console.error(`smoke: FAIL — host lib/binding leaked into flatpak sources (kept: ${tmp})`)
process.exit(1)
}

console.log('smoke: PASS')
fs.rmSync(tmp, { recursive: true, force: true })
24 changes: 23 additions & 1 deletion tools/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,27 +152,49 @@ function loadConfig(appDir, flags) {
if (!exists(path.join(appDir, entry)))
throw new Error(`entry point not found: ${entry}`)

const flatpakRaw = raw.flatpak || {}
const config = {
name,
id: raw.id || `com.example.${name}`,
version: pkg.version || '0.0.0',
summary: raw.summary || `The ${name} application`,
license: raw.license || pkg.license,
icon: raw.icon,
categories: raw.categories || ['GTK'],
gtk: raw.gtk,
entry: path.normalize(entry),
include: raw.include || ['**/*'],
// The `gi:` import scheme (the default in `node-gtk create` apps) only
// works with the loader hooks registered, so launchers pass
// `--import node-gtk/register` unless explicitly disabled. Harmless for
// CJS apps: register.mjs only installs hooks.
register: raw.register !== false,
nodeArgs: raw.nodeArgs || [],
libraries: raw.libraries || [],
omitPackages: raw.omitPackages || [],
icons: raw.icons !== false,
node: raw.node,
out: path.resolve(appDir, flags.out || raw.out
|| path.join('dist', `${name}-${process.platform}-${process.arch}`)),
flatpak: {
runtimeVersion: String(flatpakRaw.runtimeVersion || '49'),
node: Number(flatpakRaw.node || defaultFlatpakNode()),
finishArgs: flatpakRaw.finishArgs || [],
},
}

if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(config.name))
throw new Error(`invalid bundle name '${config.name}' (it becomes a file name)`)
return config
}

// The org.freedesktop.Sdk.Extension.node<N> major to build/run with: the
// running node's major when an extension exists for it.
function defaultFlatpakNode() {
const major = Number(process.versions.node.split('.')[0])
return [20, 22, 24, 26].includes(major) ? major : 24
}

function pascalCase(base) {
return base
.replace(/^@.*\//, '')
Expand Down Expand Up @@ -232,4 +254,4 @@ function parseArgs(argv) {
return flags
}

module.exports = { run }
module.exports = { run, loadConfig, prepareOutput }
Loading
Loading