Author a new package from conda-forge or Spack

This workflow turns a foreign package recipe into a canonical manifest, planned SBOM, Resolvo profile locks, conventional EasyBuild recipes, and a persisted build-evaluation campaign. It is the workflow used for conda-forge eOn and Spack QMCPACK fixtures.

Inputs

Collect these inputs:

  1. a conda-forge meta.yaml~/~recipe.yaml or Spack package.py;

  2. one or more public package-config TOML files;

  3. an EasyBuild robot tree, with site overlays passed after upstream;

  4. a stack-policy TOML file for preferred pins, locked pins, and exclusions.

  5. positional SHA-256 values for source artifacts whose foreign metadata has only a VCS commit or tag.

Package TOML carries explicit foreign-to-EasyBuild aliases, package/build policy, and installable product choices. Each profile emits a separate .eb file. MPI/OpenMP toolchain options alone do not create a suffix; an independently loadable ABI or feature variant does. Parser code handles foreign syntax, never package names.

Inspect the foreign recipe

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

Inspect package.plan.json and package.sbom.cdx.json. The parser preserves source spans, dependency roles, conda selectors, Spack when= conditions, variants, conflicts, requirements, and static build flags. Dynamic foreign logic becomes a residual rather than an invented fact.

Spack input is parsed as Python syntax but never imported or executed. Literal collections and bounded loops are evaluated statically, so data-driven variant() and depends_on() declarations behave like direct calls. A call with a runtime value is omitted from solver input and reported with its source location.

Inspection may report source:missing-sha256 for a commit-pinned Spack release. A VCS commit does not determine the byte hash of the generated source archive. Pass the reviewed archive hash to package plan with one --source-checksum per source artifact; the planner does not invent or omit packaging checksums.

For QMCPACK:

eb-stack package inspect \
  --source fixtures/foreign_ingest/spack_qmcpack/package.py \
  --format spack \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --package-config examples/packages/common.toml \
  --package-config examples/packages/qmcpack.toml \
  --out-dir /tmp/qmcpack-inspect

Define package policy and product profiles

A package layer can replace or extend parser-derived metadata without changing the parser. examples/packages/common.toml provides cross-ecosystem aliases; later layers may replace package metadata, build policy, aliases, virtuals, solver exclusions, and profiles.

An alias maps a foreign dependency identity to an EasyBuild provider. Use the short string form when both names use the same version domain:

[dependencies.aliases]
libcurl = "cURL"

Use the structured form when the foreign dependency is a component supplied by a larger EasyBuild package. constraint = "drop" prevents the component’s version from being interpreted as the provider’s version:

[dependencies.aliases]
"py-setuptools" = { provider = "Python", constraint = "drop" }

The original foreign spelling and constraint remain in dependency provenance. Aliases only control robot matching and solving; they do not teach the parser about a package.

Easyblock-specific parameters are typed TOML data. They are emitted as normal Python values, but configuration cannot inject Python expressions:

[build.easyconfig_parameters]
general_packages = ["ASPHERE", "KSPACE", "MOLECULE"]
build_shared_libs = true

[[profiles]]
name = "cuda"

[profiles.easyconfig_parameters]
cuda_compute_capabilities = ["80", "90"]

Build-level values apply to every product profile. A profile value with the same key replaces the build-level value for that emitted recipe.

Use a requirement when EasyBuild product policy needs a dependency that the foreign recipe does not declare. If a matching foreign edge exists, the requirement makes that edge unconditional and merges its roles. Otherwise it creates a canonical dependency before the SBOM and Resolvo lock are produced:

[[dependencies.requirements]]
name = "CMake"
constraint = ">=3.30"
roles = ["build"]

[[dependencies.requirements]]
name = "VTK"
roles = ["run"]

Do not put selected versions in this table merely to reproduce a neighboring easyconfig. Constraints describe package compatibility; stack preferences and distribution pins belong in Resolvo policy.

Patches are artifacts, not bare recipe text. Every patch has a filename and a reviewed SHA-256. EasyBuild checksum order is all sources followed by all patches:

