
eb-stack
io.github.stackhands/eb-stack
Plan EasyBuild recipes, CycloneDX SBOMs, and Resolvo stack locks.
Documentation
Turn foreign and EasyBuild recipes into resolved, build-evaluated package bundles.
eb-stack parses conda-forge, Spack, PyPI, Comprehensive R Archive Network (CRAN), Cargo, LuaRocks, Raku, and EasyBuild recipes into one canonical package plan. It emits a planned CycloneDX Software Bill of Materials (SBOM), solves each product profile with Resolvo, writes one conventional .eb file per installable variant, and drives the recipes through a persisted remote build campaign.
A plan is not a build. package plan and package bump can establish only resolves. campaign run owns builds and binary-verified.
The GitHub release and the crates.io package are 0.7.4 and carry every command here.
The glossary defines closure, generation, baseline, and reachability. The longer path is the tutorial.
Discovery
- llms.txt indexes the manual. llms-full.txt repeats every page in one file.
- Contributing describes contribution and validation requirements.
- The skills for inspect, plan, bump, a stack SBOM, pull-request overlap, and the stdio server are under skills/.
- The Model Context Protocol manifest is server.json. Client configuration is the manual page Model Context Protocol server.
Pipeline
conda-forge / Spack / EasyBuild
│
▼
canonical package.plan.json ─── package.sbom.cdx.json
│
▼
profile materialization + stack policy + Resolvo
│
├── locks/<profile>.lock.json
└── easyconfigs/<letter>/<name>/<variant>.eb
│
▼
target-routed campaign + typed findings
New packages and bumps use the same artifacts. A bump is an EasyBuild-origin plan plus SBOM and Resolvo lock, not a standalone text rewrite.
Claim ladder
Report these claims independently:
- resolves: every requested profile has a Resolvo lock and emitted recipe;
- builds: every emitted recipe completes through EasyBuild on the configured target;
- binary-verified: every declared profile verification command succeeds.
package inspect establishes none of them. package plan and package bump can establish only resolves. campaign run owns the other two.
Install
The crate is eb_stack on crates.io; the installed binary is eb-stack.
Rust 1.90 or newer is required when building from source. rustc 1.83
stops while reading the crate index: the Cargo feature edition2024 is
not stabilized. rustup toolchain install stable and rustup default stable provide a new enough compiler.
The GitHub release is 0.7.4, with installers and archives for Linux
(x86-64, aarch64), macOS and Windows, and eb-stack --version prints
0.7.4 for it, as a build of this tree does. cargo install eb_stack --locked builds the same 0.7.4 from crates.io.
cargo binstall is not part of a Rust install. cargo binstall eb_stack fails with no such command: binstall until that program is
installed on its own; it then installs the release binary.
From this checkout:
git clone https://github.com/stackhands/eb-stack.git
cd eb-stack
cargo build --locked --release
install -Dm755 target/release/eb-stack ~/.local/bin/eb-stack
Add ~/.local/bin to PATH. eb-stack --version then prints
eb-stack 0.7.4. The test suite is cargo test --locked, in
Tests. It is not required before the first solve.
Build the Rust binary on a suitable build host. EasyBuild installs belong on the target selected by the public target configuration, not necessarily on the machine running the CLI.
Documentation: https://eb-stack.rgoswami.me
Inspect from a fresh clone
The repository includes foreign-recipe and package-config fixtures, so the parsing boundary can be exercised without an EasyBuild installation or external robot tree:
eb-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-inspect
python3 -m json.tool /tmp/eon-inspect/package.plan.json >/dev/null
python3 -m json.tool /tmp/eon-inspect/package.sbom.cdx.json >/dev/null
This writes the canonical build manifest and planned CycloneDX SBOM. It does not solve dependencies, emit recipes, or make a build claim; those stages need an EasyBuild robot tree and a configured build target.
Documentation
The manual is organized around operator tasks:
- get started on the bundled fixtures;
- installation for Linux, macOS, Windows, crates.io, and a checkout;
- sharding and inode admission;
- foreign recipe to verified package;
- package bundle schemas;
- layered build targets;
- campaign state and typed findings;
- repair a failed campaign;
- command-line reference;
- which modules carry a component, by name and version, by package URL, or by a Common Vulnerabilities and Exposures (CVE) id resolved through Open Source Vulnerabilities (OSV);
- what a dependency closure answers.
The source manual lives in docs/orgmode/; the documentation workflow exports RST into docs/source/ and validates the Sphinx site.
Bump an existing recipe
eb-stack package bump \
--source tests/repro_fixtures/gromacs/GROMACS-2024.4-foss-2023b.eb \
--toolchain foss/2024a \
--easyconfigs tests/repro_fixtures/universe_foss_2024a \
--out-dir /tmp/gromacs-2024a
The output recipe is /tmp/gromacs-2024a/easyconfigs/g/GROMACS/GROMACS-2024.4-foss-2024a.eb; the same directory contains its manifest, SBOM, and default profile lock. Repeat --easyconfigs for overlays and use --stack-policy for European Environment for Scientific Software Installations (EESSI) or site preferences. preferred pins may fall back inside Resolvo; locked pins may not.
Known maintainer fixtures cover GROMACS, ScaFaCoS, MDTraj, Fiona, PuLP, and numba across foss-2023b to foss-2024a.
Plan a new package
Package policy is public, layered TOML. Parsers preserve foreign names and
syntax without package-name branches; Resolvo matches case/punctuation-equivalent
robot names mechanically. Explicit ecosystem aliases, EasyBuild metadata,
build policy, and independently installable profiles live in package config.
Each profile emits one .eb file; Message Passing Interface (MPI) and OpenMP toolchain options alone do not
require a suffix.
Profiles can declare platform and architecture target context. Foreign
selectors are evaluated against those values before dependency solving, so a
target-specific dependency becomes a real Resolvo requirement instead of
disappearing with no message when planning runs on another host.
The Spack adapter parses Python into an abstract syntax tree (AST) without importing the recipe. It
statically evaluates literal data, bounded loops, formatting, and when
scopes; unsupported runtime expressions become source-located residuals rather
than guessed dependencies.
Parser notes are informational. Planning consumes only typed residual records, so a note about successfully materializing dynamic source logic cannot accidentally enqueue Hermes work. Genuine residuals carry a category, severity, summary, and optional evidence and source provenance.
Conda revision numbers and compiler/stdlib/cross-build template requirements are toolchain diagnostics rather than package-authoring work. Unknown templates inside ordinary package identities remain explicit residuals.
Aliases use foreign = "EasyBuild" when both names share a version domain.
Component-to-provider mappings use
foreign = { provider = "EasyBuild", constraint = "drop" }, so a component
release such as a Python package version is never imposed on its containing
EasyBuild provider.
Package policy also has two generic EasyBuild inputs:
[build.easyconfig_parameters]
general_packages = ["ASPHERE", "KSPACE", "MOLECULE"]
[[build.patches]]
filename = "Orbit-2.0-portability.patch"
sha256 = "4f43b42fdcf84d0cf634d993dd944f252c8243dc612a919fe2825d56f937c8eb"
source = "patches/Orbit-2.0-portability.patch"
[[dependencies.requirements]]
name = "VTK"
roles = ["run"]
By default, a layer that declares build.patches replaces the preceding patch
list. Set patches_mode = "merge" in [build] to preserve patches extracted
from conda-forge or Spack while adding EasyBuild policy patches. A merged entry
with the same filename replaces that artifact, so a reviewed checksum and
source path can refine foreign metadata without creating a duplicate.
Easyconfig parameters are typed data, not Python fragments. Requirements
enter the canonical manifest and CycloneDX SBOM before Resolvo selects a
version. Package planning requires every source and patch SHA-256; emitted
checksums remain positional with sources first and patches second. Local patch
assets are verified and copied beside every emitted recipe. Checksum-addressed
remote patches retain their exact URL in patches; EasyBuild downloads and
verifies them during the build.
scripts/fetch-upstream-trees.sh creates a checkout of the pinned easybuilders trees.
Source the env.sh it writes so $EB_EASYCONFIGS is the develop
easybuild/easyconfigs directory.
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 \
--package-catalog examples/package-catalog/eon.toml \
--package-sources examples/package-sources/eon-companions.toml \
--easyconfigs "$EB_EASYCONFIGS" \
--easyconfigs fixtures/eon_foss_2026_1/easyconfigs \
--stack-policy examples/stacks/eon-foss-2026.1.toml \
--out-dir /tmp/eon
Against easybuild-easyconfigs develop 981d2123dd that prints:
closure_plan=/tmp/eon/closure.plan.json
closure_sbom=/tmp/eon/closure.sbom.cdx.json
build_order=/tmp/eon/build-order.json
manifest=/tmp/eon/package.plan.json
sbom=/tmp/eon/package.sbom.cdx.json
lock=/tmp/eon/locks/default.lock.json
easyconfig=/tmp/eon/easyconfigs/e/eOn/eOn-2.16.0-foss-2026.1.eb
patch=/tmp/eon/easyconfigs/e/eOn/eOn-2.16.0_safemath-eigen5-core-guard.patch
companion_manifest=/tmp/eon/packages/readcon-core-0.13.1-GCCcore-15.2.0/package.plan.json
companion_sbom=/tmp/eon/packages/readcon-core-0.13.1-GCCcore-15.2.0/package.sbom.cdx.json
companion_lock=/tmp/eon/packages/readcon-core-0.13.1-GCCcore-15.2.0/locks/default.lock.json
easyconfig=/tmp/eon/easyconfigs/r/readcon-core/readcon-core-0.13.1-GCCcore-15.2.0.eb
companion_manifest=/tmp/eon/packages/rgpot-2.5.3-GCCcore-15.2.0/package.plan.json
companion_sbom=/tmp/eon/packages/rgpot-2.5.3-GCCcore-15.2.0/package.sbom.cdx.json
companion_lock=/tmp/eon/packages/rgpot-2.5.3-GCCcore-15.2.0/locks/default.lock.json
easyconfig=/tmp/eon/easyconfigs/r/rgpot/rgpot-2.5.3-GCCcore-15.2.0.eb
Because --easyconfigs is also a discovery root, that plan writes
closure.plan.json, closure.sbom.cdx.json, and build-order.json next to
the manifest, even when no companion recipe was required.
The included eOn package policy is core + with_rgpot: Fortran/CuH2
client, readcon-core, and in-process RGPOT (engines dlopen at runtime). Fat
metatomic/xTB/serve are off by default; conda host deps for those names are
excluded from the solve. The stack policy only prefers Eigen 5 for the
safemath patch path. Historical full-product fixtures under
fixtures/eon_packaging and fixtures/eon_foss_2026_1 remain as regression
snapshots of the old feedstock-parity surface.
Use --format spack, examples/packages/common.toml, and
examples/packages/qmcpack.toml for QMCPACK's
package.py. Its Spack version is pinned by commit rather than archive hash,
so planning also needs
--source-checksum 511d5f368db002f2f77504619e1ada8d4a3034200d25feef6773d12a6ed6d18e.
Parser output retains source spans, conda selectors, Spack
conditions/conflicts, dependency roles, and residual dynamic logic.
Inspect without solving or emitting recipes:
eb-stack package inspect \
--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 \
--out-dir /tmp/qmcpack-inspect
Check recipes
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/robot \
--easyconfigs /tmp/eon/easyconfigs
Checksums are positional: sources first, then patches. Missing dependency output includes compatible hierarchy members and candidates found in other generations. Fix the recipe or companion; do not bypass the check.
Configure and run builds
Targets are layered as transport to executor to runtime to EasyBuild workload:
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
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
campaign run is a foreground command. Keep it under the site's normal
terminal or service supervisor and inspect the state from another shell with
campaign status. Only one run, claim, or resolution can write a state file at
a time. The process lock releases automatically if the controlling process
exits; before rerunning an interrupted state, confirm that its routed
container, scheduler job, or host command is no longer active.
Transport may be local or SSH; execution may be direct or Slurm; runtime may be host, Podman, or Docker. Site hostnames, paths, modules, and scheduler sizing belong in the site layer. Container targets must use ABI-specific install, work, and temporary roots; only source archives should be shared across runtimes.
Cargo searches ancestor directories for .cargo/config.toml. If target storage lives below a bind-mounted home directory, mount the campaign subtree again at a neutral container path such as /eb-stack-campaigns, then use that path for runtime workdir and EasyBuild work_root. The target reference includes the complete TOML pattern.
The runnable local target defaults to two parallel EasyBuild jobs so memory-heavy C++ compilations remain usable on common workstations. Treat EASYBUILD_PARALLEL, scheduler CPUs, and scheduler memory as one allocation: raise parallelism only when the target has measured headroom. A compiler process killed by the kernel or exhausted virtual memory is a retryable resource finding, not evidence that the selected dependency is incompatible.
For a local public example, build skills/new-package/container/rocky9/Containerfile, populate /tmp/eb-stack/robot, and use examples/targets/local-podman.toml. Keep bundles below /tmp/eb-stack/bundles so host and container paths match.
Hermes and OMP name campaign roles, not required orchestration software. Hermes is the single owner that reads typed findings, applies repairs, and retries through the requested claim rung. OMP workers are optional concurrent participants that coordinate exclusively through finding ownership.
Campaign failures persist as typed findings. OMP workers coordinate repairs through owned queue operations:
eb-stack campaign finding claim --state /tmp/eon.campaign.json \
--id attempt:1:finding:1 --owner omp-worker-1
eb-stack campaign finding resolve --state /tmp/eon.campaign.json \
--id attempt:1:finding:1 --owner omp-worker-1 \
--action "corrected package configuration" \
--evidence "recipe check exits successfully" \
--change examples/packages/eon.toml
The state file retains attempts, the active recipe, typed failure commands and compact logs, ownership, repair evidence, and the claim ladder. A successful retry supersedes the matching open finding.
Solve a whole stack
eb-stack stack solve \
--easyconfigs fixtures/gromacs_2025_to_next/easyconfigs \
--stack-policy fixtures/gromacs_2025_to_next/policies/prefer_newer.json \
--baseline-easyconfigs fixtures/gromacs_2025_to_next/easyconfigs \
--lock-out stack.lock.json \
--sbom-out stack.cdx.json \
--build-list-out build.list \
--stack-diff-out stack.diff.md
See which easyconfigs an easyblock change reaches
eb-stack easyblock-reach \
--easyblocks ~/src/easybuild-easyblocks \
--easyconfigs ~/src/easybuild-easyconfigs \
--file ~/src/easybuild-easyblocks/easybuild/easyblocks/generic/cmakemake.py \
--reverse \
--out-dir /tmp/cmakemake-reach
A change to CMakeMake reaches recipes that name it, recipes whose
EB_<Name> class subclasses it (GROMACS does), and extensions or bundle
components that resolve to it. --range A..B and --pr N read the
changed Python from a local clone (refs/pull/N/head for a pull request).
--reverse adds modules that depend on a reached module. The report is
reach.toml, reach.json, and reach.md.
MCP
- MCP Registry name:
mcp-name: io.github.stackhands/eb-stack
eb-stack mcp exposes the version-one workflows as Model Context Protocol (MCP) tools over stdio, and one tool
for pr-overlap, rebuild-set, component-reach, and easyblock-reach.
eb-stack mcp --workspace ROOT (or EB_STACK_WORKSPACE) is the root for the
recipe file tools.
eb_package_inspect,eb_package_plan,eb_package_bump,eb_package_latest;eb_recipe_read,eb_recipe_list,eb_recipe_edit,eb_recipe_write,eb_recipe_grep,eb_recipe_apply_suggestion,eb_recipe_diff,eb_dep_versions;eb_recipe_easyblock,eb_recipe_templates,eb_recipe_hierarchy;eb_recipe_check,eb_recipe_lint,eb_recipe_format;eb_module_lint,eb_easyblock_dry_run,eb_build_log,eb_build_test(easyblocks;eb_build_testonly withEB_STACK_ALLOW_BUILD=1);eb_stack_solve,eb_stack_sbom,eb_stack_order,eb_stack_easystack,eb_stack_gap,eb_stack_provided,eb_stack_shadow,eb_stack_batch,eb_stack_shard,eb_stack_arch,eb_stack_files,eb_stack_generation,eb_stack_cuda_cc,eb_stack_known_failures,eb_stack_oom,eb_stack_bot_classify,eb_stack_sections,eb_stack_from_commit;eb_component_reach(same query ascomponent-reach, including a purl or a CVE id);eb_target_list,eb_target_doctor;eb_campaign_run,eb_campaign_status;eb_campaign_finding_claim,eb_campaign_finding_resolve;eb_pr_overlap(kindsoftware-layeroreasyconfigs),eb_rebuild_set,eb_easyblock_reach;eb_repro_summary.
eb_recipe_edit replaces an exact string and does not write a repeated match
unless replace_all is set. eb_recipe_check requires at least one
easyconfigs path (a .eb there means its easyconfigs directory) and
refuses a recipe that is not valid Python. eb_recipe_lint reports the
easyconfigs CI lint (F541, W605, W291), the check-contrib pycodestyle codes,
the easyconfigs test-suite checks and the review conventions (EB_*), plus
E999 for syntax errors, with a clean flag; it does not rely on
eb --check-contrib but merges it when eb is installed. EB_STACK_EB
names the eb to run, and EB_STACK_EB=none skips that check with the
report saying why, which keeps a lint deterministic on a host whose eb
would judge a recipe the local checks accept. eb_recipe_grep
searches the workspace.
Skills
Two produce recipes, three gate them, three carry them somewhere, one repairs the generator.
Produce:
- New package: conda-forge/Spack to profiles to bundle to Hermes/OMP campaign.
- Annual bump: EasyBuild recipe to SBOM + Resolvo bump bundle to campaign.
Gate, before anything is built or pushed:
- Verify recipe: prove the checksum, the patches, the extension versions, the configure options and the dependency bounds against the real source, without spending a build.
- EasyBuild dos and don'ts: recipe-shape rules and the pre-PR checklist behind
recipe check. - Easyblock fix: change an easyblock from an issue report, then prove it with the generated commands (
eb -x) and a build that runs the test step.
Carry:
- Upstream PR: recipe set to maintainer-acceptable easybuild-easyconfigs PR, distilled from real merged and rejected PRs.
- Site consume: build an upstream recipe at a site through
--from-pr/--from-commitinstead of forking it into the site's repository. - EESSI extend: test inside the EESSI compatibility layer and contribute to the software-layer easystacks.
Repair:
- Tool repair: a hand correction to emitted output is a bug report; fix eb-stack, regression-test it on the remote builder, regenerate, and diff byte-for-byte.
The public issue and PR surface belongs to the human operator. The skills produce recipe sets and evidence; they do not open or mutate upstream issues or PRs.
Tests
cargo test --locked --all-targets
The suite covers foreign syntax adapters, conditions, CycloneDX generation, profile materialization, stack-policy fallback, hierarchy-aware Resolvo locks, variant emission, known bump reproduction, target routing, persisted campaigns, binary verification, finding ownership, CLI, and MCP.
License
MIT · Code of Conduct · Security · Contributing
Ledger event retries
A producer can retain one JSON event packet with a unique build.event_id
and retry ledger append with that same packet. An identical retry
acknowledges the recorded entry and signs the current head without adding
another line. Different content under the same identifier is refused.
The identifier belongs to one event, including its operation stage.
A failed signed-head write can occur after the record is durable. Retaining
the packet makes that failure recoverable without duplicating the event.
An acknowledged retry accepts --expect-head only when it matches a prefix
of the current history. A new append still requires the exact current head.
Head signature trust is checked through ledger verify; the expected-head
argument is an optimistic history check.
eb_stackcargo install eb_stack