Project

crosspack

0.0
The project is in a healthy, maintained state
The build matrix in crosspack.yml helps you to build and deliver your program without pain — one config drives a staged pipeline (deps → build → pack) that produces native packages for every platform: deb, rpm, PKGBUILD, winget and cask.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 1.0
 Project Readme

crosspack

crosspack

Painless packaging for any platform.

Cross-platform build and pack toolchain for Wails-style desktop apps, shipped as one gem, one command, one config file and a staged pipeline:

crosspack deps <target>     # verify/install the build host's dependencies
crosspack build <target>    # compile the target's matrix entry into builds/
crosspack pack <target>     # package builds/<target> as a native package

Every stage is keyed by the same target vocabulary — debian-12, ubuntu-24.04, fedora-41, arch, macos, windows-11.0 — and gated by the previous one: build refuses to start before the deps stage passes, and pack refuses to run before build filled builds/<target>/. The build stage also stamps the version into the tree, so pack normally needs no --version:

crosspack deps ubuntu-24.04
crosspack build ubuntu-24.04
crosspack pack ubuntu-24.04

The target may be omitted — the stage commands then run for the current host: macos, windows-11.0 or the distro family + version from /etc/os-release (unknown ids fall back to the first known ID_LIKE token):

crosspack deps && crosspack build && crosspack pack

crosspack.yml — one file, three sections + two host hooks

The top level carries the shared facts — name (required once) and the optional version scheme — and three sections: build: (what and where to build), package: (what and how to pack) and deps: (runtime dependencies resolved to concrete packages per distro). Two optional hooks — run: and install: — override the host-only launch commands. The shared name/version feed the build section, the name feeds the package section; each section keeps its own schema.

name: myapp
version: calver            # calver | git-tag | env:VAR | any literal string

build:
  output: builds           # default builds; mirrors package `sources`

  deps:                    # build dependencies, per-host verify/install
    imagemagick:
      hosts:               # first selector matching this host wins
        ubuntu:
          verify: dpkg -s imagemagick
          install: sudo apt-get install -y imagemagick
        darwin:
          verify: magick -version
          install: brew install imagemagick
        windows:
          verify: where magick
          install: winget install -e --id ImageMagick.ImageMagick
        "*":               # any command — package manager or a project script
          install: ./scripts/install-imagemagick.sh

  matrix:
    - build: linux/amd64   # os/arch this entry builds on (arch may be "any")
      env:                 # extra env for the steps of this entry
        CGO_ENABLED: "1"
      steps:               # shell commands, run from the project root
        - wails build -platform linux/amd64 -ldflags "-X myapp/internal/app.appVersion={{version}}"
        - cargo build --release -p helper --locked
      artifacts:
        from: build/bin    # what got built
        include: [myapp, helper, "*.onnx", "*.so*"]   # default: ["*"]
        to: [ubuntu-22.04, ubuntu-24.04, debian-12, debian-13]
        mode: symlink      # symlink | copy, default symlink

    - id: windows          # defaults to the build platform string
      build: windows/amd64
      steps:
        - wails build -platform windows/amd64 -ldflags "-X ...={{version}}"
      artifacts:
        from: build/bin
        include: [myapp.exe]
        to: [windows-11.0]

package:
  maintainer: Your Name <you@example.com>
  summary: MyApp - cross-platform desktop application   # optional
  description: |-
    MyApp - cross-platform desktop application.
    Bundles helper binaries and model assets alongside the GUI...
  license: Proprietary       # optional, default Proprietary
  section: utils             # optional, deb only

  sources: builds             # compiled artifacts tree written by the build
                              # stage: builds/<family>/<version>/<arch>/ —
                              # pack without a matching build dir fails honestly
  prefix: usr/local           # install prefix inside the package
  lib_dir: lib/myapp          # optional, default lib/<name>

  payload:                    # files from sources -> <prefix>/<lib_dir>/
    - myapp                   # flat form: the same name on every target
    - helper
    - model.onnx

  # Mapping form: per-target file names. The key is the common name (and the
  # default file name); selectors are an exact target (windows-11.0), a
  # family (windows), an os (linux — every distro family) or "*" — exact
  # target wins, then family, then os, then "*". An entry with selectors
  # applies only to targets it covers, so .so libraries and Windows DLLs can
  # share one payload:
  #
  # payload:
  #   myapp:
  #     "*": myapp
  #     windows: myapp.exe
  #   libonnxruntime.so.1:
  #     linux: libonnxruntime.so.1
  #   onnxruntime.dll:
  #     windows: onnxruntime.dll

  executables: [myapp, helper]     # common keys; chmod 755, resolved per target

  links:                     # symlinks relative to prefix
    bin/myapp: ../lib/myapp/myapp

  desktop:                   # optional, .desktop generated
    name: MyApp
    comment: Cross-platform desktop application
    categories: Utility;Audio

  icon: build/appicon.png    # optional, -> <prefix>/share/pixmaps/<name>.png

deps:
  # canonical dependency names resolved to concrete packages per distro
  # family/version; an optional `version:` declares the deps schema version
  webkit2gtk:
    targets:
      debian:
        "12": [libwebkit2gtk-4.1-0, libwebkit2gtk-4.0-37]  # deb: a | b
      fedora: { "*": [webkit2gtk4.1] }
      macos: system      # OS component
      windows: system    # preinstalled (WebView2)

