Skip to content
pkg.yaml schema

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: example

Manifests missing this field, or declaring a version that nem doesn’t understand, fail to load.

This 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

FieldTypeRequiredDescription
schemaVersionintegeryesManifest schema version. Currently 1. See Schema version.
namestringyesPackage name. Must match the parent directory name.
descriptionstringnoOne-line description.
licensestringnoSPDX-style license identifier.
homepagestringnoProject URL.
dependsOnlist of stringnoDependencies, each as name or name@version. Entries may not be empty, contain whitespace, self-reference, or duplicate.
buildFromSourceboolnoWhen true, the source artifact is the active one, a build: phase is expected, and the install is staged (see Build staging).
archiveartifactcond.Pre-built artifact. Required when buildFromSource is false (the default).
sourceartifactcond.Source artifact. Required when buildFromSource is true.
versionslistcond.Declared versions. Required when the active artifact uses the http fetcher (see Versions for the versionsFrom exception).
buildlist of stepnoSteps that run only when buildFromSource: true.
installlist of stepusuallySteps that produce the final install tree. May not contain run: actions.
testlist of stepnoVerification steps. May only contain run: steps.
binPathslist of stringnoDirectories (relative to the install dir) whose contents are exposed on PATH. Defaults to ["bin"] when omitted. See Binary paths.
libPathslist of stringnoDirectories (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: macos

When 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_64

ref 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 with buildFromSource: 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.0

Build-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.0
Version 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:

FieldWherePurpose
releaseFilter / tagFilterAll four GitHub-source blocksRegex (with exactly one capture group). Tags that don’t match are dropped.
versionTemplateAll four GitHub-source blocksGo template rendered with .Capture (the regex capture). Produces the displayed version. Default {{.Capture}}.
releaseTemplate / tagTemplateDirect github_release / github_tag fetchers onlyGo 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:

VariableValue
.VersionVersion being resolved.
.OSTarget OS (runtime.GOOS), after templateOverrides.
.ArchTarget 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:.

FieldDescription
algorithmOptional. One of sha224, sha256, sha384, sha512. Defaults to sha256 when omitted.
checksumA bare hex digest, or a checksum-file body (<digest> <filename> lines) that several platform archives can share. Applied to every version.
checksumURLTemplated 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.
checksumGitHubAssetAsset name on the same github_release whose body is parsed the same way. Requires github_release.
githubAssetDigesttrue 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.
sha256Alias for checksum with algorithm: sha256 (always SHA-256). Kept for backward compatibility.
sha256URLAlias for checksumURL with algorithm: sha256 (always SHA-256). Kept for backward compatibility.
sha256GitHubAssetAlias 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.2

Per-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 http fetcher, unless fetch.http.versionsFrom is set and the artifact’s verify: can produce a checksum for any version (checksum/checksumURL or the sha256* 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:

ActionAllowed inEffect
extractbuild, installExtract an archive (tar.gz, zip, …) into a fresh directory under the staging area.
copybuild, installCopy 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).
mkdirbuild, installCreate a directory in the staging area, with the same destination rule as copy.
runbuild, testRun a command list (cmd) or a shell script — optionally with a per-command env: map.

Phase rules:

  • install: may not contain run: (move it to build:).
  • test: may only contain run: 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 formMeaningExample
osthat OS, any architecture (same as os/*)linux
os/archexactly that pairdarwin/arm64
*/archany 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 / script per run step.

  • -e semantics — the script runs under sh -e, so the step fails as soon as any command fails; no leading set -e is needed.

  • The body is never templated. Unlike cmd, cwd, and env values, a script body is passed to the shell verbatim — {{ ... }} is literal text. This keeps manifest- and version-derived values out of the shell parser. A script reads nem-provided paths from the NEM_* environment instead:

    VariableValue
    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 every build: run step (both cmd and script). cwd: and env: are still templated, so cwd: "{{.Var.src}}" and templated env: values work as usual — only the script body 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:

VariableSet when
.Archivearchive: is declared.
.Sourcesource: is declared.
.InstallDirAlways. The package’s final install path — what artifacts should bake (e.g. --prefix), not necessarily where writes go (see Build staging).
.StagingDirbuildFromSource: true only. The writable build root; install output goes under {{.StagingDir}}{{.InstallDir}}.
.NameAlways.
.VersionAlways.
.OSAlways (target OS, runtime.GOOS).
.ArchAlways (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}} — the DESTDIR convention (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 binPaths is set, it fully replaces the bin default. List bin explicitly if you also want it.
  • A declared directory must exist after install — both nem author lint’s install pass and nem use fail otherwise. The implicit bin default is optional: a library-only package with no bin/ is valid.
  • Entries are templated with a narrow variable set: .Version, .OS, .Arch, .Name. .InstallDir, .Var, .Archive, and .Source are 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 libPaths is set, it fully replaces the lib default. List lib explicitly if you also want it.
  • A declared directory must exist after install — both nem author lint’s install pass and nem use fail otherwise. The implicit lib default 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 |.

FunctionSignatureExample
trimPrefixtrimPrefix prefix s{{.Version | trimPrefix "v"}}1.2.3
trimSuffixtrimSuffix suffix s{{.Asset | trimSuffix ".tar.gz"}}
replacereplace 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"]