Skip to content

cargo-packager (Rust / Node.js)

A cross-platform, framework-neutral application packager that turns already-built executables into native desktop bundles and installers, with a separate embedded updater and resource resolver.

FieldValue
LanguageRust core; Node.js bindings and plugin layer in TypeScript
LicenseApache-2.0 OR MIT
Repositorycrabnebula-dev/cargo-packager
DocumentationRust crate documentation · configuration schema
Version at reviewed HEADcargo-packager 0.11.8; updater 0.2.3
Reviewed source37a538e76608b33eaa3f36f7c57b30b284dfa5a9 (March 21, 2026)
CategoryApplication packager and updater; not a control plane and not merely a format primitive
Commercial modelOpen source; the repository and these components are not paid products
Interfacescargo packager, Rust library, N-API Node.js library/CLI
Host/targetsmacOS: .app, .dmg; Linux/BSD hosts: .deb, .AppImage, Pacman payload; Windows: NSIS .exe, WiX .msi

Last reviewed: July 12, 2026

IMPORTANT

Classification: cargo-packager is an OSS app packager plus an optional in-application updater. It is not a release control plane: it has no hosted build fleet, artifact registry, rollout dashboard, account model, or release database. It is also not a format primitive like tar, ar, WiX, NSIS, or hdiutil; it orchestrates those primitives and generates their metadata. The CrabNebula company offers other services, but no paid service is required by the source reviewed here.


Overview

What it solves

cargo-packager gives desktop applications a shared configuration and API over seven otherwise unrelated packaging paths. Its scope begins with compiled executables and resources and ends with local package files. It deliberately does not own compilation:

“By default, the packager doesn't build your application”

README.md. A beforePackagingCommand hook can invoke any build system, but the separation is architectural, not just a default. This lets the examples package Rust, Deno, Electron, Wails, Slint, Dioxus, egui, and Wry applications through the same backend (examples/).

The companion updater closes part of the post-install lifecycle. An application embeds cargo-packager-updater, queries developer-controlled HTTP endpoints, compares SemVer, downloads an artifact, verifies a Minisign signature, and replaces or invokes the installed application according to the platform (crates/updater/src/lib.rs). The packager itself neither hosts those endpoints nor publishes artifacts.

Design philosophy

The design is a thin common model with format-specific escape hatches:

  1. Config describes shared identity, binaries, resources, icons, associations, protocols, hooks, and output location, then embeds macos, windows, deb, appimage, pacman, nsis, wix, and dmg sub-configurations (config/mod.rs).
  2. package() normalizes default/all, sorts formats by dependency priority, runs hooks, creates one clean intermediate context, and dispatches each target to a dedicated backend (package/mod.rs).
  3. Backends either write the format directly (.deb, Pacman staging, .app) or render templates and invoke established platform tooling (linuxdeploy, NSIS, WiX, create-dmg) (package/ source tree).
  4. The Rust API exposes the same Config, package, package_and_sign, and PackageOutput abstractions as the CLI; Node.js wraps that API and adds a plugin merge step (lib.rs, src-ts/index.ts).

This is intentionally local-first tooling. The developer selects where CI runs, where artifacts are stored, how updates are segmented, and what release page or repository advertises them.

How it works

The high-level execution graph is:

text
config discovery
    -> Cargo metadata/default enrichment
    -> CLI overrides
    -> beforePackagingCommand
    -> clean <outDir>/.cargo-packager staging context
    -> [beforeEachPackageCommand -> target backend]...
    -> native code signing/notarization where configured
    -> optional Minisign sidecar signatures
    -> PackageOutput { format, paths }

detect_configs() accepts a raw JSON object/array, an explicit TOML/JSON path, all case-insensitive **/packager.{toml,json} files, and every Cargo workspace package with [package.metadata.packager] (cli/config.rs). Cargo-backed configuration is enriched from cargo metadata: package name, product name, version, authors, description, license file, target directory/profile, identifier, and binary targets are filled when absent. Standalone config is not merged with Cargo metadata; it must be complete itself.