# host-only launch hooks (optional): same host selectors as build.deps
run:                       # `crosspack run`: how to launch the built binary
  hosts:
    darwin: open build/bin/MyApp.app     # .app bundles must be opened, not exec'd

install:                   # `crosspack install`: how to launch this host's package
  hosts:
    ubuntu: sudo apt-get install -y "{{path}}"

Placeholders {{version}}, {{name}}, {{platform}}, {{os}}, {{arch}} and {{id}} are expanded by the gem itself (works the same under POSIX shells and cmd.exe); the same facts are exported as CROSSBUILD_VERSION, CROSSBUILD_NAME, ... environment variables. Steps may freely call existing rake tasks (rake build:prepare) or helper scripts.

Artifacts land in the shared layout, e.g.:

builds/ubuntu/24.04/amd64/myapp -> build/bin/myapp   (symlink)
builds/windows/11.0/x86_64/myapp.exe

Build dependencies (build.deps)

build.deps is the host axis to the top-level deps: section's target axis: the deps: section declares what a package needs at runtime; build.deps declares what the build host must have. Checks differ per system, so every host rule carries its own commands: verify (presence probe; exit 0 = present; without it the name is looked up in PATH) and install (how to install it; may be omitted for verify-only rules). Selectors are tried in order: distro id from /etc/os-release (ubuntu), then ID_LIKE tokens (debian), then the os (linux, darwin, windows), then "*". The deps stage runs verify → install → re-verify for every dependency; a dependency that is still missing fails the stage. Commands support the {{name}}/{{os}}/... placeholders.

Version schemes

scheme value
calver YYYY.MM.DD-<secs since local midnight>, e.g. 2026.08.31-33837 (default)
git-tag latest git describe --tags --abbrev=0, fallback 0.0.0-dev
env:VAR taken from VAR; build fails loudly when unset
other string used verbatim, e.g. version: 1.2.3

The whole file is validated against embedded schemas with actionable, path-pointing error messages; every stage refuses to run on an invalid config.

CLI

# staged pipeline
crosspack deps <target> [--check]        # verify/install build deps (--check: report only)
crosspack build <target>|--all [--no-deps] [--version X]
crosspack pack <target> [--version X]    # version defaults to the build stamp

# host-only: this machine, no target argument
crosspack run [-- --dev]                 # build for this host, then launch the main binary
crosspack install                        # launch this host's package with its installer

# inspection
crosspack validate                       # crosspack.yml: top level + all sections
crosspack resolve <target>               # Depends line for the target
crosspack matrix                         # deps x target + build matrix tables
crosspack version                        # computed build version
crosspack targets                        # what is in builds/ ready to pack

# options
-f, --file PATH                          # path to crosspack.yml (default: ./crosspack.yml)

pack picks the builder from the target and writes to crosspacks/<family>/<version>/<arch>/:

crosspacks/ubuntu/26.04/amd64/myapp_2026.08.31-1234_amd64.deb
crosspacks/fedora/41/x86_64/myapp-2026.08.31-1234.x86_64.rpm   (needs rpmbuild)
crosspacks/arch/x86_64/PKGBUILD

Host-only commands: run and install

run and install apply to the current host only — the build target is the run target, so they take no target argument. crosspack run runs the build stage for this host (same as crosspack build <host target>), then launches the first package.executables binary straight from builds/ — the binary, not the package. Everything after -- goes to the app:

crosspack run -- --dev

crosspack install takes the newest package packed for this host in crosspacks/ and hands it to the system installer. Defaults: xdg-open for deb/rpm (opens GNOME Software/installer), msiexec /i for the MSI, open for a macOS dmg/.app; for arch crosspack only generates a PKGBUILD, so it points you at makepkg -si instead.

Both are per-system by nature, so each accepts a hosts: override with the same selectors as build.deps (distro id, ID_LIKE, os, "*" — first match wins). Commands support the {{path}} placeholder (the binary in builds/ for run, the package file for install) plus the usual {{name}}/{{version}}/{{os}}/{{arch}} facts; quote values that start with {{ (YAML flow indicators). Without a run: section the binary is executed directly, except a macOS .app bundle which is opened.

Library

require 'crosspack'   # pulls in Crossbuild too

Crossbuild.build('crosspack.yml', root: Dir.pwd)            # all host entries
Crossbuild.build('crosspack.yml', target: 'ubuntu-24.04')   # the stages' build, library-level

Crosspack.pack(
  config: 'crosspack.yml',
  target: Crosspack::Target.parse('debian-12', arch: 'amd64'),
  version: nil,        # nil -> the version stamped by the build stage
  root: Dir.pwd, output_base: 'crosspacks'
)

Crosspack::Config.load('crosspack.yml') parses and validates the file once and exposes the sections as manifests. Lower-level pieces are public too: on the build side Crossbuild::BuildManifest, VersionScheme, Runner, Distributor, Matrix, Builder, DepInstaller, Platform; on the pack side Crosspack::PackageManifest, Manifest, Resolver, Matrix, Target, Builds, Builders::*.

Deb needs dpkg-deb (present on any Debian/Ubuntu), rpm needs rpmbuild (sudo apt install rpm), PKGBUILD generation needs nothing. The WiX builder generates .wxs sources (run the WiX Toolset yourself to get an MSI), the macOS builder stages the .app bundle (signing, notarization and .dmg creation are out of scope). Requires Ruby >= 3.2.

Tests

cd crosspack && rake test