Skip to content

Windows artifacts

gjsify ship windows turns a built app into a Windows program directory and a zip around it. Both assemble on any operating system, so a Linux or macOS workstation can produce them. The .msi needs one extra tool, on Linux or on Windows.

Terminal
gjsify ship windows
Text
ship/out/My App/
ship/out/my-app-1.2.3-1.windows.x64.zip

The program directory is what an installer lays down and a user browses to. The zip is what a user downloads, so its filename carries the version and the architecture.

x64 is the only architecture. gjsify, the project that builds GTK for Windows, publishes no arm64 runtime bundle yet, so there is no GTK for a Windows on ARM artifact to load. --arch arm64 is refused by name rather than producing a directory that cannot start.

There is no GJS on Windows, so nothing there can run a gjs payload, and ship says so before it packs anything. Set gjsify.ship.app.win32 to "node", as Ship your app shows, and install @gjsify/node-runtime-win32-x64, @gjsify/gtk-runtime-win32-x64 and @gjsify/node-gi.

The key is win32, the process.platform spelling, not windows, which is what the command positional takes. A key nothing reads would leave the target on the project default with nothing to say so, so gjsify ship and the manifest audit both refuse it by name.

GJSIFY_GTK_RUNTIME, when set, names a directory holding bin/ and girepository-1.0/.

Text
My App/
├── my-app.cmd the launcher
├── node.exe the carried interpreter
├── app/ your built bundle
├── lib/node-gi/ the GTK closure and the node-gi addon
└── share/ icon, metainfo, schemas, licences

There is no metadata file in the directory. What a Windows installer says about an application lives in the .msi’s own tables, so the directory holds only the program.

my-app.cmd finds its own directory from %~dp0, so the tree works unzipped anywhere and works again once an installer lays it into C:\Program Files. It runs node.exe from beside itself rather than a bare node, and it points GJSIFY_GTK_RUNTIME at the carried closure so node-gi loads that one. The file uses CRLF line endings, which is what cmd.exe reads.

The zip carries a top level the staged tree does not. Without it, unzipping would scatter app\, share\ and a loose .cmd into whatever folder the user was in.

The directory is named after gjsify.ship.name. Windows reserves < > : " / \ | ? *, the control characters, and the device names CON, PRN, AUX, NUL, COM1 to COM9 and LPT1 to LPT9 at every path, and it silently strips a trailing dot or space. Ship refuses a display name containing any of those rather than producing an archive that extracts under a different name than the launcher resolves against.

--target msi wraps the same program directory in a Windows Installer package.

Terminal
gjsify ship windows --target windows-dir,windows-dir-zip,msi

That produces ship/out/my-app-1.2.3-1.x64.msi beside the other two artifacts. The installed tree is the tree the zip expands to. Nothing about the payload changes.

The installer adds three things a directory cannot do on its own:

  • It lays the program directory under %ProgramFiles%\My App, which msiexec INSTALLDIR=… overrides.
  • It writes one Start-Menu shortcut, aimed at the same .cmd launcher.
  • It appears in Add/Remove Programs, taking its name, version and publisher from the MSI, and msiexec /x removes every file it installed.

The upgrade code is derived from your app id, so installing a newer version replaces the older one instead of leaving both on the machine. Keep the app id stable across releases.

msi is not in the default set, because it is the one Windows format that needs a program this CLI does not carry. Ship writes the installer’s source document itself and hands it to whichever compiler the host has:

HostInstall
Fedorasudo dnf install msitools
Debian, Ubuntusudo apt install msitools
WindowsWiX Toolset v3.14, with its bin directory on PATH

So you can build a Windows installer from a Linux workstation, or from Windows without installing anything from the Linux side. macOS cannot pack this format.

A missing compiler is a message naming the package, before your build script runs:

Text
gjsify ship: packing a msi on linux needs glib-compile-schemas and wixl, and
wixl is not on PATH.

Windows Installer’s ProductVersion is major.minor.build and nothing else. No prerelease suffix, no build metadata. major and minor are at most 255 and build at most 65535, and a field over the limit is truncated by the installer rather than rejected.

Ship refuses 1.2.0-rc.1 instead of dropping the suffix, because 1.2.0~rc.1 and 1.2.0 would then carry the same ProductVersion, the upgrade rule could not tell them apart, and installing one over the other would leave both on the machine. Set gjsify.ship.version to a plain x.y.z, or drop msi from the targets for prerelease builds.

Shipped fonts need one call from your app here

Section titled “Shipped fonts need one call from your app here”

gjsify.ship.fonts stages your faces into share/fonts/<appId>/ and the launcher exports GJSIFY_FONT_DIR pointing at it. On Windows that is where the command’s job ends: pangocairo selects the win32 backend, which populates from DirectWrite alone, so a fontconfig directory is inert here. Measured: a config naming your staged directory moves the default font map by zero families even when it is the only one loaded.

So register them, once, before you build any UI. One call from the GTK host layer does it:

TypeScript
import { initFonts } from '@gjsify/gtk-host/fonts';
initFonts();

It reads GJSIFY_FONT_DIR itself, hands every face it finds to PangoCairo.FontMap.get_default().add_font_file(), never throws, and does nothing when the payload carries no font. So the same line goes in your Linux and macOS builds with no OS branch around it. Skip it on Windows and Pango falls back to a system face silently: the app merely looks wrong, with no error and no exit code, which is why this section exists rather than a warning at build time.

Ship your own fonts has the staging key, where in startup the call belongs, and a check that tells a registered family apart from a substituted one.

node.exe is a console-subsystem program and the Node release ships no windowed variant, so starting the app leaves a console window open behind it. That applies to the .cmd launcher and to the shortcut the .msi writes. Nothing here hides it.

Signing is optional here in a way it is not on macOS

Section titled “Signing is optional here in a way it is not on macOS”

SmartScreen only warns about an unsigned download, until per-file reputation accrues. Gatekeeper blocks an unsigned macOS bundle outright. So an unsigned Windows program directory is a usable artifact in a way a .app is not.

--sign <identity> passes a name to signtool on a Windows host. See Sign your artifacts, which also records that no run in the gjsify repository has ever invoked signtool.

A project on any operating system, producing all three Windows artifacts. The .msi step needs msitools on Linux, so it can run in the same place.

Terminal
# 1. Declare the runtime, on the packaging host.
npm install --save-dev @gjsify/node-runtime-win32-x64 @gjsify/gtk-runtime-win32-x64
npm install --save @gjsify/node-gi
# 2. Install the MSI compiler.
sudo dnf install msitools glib2 # Fedora
# 3. Build everything.
gjsify ship windows \
--target windows-dir,windows-dir-zip,msi --verbose

To split the work instead, assemble here and pack on Windows:

Terminal
gjsify ship windows --stage --target windows-dir,windows-dir-zip,msi
gjsify ship --from-stage ./stage --expect-target win32-x64 --target msi

--expect-target uses the win32 spelling, because that is what a running process computes about itself. The positional accepts both windows and win32.

Message saysFix
the windows layout cannot run a gjs appset gjsify.ship.app.win32 to "node" (or gjsify.app, for every target) and rebuild the bundle for Node
the windows layout is not assemblable for --arch arm64use --arch x64; there is no Windows on ARM GTK to load
packing a msi on … needs … wixlinstall msitools, or WiX Toolset v3.14 on Windows
is not a version an .msi can carryset gjsify.ship.version to a plain x.y.z
the windows layout would put this app in a directory called …set gjsify.ship.name to a name Windows can hold
no bundled interpreter, naming @gjsify/node-runtime-win32-x64install it, or set GJSIFY_NODE_RUNTIME