package() expands platform defaults, sorts Dmg after App, executes the one-shot hook with CARGO_PACKAGER_FORMATS, then executes the per-format hook with both CARGO_PACKAGER_FORMATS and CARGO_PACKAGER_FORMAT (package/mod.rs). A DMG implicitly builds an .app, consumes it, and removes the temporary app if app was not explicitly requested. All selected formats share one Context; its <outDir>/.cargo-packager intermediates directory is deleted and recreated, while external tools persist under the user cache directory's .cargo-packager (context.rs).

PackageOutput preserves one format and one-or-more paths—for example, WiX can emit one MSI per configured locale—until the CLI flattens and prints paths. Library users retain the format association and can split packaging from signing (package/mod.rs, lib.rs).


Analysis dimensions

Input / staging

The primary inputs are prebuilt main/additional executables, target-qualified sidecar binaries, resource files or globs, icons, framework-specific files, and declarative metadata. Binary.path resolves under binariesDir (falling back to outDir), while absolute paths remain absolute. Each app must identify one main binary (config/mod.rs). In Cargo mode, binary targets are auto-detected and the only target—or the one matching the package name—is marked main (cli/config.rs).

Resources have two forms:

  • a string path/glob, retaining its basename or recursively retaining a directory subtree; or
  • { src, target }, allowing controlled relocation inside the target's resource root.

Mapped targets are sanitized to normal path components before joining, preventing absolute or parent components from escaping the package resource root. Source traversal uses walkdir, and copies are materialized rather than linked (config/mod.rs). External binaries use a convention rather than a manifest: configure sqlite3, provide sqlite3-<target-triple>[.exe], and the staged name becomes sqlite3[.exe].

Staging is backend-specific beneath <outDir>/.cargo-packager:

BackendStaging mechanics
.appConstructs Contents/{MacOS,Resources,Frameworks}, generates/merges Info.plist, and preserves copied framework symlinks (app/mod.rs)
.debBuilds data/ and control/, writes desktop/icon trees, control, and md5sums, then emits the three-member ar archive (deb/mod.rs)
AppImageReuses Debian filesystem staging, constructs an AppDir, renders a shell script, and runs downloaded linuxdeploy tooling (appimage/mod.rs)
PacmanReuses Debian filesystem staging, creates a payload .tar.gz, and writes a sibling PKGBUILD with SHA-512 (pacman/mod.rs)
NSISRenders UTF-16LE NSIS scripts plus resources/languages, then runs makensis (nsis/mod.rs)
WiXRenders .wxs/.wxl, compiles with Candle, links with Light, and repeats linking per locale (wix/mod.rs)
DMGStarts from the generated .app, downloads create-dmg, renders support files, and invokes the script (dmg/mod.rs)

There is no sandbox: hooks and backend tools inherit filesystem/network access, and hooks are arbitrary shell commands. Config-relative execution is convenient—the CLI changes to the config file's parent—but also means multiple configs run serially while mutating the process working directory (cli/mod.rs).

Outputs / targets

PackageFormat::platform_default() selects .app + .dmg on macOS, NSIS on Windows, and .deb + AppImage + Pacman on Linux/BSD-family compile targets. all adds WiX on Windows; explicit formats can narrow the set (utils/src/lib.rs). Backends are compile-time host-gated except NSIS, which can use host makensis away from Windows. Cross-target metadata uses targetTriple, but this is not general cross-packaging: AppImage explicitly depends on same-platform linuxdeploy, WiX is Windows-gated, and macOS signing/notarization requires Apple tools.