[build]
patches_mode = "merge"

[[build.patches]]
filename = "Orbit-2.0-portability.patch"
sha256 = "4f43b42fdcf84d0cf634d993dd944f252c8243dc612a919fe2825d56f937c8eb"
source = "patches/Orbit-2.0-portability.patch"

A layer that declares build.patches replaces the preceding list unless it sets patches_mode = "merge". Merge mode preserves foreign recipe patches, adds new filenames, and replaces an existing artifact when the layer supplies the same filename. This lets reviewed EasyBuild metadata refine a foreign patch without duplicating it.

package inspect records a blocking patch:missing-sha256 residual for an unchecked patch. package plan refuses to emit a buildable bundle until every source and patch checksum is present. Relative patch sources resolve against the package TOML file, and package plan copies verified patch bytes beside the emitted easyconfig.

A QMCPACK profile set commonly contains a default MPI+OpenMP build and a separate -complex recipe:

schema_version = 1

[package]
name = "QMCPACK"

[[profiles]]
name = "default"
default = true
platform = "linux"
architecture = "x86_64"
versionsuffix = []
config_options = ["-DQMC_MPI=ON", "-DQMC_OMP=ON", "-DQMC_COMPLEX=OFF"]

[profiles.toolchain_options]
usempi = true
openmp = true

[[profiles.verification_commands]]
program = "bash"
args = ["-lc", "module load {module} && qmcpack --version"]

[[profiles]]
name = "complex"
default = false
versionsuffix = ["-complex"]
config_options = ["-DQMC_MPI=ON", "-DQMC_OMP=ON", "-DQMC_COMPLEX=ON"]

platform and architecture are target context for foreign selectors. Put a site-wide value such as platform = "linux" in a common package layer, then override it in a product profile only when that output targets a different environment. Resolvo sees dependencies only after selectors are materialized against this explicit context.

Verification placeholders are {module}, {package}, {version}, {profile}, and {versionsuffix}.

Put stack knowledge inside Resolvo

Site or distribution pins are solver input, not a text rewrite after solving:

schema_version = 1
name = "foss-2026.1"
toolchain = { name = "foss", version = "2026.1" }

[[pins]]
name = "HDF5"
version_requirement = "==2.1.1"
mode = "preferred"
source = "site stack"

[[pins]]
name = "PyTorch"
version_requirement = "==2.9.1"
toolchain = { name = "foss", version = "2024a" }
versionsuffix = ""
mode = "preferred"
source = "distribution stack"

A preferred pin is favored when jointly feasible. Resolvo may choose another compatible candidate and records the requested version, toolchain, versionsuffix, selected identity, and fallback. A locked pin cannot fall back. Omitting toolchain leaves the pin unconstrained within the target hierarchy; cross-generation admission requires an explicit toolchain. Omitting versionsuffix leaves that part of the artifact identity unconstrained. Candidate exclusions record known target/build evidence and stay visible in the profile lock.

Plan the complete bundle

eb-stack package plan \
  --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --package-config examples/packages/common.toml \
  --package-config examples/packages/eon.toml \
  --easyconfigs /path/to/easybuild-easyconfigs/easybuild/easyconfigs \
  --easyconfigs fixtures/eon_core_rgpot/easyconfigs \
  --stack-policy examples/stacks/eon-foss-2026.1.toml \
  --out-dir /tmp/eon

The command must produce a manifest, SBOM, one lock per requested profile, and one recipe per profile. Run recipe format, recipe lint, and recipe check against the upstream tree plus the bundle overlay.

