pkg.yaml schema
A pkg.yaml manifest declares a single package: where to fetch it, how to
verify it, and how to install it. Manifests live inside a
catalog as pkgs/<name>/pkg.yaml; this page
documents the schema of one manifest.
Schema version
schemaVersion is a required integer; current value is 1.
This field is bumped only for incompatible changes that an older nem would misread.
schemaVersion: 1
name: exampleManifests missing this field, or declaring a version that nem doesn’t understand, fail to load.
schemaVersion versions the manifest format. The catalog OCI image
index carries its own, separate org.vi-dev.nem.catalog.schemaVersion — see
Catalog.pkg.yaml schema
A pkg.yaml declares a single package.
Top-level fields
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | integer | yes | Manifest schema version. Currently 1. See Schema version. |
name | string | yes | Package name. Must match the parent directory name. |
description | string | no | One-line description. |
license | string | no | SPDX-style license identifier. |
homepage | string | no | Project URL. |
dependsOn | list of string | no | Dependencies, each as name or name@version. Entries may not be empty, contain whitespace, self-reference, or duplicate. |
buildFromSource | bool | no | When true, the source artifact is the active one, a build: phase is expected, and the install is staged (see Build staging). |
archive | artifact | cond. | Pre-built artifact. Required when buildFromSource is false (the default). |
source | artifact | cond. | Source artifact. Required when buildFromSource is true. |
versions | list | cond. | Declared versions. Required when the active artifact uses the http fetcher (see Versions for the versionsFrom exception). |
build | list of step | no | Steps that run only when buildFromSource: true. |
install | list of step | usually | Steps that produce the final install tree. May not contain run: actions. |
test | list of step | no | Verification steps. May only contain run: steps. |
binPaths | list of string | no | Directories (relative to the install dir) whose contents are exposed on PATH. Defaults to ["bin"] when omitted. See Binary paths. |
libPaths | list of string | no | Directories (relative to the install dir) added to LD_LIBRARY_PATH so dependents can load the package’s shared libraries. Defaults to ["lib"] when omitted. See Library paths. |
Artifacts (archive, source)
Both archive and source share the same shape:
archive:
fetch: # exactly one of github_release, github_tag, http, oci
...
verify: # at most one of checksum, checksumURL, checksumGitHubAsset, githubAssetDigest (or their sha256* aliases)
...Every artifact must yield a checksum, either through verify: on the
artifact or through a per-version override (see Versions).
Fetchers
Exactly one fetch block must be set.
github_release — a named asset attached to a GitHub Release.
fetch:
github_release:
repo: owner/name # required
assetName: pkg-{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz # required, Go template
templateOverrides: # optional
darwin: macos
amd64: x86_64
releaseFilter: '^(v\d+\.\d+\.\d+)$' # optional regex (one capture group)
versionTemplate: '{{.Capture}}' # optional; default '{{.Capture}}'
releaseTemplate: '{{.Version}}' # optional; default '{{.Version}}'github_tag — the source tarball at
https://codeload.github.com/<repo>/tar.gz/refs/tags/<tag>, where
<tag> is tagTemplate rendered with .Version (default
{{.Version}}).
fetch:
github_tag:
repo: owner/name # required
tagTemplate: 'v{{.Version}}' # optional; default '{{.Version}}'
tagFilter: '^v(\d+\.\d+\.\d+)$' # optional regex (one capture group)
versionTemplate: '{{.Capture}}' # optional; default '{{.Capture}}'http — a templated URL.
fetch:
http:
url: https://example.com/pkg-{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz # required
templateOverrides: # optional
darwin: macosWhen the active artifact uses http, versions: must be declared unless
versionsFrom is set and the artifact’s verify: can produce a checksum for
any version (checksum/checksumURL or the sha256* aliases).
oci — an OCI registry artifact.
fetch:
oci:
ref: "ghcr.io/example/archives/pkg:{{.Version}}" # optional; see below
templateOverrides: # optional
darwin: macos
amd64: x86_64ref is a Go template rendered with .Version, .OS, and .Arch (after
templateOverrides). The grammar is [<base>][:<tag>][@sha256:<digest>]:
- When the tag/digest is omitted,
:{{.Version}}is appended automatically. - When
<base>(registry/repository) is present the ref is absolute and resolved directly against that registry. - When
<base>is absent (ref is empty, or starts with:or@) the ref is relative and is resolved against the catalog’s base as<base>/<archives|sources>/<name>. A relative ref needs that catalog base to resolve against, so it only works inside an OCI catalog; any other catalog type (git,local-dir) requires an absolute ref.
Artifact shape:
archive— must be an OCI Image Index (application/vnd.oci.image.index.v1+json). The fetcher selects the entry matching the runtime OS/Arch. Each index entry contains a plain image manifest with a single layer blob. The index must contain at most one entry per os/architecture;platform.variant(e.g. arm/v6 vs v7) is not considered, and the first matching entry wins.source(used withbuildFromSource: true) — must not be an Image Index; a plain image manifest (application/vnd.oci.image.manifest.v1+json) with a single layer is the expected shape. Source is platform-independent; no index selection occurs.
verify is not used for oci — the within-artifact digest chain
(index → per-platform manifest → layer blob) is digest-verified, and a
digest-pinned catalog ref pins the whole metadata tree. Archive/source bytes
behind a :{{.Version}} tag are only as immutable as that registry tag, so pin
a digest in oci.ref when reproducible bytes matter. The linter does not
require a checksum source on OCI artifacts.
Auth: OCI registries authenticate via the Docker credential store (loaded
lazily from ~/.docker/config.json), falling back to auth.oci config (see
auth.md), then anonymous.
Examples:
Archive via an OCI Image Index (absolute ref):
schemaVersion: 1
name: kubectl
archive:
fetch:
oci:
ref: "ghcr.io/vi-dev/nem-catalog/archives/kubectl:{{.Version}}"
versions:
- version: 1.30.0Build-from-source via an OCI plain manifest:
schemaVersion: 1
name: mytool
buildFromSource: true
source:
fetch:
oci:
ref: "ghcr.io/vi-dev/nem-catalog/sources/mytool:{{.Version}}"
build:
- run:
# Bake the final prefix ({{.InstallDir}}) but install into the staging
# tree ({{.StagingDir}}) — see Build staging below.
cmd: ["make", "install", "PREFIX={{.InstallDir}}", "DESTDIR={{.StagingDir}}"]
versions:
- version: 2.1.0Version discovery from a GitHub repo
When the binaries live off-GitHub but releases are tagged on GitHub, an
http fetcher may declare versionsFrom to enable version discovery:
fetch:
http:
url: https://dl.example.com/pkg-{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz
versionsFrom:
github_release: # exactly one of github_release | github_tag
repo: owner/name
releaseFilter: '^v(.+)$' # optional, same shape as on the direct fetcher
versionTemplate: '{{.Capture}}' # optional, same shape as on the direct fetcher
# github_tag:
# repo: owner/name
# tagFilter: '^v(.+)$'
# versionTemplate: '{{.Capture}}'Discovery uses the GitHub Releases or Tags API. Draft releases are skipped (release discovery only — the Tags API has no draft concept).
Filtering and transforming version names
GitHub-source fetchers — github_release, github_tag, and the
versionsFrom sub-blocks — accept three optional fields that shape
the set of discovered versions and how they map to upstream tags:
| Field | Where | Purpose |
|---|---|---|
releaseFilter / tagFilter | All four GitHub-source blocks | Regex (with exactly one capture group). Tags that don’t match are dropped. |
versionTemplate | All four GitHub-source blocks | Go template rendered with .Capture (the regex capture). Produces the displayed version. Default {{.Capture}}. |
releaseTemplate / tagTemplate | Direct github_release / github_tag fetchers only | Go template rendered with .Version (the displayed version). Produces the tag used at fetch time. Default {{.Version}}. |
The HTTP versionsFrom sub-blocks do not need an inverse template
because the download URL is fully expressed by http.url.
Example — upstream tags releases as prefix1.2.3, but the author
wants nem use foo@v1.2.3 and clean URL templates that use v1.2.3:
fetch:
github_tag:
repo: owner/name
tagFilter: '^prefix(.+)$'
versionTemplate: 'v{{.Capture}}'
tagTemplate: 'prefix{{.Version | trimPrefix "v"}}'tagTemplate and releaseTemplate use the same template helpers as
the rest of the document — see Template functions.
Author them so the inverse template reproduces the upstream tag from the
displayed version (stripping any v the filter added). nem author lint
checks offline that the filter compiles and the templates render; its
--install pass verifies the inverse template against real tags by
fetching the resolved version.
Template variables
Strings in assetName, http.url, github_tag.tagTemplate,
github_release.releaseTemplate, and verify.checksumURL /
verify.checksumGitHubAsset are Go templates rendered with:
| Variable | Value |
|---|---|
.Version | Version being resolved. |
.OS | Target OS (runtime.GOOS), after templateOverrides. |
.Arch | Target arch (runtime.GOARCH), after templateOverrides. |
templateOverrides maps an original runtime.GOOS/GOARCH value to a
replacement; only keys matching the current OS or Arch are consulted. The
verify fields inherit the active fetcher’s templateOverrides, so a checksum URL
resolves the same OS/Arch as the asset it verifies.
See Template functions for the helpers available
inside {{ ... }}.
Verify
At most one verify mode may be set on an artifact. A per-version override nested
under a version entry (see Versions) takes precedence over the
artifact-level verify:.
| Field | Description |
|---|---|
algorithm | Optional. One of sha224, sha256, sha384, sha512. Defaults to sha256 when omitted. |
checksum | A bare hex digest, or a checksum-file body (<digest> <filename> lines) that several platform archives can share. Applied to every version. |
checksumURL | Templated URL whose body holds a digest (bare digest, or digest filename lines as in shasum). Rendered with the fetcher’s templateOverrides, like the asset it verifies. |
checksumGitHubAsset | Asset name on the same github_release whose body is parsed the same way. Requires github_release. |
githubAssetDigest | true to verify the asset against the digest GitHub publishes for it — no checksum file needed. Requires github_release. Its algorithm comes from GitHub’s digest and cannot be combined with algorithm. |
sha256 | Alias for checksum with algorithm: sha256 (always SHA-256). Kept for backward compatibility. |
sha256URL | Alias for checksumURL with algorithm: sha256 (always SHA-256). Kept for backward compatibility. |
sha256GitHubAsset | Alias for checksumGitHubAsset with algorithm: sha256 (always SHA-256). Kept for backward compatibility. |
Per-version overrides accept the same body shape (a bare digest or a
shasum-style file).
Versions
versions:
- version: 1.2.3
archive:
verify:
algorithm: sha512
checksum: <hex digest or shasum body> # optional, overrides archive.verify
source:
verify:
checksum: <hex digest or shasum body> # optional, overrides source.verify
- version: 1.2.2Per-version archive.verify and source.verify blocks accept the same fields
as the artifact-level verify: block (algorithm, checksum, checksumURL,
checksumGitHubAsset, githubAssetDigest, and their sha256* aliases).
Rules:
- Required when the active artifact uses the
httpfetcher, unlessfetch.http.versionsFromis set and the artifact’sverify:can produce a checksum for any version (checksum/checksumURLor thesha256*aliases). version:must be non-empty and unique within the list.- Either the artifact-level
verify:or a per-version checksum override must be present for the active artifact.
Steps (build / install / test)
build, install, and test are ordered lists of step objects. Each step
contains exactly one of the four actions:
| Action | Allowed in | Effect |
|---|---|---|
extract | build, install | Extract an archive (tar.gz, zip, …) into a fresh directory under the staging area. |
copy | build, install | Copy a file or directory into the writable staging area. The destination must be inside it ({{.InstallDir}} directly, or {{.StagingDir}}… for a source build — see Build staging). |
mkdir | build, install | Create a directory in the staging area, with the same destination rule as copy. |
run | build, test | Run a command list (cmd) or a shell script — optionally with a per-command env: map. |
Phase rules:
install:may not containrun:(move it tobuild:).test:may only containrun:steps.- A step with zero or more than one action set is invalid.
Gating a step by platform (platforms)
Any step may declare platforms, a list of targets the step runs on; a step
without it runs everywhere. Entries match the raw GOOS/GOARCH values —
never templateOverrides tokens — and the step runs when any entry
matches.
| Entry form | Meaning | Example |
|---|---|---|
os | that OS, any architecture (same as os/*) | linux |
os/arch | exactly that pair | darwin/arm64 |
*/arch | any OS, that architecture | */arm64 |
Valid tokens: linux, darwin, amd64, arm64, *. Anything else — and an
empty list, a duplicate entry, or an all-platform * — fails validation.
build:
- run:
script: |
patchelf --set-rpath '$ORIGIN/../lib' "$NEM_STAGING_DIR$NEM_INSTALL_DIR/bin/tool"
platforms: ["linux"]A step that consumes {{.Var.<name>}} must run on a subset of the platforms
where the defining extract.as step runs; nem author lint enforces this.
Older nem releases ignore platforms and run every step everywhere; adopt
it only for packages whose minimum supported nem includes it.
Action shapes
- extract:
from: "{{.Archive}}" # required
as: src # optional; bound to {{.Var.<name>}} in later steps
stripComponents: 1 # optional; drop N leading path components (default 0)
- copy:
from: "{{.Var.src}}/bin/foo" # required
to: "{{.InstallDir}}/bin/foo" # required
- mkdir:
path: "{{.InstallDir}}/share" # required
- run:
cmd: ["{{.InstallDir}}/bin/foo", "--version"] # one of cmd / script (see Build scripts)
cwd: "{{.InstallDir}}" # optional
env: # optional; per-command vars, values templated
TMPDIR: "{{.StagingDir}}/.tmp" # keys may not be empty or contain '='env: entries are added to the command’s environment (on top of the dependency
PATH); quote any value that starts with {{ or that YAML would otherwise read
as a non-string (e.g. CGO_ENABLED: "1").
stripComponents on an extract: step drops the first N leading path
components from every archive entry, like tar --strip-components=N, for both
tar and zip archives. Entries with N or fewer components are skipped. If N
strips every entry, the install fails. The default is 0 (no stripping).
Symbolic links in tar and zip archives (and hard links in tar) are extracted as links when the target stays inside the extracted tree. Entries whose link target is absolute are skipped; a relative target that escapes the extracted tree fails the install.
Build scripts (run.script)
A run step may carry a script instead of cmd — a multi-line shell script
run as a single /bin/sh -e -c process inside the sandbox (network denied,
writes confined). It is allowed in both build: and test: and is the
ergonomic way to express a multi-command step.
- run:
cwd: "{{.Var.src}}"
env:
TMPDIR: "{{.StagingDir}}/.tmp"
script: |
./configure --prefix="$NEM_INSTALL_DIR"
make
make install DESTDIR="$NEM_STAGING_DIR"Exactly one of
cmd/scriptperrunstep.-esemantics — the script runs undersh -e, so the step fails as soon as any command fails; no leadingset -eis needed.The body is never templated. Unlike
cmd,cwd, andenvvalues, ascriptbody is passed to the shell verbatim —{{ ... }}is literal text. This keeps manifest- and version-derived values out of the shell parser. A script readsnem-provided paths from theNEM_*environment instead:Variable Value NEM_PKG_NAMEpackage name NEM_PKG_VERSIONresolved version NEM_OStarget OS NEM_ARCHtarget arch NEM_STAGING_DIRwritable build root ( {{.StagingDir}})NEM_INSTALL_DIRfinal install dir ( {{.InstallDir}})NEM_PKG_ARCHIVEfetched archive path NEM_PKG_SOURCEsource path NEM_VAR_<NAME>each extract:as: <name>dir (name upper-cased)These
NEM_*variables are set for everybuild:run step (bothcmdandscript).cwd:andenv:are still templated, socwd: "{{.Var.src}}"and templatedenv:values work as usual — only thescriptbody is exempt.
In test: the sandbox keeps the committed install read-only and confines writes
to an ephemeral scratch dir (also TMPDIR, and the default cwd). Test steps
get the package’s own binPaths on PATH (so a script runs foo --version by
name) plus a NEM_* subset: NEM_INSTALL_DIR, NEM_PKG_NAME,
NEM_PKG_VERSION, NEM_OS, NEM_ARCH (the build-only NEM_STAGING_DIR /
NEM_VAR_* / NEM_PKG_ARCHIVE / NEM_PKG_SOURCE are not set for test).
Template variables in step fields
Every string in a step is a Go template (missingkey=error) rendered with:
| Variable | Set when |
|---|---|
.Archive | archive: is declared. |
.Source | source: is declared. |
.InstallDir | Always. The package’s final install path — what artifacts should bake (e.g. --prefix), not necessarily where writes go (see Build staging). |
.StagingDir | buildFromSource: true only. The writable build root; install output goes under {{.StagingDir}}{{.InstallDir}}. |
.Name | Always. |
.Version | Always. |
.OS | Always (target OS, runtime.GOOS). |
.Arch | Always (target arch, runtime.GOARCH). |
.Var.<name> | A previous extract: step in the same package declared as: <name>. |
A {{.Var.x}} reference is only valid after an earlier step in the same
package declares as: x. The same as: value may not be reused.
A run.script body is the exception: it is never templated (see
Build scripts).
See Template functions for the helpers available
inside {{ ... }}.
Build staging
nem installs every package by building under <install-dir>.tmp and committing
the result into place only once all steps succeed, so a half-finished build never
looks installed.
For buildFromSource: true packages this matters to the manifest, because
compiled artifacts often bake their install prefix (e.g. a shared library’s
install name, .pc files, *-config scripts). The build must therefore bake the
final path while writing somewhere else:
{{.InstallDir}}is the final path — pass it where the build records the prefix (e.g../configure --prefix={{.InstallDir}}). It does not exist yet during the build.{{.StagingDir}}is the writable build root. Install output must land at{{.StagingDir}}{{.InstallDir}}— theDESTDIRconvention (make install DESTDIR={{.StagingDir}}). Put caches and scratch elsewhere under{{.StagingDir}}; only the{{.StagingDir}}{{.InstallDir}}subtree is committed, so the rest is discarded automatically.
A dependency’s install directory is not available as a template variable.
Instead, a build reaches its dependsOn packages through the directories nem
injects: their bin/ entries on PATH (e.g. autotools *-config scripts, or
pkg-config via a pkgconf dependency), their library directories on
LD_LIBRARY_PATH, and their .pc files on PKG_CONFIG_PATH — never a path
computed in the manifest.
Prebuilt (archive) installs are not staged this way: their copy/extract
steps write directly under {{.InstallDir}}.
Binary paths (binPaths)
nem exposes a package’s executables on PATH by prepending directories from
its install tree. By default that directory is bin/ — equivalent to
binPaths: ["bin"]. Set binPaths to expose a different set of directories:
binPaths:
- node/bin- Each entry is a directory relative to the install dir. Absolute paths and paths that escape the install dir are rejected.
- When
binPathsis set, it fully replaces thebindefault. Listbinexplicitly if you also want it. - A declared directory must exist after install — both
nem author lint’s install pass andnem usefail otherwise. The implicitbindefault is optional: a library-only package with nobin/is valid. - Entries are templated with a narrow variable set:
.Version,.OS,.Arch,.Name..InstallDir,.Var,.Archive, and.Sourceare not available.
Library paths (libPaths)
nem bakes no RPATH into the binaries it installs, so a package that links
another package’s shared libraries needs those libraries on LD_LIBRARY_PATH to
load them at run time. nem builds that path from each package’s library
directories — its own and those of its dependsOn closure — when running build
and test steps and when nem use activates a package. By default that directory
is lib/ — equivalent to libPaths: ["lib"]. Set libPaths to expose a
different set of directories:
libPaths:
- lib
- lib64- Each entry is a directory relative to the install dir. Absolute paths and paths that escape the install dir are rejected.
- When
libPathsis set, it fully replaces thelibdefault. Listlibexplicitly if you also want it. - A declared directory must exist after install — both
nem author lint’s install pass andnem usefail otherwise. The implicitlibdefault is optional: a package with no shared libraries is valid. - Entries are templated with the same narrow variable set as
binPaths:.Version,.OS,.Arch,.Name.
Pkg-config paths
During build and test steps, nem also prepends each dependency’s pkg-config
search directories to PKG_CONFIG_PATH: <libdir>/pkgconfig (one entry per
directory listed in libPaths, defaulting to lib/pkgconfig) and
<prefix>/share/pkgconfig. This covers the full dependsOn closure, in the
same way as PATH and LD_LIBRARY_PATH.
A package whose build locates dependencies via pkg-config therefore needs no
manual path computation — the search path is wired automatically. The package
must still declare pkgconf as a dependency to provide the pkg-config
binary. A dependency that ships no .pc files (e.g. bzip2) contributes
nothing to the path, so its headers and libraries still need explicit -I/-L
flags when the build system does not use pkg-config for them.
Template functions
Every templated string — in fetch fields and in step fields — can use
the following helpers. Signatures are pipelineable (the data is the last
argument), so they compose naturally with |.
| Function | Signature | Example |
|---|---|---|
trimPrefix | trimPrefix prefix s | {{.Version | trimPrefix "v"}} → 1.2.3 |
trimSuffix | trimSuffix suffix s | {{.Asset | trimSuffix ".tar.gz"}} |
replace | replace old new s (replaces all) | {{.Arch | replace "amd64" "x86_64"}} |
These exist so a manifest can adapt to releases where the git tag and the
asset filename disagree (for example, cli/cli tags as v2.50.0 but
publishes gh_2.50.0_macOS_amd64.zip):
archive:
fetch:
github_release:
repo: cli/cli
assetName: 'gh_{{.Version | trimPrefix "v"}}_{{.OS}}_{{.Arch}}.zip'
verify:
sha256GitHubAsset: 'gh_{{.Version | trimPrefix "v"}}_checksums.txt'Example
name: example
description: An example package
license: MIT
homepage: https://example.com/example
archive:
fetch:
github_release:
repo: example/example
assetName: example-{{.Version}}-{{.OS}}-{{.Arch}}.tar.gz
verify:
sha256GitHubAsset: example-{{.Version}}-checksums.txt
versions:
- version: 1.0.0
install:
- extract:
from: "{{.Archive}}"
as: src
- copy:
from: "{{.Var.src}}/example"
to: "{{.InstallDir}}/bin/example"
test:
- run:
cmd: ["{{.InstallDir}}/bin/example", "--version"]