Logical targetOutput naming / contentsInstallation authority
macOS app<Product Name>.appFinder copy or other distributor; no installer transaction
macOS disk image<binary>_<version>_<arch>.dmgUser drags app from mounted DMG
Debian<binary>_<version>_<deb-arch>.debdpkg/APT owns files and removal
AppImage<binary>_<version>_<arch>.AppImagePortable executable; user chooses location
Pacman path<binary>_<version>_<arch>.tar.gz plus PKGBUILDmakepkg/pacman only after downstream package build
NSIS<binary>_<version>_<arch>-setup.exeGenerated NSIS installer/uninstaller
WiX<binary>_<version>_<arch>_<locale>.msiWindows Installer

The Pacman .tar.gz is a staged source payload, not a finished .pkg.tar.zst; the sibling PKGBUILD copies it into ${pkgdir} and declares dependencies, conflicts, provides, replaces, source, and SHA-512 (pacman/mod.rs). That distinction makes cargo-packager a generator feeding the Arch format primitive rather than a complete repository-ready Pacman build.

Metadata / dependencies

Common metadata fans out into native fields: identifier, product name, SemVer, description, long description, homepage, authors, publisher, license, copyright, category, icons, file associations, and deep-link schemes (config/mod.rs). Examples include:

  • .app: CFBundleIdentifier, display/executable names, version, category, document types, URL schemes, minimum OS, background-app flag, and optional custom plist merge (app/mod.rs);
  • Debian: package/version/architecture/installed size, maintainer, section, priority, homepage, Depends, and folded descriptions (deb/mod.rs);
  • WiX/NSIS: manufacturer, identity, installer version, shortcuts, associations, protocols, languages, license UI, downgrade policy, and generated upgrade identity (wix/mod.rs, nsis/mod.rs);
  • Pacman: depends, provides, conflicts, and replaces (pacman/mod.rs).

Dependency declaration is deliberately format-local. Debian and Pacman accept either a list or a newline-delimited file. AppImage instead bundles selected host libraries and binaries through linuxdeploy; macOS copies named/path frameworks but explicitly leaves link flags and rpath correctness to the application (config/mod.rs). NSIS/WiX contain files but do not model a package-manager dependency solver. There is no cross-platform dependency graph, lockfile for bundled runtime libraries, license inventory, SBOM, or automatic runtime dependency inference shared by all targets.

The checked-in JSON Schema is generated from Rust types and supports editor validation, while deny_unknown_fields rejects misspelled keys in most structures (schema.json, config/mod.rs). The CLI also accepts kebab-case and snake_case aliases for many fields, although serialization is camelCase.

Install / upgrade / uninstall

cargo-packager generates lifecycle behavior but does not provide a universal install, upgrade, or uninstall command. Native artifacts delegate those actions:

  • Debian package metadata and file ownership let dpkg/APT install, replace, and remove the application. No maintainer scripts are generated by this backend (deb/mod.rs).
  • WiX emits a stable upgrade code and Windows Installer product; NSIS supports current-user, per-machine, or chooser modes. Both can reject downgrades when windows.allowDowngrades is false (wix/mod.rs, config/mod.rs).
  • The NSIS template registers uninstall metadata and creates an uninstaller. Optional nsis.appdataPaths makes uninstall offer a disabled-by-default checkbox for deleting application data (installer.nsi, config/mod.rs).
  • .app, DMG, and AppImage have no package database in cargo-packager. Their ordinary uninstall is deletion; upgrades are replacement or the companion updater.
  • Pacman lifecycle semantics apply only after PKGBUILD is built and installed by standard Arch tooling.

The updater supports only .app, AppImage, NSIS, and WiX—not DMG, Debian, or Pacman. On Windows it writes a temporary .exe/.msi, starts NSIS or msiexec, and exits; WiX attempts to relaunch the current app. On Linux it requires an existing AppImage, chooses a temporary directory on the same device, renames the old image to a backup, writes the verified new bytes with old permissions, and restores the backup if writing fails. On macOS it extracts a tar-gzipped app into a temporary directory, moves the old bundle aside, then renames the replacement or asks for authorization through AppleScript (updater/src/lib.rs). These are replacement strategies, not a transactional cross-platform package manager; Windows success is delegated to a spawned installer, and rollback guarantees differ by OS.

