Tutorial: foreign recipe to verified EasyBuild package#

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 profile selection.

Prepare the runnable local target#

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

rsync -a /path/to/easybuild-easyconfigs/easybuild/easyconfigs/ \
  /tmp/eb-stack/robot/
rsync -a fixtures/eon_foss_2026_1/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.json

The 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-rocky9

Inspect the conda-forge recipe#

eb-stack package inspect \
  --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --profile-config examples/profiles/eon.toml \
  --out-dir /tmp/eon-inspect

Open /tmp/eon-inspect/package.plan.json. Important sections are:

  • origin and package: normalized identity;

  • 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.

Resolve and emit#

eb-stack package plan \
  --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --profile-config examples/profiles/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
locks/default.lock.json
easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb

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.

Understand stack pin fallback#

examples/stacks/eon-foss-2026.1.toml records the reviewed cross-generation PyTorch, xtb, Eigen, and Meson identities needed by the complete eOn product without imposing them on unrelated package plans. Each 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.

This matters because 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.

Check the emitted recipe#

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.

Probe the target#

eb-stack target list \
  --config examples/targets/local-podman.toml

eb-stack target doctor \
  --config examples/targets/local-podman.toml \
  --target local-rocky9

Read all four checks. A healthy transport does not imply a healthy executor, runtime, or EasyBuild command.

Run and repair the campaign#

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, claim its finding before editing:

eb-stack campaign finding claim \
  --state "$STATE" \
  --id attempt:1:finding:1 \
  --owner omp-worker-1

Repair the layer named by the class, then record evidence:

eb-stack campaign finding resolve \
  --state "$STATE" \
  --id attempt:1:finding:1 \
  --owner omp-worker-1 \
  --action "corrected package configuration" \
  --evidence "recipe check and target command reproduce successfully" \
  --change examples/profiles/eon.toml

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.

Route the same bundle to a deployed site#

The local target is a complete public example, not a required deployment shape. For SSH, Slurm, or a site-owned container, copy examples/targets/site.example.toml to a private file and fill in its host, durable roots, image, modules, and scheduler settings. Layer it after the public topology:

eb-stack target doctor \
  --config examples/targets/base.toml \
  --config ~/.config/eb-stack/site.toml \
  --target site-builder

eb-stack campaign run \
  --bundle /shared/eb-stack/bundles/eon-tutorial \
  --config examples/targets/base.toml \
  --config ~/.config/eb-stack/site.toml \
  --target site-builder \
  --state /shared/eb-stack/bundles/eon-tutorial.campaign.json

The bundle must be visible at the same absolute path used by the selected runtime. Site credentials remain outside the public TOML.

Try QMCPACK variants#

Replace the source and profile arguments:

eb-stack package plan \
  --source fixtures/foreign_ingest/spack_qmcpack/package.py \
  --format spack \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --profile-config examples/profiles/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-tutorial

QMCPACK 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.