-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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| 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). |
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.
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: nomklthen 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-pkgThe 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.
platformis 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-environmentplatformsto scope by platform.
Named, composable dependency groups. An environment lists the features it wants; their dependencies union into the solve.
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 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.
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 againstextends 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.
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.
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).
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 recordingApplying 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 (
numpy→numpy >=2,<2.2), keyed by package name, - records global virtual packages, and
-
prunes the build matrix via the
excludedenylist andincludeallowlist (a cell is built only if its environment is allowed byincludefor that python — when present — and not named inexclude;excludewins on overlap).
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.
This wiki is autogenerated. To made updates, open a PR against the original source file in docs/wiki.