Signing / trust

There are three distinct trust layers:

  1. Native platform signing. macOS finds nested Mach-O/framework targets, signs inside-out with hardened runtime and timestamping, signs the app, submits it with xcrun notarytool, and staples an accepted ticket. CI can import a base64 PKCS#12 into a temporary keychain. Windows signs binaries/installers with SDK signtool.exe by certificate thumbprint, digest, and optional timestamp URL, or runs a custom %1 command such as osslsigncode (codesign/macos.rs, codesign/windows.rs).
  2. Updater artifact signing. sign_outputs() creates Minisign .sig sidecars. Directory outputs such as .app are first archived as .tar.gz. The CLI reads the private key from CARGO_PACKAGER_SIGN_PRIVATE_KEY, a file, or an inline value, and the password from CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD (lib.rs, sign.rs, cli/mod.rs).
  3. Update verification. The app embeds a base64-encoded Minisign public key. Every downloaded byte sequence is fully buffered and verified before installation; signature failure aborts the update (updater/src/lib.rs).

Minisign authenticates the artifact but not the release manifest: endpoint JSON is accepted over whatever URL the application configured, and the code does not require HTTPS. An attacker who cannot sign an artifact cannot pass verification, but endpoint metadata still controls availability, notes, version, URL, and format. Key rotation is not modeled; applications must ship new updater configuration/public keys themselves. The native and updater signatures are independent—platform trust does not replace the embedded Minisign policy, nor vice versa.

Publication / discovery

The tool itself is discoverable as the cargo-packager crate/cargo subcommand and as @crabnebula/packager on npm (README.md, bindings/packager/nodejs/README.md). Generated applications are not published anywhere automatically. There is no GitHub Releases uploader, package repository client, App Store submission, Microsoft Store submission, Homebrew formula, APT repository metadata, Arch repository database, CDN, release index, or artifact retention policy in the packager API.

Publication is therefore an explicit downstream CI step: upload PackageOutput.paths and .sig files, construct the updater JSON, and expose discovery through a website, store, OS repository, or update endpoint. This is the clearest boundary between cargo-packager and a release control plane.

Updates / channels

Updater::check() substitutes {{target}}, {{arch}}, and {{current_version}} in each configured endpoint, trying endpoints sequentially. A 204 means no update; a successful JSON response can describe one dynamic target or a static platforms["<os>-<arch>"] map. Default selection is strictly remote.version > current_version, with a caller-supplied comparator available for custom policy (updater/src/lib.rs, updater README).

Channels are not first-class objects. Stable/beta/nightly segmentation must be encoded in endpoint URLs, headers, manifests, or the custom comparator. The library has no staged rollout percentage, cohort assignment, mandatory update, minimum supported version, delta update, background scheduler, resumable download, or server component. It does support multiple fallback endpoints, custom request headers, timeouts, release notes/dates, progress callbacks, and installer arguments.

The manifest signature is the complete base64 .sig content, not merely a checksum; a signature may change each build and publication must keep artifact and manifest in sync (updater README). The updater downloads into memory before verification and installation, so update size contributes directly to application memory use.

Automation / CI

The CLI is automation-friendly: config files are source-controlled, output paths are predictable, --formats, --packages, --out-dir, --binaries-dir, --profile, --target, and environment-backed signing inputs make a CI matrix straightforward (cli/mod.rs). beforePackagingCommand builds once; the per-format hook is better when format-sensitive compile-time resource resolution is required. Hook environment variables bridge the build and package phases (package/mod.rs, resource resolver).

Upstream CI demonstrates the intended matrix:

  • Rust and Node integration tests run on Ubuntu, macOS, and Windows (integration-tests.yml);
  • example packaging runs the same three hosts, installs language/framework tools, generates a Minisign key, then requests --formats all (build-examples.yml);
  • formatting, Clippy, unit tests with all features, and cargo-deny are separate checks (check.yml).

