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> names whose layout to assemble. A bare gjsify ship
assembles the layout of the host you are sitting at, so name the operating
system whenever you want one you are not on.
| 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.arm64.zip | node |
gjsify ship windows | My App/, my-app-1.2.3-1.x64.zip | node |
Each target’s runtime is its own: gjsify.ship.app.<os>, keyed linux,
darwin, win32, falling back to gjsify.app. So one project can be GJS on
Linux and Node where no GJS exists, and stating the second half does not restate
the first — before that key, asking for a .app moved the Linux package’s
Depends: with it.
Assembling is never host-bound, so one workstation produces all three artifact sets. Packing is a different question, and two formats out of nine answer it differently. 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 a Node interpreter and a GTK closure inside themselves, because
neither operating system ships one. That is why those two accept node only,
and why they need extra packages in your package.json.
- 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 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”Nine formats wrap three layouts. Seven of them pack on any host, offline, with no tool outside this CLI. Two 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 |
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 |
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, plus 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 |
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.
Choose gjsify flatpak init on top of it when you are submitting to Flathub,
which wants the manifest and the AppStream files committed in your repository.
Choose the one-line installer while you are still iterating and your users are
early adopters. Choose dlx for a tool nobody wants permanently installed.
There is no AppImage target. The nearest thing that exists is the Flatpak bundle, which is also one file, needs no repository and works offline.
- 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.