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:
a conda-forge
meta.yaml~/~recipe.yamlor Spackpackage.py;one or more public package-config TOML files;
an EasyBuild robot tree, with site overlays passed after upstream;
a stack-policy TOML file for preferred pins, locked pins, and exclusions.
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:
optional catalog override for that identity;
EasyBuild recipes under easybuild source roots (annual-bump pipeline; source toolchain family is preserved — a
GCCcorerecipe retargets to theGCCcoremember of the target hierarchy, not the composite parent);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 |
When to use |
|---|---|
|
The dependency has no EasyBuild recipe; plan from its conda-forge or Spack recipe, package-config layers, checksums, and product profile. |
|
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.