This is test automation, not turnkey consumer release automation. The repository has no reusable GitHub Action for package/sign/upload/update-manifest orchestration. Consumers must provision native SDKs and secrets, and typically need one job per host.

Supply chain / reproducibility

The project checks in Cargo.lock, and installation guidance uses cargo install cargo-packager --locked, which pins the packager's Rust dependency resolution (README.md, Cargo.lock). Upstream runs cargo-deny, and source/tool downloads use TLS by default. Several backend tools are cached outside the output tree, reducing repeat downloads.

Artifact reproducibility is nevertheless not a stated or achieved invariant:

  • .app writes CFBundleVersion from the current UTC timestamp (app/mod.rs);
  • WiX assigns random v4 UUIDs to resource/additional-binary components, while only selected package identities use deterministic v5 UUIDs (wix/mod.rs);
  • Minisign's trusted comment includes the current Unix timestamp (sign.rs);
  • Debian tar headers use source file mtimes, despite deterministic tar header mode (deb/mod.rs);
  • code signing and notarization introduce external timestamps/services;
  • hooks can perform arbitrary nondeterministic builds.

Downloaded-tool integrity is uneven. WiX is pinned to a release URL and SHA-256; NSIS and one plugin use SHA-1, but the NSIS ApplicationID archive is downloaded without an expected hash. AppImage downloads AppRun, linuxdeploy, and a plugin from URLs without hash verification, including a mutable continuous release URL. User-supplied linuxdeployPlugins are also fetched and executed without a configured checksum (wix/mod.rs, nsis/mod.rs, appimage/mod.rs). The tool cache persists those bytes until absent or backend-specific validation triggers.

No SBOM, provenance statement, SLSA attestation, package content manifest, dependency license report, or reproducible-build metadata is emitted. Debian's md5sums and Pacman's generated SHA-512 protect/identify package contents in their native workflows; they are not build provenance. For a hardened pipeline, downstream CI should pre-pin or mirror tools, isolate hooks, hash all fetched inputs, generate SBOM/provenance, and sign those attestations alongside packages.

Extensibility / UX

The Rust crate can be embedded with default features disabled, and its builder/types provide programmatic configuration. The Node N-API binding exposes packageApp, packageAndSignApp, and CLI invocation. Its TypeScript layer runs plugins, deep-merges their config with caller config, then passes JSON into Rust (Cargo.toml, src-ts/index.ts). The bundled Electron plugin prunes development dependencies before packaging (plugins/electron).

Format customization is deliberately asymmetric:

  • WiX accepts a complete template, inline/path fragments, references, merge modules, localization, FIPS mode, and UI art;
  • NSIS accepts a complete template, pre-install sections, custom language files, compression, modes, and UI art;
  • Debian accepts a desktop template and arbitrary source-to-package file mappings;
  • DMG exposes layout/background controls;
  • AppImage exposes bundled libs/binaries/files, excluded libraries, and arbitrary downloaded linuxdeploy plugins;
  • macOS accepts frameworks, entitlements, custom Info.plist, provisioning profile, and embedded apps (config/mod.rs).

There is no stable backend plugin trait in Rust: PackageFormat and dispatch are a closed internal match, so adding Flatpak, RPM, Snap, PKG, or a custom format requires a fork/upstream change. Hooks can prepare inputs but cannot register a new output type. The Node plugin layer composes configuration rather than implementing package formats.

UX strengths include one schema across ecosystems, auto-detection from Cargo metadata, multi-app arrays/workspaces, glob resources, human-readable tracing, and format-specific escape hatches. UX sharp edges include the typoed --quite flag in the reviewed CLI, network downloads during packaging, host-tool requirements, no dry-run/package-plan output, no manifest describing all emitted files, and configuration whose portable surface obscures substantial host/format asymmetry (cli/mod.rs).


