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
2 changes: 1 addition & 1 deletion .github/workflows/flatpak-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
key: flatpak-org.gnome.Sdk-49-node24-v1
key: flatpak-org.gnome.Sdk-50-node24-v1

# Staging only needs the JS dependency tree (node-gtk ships its compile
# inputs; the addon builds inside the flatpak sandbox), so no host GTK
Expand Down
74 changes: 58 additions & 16 deletions doc/bundling.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,25 @@ The shared `"bundle"` key, plus a `"flatpak"` sub-key:
```jsonc
"bundle": {
"name": "MyApp",
"id": "com.example.MyApp", // MUST be your GApplication application-id
"id": "io.github.you.myapp", // MUST be your GApplication application-id;
// Flathub requires io.github.*/io.gitlab.*
// for code-hosting-based ids
"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 (50 is also current;
// pick one and test against it)
"runtimeVersion": "50", // org.gnome.Platform version — pin the one
// you tested against
"node": 26, // SDK extension major (20/22/24/26)
"finishArgs": [ // sandbox permissions beyond the GUI defaults
"--share=network",
"--filesystem=home"
]
],
"lintExceptions": { // linter errors your app stands by; each one
// must be requested (with this justification)
// in the Flathub submission PR
"finish-args-host-filesystem-access": "MyApp is a file manager; …"
}
}
}
```
Expand All @@ -203,6 +209,24 @@ Defaults grant only GUI access (`wayland`, `fallback-x11`, `ipc`, `dri`) —
network and filesystem are deliberately opt-in; request the minimum, Flathub
reviews it.

## Desktop integration files

Ship your own files at the conventional locations and they are used as-is;
anything missing gets a minimal generated stub:

- `data/<id>.desktop` — copied with only `Exec=` rewritten to the sandbox
command, so one file serves host installs and the flatpak. Keep
`Icon=<id>`: flatpak only exports icons named after the app id.
- `data/<id>.metainfo.xml` — copied verbatim. The generated stub passes
validation, but Flathub review wants a real description, screenshots and
release notes only you can write — start from the stub in `dist/flatpak/`.
- `data/icons/hicolor/<size>/apps/<id>.(svg|png)` — the whole theme tree
ships (sized, scalable and symbolic variants). Single-file alternative:
`"bundle": { "icon": "assets/icon.svg" }`.

Override the discovery with `"bundle": { "desktopFile": …, "metainfo": …,
"iconsDir": … }` when your layout differs.

Three things that bite:

- **The flatpak id must equal your `Gtk.Application` `applicationId`** —
Expand Down Expand Up @@ -231,19 +255,37 @@ npx node-gtk flatpak --release --lint

- `<Name>-<version>-flatpak-src.tar.gz` — the staged sources (app +
production node_modules + desktop files) as one tarball
- `<id>.flathub.yml` — the manifest referencing that tarball by URL + sha256
- `flathub/<id>.yml` — the manifest referencing that tarball by URL + sha256
(URL derived from package.json `"repository"`:
`https://github.com/<you>/<app>/releases/download/v<version>/<tarball>`;
override with `--release-url`)

The flow: **1.** fix everything `--lint` reports (the metainfo TODOs —
description, screenshots, releases, content rating — and a real icon);
**2.** create the GitHub release `v<version>` and upload the tarball;
**3.** submit `<id>.flathub.yml` via PR to
[flathub/flathub](https://github.com/flathub/flathub). After acceptance,
users find the app in GNOME Software and every update ships automatically —
new releases are a version bump + new tarball + manifest update in your
Flathub repo.
override with `--release-url`). The `flathub/` directory is exactly what
the submission PR contains, and the manifest is named `<id>.yml` because
the linter requires it.

The flow:

