Bundling
sabine bundle turns a project into something you can hand to people: a portable archive, a deb,
rpm or AppImage, a Windows MSI or setup program, or a macOS disk image. It reads Sabine.toml,
builds your web assets and Rust executable, lays the app out the way its platform expects, and
packages it with that platform’s own tool.
sabine bundle . --target debWithout --target, sabine bundle builds a portable archive for the machine it runs on. Bundles
use Cargo’s release profile; --debug packages a debug build instead, which is quicker to build
for checking a package but is not what you ship.
What a bundle build does
Section titled “What a bundle build does”-
Reads
Sabine.tomland the Cargo manifest, and resolves the app’s id, name, version and metadata. See App metadata for the fallbacks. -
Loads the production environment files and runs your web build command in the web root, such as
bun run build. Pass--no-web-buildto package assets you already built. -
Builds the executable with
cargo build --bin <package>, for the target’s Rust triple when it differs from the machine you build on. -
Stages the app: the executable, your built web assets,
[resources], icons, desktop metadata and a productionSabine.tomlthat the app reads at runtime. Development settings such asdev_url,dev_portanddev_commandare left out. -
Signs the staged app when code signing is configured. See Code signing.
-
Packages it, then signs and notarizes installers and disk images the same way.
The app never looks for its manifest itself. The production Sabine.toml sits where Sabine finds
it next to the executable, so the same code runs from sabine dev, cargo run and an installed
package.
Targets
Section titled “Targets”| Target | Also accepted as | Produces | Packaging tool |
|---|---|---|---|
portable |
tar, tar.gz |
The app for the machine building it, ready to unpack anywhere | tar |
linux |
A tarball of the Linux filesystem tree | tar |
|
deb |
A Debian package | dpkg-deb and dpkg-shlibdeps (from dpkg-dev) |
|
rpm |
An RPM package | rpmbuild |
|
appimage |
An AppImage | appimagetool |
|
windows |
A tarball of the Windows app folder | tar |
|
exe |
nsis |
A setup program | NSIS (makensis) |
msi |
A Windows Installer package | WiX 7 (wix) with the WixToolset.UI.wixext extension |
|
macos |
app |
A tarball of the .app bundle |
tar |
dmg |
A disk image holding the .app |
hdiutil |
When a tool is missing, sabine bundle stops with an error that names it, such as
rpmbuild is required to build this package; install it and retry.
portable
Section titled “portable”A portable bundle is the app for the platform you run sabine bundle on, packed so it runs in place
wherever it is unpacked:
A gzip tarball of the filesystem tree, with the program in usr/lib/sabine/<app id> and a
launcher in usr/bin.
A zip of the app folder, with the program and its resources folder.
A gzip tarball of the .app bundle.
On first launch the app prepares Sabine’s shared service and runtime if the machine has none,
registers itself like any installed app, and later updates from the same kind of archive. The
linux, windows and macos targets stage the same layouts for their platform, so you can build
them from another platform, and always pack them as gzip tarballs.
Linux packages
Section titled “Linux packages”deb, rpm and AppImage packages keep the program, its resources and anything bundled with it under
/usr/lib/sabine/<app id>, with a relative symlink in /usr/bin named after the executable. They
also install:
- a desktop entry at
/usr/share/applications/<app id>.desktop, withIcon=<app id>, the app’smime_types,generic_name,categoriesandkeywords, andNoDisplay=truefor apps withlisted = false - the icon in the hicolor theme under
/usr/share/icons/hicolor - AppStream metadata at
/usr/share/metainfo/<app id>.metainfo.xml(see AppStream) - a shared MIME package at
/usr/share/mime/packages/<app id>.xmlwhen the app lists file extensions for its document types
deb packages are named after the app id. Their dependencies come from dpkg-shlibdeps, which
reads the libraries your executable links, plus the desktop libraries Chromium needs (GTK 3, NSS,
NSPR, ALSA, CUPS, the X composite, damage and randr extensions, GBM, xkbcommon, udev and the
Wayland client library). dpkg-shlibdeps has to run on the Debian or Ubuntu release you are
packaging for, so build deb packages there.
rpm packages are named after the app id with dots replaced by hyphens. They require Chromium’s
desktop libraries by soname, such as libgtk-3.so.0()(64bit), so Fedora, openSUSE and other RPM
distributions resolve them from their own package names, and keep RPM’s automatic dependency
generation for your executable.
AppImage builds put an AppRun script, the desktop entry and the icon at the top of the AppDir,
and name the AppStream file <app id>.appdata.xml, as appimagetool expects.
Flatpak packaging is not part of sabine bundle.
Windows packages
Section titled “Windows packages”Windows builds link the C runtime statically and use the GUI subsystem, so the app needs no Visual C++ redistributable and opens no console. The app’s icon is written into the executable, so Explorer, the Start menu, the setup program, its windows and taskbar button all show it.
exe builds an NSIS setup wizard and msi a Windows Installer package. Both install for the
current user under %LOCALAPPDATA%\Programs\<app id> and prepare Sabine’s shared runtime during
setup. Installing describes what people
see.
MSI builds use WiX 7. Before building one on your own machine, review the
WiX EULA and accept it with wix eula accept wix7, then add
the UI extension with wix extension add --global WixToolset.UI.wixext. MSI versions are limited
by Windows Installer: the major and minor version must be 255 or lower and the patch 65535 or lower.
macOS bundles
Section titled “macOS bundles”The .app bundle has an Info.plist with the app id, name and version, a minimum system version of
macOS 12, the URL schemes and document types from mime_types and [app.extensions], and an
application category mapped from your first main category (for example Development becomes
public.app-category.developer-tools). Its icon is Contents/Resources/app.icns with standard and
Retina sizes. CFBundleVersion uses the numeric major.minor.patch of your version; the full
SemVer version, prerelease included, stays in the production manifest.
dmg packs the bundle in a compressed disk image named after the app.
Architectures
Section titled “Architectures”Packages take their architecture from the executable they contain:
- deb, rpm and AppImage read the ELF header and accept 64-bit x86_64 and aarch64 executables. A deb
is
amd64orarm64. - MSI and setup programs read the PE header and accept x86_64 and ARM64. Running
sabine bundleon an ARM64 Windows machine builds an ARM64 app. - macOS bundles require an Apple Silicon (aarch64) Mach-O executable, also when you pass one with
--binary.
Cargo builds for the target’s platform on your machine’s architecture: x86_64-pc-windows-msvc or
aarch64-pc-windows-msvc for Windows targets, x86_64-unknown-linux-gnu or
aarch64-unknown-linux-gnu for Linux targets built elsewhere, and aarch64-apple-darwin for macOS.
Staging works from any platform, but each package format needs its own tool, and the release
workflow builds every package on the platform it is for.
Offline bundles
Section titled “Offline bundles”--offline ships the machine’s Sabine service, CEF host and CEF runtime inside the bundle, so the
app starts without downloading anything:
sabine bundle . --target msi --offlineAn offline bundle must be built on the operating system and CPU architecture it is for, so the
service, host and runtime match the app; sabine bundle refuses otherwise. The runtime goes in
resources/runtimes/cef beside the production manifest (Contents/Resources/runtimes/cef on macOS).
On first launch the app adopts the bundled pieces into Sabine’s normal shared layout, and from then
on they update like any other installation.
Choosing what goes in
Section titled “Choosing what goes in”| Flag | Effect |
|---|---|
--debug |
Builds and packages a debug build instead of Cargo’s release profile |
--no-build |
Skips cargo build and packages the executable already in Cargo’s default output folder |
--binary <path> |
Packages this executable instead of building one |
--no-web-build |
Skips the web build command and packages the existing output |
--web-build <command> |
Runs this web build command instead of [web] build |
--web-root <path> |
Uses this folder as the web root instead of [web] root |
--web-dist <path> |
Packages this folder as the built web assets instead of [web] dist |
--id, --name, --version |
Overrides the app id, name or version for this build |
--env-file <path> |
Loads another environment file for the build; repeat it for several |
--out <path> |
Writes the bundle under this folder instead of dist |
--json |
Prints one line of JSON describing the result |
The web entry has to exist in the built output before staging, and the output folder may not overlap your web assets or contain the project itself.
Build environment
Section titled “Build environment”Bundle builds use the production environment. Sabine reads .env, .env.local, .env.production
and .env.production.local from the project and, when it differs, from the web root, then any
--env-file files, which are resolved from the project. Later files override earlier ones, and
variables already set in your shell win over all of them. The web build and Cargo see the values
with NODE_ENV and SABINE_ENV set to production, and without SABINE_DEV_URL, SABINE_APP_ID
and SABINE_MANIFEST_PATH. Cargo also gets SABINE_BUILD_TARGET set to
linux, windows or macos.
Environment files are build inputs only and are never copied into the bundle. Your frontend framework decides which of their values end up in the built pages.
A bundle built inside GitHub Actions updates from the repository’s releases unless [updates]
says otherwise, so it needs the update signing key in SABINE_UPDATE_SIGNING_KEY; see
Update settings.
App metadata
Section titled “App metadata”Packages describe your app with the [app] table of Sabine.toml, falling back to the Cargo
package where it can:
| Key | Used for | Fallback |
|---|---|---|
id |
Package names, install folders, desktop ids, the macOS bundle identifier | dev.sabine.<name> |
name |
Display name everywhere | The Cargo package name, with underscores as spaces |
version |
Package version; must be SemVer | The Cargo package version, including workspace values |
publisher |
The MSI manufacturer and the AppStream developer | The name part of the first Cargo author, then the app name |
maintainer |
The deb Maintainer field, in Name <email> form |
The first Cargo author with an email |
license |
The rpm License field and AppStream project license |
The Cargo license |
homepage |
The AppStream homepage | The Cargo homepage, then repository |
description |
The long description in software centers and of deb and rpm packages | The generic name, then the app name |
generic_name |
The desktop entry’s GenericName, the AppStream summary and the Windows shortcut description |
|
categories |
The desktop entry’s Categories and the macOS application category |
["Utility"] |
keywords |
The desktop entry’s Keywords and AppStream keywords |
|
icon |
Every icon the app ships | Found in the project, see Icons |
installer_image |
Artwork on the Windows setup pages |
Debian packages need a maintainer and RPM packages a license; the build stops with an error that
says so when one is missing. Sabine never puts in a placeholder publisher or email address. A
prerelease version such as 1.2.0-beta.1 becomes 1.2.0~beta.1 in deb and rpm metadata, so it sorts
before 1.2.0.
Write description as plain paragraphs separated by blank lines:
[app]id = "com.example.tern"name = "Tern"publisher = "Example"maintainer = "Ada Example <ada@example.com>"license = "MIT"homepage = "https://example.com/tern"generic_name = "Terminal"categories = ["System", "TerminalEmulator"]keywords = ["console", "shell"]description = """Tern is a terminal that keeps every session you open in tabs.
It restores them when you come back."""The manifest reference lists every key with its rules.
AppStream
Section titled “AppStream”deb, rpm and AppImage packages of apps that appear in launchers ship AppStream metadata so software
centers can show them: the name, the generic name as summary, the description, the publisher as
developer (with the app id’s reverse-DNS domain as developer id, such as com.example for
com.example.tern), the license, the homepage, keywords, document types, the executable and this
release. AppStream needs a homepage, so without one no metadata is written. Apps with
listed = false get none either. The release timestamp honours SOURCE_DATE_EPOCH.
icon names an SVG, or a PNG, JPEG, WebP, BMP or ICO image. Without it, Sabine uses the first of
these that exists in the project: static/icon.svg, static/favicon.svg,
src/lib/assets/favicon.svg, favicon.svg, icon.svg, icon.png, icons/icon.svg,
icons/icon.png, desktop/icons/icon.svg, static/icon.png, desktop/icons/icon.png.
From that one image, Sabine renders PNGs from 16 to 1024 pixels into a hicolor icon set (plus the
SVG itself as the scalable icon), a Windows .ico that it embeds in the executable, and the macOS
app.icns. SVGs are rendered once and reused for every size, and images that aren’t square are
centered on a transparent square.
Setup artwork
Section titled “Setup artwork”Windows setup programs show your own artwork on their welcome and finish pages when
installer_image names a PNG, JPEG or WebP image:
[app]installer_image = "installer.png"The image is scaled to cover the space and cropped from its center: 164×314 pixels down the side of the NSIS pages, and the left 164 pixels of the MSI dialogs, which keep their white text area. A portrait image of about that shape, at twice the size, crops least. Without it the installers keep their default artwork.
Where bundles land
Section titled “Where bundles land”Each target stages under dist/<target>/<app id>/ and writes its package to dist/artifacts/,
named <app id>-<version>-<target>.<extension>. A deb build of com.example.tern 1.0.0 with an
executable named tern looks like this:
Directorydist/
Directoryartifacts/
- com.example.tern-1.0.0-deb.deb
Directorydeb/
Directorycom.example.tern/
Directoryroot/
DirectoryDEBIAN/
- control
Directoryusr/
Directorybin/
- tern a relative symlink to the program
Directorylib/
Directorysabine/
Directorycom.example.tern/
- tern
Directoryresources/
- Sabine.toml the production manifest
Directoryweb/ your built web assets
- …
Directoryfiles/
[resources]- …
Directoryicons/
- …
Directoryshare/
Directoryapplications/
- …
Directoryicons/
- …
Directorymetainfo/
- …
Directorymime/
- …
linux, deb and rpm stage the tree above under root/; appimage stages it under
AppDir/ with AppRun beside it.
The app folder is named after the app, with characters other than letters, digits, ., -
and _ replaced by -: dist/msi/com.example.tern/Tern/tern.exe, with
resources/Sabine.toml, resources/windows-app.ico and the web assets beside it.
The bundle is dist/dmg/com.example.tern/Tern.app, with the executable in Contents/MacOS and
the production manifest, icon and web assets in Contents/Resources.
The executable is named after the Cargo package, with .exe on Windows. Bundling again replaces
the staged folder.
JSON output
Section titled “JSON output”With --json, sabine bundle prints a single line that scripts can read:
{"ok":true,"target":"deb","app":{"id":"com.example.tern","name":"Tern","version":"1.0.0"},"path":"/home/ada/tern/dist/deb/com.example.tern","executable":"com.example.tern/root/usr/lib/sabine/com.example.tern/tern","artifacts":["/home/ada/tern/dist/artifacts/com.example.tern-1.0.0-deb.deb"]}executable is the program’s path inside an archive of the staged folder, which the
release workflow records in the update manifest.