Skip to content

Ship your app

Your app runs on your machine. This section is about getting it onto someone else’s.

Ship it
Terminal
gjsify ship
Terminal
npx @gjsify/cli ship
Terminal
bunx @gjsify/cli ship
Terminal
deno run -A npm:@gjsify/cli ship

That 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.

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.

CommandWhat lands in ship/out/That target runs
gjsify ship linuxmy-app_1.2.3-1_all.deb, my-app-1.2.3-1.noarch.rpmgjs or node
gjsify ship darwinMy App.app, my-app-1.2.3-1.arm64.zipnode
gjsify ship windowsMy App/, my-app-1.2.3-1.x64.zipnode

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.

The same fields feed all three operating systems. Set these before the first run.

package.jsonWhat it becomesOverride
namethe package name and the launcher’s filenamegjsify.ship.binaryName
versionthe version in every artifactgjsify.ship.version
license (an SPDX id such as "MIT")a required field in .deb and .rpmgjsify.ship.license.project
author as "Name <you@example.org>"Maintainer:, Packager: and the MSI’s Publishergjsify.ship.maintainer
gjsify.main (or main)the built bundle the launcher runsgjsify.ship.bundle
gjsify.appthe interpreter the launcher execs, gjs -m or node. Ship refuses a package whose launcher and declared dependency disagreegjsify.ship.app.<os>, per target
scripts.buildthe build step ship runs firstpass --skip-build instead

Two more you have to set yourself, because nothing can derive them:

  • gjsify.ship.appId, a reverse-DNS id such as org.example.MyApp. It names the desktop entry, the AppStream component, the installed icon, the macOS CFBundleIdentifier and the MSI upgrade code. gjsify.flatpak.appId is used when it is set, and a reverse-DNS package name is used after that.
  • gjsify.ship.name, the display name. It becomes the .desktop Name=, the <App>.app directory, 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.

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.

FormatLayoutIn the default setPacks onNeeds installed
deblinuxyesany hostnothing
rpmlinuxyesany hostnothing
flatpaklinuxnoLinuxflatpak-builder, flatpak
macos-appdarwinyesany hostglib-compile-schemas
macos-app-zipdarwinyesany hostglib-compile-schemas
macos-app-dmgdarwinnomacOShdiutil, which is part of macOS
windows-dirwindowsyesany hostglib-compile-schemas
windows-dir-zipwindowsyesany hostglib-compile-schemas
msiwindowsnoLinux or Windowswixl 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.

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.

Terminal
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:

Text
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.

Terminal
gjsify ship --from-stage ./stage --expect-target darwin-arm64

One 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.

Everything lands under ship/, which --out <dir> moves.

Text
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 artifacts

Both 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.

gjsify ship is one option of several, and they stack. The same built bundle feeds all of them.

You wantReach forGuide
A file a Linux user installs with their own package managergjsify ship linuxLinux packages
A single Flatpak file, sandboxed, with a pinned GTK on every distrogjsify ship linux --target flatpakLinux packages
An application a Mac user drags into /Applicationsgjsify ship darwinmacOS app bundles
An installer a Windows user double-clicksgjsify ship windows --target msiWindows artifacts
A GUI app on Flathub, with the manifest and metadata files in your repogjsify flatpak initFlatpak: GUI app
The same for a headless command-line toolgjsify flatpak init --kind cliFlatpak: CLI tool
A curl … | gjs one-liner that installs into ~/.local, no rootgjsify generate-installerOne-line installer
A single executable file people download, chmod +x and rungjsify build --shebangSelf-executing bundle
No installation at all, run it straight from npmpublish a dlx-friendly packageRun 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.