Ship your app
Your app runs on your machine. This section is about getting it onto someone else’s.
gjsify shipnpx @gjsify/cli shipbunx @gjsify/cli shipdeno run -A npm:@gjsify/cli shipThat runs your project’s build script, collects everything the app needs into
one payload under ship/stage/, and wraps that payload once per format into
ship/out/. You write no manifest, no meson file and no spec file.
One command per operating system
Section titled “One command per operating system”gjsify ship <os> says which layout to assemble. A bare gjsify ship assembles
the layout of the host you are on, so name the operating system when you want a
different one.
| Command | What lands in ship/out/ | That target runs |
|---|---|---|
gjsify ship linux | my-app_1.2.3-1_all.deb, my-app-1.2.3-1.noarch.rpm | gjs or node |
gjsify ship darwin | My App.app, my-app-1.2.3-1.macos.arm64.zip | node |
gjsify ship windows | My App/, my-app-1.2.3-1.windows.x64.zip | node |
Each target picks its own runtime from gjsify.ship.app.<os>, keyed linux,
darwin and win32, falling back to gjsify.app. So one project can be GJS on
Linux and Node where no GJS exists.
Assembling is never host-bound, so one workstation produces all three artifact sets. Packing is another matter: two of the nine formats need a particular host. See where each format can be packed.
A Linux package declares its interpreter and its GTK libraries as dependencies and takes them from the distribution. The macOS bundle and the Windows program directory carry them inside, as described below.
- Linux packages covers
.deb,.rpmand the Flatpak bundle. - macOS app bundles covers the
.app, its zip and the.dmg. - Windows artifacts covers the program directory, its
zip and the
.msi. - Sign your artifacts covers
--signand--notarize.
What macOS and Windows carry
Section titled “What macOS and Windows carry”Neither operating system ships GTK, and neither has a GJS you could put in a
download, so the darwin and windows layouts carry a Node interpreter and a GTK
closure and run node only. Set that for those targets alone, and the Linux
package of the same project stays on GJS:
{ "gjsify": { "app": "gjs", // Linux keeps it "ship": { "app": { "darwin": "node", "win32": "node" } } }}Setting gjsify.app to "node" works too, and moves every target with it,
including the Linux .deb from Depends: gjs to Depends: nodejs.
gjsify ship resolves the interpreter and the closure by name out of your
node_modules, so they are installed on the machine that packages the app, not
on the one that runs it. Each operating system has its own pair, listed on its
page. @gjsify/node-gi is a dependency rather than a devDependency, because a
--app node bundle keeps @gjsify/node-gi/* external and requires it at run
time:
{ "devDependencies": { "@gjsify/node-runtime-<os>-<arch>": "^0.44.0", // Node and its LICENSE "@gjsify/gtk-runtime-<os>-<arch>": "^0.44.0" // GTK and GObject-Introspection }, "dependencies": { "@gjsify/node-gi": "^0.44.0" }}When one is missing, ship names the package to install and still produces an
artifact. That artifact starts only where Node and GTK are already installed,
so use it as an intermediate and do not hand it to a user. For a patched or
unpublished build, GJSIFY_NODE_RUNTIME names a directory holding the node
binary (node.exe on Windows) and its LICENSE, and GJSIFY_GTK_RUNTIME one
holding the GTK closure.
The .desktop entry and the AppStream component travel along and are never
read, because neither macOS nor Windows reads them. Ship lists every such file on
each run. The zips carry the operating system in their filename, macos or
windows, because .zip is the one suffix two layouts share.
What ship reads from your package.json
Section titled “What ship reads from your package.json”The same fields feed all three operating systems. Set these before the first run.
| package.json | What it becomes | Override |
|---|---|---|
name | the package name and the launcher’s filename | gjsify.ship.binaryName |
version | the version in every artifact | gjsify.ship.version |
license (an SPDX id such as "MIT") | a required field in .deb and .rpm | gjsify.ship.license.project |
author as "Name <you@example.org>" | Maintainer:, Packager: and the MSI’s Publisher | gjsify.ship.maintainer |
gjsify.main (or main) | the built bundle the launcher runs | gjsify.ship.bundle |
gjsify.app | the interpreter the launcher execs, gjs -m or node. Ship refuses a package whose launcher and declared dependency disagree | gjsify.ship.app.<os>, per target |
scripts.build | the build step ship runs first | pass --skip-build instead |
Two more you have to set yourself, because nothing can derive them:
gjsify.ship.appId, a reverse-DNS id such asorg.example.MyApp. It names the desktop entry, the AppStream component, the installed icon, the macOSCFBundleIdentifierand the MSI upgrade code.gjsify.flatpak.appIdis used when it is set, and a reverse-DNS package name is used after that.gjsify.ship.name, the display name. It becomes the.desktopName=, the<App>.appdirectory, the Windows program directory and the Start-Menu entry. Without it ship title-cases the binary name.
Icons under data/icons/, GSettings schemas under data/, and a LICENSE,
COPYING or LICENSE.md in the project root are found on their own.
Your version is rewritten on the way in, because npm and dpkg disagree about
prereleases. A leading v is dropped, +buildmetadata is dropped with a
warning, and 1.2.0-rc.1 becomes 1.2.0~rc.1. Both dpkg and rpm sort
1.2.0~rc.1 before 1.2.0, the way npm does, while they read 1.2.0-rc.1 as
release rc.1 of version 1.2.0 and sort it after. A version neither format
can spell stops the run.
Where each format can be packed
Section titled “Where each format can be packed”Ten formats wrap three layouts. Seven of them pack on any host, offline, with no tool outside this CLI. Three do not, and ship refuses those on the wrong host rather than writing a broken file.
| Format | Layout | In the default set | Packs on | Needs installed |
|---|---|---|---|---|
deb | linux | yes | any host | nothing |
rpm | linux | yes | any host | nothing |
flatpak | linux | no | Linux | flatpak-builder, flatpak |
appimage | linux | no | Linux | appimagetool, which no distribution packages |
macos-app | darwin | yes | any host | glib-compile-schemas |
macos-app-zip | darwin | yes | any host | glib-compile-schemas |
macos-app-dmg | darwin | no | macOS | hdiutil, which is part of macOS |
windows-dir | windows | yes | any host | glib-compile-schemas |
windows-dir-zip | windows | yes | any host | glib-compile-schemas |
msi | windows | no | Linux or Windows | wixl from msitools, or WiX Toolset v3.14 |
appimage is the one row that embeds something this project did not write: the
AppImage runtime the finished file starts with. gjsify ship passes a pinned one
when your machine has it at
/usr/local/share/gjsify/appimage-runtime/runtime-<arch> (or wherever
GJSIFY_APPIMAGE_RUNTIME_DIR points), and says so on every pack. Without one,
appimagetool downloads a runtime, so that pack needs a network, and the pack
log says that too.
glib-compile-schemas ships in glib2 on Fedora and libglib2.0-bin on
Debian and Ubuntu. It is a tool rather than a host, so the four formats that
declare it still pack anywhere. A layout other than Linux has no install step to
compile your GSettings schemas later, so ship compiles them while it assembles.
A missing tool and a wrong host are separate messages, because the fixes
differ, and both fire before your build script runs. Asking for a format that
wraps another operating system’s layout is an error by name, so
gjsify ship darwin --target deb stops immediately.
Assemble here, pack there
Section titled “Assemble here, pack there”The .dmg needs a Mac and the .msi needs wixl or WiX. --stage and
--from-stage are how you produce them from a host that has neither.
gjsify ship darwin --stage --target macos-app,macos-app-zip,macos-app-dmg# move ship/stage/ to a Mac, then:gjsify ship --from-stage ./stage --target macos-app-dmg--stage writes the payload and stops, leaving
ship/stage/.gjsify-ship-stage.json beside it. That file records the file modes,
the pre-rendered licence overlays, the GI namespaces read out of your bundle and
the build stamp, so a packing host needs nothing else. --from-stage reads no
project at all. No package.json, no config, no built bundle. That is what lets
one CI job assemble and another finish.
Name every format you intend to pack in the --stage run. Phase one renders one
licence overlay per format, and a format the stage never saw is refused on the
packing host:
gjsify ship: this stage was assembled for deb, and --target names rpm.--expect-target <os>-<arch> on the finish step refuses a stage assembled for a
different matrix leg, so a job that downloaded the wrong artifact stops instead
of packing it. It compares against the value the stage recorded, not against the
machine you are packing on, because packing an arm64 stage on an x64 runner
is a supported path.
gjsify ship --from-stage ./stage --expect-target darwin-arm64One check needs no flag and cannot be turned off. If the payload carries a
native image whose ELF e_machine or Mach-O cputype contradicts the
architecture the artifact is being labelled with, ship refuses and names the
file and both architectures.
Look at what you are about to publish
Section titled “Look at what you are about to publish”Everything lands under ship/, which --out <dir> moves.
ship/├── stage/ the payload, exactly as a user gets it│ └── .gjsify-ship-stage.json├── overlay/<format>/ what one format wants elsewhere, today the licence├── schemas/ off Linux: where gschemas.compiled is built├── flatpak/ --target flatpak: the generated manifest and repo└── out/ the artifactsBoth packers read ship/stage/ back off disk rather than keeping it in memory,
so what you inspect there is what a user installs. --verbose prints every
staged file, the GI namespaces the bundle imports, and every tool a packer runs.
Pick your distribution channel
Section titled “Pick your distribution channel”gjsify ship is one option of several, and they stack. The same built bundle
feeds all of them.
| You want | Reach for | Guide |
|---|---|---|
| A file a Linux user installs with their own package manager | gjsify ship linux | Linux packages |
| A single Flatpak file, sandboxed, with a pinned GTK on every distro | gjsify ship linux --target flatpak | Linux packages |
| One portable file for someone else’s distro, with no install and no root | gjsify ship linux --target appimage | Linux packages |
An application a Mac user drags into /Applications | gjsify ship darwin | macOS app bundles |
| An installer a Windows user double-clicks | gjsify ship windows --target msi | Windows artifacts |
| A GUI app on Flathub, with the manifest and metadata files in your repo | gjsify flatpak init | Flatpak: GUI app |
| The same for a headless command-line tool | gjsify flatpak init --kind cli | Flatpak: CLI tool |
A curl … | gjs one-liner that installs into ~/.local, no root | gjsify generate-installer | One-line installer |
A single executable file people download, chmod +x and run | gjsify build --shebang | Self-executing bundle |
| No installation at all, run it straight from npm | publish a dlx-friendly package | Run via dlx |
Choose gjsify ship when you want a user to install once and forget about it.
Add gjsify flatpak init on top when you are submitting to Flathub, which wants
the manifest and the AppStream files committed in your repository. The one-line
installer suits early adopters while you are still iterating, and dlx suits a
tool nobody wants permanently installed.
The AppImage and the Flatpak bundle are both one file with no repository behind
it, and they answer different questions. An AppImage needs no installation and
no root at all, and takes GJS and GTK from the machine it runs on. gjsify ship prints that list, and the file repeats it if the runtime is missing. A
Flatpak carries its own runtime and needs flatpak installed to put it anywhere.
- Linux packages, macOS app bundles and Windows artifacts walk through one operating system each.
- Sign your artifacts covers
--sign,--notarize, and why unsigned is a legitimate result. - CLI Reference →
gjsify shipis the terse flag and configuration reference. - How It Works explains why packing the same build twice gives byte-identical files.