PACKAGING_GUIDE.md 12 KB

USM Packaging Guide

Best practices for writing MANIFEST.usm files and creating packages that install correctly across platforms.


The manifest

{
    "name": "mylib",
    "version": "1.2.3",
    "summary": "A concise one-line description",
    "licences": ["MIT"],
    "flags": ["ninjaStyleProgress"],
    "provides": { "lib:libmylib.so": "as-expected" },
    "depends": {
        "runtime": ["lib:libglib-2.0.so.0"],
        "build": ["bin:valac", "pc:glib-2.0.pc"],
        "manage": ["bin:bash"]
    },
    "execs": {
        "build": "usm-scripts/build.sh",
        "install": "usm-scripts/install.sh"
    }
}

Naming

  • Package names are lowercase kebab-case (invercargill-sql, not InvercargillSql)
  • Match the meson project name
  • The version comes from the meson project version — keep them in sync

Summary

One line, lowercase, no trailing period. This is what usm search shows.


Declaring dependencies

The most common mistake: trusting ldd

ldd shows what the linker resolved at load time on this specific machine. It is not a complete picture of what your program needs, and it often shows things you don't actually depend on:

ldd says Reality Declare it?
libselinux.so.1 Indirect via glib — only needed on SELinux systems No — it's a transitive dep of glibc/glib, not yours
libgcrypt.so.20 Indirect via glib (gcrypt backend) No — same reason
liblzma.so.5 Indirect via libarchive No — if you depend on lib:libarchive.so, its own manifest declares lzma
libglib-2.0.so.0 You linked against it directly Yeslib:libglib-2.0.so.0
libjson-glib-1.0.so.0 You linked against it directly Yeslib:libjson-glib-1.0.so.0
(missing from ldd) bin:valac Compile-time tool, not a library Yes in build — ldd can't see build tools
(missing from ldd) pc:glib-2.0.pc Pkg-config discovery for meson Yes in build — ldd can't see header files
(missing from ldd) gio:libgiognutls.so GIO TLS plugin loaded at runtime Yes in runtime — ldd can't see dlopen'd modules

Rule of thumb: declare what you link against or invoke, not what your dependencies pull in transitively. Each package's manifest handles its own dependency chain.

Versioned library and pkg-config dependencies

Hardcoding the full soname you happened to link against makes a package uninstallable on systems carrying a different (possibly newer) build of the same library: a package wanting lib:libcmark.so.0.30.0 fails against a system providing libcmark.so.0.31.1. Since USM builds packages from source on the target system, a dependency is usually an API floor, not an ABI pin — express it with a version constraint on the base soname instead:

"runtime": ["lib:libcmark.so>=0.30.0"]

This is satisfied by any libcmark.so.<tail> whose trailing dotted numbers meet the operator (0.31.1 qualifies for >=0.30.0). Operators: >=, <=, == (equal, prefix-tolerant: 0.31 matches 0.31.0) and ~= (compatible release — ~=0.31.0 accepts 0.31.x only, ~=0.31 accepts 0.x from 0.31 up). The same syntax works for pc: refs, checked against the .pc file's Version: field:

"build": ["pc:gtk4.pc>=4.12"]

When to use what:

  • >= — the normal choice: "the API I use exists since this version".
  • ~= — when you know the library breaks ABI within the family you tested (common for pre-1.0 libraries that version their soname by minor).
  • Exact full soname (lib:libcmark.so.0.31.1) — only when you truly need one specific build.
  • Constraints on other resource types (bin:, vapi:, …) are rejected: those names carry no version.

Packages that themselves provide a pc: file may declare its version in provides ("pc:cmark.pc==0.31.1"); without a declaration the file is assumed to match the package version, and validation fails a package whose installed .pc Version: field contradicts either.

Note: constrained dependencies do not yet resolve through system package managers — only from local presence and USM packages.

Runtime dependencies (depends.runtime)

