This tutorial plans conda-forge eOn, explains the artifacts, and routes the result into a build campaign. The QMCPACK path differs only in parser and package configuration. Closure, generation, baseline, and reachability are in the glossary. The same steps, without these fixed paths, are author a new package.
The public tutorial target runs EasyBuild inside the repository’s Rocky Linux Podman image. Build the image and copy the upstream robot plus the eOn fixture overlay into the path namespace mounted by the target:
podman build -t eb-stack-rocky9 \
-f skills/new-package/container/rocky9/Containerfile \
skills/new-package/container/rocky9
mkdir -p \
/tmp/eb-stack/robot \
/tmp/eb-stack/overlay \
/tmp/eb-stack/sources \
/tmp/eb-stack/bundles
scripts/fetch-upstream-trees.sh
set -a
source "${EB_UPSTREAM_CACHE:-$HOME/.cache/eb-stack/upstream}/env.sh"
set +a
rsync -a "$EB_EASYCONFIGS"/ \
/tmp/eb-stack/robot/
rsync -a fixtures/eon_core_rgpot/easyconfigs/ \
/tmp/eb-stack/overlay/Set the paths used throughout the tutorial:
export ROBOT=/tmp/eb-stack/robot
export BUNDLE=/tmp/eb-stack/bundles/eon-tutorial
export STATE=/tmp/eb-stack/bundles/eon-tutorial.campaign.jsonThe bundle path is identical on the host and in the container. Probe every target layer before planning a long build:
eb-stack target doctor \
--config examples/targets/local-podman.toml \
--target local-rocky9eb-stack package inspect \
--source fixtures/foreign_ingest/conda_eon/recipe.yaml \
--format conda-forge \
--toolchain foss/2026.1 \
--package-config examples/packages/common.toml \
--package-config examples/packages/eon.toml \
--out-dir /tmp/eon-inspectOpen /tmp/eon-inspect/package.plan.json. Important sections are:
originandpackage: foreign identity with configured EasyBuild policy;sources: every source artifact and checksum;dependencies: roles, constraints, conditions, and provenance;build: EasyBuild easyblock, toolchain, and configuration;profiles: independently installable products;outputs: requested profile/stack pairs;residuals: dynamic or ambiguous input that needs judgment.
The planned CycloneDX document is emitted at the same boundary. It describes the requested pre-build inventory and never claims installed evidence.
eb-stack package plan \
--source fixtures/foreign_ingest/conda_eon/recipe.yaml \
--format conda-forge \
--toolchain foss/2026.1 \
--package-config examples/packages/common.toml \
--package-config examples/packages/eon.toml \
--easyconfigs "$ROBOT" \
--easyconfigs /tmp/eb-stack/overlay \
--stack-policy examples/stacks/eon-foss-2026.1.toml \
--out-dir "$BUNDLE"The generated tree contains:
package.plan.json
package.sbom.cdx.json
closure.plan.json
closure.sbom.cdx.json
build-order.json
locks/default.lock.json
easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb
easyconfigs/e/eOn/eOn-2.16.0_safemath-eigen5-core-guard.patch
The lock is the actual Resolvo result. Its direct dependencies name selected versions, toolchains, versionsuffixes, source easyconfig paths, build/runtime roles, pin outcomes, and candidate exclusions.
examples/stacks/eon-foss-2026.1.toml records the Eigen 5 identity required
by the safemath core-guard patch without imposing it on unrelated package
plans. It is a preferred pin with the complete EasyBuild toolchain and
versionsuffix identity. Resolvo admits the pinned artifact’s dependency
closure, then first tries that favored candidate. If another declared
requirement or known exclusion makes it infeasible, the solver selects a
compatible alternative and sets fallback = true with a reason in the lock.
Change the mode to locked only when the stack requires that exact candidate
and must fail otherwise. The core + rgpot product needs no cross-generation
pins; every dependency sits on the foss-2026.1 / GCCcore-15.2.0 generation.
A version range is not build evidence. A candidate can be SAT-compatible and still fail configure, compile, link, tests, install, or sanity. Those outcomes belong to the campaign finding model.
RECIPE="$BUNDLE/easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb"
eb-stack recipe format "$RECIPE"
eb-stack recipe lint "$RECIPE"
eb-stack recipe check \
--recipe "$RECIPE" \
--easyconfigs "$ROBOT" \
--easyconfigs /tmp/eb-stack/overlay \
--easyconfigs "$BUNDLE/easyconfigs"At this point the bundle may claim resolves. No build has occurred.
eb-stack target list \
--config examples/targets/local-podman.toml
eb-stack target doctor \
--config examples/targets/local-podman.toml \
--target local-rocky9Read all four checks. A healthy transport does not imply a healthy executor, runtime, or EasyBuild command.
eb-stack campaign run \
--bundle "$BUNDLE" \
--config examples/targets/local-podman.toml \
--target local-rocky9 \
--state "$STATE"
eb-stack campaign status --state "$STATE"campaign run stays in the foreground. Keep that shell under the process
supervisor used by the target site and use another shell for campaign status.
If the controlling process is interrupted, first confirm that the routed
container, scheduler job, or host command has stopped, then rerun with the same
state path. The state remains the audit record; do not start a duplicate state
for the same bundle.
If the campaign fails, campaign status prints the finding id. Claim that
id and record the repair with the commands in
Repair a failed campaign. Run the campaign
again with the same state. Successful stages supersede stale open findings.
builds becomes true only after all emitted recipes succeed.
binary_verified becomes true only when at least one declared verification
command exists and all such commands succeed.
The same public target runs a second time against the bundle this tutorial
already planned. examples/targets/site.example.toml is the file to copy
when a site fills in its own host, image, and scheduler. This checkout runs
the local target:
eb-stack target doctor \
--config examples/targets/local-podman.toml \
--target local-rocky9
eb-stack campaign run \
--bundle "$BUNDLE" \
--config examples/targets/local-podman.toml \
--target local-rocky9 \
--state "$STATE"The bundle must be visible at the same absolute path used by the selected
runtime. The local target mounts /tmp/eb-stack, which is why $BUNDLE
lives there.
Replace the source and package-config arguments:
eb-stack package plan \
--source fixtures/foreign_ingest/spack_qmcpack/package.py \
--format spack \
--toolchain foss/2026.1 \
--package-config examples/packages/common.toml \
--package-config examples/packages/qmcpack.toml \
--source-checksum 511d5f368db002f2f77504619e1ada8d4a3034200d25feef6773d12a6ed6d18e \
--easyconfigs "$ROBOT" \
--easyconfigs fixtures/qmcpack_foss_2026_1/easyconfigs \
--stack-policy examples/stacks/foss-2026.1.toml \
--out-dir /tmp/qmcpack-tutorialQMCPACK emits one unsuffixed default recipe and one -complex recipe because
the latter is an independently loadable product variant. Both profiles get
their own lock and must build in the campaign. The source checksum is explicit
because this Spack package pins the release by Git commit rather than by the
SHA-256 of its GitHub archive.