Skip to content

Manifests

github-actions[bot] edited this page Jul 19, 2026 · 3 revisions

Manifests

A manifest is the declarative YAML description of one or more environments. It replaces a folder of requirements*.txt files: channels, base dependencies, composable features, build variants, and named environments all live in one (optionally split) document.

This page covers the manifest format and how it composes. For terminology, see Concepts; for pointing channels at your own servers, see Channels & Artifactory.

A complete example

project:
  name: demo
  channels: [conda-forge]          # priority order
  platforms: [linux-64]
  python: ["3.11", "3.12"]         # the python axis
  default-python: "3.11"

# base dependencies — injected into every environment.
# each entry is a conda match-spec STRING (quote-free, like environment.yml).
dependencies:
  - numpy >=2
  - pip

# build variants: a flavor an environment selects.
variants:
  cpu: {}                          # constraints can be filled by an override layer
  gpu:
    dependencies: [cuda >=12]
    constraints: ["pytorch * cuda*"]

# composable feature groups.
features:
  dev:
    dependencies: [pytest, ruff]
  ray:
    dependencies: [ray]
  docs:
    pypi-dependencies: [gendoc]    # pip-only routing is explicit, not name-munging

# environments compose features and fan out over variant × python.
environments:
  app:
    features: [dev, ray]
    variants: [cpu, gpu]
    default-variant: cpu

Sections

project

Key Meaning
name Project name.
channels Channel names in priority order (resolved to URLs — see Channels).
channel-alias Base URL that bare channel names resolve against (e.g. an internal Artifactory).
platforms Target platforms (linux-64, osx-arm64, win-64, …).
python The python axis — each value produces one build target.
default-python The python chosen when a selection omits one (must be in python).

dependencies / pypi-dependencies

Base dependencies injected into every environment. Conda deps are match-spec strings (numpy >=2); PyPI deps are routed explicitly via pypi-dependencies rather than name prefixes.

PyPI resolution is not implemented yet. A manifest may declare pypi-dependencies, but driving a full solve over an environment that has them fails fast rather than silently producing a conda-only lock.

Conditional dependencies (if / then / else)

Any dependencies / pypi-dependencies list — at the manifest base, on a feature, or on a variant — may contain rattler-build-style conditional entries alongside plain match-specs. Each is evaluated per build cell and its then (or else) entries are spliced into the list:

features:
  core:
    dependencies:
      - numpy
      - if: python != "3.13"          # drop a dep on a python with no build yet
        then: [tensorflow, tensorboard]
      - if: variant == "gpu"
        then: cuda-nvcc               # scalar or list
        else: nomkl

then and else may each be a single entry or a list, and either may itself contain another if — nesting under else expresses elseif:

      - if: python == "3.11"
        then: legacy-pkg
        else:
          - if: cmp(python, ">=3.13")
            then: new-pkg
            else: mid-pkg

The if is a minijinja expression over the cell's two axes:

Variable Value
python The selected python (e.g. "3.13"), or "" if the env has none.
variant The selected variant (e.g. "gpu"), or "" if the env has none.

Supported operators are minijinja's: ==, !=, <, >, and, or, not, in, parentheses. Because bare comparisons are lexical ("3.9" > "3.13"), use the cmp(version, spec) helper for version-aware checks against a conda version spec — cmp(python, ">=3.12"), cmp(python, "3.11.*"). A malformed expression fails the solve rather than silently dropping a dependency.

platform is intentionally not available: a resolved cell's dependency set is solved for every one of its platforms, so there is no single platform value at resolve time. Use per-environment platforms to scope by platform.

features

Named, composable dependency groups. An environment lists the features it wants; their dependencies union into the solve.

variants

A build flavor (cpu, gpu, …) an environment selects. A variant carries its own dependencies, pypi-dependencies, constraints (bound the solve without adding a dependency), and virtual-packages (e.g. cuda: "12.9").

activation

Activation hooks run when the environment is activated (via activate / conda activate), not merely placed on PATH. They are materialized into the prefix's etc/conda/activate.d/ on create, and may be declared at the manifest base, on a feature, and on a variant — they merge (env vars: later wins on a key clash; scripts: appended in order). This replaces the need for per-environment activation packages (e.g. ones that emit telemetry when an environment is activated).