Strengths

  • Broad native output coverage behind one model: seven output paths across the three desktop OS families, without tying packaging to a GUI framework.
  • Real native semantics: desktop integration, associations, protocols, native dependencies, installer modes, localization, code signing, notarization, and uninstall metadata are delegated to or rendered for standard platform tooling.
  • Library-first reuse: Rust and Node.js callers can integrate packaging without shell-output scraping; PackageOutput retains format/path relationships.
  • Useful separation of concerns: compilation is an explicit hook, publication is downstream, and updates are an optional application dependency rather than hidden behavior in every installer.
  • Layered trust support: Apple/Windows platform signing plus Minisign artifacts and mandatory updater verification cover both OS reputation and app-controlled updates.
  • Escape hatches where formats demand them: full WiX/NSIS templates, WiX fragments, custom signing, Debian files/templates, and AppImage plugins prevent the common model from becoming a hard ceiling.
  • Good ecosystem portability: examples prove the packager is useful beyond Rust applications.

Weaknesses

  • Not a release control plane: no publication, stores/repositories, hosted update service, channels/rollouts, artifact inventory, or release state machine.
  • Host-dependent and only partly cross-compilable: serious releases still require macOS, Windows, and Linux jobs plus native tools and credentials.
  • Weak reproducibility: wall-clock values, random UUIDs, source mtimes, signatures, arbitrary hooks, and external services make byte-identical rebuilds unlikely.
  • Inconsistent tool-download verification: mutable/unhashed executable downloads in AppImage and NSIS paths are a material supply-chain risk.
  • Updater is intentionally narrow: only four formats, full in-memory downloads, no deltas/resume/key rotation/manifest authentication/rollout model, and OS-specific rollback behavior.
  • No backend extension API: adding formats requires changing internals; Node plugins only generate/merge configuration.
  • Metadata/dependency abstraction is shallow: no shared dependency discovery, SBOM, provenance, package-content inventory, or license closure.
  • Lifecycle semantics vary sharply: .deb/MSI/NSIS have package-manager or installer ownership, while .app/AppImage are replacement-by-file and Pacman output still needs makepkg.

Key design decisions and trade-offs

DecisionRationaleTrade-off
Package prebuilt binaries rather than own compilationSupports any language/framework and keeps build policy externalHooks are unsandboxed; binary/resource completeness is the caller's responsibility
One common Config with nested backend configsMakes multi-target metadata approachable without hiding native controlsThe apparent uniformity masks target-specific meaning and requirements
Use native generators/tools where practicalReuses mature installer semantics, UI, signing, and OS integrationRequires host-specific CI and introduces downloaded-tool supply-chain risk
Directly implement .deb, app bundle, and Pacman stagingReduces external dependencies and gives precise layout controlMust track evolving format policy; Pacman output is not a final installable package
Clean intermediate output but persist tool cacheAvoids stale staging while accelerating subsequent buildsCache provenance is not recorded, and not every cached executable is hash-verified
Treat publishing as downstreamKeeps the project local, OSS, and provider-neutralEvery consumer must assemble upload, discovery, retention, and manifest generation
Separate native signing from Minisign update signingMeets platform trust requirements while supporting app-controlled updatesTwo key systems and publication paths must be operated correctly
Require signature verification before updater installationPrevents unsigned update payload execution even if hosting is compromisedPublic-key rotation and signed manifests are not built in; downloads are fully buffered
Model channels through endpoints/custom comparisonKeeps updater protocol small and server-agnosticNo explicit channels, cohorts, phased rollout, mandatory versions, or downgrade policy
Expose templates/fragments/hooks instead of a backend plugin ABIProvides practical customization without stabilizing internal traitsNew package formats still require source changes or an external wrapper
Auto-enrich Cargo metadata but accept standalone TOML/JSONExcellent Rust ergonomics while remaining language-neutralBehavior differs by configuration source; standalone users supply more metadata
Generate per-format native lifecycle behaviorPreserves standard OS installation and uninstall expectationsUpgrade/rollback guarantees cannot be uniform across formats

Sources