eb-stack recipe format /tmp/eon/easyconfigs/e/eOn/*.eb
eb-stack recipe lint /tmp/eon/easyconfigs/e/eOn/*.eb
eb-stack recipe check \
  --recipe /tmp/eon/easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb \
  --easyconfigs /path/to/easybuild-easyconfigs/easybuild/easyconfigs \
  --easyconfigs /tmp/eon/easyconfigs

Missing robot dependencies are real package work. Close them with ordered source roots and optional catalog overrides rather than inventing placeholders or dummy checksums.

Source-root discovery (package-neutral)

Configure local indexes so a package closure can resolve robot holes without a committed per-package catalog entry:

eb-stack package plan \
  --source path/to/root-recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --source-checksum SHA256 \
  --easyconfigs /path/to/robot \
  --stack-policy stacks/site.toml \
  --package-sources examples/package-sources/local-roots.toml \
  --out-dir work/closed-root

Or pass roots directly with --easybuild-source, --conda-source, --spack-source, and --cargo-source (repeatable; order is discovery order).

Resolution for each hole is robot-first, then:

  1. optional catalog override for that identity;

  2. EasyBuild recipes under easybuild source roots (annual-bump pipeline; source toolchain family is preserved — a GCCcore recipe retargets to the GCCcore member of the target hierarchy, not the composite parent);

  3. exactly one compatible conda-forge or Spack recipe under foreign roots.

Ambiguous or incompatible providers fail with typed errors and candidate evidence. Catalog entries remain optional ordered overrides.

Catalog-backed robot holes

Pass one or more --package-catalog layers when package plan must recursively close robot holes with explicit authoring inputs. Compatible robot candidates still win; the catalog is only consulted for unsatisfied direct dependencies before source-root discovery.

eb-stack package plan \
  --source path/to/root-recipe.yaml \
  --format conda-forge \
  --toolchain-name foss \
  --toolchain-version 2026.1 \
  --source-checksum SHA256 \
  --easyconfigs /path/to/robot \
  --stack-policy stacks/site.toml \
  --package-catalog examples/package-catalog/mixed-providers.toml \
  --out-dir work/closed-root

Each catalog entry maps an EasyBuild provider identity to authoring inputs. Provider kinds:

TOML provider

When to use

foreign (default; may be omitted)

The dependency has no EasyBuild recipe; plan from its conda-forge or Spack recipe, package-config layers, checksums, and product profile.

easybuild-bump

A compatible EasyBuild recipe already exists at another generation. Retarget it through the annual-bump pipeline (EasyBuild-origin plan, CycloneDX SBOM, Resolvo lock, deterministic emission). Do not replace that recipe with a foreign archive of a different artifact identity.

easybuild-bump fields: source is the existing .eb, toolchain is the target generation, optional version is the emitted package version used for provider matching, and at most one source_checksums entry has annual-bump semantics. Foreign-only fields (format, package_config, non-default profile, multiple checksums) are typed catalog errors.

Companions land under packages/<name>-<version>-<toolchain>/, share one easyconfigs/ overlay with the root, appear in topological build-order.json, and contribute to closure.sbom.cdx.json. A bumped companion whose own dependency is another catalog hole builds after that child and before its consumer. See examples/package-catalog/mixed-providers.toml.

Route the build

Keep reusable topology in a public base file and site details in a later layer:

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

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

The selected target decides transport, executor, runtime, and EasyBuild workload. A control host can therefore stage over SSH, submit through Slurm, run on the host or in Podman/Docker, and invoke the site’s EasyBuild command. Use a runtime-specific EASYBUILD_INSTALLPATH, work_root, and tmp_root so host-linked modules cannot leak into a container build. A shared EASYBUILD_SOURCEPATH is safe for source archives.

Run the Hermes/OMP repair loop

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

Hermes owns the campaign loop. OMP workers claim typed findings, repair the manifest/profile/recipe/target layer appropriate to the class, record evidence, and rerun the campaign. Build compatibility claims are not proof: configure, compile, link, test, install, and sanity failures stay empirical findings until the target build succeeds.

The endpoint is three separate claims:

  • resolves: all profile locks and recipes exist;

  • builds: every recipe succeeds through EasyBuild on the selected target;

  • binary-verified: every declared verification command succeeds.

Follow skills/new-package/SKILL.md for the exact campaign procedure and human-owned PR handoff.