activation:
  env:                             # exported on activation
    MY_FLAG: "1"
  scripts:                         # run after the env vars are set
    - 'echo "activated $NEPENTHE_ENVIRONMENT $NEPENTHE_VERSION"'

nepenthe always injects the environment's identity — NEPENTHE_ENVIRONMENT, NEPENTHE_PLATFORM, and NEPENTHE_VERSION — before the declared env vars, so hooks can reference it. Hooks run only under a full activation; run / shell use path activation and do not execute them.

environments

Either a bare list of feature names, or a detailed form:

environments:
  app: [ray]                       # shorthand: just features

  full:
    features: [ray, dev]
    variants: [cpu, gpu]           # variant axis
    default-variant: cpu
    python: ["3.11"]               # narrow the python axis for this env
    platforms: [linux-64]

  win:
    extends: full                  # inherit features from another environment
    platforms: [win-64]            # override platforms

  private:
    features: [ray, dev]
    channels: [my-private-channel] # extra channel, only this env solves against

extends inherits the base environment's features; variant/variants, python, and platforms can be set or overridden per environment. channels adds channels (by name, resolved like any other) on top of the project channels for just this environment — use it for a private channel only one environment needs, so other environments don't require its credentials. See Channels.

Composition & the build matrix

Resolving a (variant, python) cell unions: base deps → selected features (incl. inherited) → selected variant, evaluating each list's conditional (if:) entries for the cell, then adds the chosen python (python <ver>.*). Conda and PyPI deps stay separate.

use nepenthe_core::manifest::{Manifest, Selector};

let manifest = Manifest::load("environment.yaml")?;

// every build cell (variant × python), minus excluded ones
let cells = manifest.targets("app")?;

// the default cell (defaults for each axis)
let default = manifest.resolve_default("app")?;

// a specific cell
let gpu_312 = manifest.resolve("app", &Selector {
    variant: Some("gpu".into()),
    python: Some("3.12".into()),
})?;

A default that isn't a member of its axis (a typo in default-variant or default-python) is rejected rather than silently producing an undeclared cell.

Splitting across files: imports

A large definition can be split; each fragment is merged before the importing file's own definitions:

# environment.yaml
imports:
  - features.yaml
  - variants.yaml
dependencies: [numpy >=2]
environments:
  app: { features: [dev], variants: [cpu, gpu], default-variant: cpu }

Imports are sandboxed: an absolute path or one containing .. is rejected, so a manifest can't read files outside its own tree. Merging carries every field — including virtual-packages and the exclude/include matrix tables.

See the runnable fixture set in rust/testdata/e2e/ (manifest.yaml imports features.yaml + variants.yaml, plus overrides.yaml).

Override layers

An override layer is a separate YAML file that adjusts a solve without editing the manifest. It supplies version pins, variant constraints, virtual-package assumptions, and per-python matrix exclude/include tables:

# overrides.yaml
pins:
  numpy: ">=2,<2.2"              # bake a version onto a bare dependency

variants:
  cpu:
    constraints: ["pytorch * cpu*"]   # fill the empty cpu variant

virtual-packages:
  cuda: "12.9"
  archspec: skylake_avx512

exclude:
  "3.13": [altenv]                # denylist: don't build altenv on python 3.13

include:
  "3.13": [myenv, otherenv]          # allowlist: on 3.13 build ONLY these (shorthand
                                # when fewer are built than excluded)

Apply it to a manifest:

use nepenthe_core::manifest::{Manifest, Overrides};

let mut manifest = Manifest::load("environment.yaml")?;
let overrides = Overrides::from_yaml_path("overrides.yaml")?;
manifest.apply(&overrides);       // variant merge, pin baking, matrix pruning, vpkg recording

Applying an override:

  • merges each variant's deps/constraints/virtual-packages into the manifest's variant (filling, e.g., an empty cpu: {}),
  • bakes pins into every matching conda dependency (numpynumpy >=2,<2.2), keyed by package name,
  • records global virtual packages, and
  • prunes the build matrix via the exclude denylist and include allowlist (a cell is built only if its environment is allowed by include for that python — when present — and not named in exclude; exclude wins on overlap).

Validation lints

Manifest::lint() reports common problems before a solve:

  • HardPin — a == pin in base or a feature (locations reported).
  • TooManyHardPins — an environment exceeds the hard-pin budget.
  • BaseFeatureCollision — a feature redeclares a base package.

Clone this wiki locally