1. Fix everything `--lint` reports. Common gates: the app id — Flathub
rejects `com.github.*`; a GitHub-hosted app must use `io.github.<you>.<app>`
(and `applicationId` must match) — and broad permissions like
`--filesystem=host`, which are declared in `"lintExceptions"` with a
justification and granted per-app during review.
2. Create the GitHub release `v<version>` and upload the tarball:
`gh release create v<version> dist/flatpak/<tarball>`.
3. Fork [flathub/flathub](https://github.com/flathub/flathub), branch off
`new-pr` (not master), add the contents of `dist/flatpak/flathub/`, and
open a PR against `new-pr`. Request any `lintExceptions` in the PR
description with their justifications.

After acceptance, users find the app in GNOME Software and every update
ships automatically — new releases are a version bump + new tarball +
manifest update in your Flathub repo.

One caveat if you run the deepest check locally
(`flatpak-builder-lint repo dist/flatpak/repo`): it reports
`appstream-screenshots-not-mirrored-in-ostree` /
`appstream-external-screenshot-url`. Both are expected outside Flathub's
infrastructure — their pipeline mirrors screenshots into
`dl.flathub.org/media` automatically.

Alternative without Flathub: host your own flatpak repository (an ostree
repo is static files — GitHub Pages works) and point users at a `.flatpakref`;
Expand Down
27 changes: 24 additions & 3 deletions scripts/flatpak-smoke-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ process.exit(0)
fs.mkdirSync(path.join(appDir, 'node_modules'))
fs.symlinkSync(repoRoot, path.join(appDir, 'node_modules', 'node-gtk'), 'dir')

// App-provided desktop-integration files at the conventional data/ locations:
// the metainfo and icon theme must be discovered and used instead of stubs
// (the missing .desktop still exercises the generated-stub path).
fs.mkdirSync(path.join(appDir, 'data', 'icons', 'hicolor', 'scalable', 'apps'), { recursive: true })
fs.writeFileSync(path.join(appDir, 'data', `${id}.metainfo.xml`), `<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>${id}</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license>
<name>FlatpakSmoke</name>
<summary>Flatpak generation smoke test</summary>
<description><p>SMOKE_MARKER_APP_PROVIDED_METAINFO</p></description>
<launchable type="desktop-id">${id}.desktop</launchable>
</component>
`)
fs.writeFileSync(path.join(appDir, 'data', 'icons', 'hicolor', 'scalable', 'apps', `${id}.svg`),
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16"/></svg>\n')

const outDir = path.join(tmp, 'out')
const args = [path.join(repoRoot, 'bin', 'node-gtk.js'), 'flatpak', appDir, '--out', outDir, '--release']
if (!full)
Expand All @@ -80,9 +98,12 @@ const checks = [
// 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>`)],
// --release: Flathub manifest referencing the tarball by url + sha256
[`${id}.flathub.yml`, content => /type: archive/.test(content) && /sha256: [0-9a-f]{64}/.test(content)],
// app-provided data/ files must be used instead of generated stubs
[`${id}.metainfo.xml`, content => content.includes('SMOKE_MARKER_APP_PROVIDED_METAINFO')],
[`icons/hicolor/scalable/apps/${id}.svg`, undefined],
// --release: Flathub manifest (named <id>.yml for the linter) referencing
// the tarball by url + sha256
[`flathub/${id}.yml`, content => /type: archive/.test(content) && /sha256: [0-9a-f]{64}/.test(content)],
['FlatpakSmoke-1.0.0-flatpak-src.tar.gz', undefined],
// the staged tree must be COMPILABLE in the sandbox
['app/node_modules/node-gtk/binding.gyp', undefined],
Expand Down
18 changes: 16 additions & 2 deletions tools/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,17 @@ function loadConfig(appDir, flags) {
repository: typeof pkg.repository === 'string' ? pkg.repository : (pkg.repository || {}).url,
summary: raw.summary || `The ${name} application`,
license: raw.license || pkg.license,
author: pkg.author,
icon: raw.icon,
categories: raw.categories || ['GTK'],
// App-provided desktop-integration files (flatpak): explicit paths here,
// else discovered at the conventional data/ locations.
desktopFile: raw.desktopFile,
metainfo: raw.metainfo,
iconsDir: raw.iconsDir,
// The default needs a valid MAIN category (Utility): appstreamcli
// compose, which flatpak-builder runs, rejects the app with
// no-valid-category otherwise — GTK alone is additional-only.
categories: raw.categories || ['Utility', 'GTK'],
gtk: raw.gtk,
entry: path.normalize(entry),
include: raw.include || ['**/*'],
Expand All @@ -183,9 +192,14 @@ function loadConfig(appDir, flags) {
out: path.resolve(appDir, flags.out || raw.out
|| path.join('dist', `${name}-${process.platform}-${process.arch}`)),
flatpak: {
runtimeVersion: String(flatpakRaw.runtimeVersion || '49'),
runtimeVersion: String(flatpakRaw.runtimeVersion || '50'),
node: Number(flatpakRaw.node || defaultFlatpakNode()),
finishArgs: flatpakRaw.finishArgs || [],
// { "<linter-check>": "<justification>" } — permissions the app stands
// by (a file manager needs --filesystem=host). Tolerated by --lint via
// --user-exceptions; each needs the justification restated in the
// Flathub submission PR.
lintExceptions: flatpakRaw.lintExceptions || {},
},
}

Expand Down
9 changes: 6 additions & 3 deletions tools/create-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const TEMPLATE_DIR = path.join(__dirname, 'templates', 'app')
// template file -> destination path (relative to the new project root).
// Templates carry a `.tmpl` suffix so npm never rewrites `.gitignore` to
// `.npmignore` and never treats a nested `package.json` as a real manifest.
// Destinations get token substitution too: the icon lands at the hicolor
// location `node-gtk flatpak` discovers (and freedesktop tooling expects),
// named after the app id.
const FILES = [
['package.json.tmpl', 'package.json'],
['tsconfig.json.tmpl', 'tsconfig.json'],
Expand All @@ -25,7 +28,7 @@ const FILES = [
['style.css.tmpl', 'style.css'],
['src/main.ts.tmpl', path.join('src', 'main.ts')],
['src/welcome.ts.tmpl', path.join('src', 'welcome.ts')],
['data/icon.svg.tmpl', path.join('data', 'icon.svg')],
['data/icon.svg.tmpl', path.join('data', 'icons', 'hicolor', 'scalable', 'apps', '__APP_ID__.svg')],
]

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -105,10 +108,10 @@ function createProject(opts) {
const written = []
for (const [src, dest] of FILES) {
const content = substitute(fs.readFileSync(path.join(TEMPLATE_DIR, src), 'utf8'))
const destPath = path.join(dir, dest)
const destPath = path.join(dir, substitute(dest))
fs.mkdirSync(path.dirname(destPath), { recursive: true })
fs.writeFileSync(destPath, content)
written.push(dest)
written.push(path.relative(dir, destPath))
}
return written
}
Expand Down
Loading
Loading