Repository avatar
Other Tools
v0.7.4
active

eb-stack

io.github.stackhands/eb-stack

Plan EasyBuild recipes, CycloneDX SBOMs, and Resolvo stack locks.

Documentation

eb-stack

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.

CI Docs crates.io docs License: MIT

Discovery

Pipeline

eb-stack data flow from foreign recipes through Resolvo locks to campaigns

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

Claim ladder: inspect has no claim; plan/bump establish resolves; campaign establishes builds and binary-verified

Report these claims independently:

  1. resolves: every requested profile has a Resolvo lock and emitted recipe;
  2. builds: every emitted recipe completes through EasyBuild on the configured target;
  3. 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:

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_test only with EB_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 as component-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 (kind software-layer or easyconfigs), 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-commit instead 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.

Part of stackhands

eb-stack is one of the stackhands tools for building, installing and maintaining an HPC software stack with EasyBuild on top of EESSI.

ToolJobDocs
eb-stackSolve, shard and plan an EasyBuild build list; admit builds under a scratch inode budgetdocs
eb-staged-rebuildRebuild an installation in a staged tree and swap it in, so running jobs keep their pathsdocs
eessi-arch-guardReload EESSI in a Slurm task that inherited it for another CPU or GPUdocs
eb-localInstall software with EasyBuild on top of EESSI into a directory a user or project ownsdocs
eb-keep-logsKeep a failed build's EasyBuild log and command output; rotate a pipeline's log treedocs
rfc9162Append-only Merkle logs hashed as RFC 9162, with tiled storage and signed checkpointsdocs
zig-commonThe shared Zig library under the Zig tools: system calls, checked files, child processesdocs
CARGO
eb_stack
Install Command
cargo install eb_stack