# USM Packaging Guide Best practices for writing `MANIFEST.usm` files and creating packages that install correctly across platforms. --- ## The manifest ```json { "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 | **Yes** — `lib:libglib-2.0.so.0` | | `libjson-glib-1.0.so.0` | You linked against it directly | **Yes** — `lib: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. ### 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: ```json "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: ```json "bin:myhelper": "usr/lib/myapp/myhelper" ``` ### What to provide Provide what downstream packages might want to depend on: - `lib:` — the shared library (use the soname, not the full path) - `bin:` — installed executables - `pc:.pc` — the pkg-config file - `inc:.h` or `inc:` — headers (use the include directory name for multi-file headers) - `vapi:.vapi` — Vala API definitions - `gir:.gir` — GObject introspection XML - `typelib:.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: ```json "depends": { "build": [ "bin:valac", ["pc:gtk4.pc", "pc:libadwaita-1.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., OpenSSL OR GnuTLS) - 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) --- ## Scripts (`usm-scripts/`) ### `build.sh` ```bash #!/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` ```bash #!/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 ```bash # 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 ```bash # 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.