Things the installed program needs at run time:

  • Directly linked libraries: lib:libglib-2.0.so.0 (check with ldd ./myprogram | grep -v linux-vdso)
  • GIO plugin modules: gio:libgiognutls.so if the program uses HTTPS via GLib's GIO
  • Data files: res: refs for share-time resources the program reads

What NOT to put in runtime:

  • lib:libc.so.6 — the C library is implicit on every Linux system; musl doesn't even have it
  • lib:libm.so.6 — same, the math library is always present
  • Transitive dependencies (glib pulls in pcre, ffi, selinux — don't declare those)
  • Build tools (valac, meson — those go in build)

Build dependencies (depends.build)

Things needed to compile the package from source:

  • Compilers: bin:valac, bin:gcc (gcc is not always transitive — alpine's apk doesn't pull it)
  • Build systems: bin:meson, bin:ninja
  • Pkg-config files: pc:glib-2.0.pc, pc:json-glib-1.0.pc (these map to -dev/-devel system packages)
  • Code generators: bin:g-ir-compiler (if GIR/typelib are generated), bin:statum-mkpstm (for Statum pages)
  • Scripting: bin:python3 if the build uses Python tools
  • Stack-internal libraries: pc:invercargill-1.pc (resolved from USM repositories, not the system)

What NOT to put in build:

  • bin:bash — that goes in manage (build scripts run under the install environment's shell)
  • Runtime-only libraries the build doesn't link against

Manage dependencies (depends.manage)

Things needed to run the package's management scripts:

  • bin:bash — almost always needed (usm-scripts are bash)
  • bin:curl — if the scripts download anything

Declaring provides

provides tells USM what your package installs and where to find it.

"as-expected" paths

The default for most resources — USM knows the standard install location:

"lib:libmylib.so": "as-expected",
"inc:mylib.h": "as-expected",
"pc:mylib.pc": "as-expected",
"vapi:mylib.vapi": "as-expected"

USM derives the path from Paths.get_suggested_base_path_for_type() + the resource name.

Explicit paths

For non-standard locations:

"bin:myhelper": "usr/lib/myapp/myhelper"

What to provide

Provide what downstream packages might want to depend on:

  • lib:<soname> — the shared library (use the soname, not the full path)
  • bin:<name> — installed executables
  • pc:<name>.pc — the pkg-config file
  • inc:<name>.h or inc:<dirname> — headers (use the include directory name for multi-file headers)
  • vapi:<name>.vapi — Vala API definitions
  • gir:<name>.gir — GObject introspection XML
  • typelib:<name>.typelib — compiled introspection data

Don't provide internal implementation details that nothing else should link against.


Dependency groups (alternatives)

When a package can be satisfied by either of multiple dependency sets, use the nested array form. A phase must be either all plain strings (standard) or all arrays (alternatives) — mixing strings and arrays in the same phase is a validation error.

Each inner array is a complete, self-contained candidate group. Common dependencies are repeated in every group:

"depends": {
    "build": [
        ["bin:valac", "pc:glib-2.0.pc", "pc:gtk4.pc", "pc:libadwaita-1.pc"],
        ["bin:valac", "pc:glib-2.0.pc", "pc:gtk3.pc", "pc:libhandy-1.pc"]
    ]
}

USM picks whichever group is fully satisfiable, preferring the one that installs the fewest packages.

When to use groups:

  • A library supports multiple backends (e.g., GTK4 or GTK3)
  • A newer monolithic package replaces two older split ones

When NOT to use groups:

  • Optional features (use separate packages or flags instead)
  • Platform-specific deps (declare both; the resolver picks what's available)
  • When you only need "one of these" for a single dependency — that's what the resolver already does with SPM + USM fallback

Scripts (usm-scripts/)

build.sh

#!/bin/bash
set -e

SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_DIR="$1"
PREFIX="${PREFIX:-/usr}"
LIBDIR="${LIBDIR:-lib}"

cd "$SRC_DIR"
meson setup "$BUILD_DIR" --prefix="$PREFIX" --libdir="$LIBDIR"
ninja -C "$BUILD_DIR"

Rules:

  • Accept the build directory as $1 — USM provides it
  • Respect PREFIX, LIBDIR, BINDIR, INCLUDEDIR environment variables
  • Be idempotent — ninja handles incremental builds; don't rm -rf the build dir
  • Don't install — that's install.sh's job

install.sh

#!/bin/bash
set -e

SRC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_DIR="$1"
INSTALL_DIR="$2"
TYPE="$3"

cd "$SRC_DIR"
DESTDIR="$INSTALL_DIR" meson install -C "$BUILD_DIR"

Rules:

  • $1 = build dir, $2 = install staging dir (DESTDIR), $3 = install type (FRESH, UPDATE, DOWNGRADE)
  • Install into the staging dir, not the real system — USM handles the final placement
  • Match the provides entries exactly — if you provide lib:libfoo.so, the library must actually land where USM expects it

.usmignore

Exclude files that shouldn't be in the .usmc archive:

builddir*/
*.sqlite*
db.sqlite
web-config.json
.kilo/
.git/
*.tar.xz

Always exclude:

  • Build directories (builddir*/, build/)
  • Databases (*.sqlite*)
  • Secrets and keys (web-config.json, *-key*, *.env)
  • VCS (.git/, .hg/)
  • Editor artifacts (.kilo/, .vscode/, *.swp)

Common pitfalls

1. Over-declaring dependencies

Symptom: The resolver pulls in packages you don't need; installs are slow.

Fix: Remove transitive dependencies. If you depend on lib:libglib-2.0.so.0, you don't also need lib:libpcre2-8.so.0 — glib's own manifest declares that.

2. Under-declaring build deps

Symptom: Build fails in a container but works on the dev machine.

Fix: The dev machine has tools installed that the container doesn't. Check: bin:gcc (alpine doesn't pull it), bin:g-ir-compiler (GIR generation), bin:python3 (build tooling).

3. Wrong soname in provides

Symptom: Downstream packages can't resolve lib:libfoo.so.1 because you provided lib:libfoo.so.

Fix: Use the actual soname (what readelf -d yourlib.so | grep SONAME reports). The soname includes the version suffix.

4. Platform-specific deps on the wrong platform

Symptom: usm install fails on alpine because lib:libc.so.6 doesn't exist on musl.

Fix: Don't declare the C library. If you must (rare), use dependency groups with platform alternatives.

5. GIR/typelib not generated on musl

Symptom: Install fails on alpine with "Expected to find file at gir-1.0/..."

Fix: Only declare gir: and typelib: in provides if gobject-introspection is available on all target platforms. If it's not, omit them — they're only needed for Python/JS bindings.


Version bumping

# Bump patch version (1.2.3 → 1.2.4)
usm manifest bump patch

# Bump + git commit + tag
usm manifest bump minor --git

# Bump + commit + tag + push
usm manifest bump major --git --push

# Set explicit version
usm manifest bump 2.0.0

# Manage the rebuildDependants flag
usm manifest bump patch --rebuild-dependants
usm manifest bump patch --no-rebuild-dependants

Only set rebuildDependants when the update includes C API changes that break downstream compilation. Don't set it for bug fixes or internal changes.


Testing your package

# Validate the manifest
usm manifest validate

# Build + install to a scratch DESTDIR
USM_DESTDIR=/tmp/kilo/test-stage usm manifest install

# Package it
usm manifest package

# Add to a scratch repository and test resolution
cd /tmp/kilo/test-repo && usm repository init test && usm repository add ../mylib-1.0.0.usmc
USM_CONFIGDIR=/tmp/kilo/test-config usm install mylib

Verify that every provides entry actually exists at the expected location in the DESTDIR. If it doesn't, either the install script or the manifest is wrong.