Skip to content

Latest commit

 

History

History
2390 lines (2133 loc) · 133 KB

cli.org

File metadata and controls

2390 lines (2133 loc) · 133 KB

Command-line reference

eb-stack has seven namespaced command groups (package, recipe, stack, target, campaign, repro, ledger), four reports (easyblock-reach, pr-overlap, rebuild-set, component-reach), and a Model Context Protocol (MCP) server. --version prints the crate version. All paths written by package commands are deterministic bundle artifacts. Closure, generation, baseline, reachability, toolchain family, and the claim ladder are defined in the glossary.

Exit status

CodeWhen
0The command finished the work it was asked to do
1The work was attempted and failed: a missing or unreadable file, a document that does not parse, a check that found errors, a network fetch that failed, a doctor probe, or a campaign whose build failed
2The command line was rejected before that work: a missing or unknown flag (clap), or a flag value or combination the command does not accept
78stack admit refused the job. The quota leaves too few inodes for the footprint and the margin, or the quota command or the scheduler command failed

A rejected value names the flag and the value. A missing file names the flag and the path. Status 2 is the same status clap already uses. A recipe that fails recipe lint or recipe check is status 1: the files were read and the check failed. stack from-commit is status 1 when any pin is released, and status 0 when every pin is newer or unknown or the files name no pin. A file name with no eb-X.Y.Z, and no --easybuild-version, is status 2. stack arch is status 1 when an entry is unsupported or flagged, or when --probe-sources reports unreachable or arch-missing sources. stack files is status 1 when a patch or an easyconfig is missing, and status 0 when the only lines are no_url or unchecked. stack generation is status 1 on any finding, and status 2 when an easystack path names no EESSI version and --eessi-version is absent. stack known-failures is status 0 when it prints warnings: they are advice for the pull request, not a refusal; with --strict it is status 1 when a warning remains, and status 0 when every hit is remedied. stack oom is status 0 when it warns: the warnings are advice, not a failed check. A --node value that is not CORES:MEMORY_MIB or CORES:MEMORY_MIB:TARGET is status 2, and so is a command with none of --easystack, --node, and --hooks, or a --parallel or --max-parallel of 0. A --hooks file that is not valid Python, has no module-level PARALLELISM_LIMITS, or has a row the model cannot follow is status 1, and the message names the line. stack bot-classify is status 0 for every class, and status 2 when a path is a directory. stack batch is status 1 when the plan includes, or depends on, an entry the architecture cannot build. stack provided is status 1 when a build-list line names software the baseline already provides, names no package, uses a toolchain other than 2026.1 or GCCcore-15.2.0 below it, sets --hooks, or belongs in a --family-lock. A group-restricted warning is status 0 unless --strict. Status 2 is when neither --installed nor --baseline-lock is set. stack shard is status 1 when a build-list line cannot be resolved and is not fetched by EasyBuild, or when a --commit-cache download fails before an HTTP status, and writes nothing in either case. --shards 0, a list with no lines, and a --commit-tree or --pr-tree that is not COMMIT=DIR or N=DIR are status 2. stack shadow is status 1 when a site-layer recipe shadows a base-stack module, or when a recipe does not parse, and status 0 when every recipe names software the base does not provide. ledger verify is status 1 when a proof or a head signature does not check, and status 2 without exactly one of --record with --head and --from with --to. ledger append is status 1 when the record is outside the format, the log no longer reads, or the log is not at --expect-head; it then writes nothing.

--help for every command has an example and a description of every flag. A default that the command applies is printed in that help. A directory that receives several artifacts is --out-dir. A single output file is --out (--lock-out for the lock stack solve writes). --installed, --easyconfigs, --filter-deps, and --generated-at use those names on every command that takes them.

Package commands take the toolchain as --toolchain name/version, the spelling stack solve uses. --toolchain system, --toolchain SYSTEM, and --toolchain system/ select the SYSTEM toolchain. Its version is empty. package inspect and package plan accept a lone --toolchain-version and use foss. package bump does not: it accepts --toolchain-name only together with --toolchain-version. Both split flags are hidden compatibility spellings. An empty --toolchain-name is status 2.

Command map

CommandPurpose
package inspectParse conda-forge, Spack, PyPI, Comprehensive R Archive Network (CRAN), Cargo, LuaRocks, or Raku into a manifest and planned software bill of materials (SBOM)
package planMaterialize profiles, solve with Resolvo, and emit recipes
package bumpRetarget an EasyBuild recipe and write the same canonical bundle
package latestReport the newest stable upstream release of EasyBuild recipes
recipe easyblockPrint the easyblock class name EasyBuild derives for a software name
recipe templatesEmit template constants and derived-key rules, or the values `–name` and `–version` resolve
recipe hierarchyEmit known toolchain hierarchies as JSON
recipe checkValidate metadata and robot dependency availability
recipe lint / recipe formatReport upstream CI, test-suite, and review findings (E999, F541, W605, W291, E501, E2xx, EB_*; eb --check-contrib merged when eb is installed) or mechanically wrap E501 lines
stack solve / stack sbomSolve a multi-root stack or emit SBOM from a lock
stack orderSequence every pinned build a set of roots needs
stack gapRoute each site module the stack lacks, name the recipe that closes it, and write one TSV per route
stack batchPlan software-layer pull requests whose builds do not overlap
stack shardSplit a Jenkins build list into shards that build at once, with a prelude of shared dependencies, capped by a scratch inode quota
stack capacityRead timestamped advisory scheduler capacity with exact partition, host profile and resource constraints
stack footprintsWrite the peak build-tree inode count per software name from ledger records, a static table, and a class table
stack admitAdmit one build job under the scratch inode quota, counting the reservations of the jobs still running
stack easystackWrite a lock as the YAML eb --easystack reads
stack sectionsRegroup a Jenkins build list under a previous list’s headers
stack from-commitReport from-commit pins already contained in an EasyBuild release
stack providedFail when a build list names software a baseline already provides
stack shadowReport a site-layer recipe that shadows an EESSI base-stack module
stack archRefuse a plan an architecture cannot build, and flag entries never installed there
stack filesFind the patch and source files an easystack’s easyconfigs need that no tree has
stack generationRefuse easystack entries outside their EESSI version’s toolchain generations
stack cuda-ccRefuse CUDA compute-capability combinations a plan cannot build on EESSI accelerator targets
stack known-failuresWarn about known per-architecture test, build, fetch and sanity failures in a plan, with the easyconfig remedy
stack oomWarn about plan entries likely to run out of memory in a bot job, with remedies
stack bot-classifyClassify EESSI bot job results as real failure, ERROR-only noise, empty tarball, or infra
repro summaryPrint a reproduction scoreboard from collected case scores
pr-overlapClose open pull-request easystack additions against an installed stack and report shared builds
rebuild-setClassify the installed reverse dependents of easyconfigs patched in place
component-reachList who carries a component in one generation and close their rebuild set
target list / target doctorResolve layered target configuration and probe it
campaign run / campaign statusExecute or inspect a persisted build campaign
campaign finding claimClaim an open finding for one worker
campaign finding resolveResolve a claimed finding with an action, evidence, and the paths it changed
ledger appendAppend one event record to a per-stack ledger as a canonical JSON line and to its tile log, and sign the new head
ledger headSign a ledger’s current head and the tile log’s checkpoint, for a published anchor
ledger tilesWrite the tile log beside a ledger, or bring it up to date
ledger findFind records by field=value keys through the tile log’s sidecars, each proven in the checkpoint
ledger showList ledger records newest first, for one module or software name
ledger verifyCheck that a record is in a signed head, or that one signed head extends another, and that the tile log agrees
mcp [--workspace]Serve each CLI workflow over stdio, including pr-overlap, rebuild-set, component-reach, and easyblock-reach, with recipe file tools rooted at --workspace
easyblock-reachReport easyconfigs and installed modules reached by an easyblock or framework change

Package commands

package inspect

eb-stack package inspect \
  --source PATH \
  --format auto|conda-forge|spack|pypi|cran|cargo|luarocks|raku \
  --toolchain foss/2026.1 \
  [--package-config PACKAGE.toml]... \
  --out-dir BUNDLE

This writes package.plan.json and package.sbom.cdx.json. It does not solve profiles or emit recipes, so it establishes no claim-ladder rung.

Format notes. A rockspec’s ~~> 1.8~ is >=1.8,<1.9 and ~~= X~ is !=X; build_dependencies are build edges; a git+ source.url is a git remote with its tag and emits a git_config source; source.hash counts only as a 64-hex SHA-256; external_dependencies become residuals; the plan’s easyblock is LuaRocks. A META6 ver<0.10+> is >=0.10, and the ver, auth, api and from adverbs are not part of the module name. A CRAN package’s sanity dir is lib/R/library/<name>. A Spack depends_on with type=("build", "link") is a runtime edge. A PyPI or PEP 518 requirement gated by an environment marker is an opaque condition: it is kept out of the solve and reported as a pypi-marker residual. A build backend or overlay build tool is named only when the plan’s own toolchain hierarchy provides it, as a recipe or as a bundle extension.

package plan

eb-stack package plan \
  --source PATH \
  --format auto|conda-forge|spack|pypi|cran|cargo|luarocks|raku \
  --toolchain foss/2026.1 \
  [--package-config PACKAGE.toml]... \
  [--source-checksum DIGEST]... \
  [--package-index FILE] \
  --easyconfigs UPSTREAM \
  [--easyconfigs OVERLAY]... \
  --stack-policy STACK.toml \
  [--package-catalog CATALOG.toml]... \
  [--package-sources SOURCES.toml]... \
  [--easybuild-source DIR]... \
  [--conda-source DIR]... \
  [--spack-source DIR]... \
  [--cargo-source DIR]... \
  --out-dir BUNDLE

Package configs apply in argument order; later metadata, build policy, aliases, and profile values override earlier layers. Later robot paths override earlier paths for the same EasyBuild identity. build.patches replaces the existing patch list by default. Set patches_mode = "merge" in the layer’s [build] table to retain foreign or preceding patches while adding or refining artifacts by filename. Each requested product profile is solved independently and emits one lock plus one conventional .eb file. The default profile has no suffix; non-default profiles use their declared versionsuffix.

Every emitted source needs a SHA-256 checksum. When a foreign recipe provides only a version control system (VCS) commit, repeat --source-checksum once for every source artifact in manifest order. The values replace the positional source checksums in both the manifest/SBOM and every emitted recipe. Planning fails before emission when the count, shape, or completeness is invalid.

--package-index FILE supplies a version for a dependency that states none. The file is either a CRAN PACKAGES index or a name==version list, one pin per line. A range is not a version and supplies nothing.

Passing --easyconfigs always uses the closure writer, because that tree is also a discovery root. The bundle then contains closure.plan.json, closure.sbom.cdx.json, and build-order.json even when no companion was needed. --emit-easyblock does not combine with that writer.

When a direct dependency has no compatible robot candidate, further closure inputs close the hole without inventing versions:

  • --package-sources layers ordered EasyBuild / conda-forge / Spack / Cargo roots;
  • --easybuild-source, --conda-source, --spack-source, and --cargo-source push roots without a TOML file (order is discovery order);
  • --package-catalog layers explicit foreign or easybuild-bump providers as ordered overrides.
  • --emit-easyblock renders an easyblock skeleton for the package into easyblocks/<letter>/<module>.py, writes it next to the recipes, names the derived class as the easyblock and moves the variant options out of the emitted recipes. Without it a package that owes an easyblock gets a missing-easyblock residual. It does not combine with closure planning. package bump takes the same flag.

Robot candidates win over catalog entries. A closed plan writes companion packages under packages/, a shared easyconfigs/ overlay, topological build-order.json, and closure.sbom.cdx.json. See package bundles and the new-package how-to.

--format cargo reads Cargo.toml or crates.io JSON. A Cargo.toml names no crate tarball, so plan stops with foreign package plan has no source artifacts. The crates.io JSON carries the checksum, and that is what plan emits from. A PyPI leftover whose run dependency is a PyO3 crate is closed by --cargo-source, not by a pip extra. Extra site packages that Warehouse omits belong in the ingest: a JSON array of Warehouse objects, or later requirements.txt lines. Resolvo then takes the robot module when --easyconfigs provides it. Do not hand-edit the emitted recipe for those names. A PEP 518 build_system object with build-backend = mesonpy is the mesonpy hint: listed requires become build deps in the Boolean satisfiability (SAT) solve, and the emitter writes wrap_mode=default plus the shared cargo-on-EESSI isolation prelude (host wrappers off, LINKER=${CC:-gcc}, compat ld from uname -m). Hatchling stays a Python-bundle-PyPI module when the robot provides it. See site Python extras.

package bump

eb-stack package bump \
  --source RECIPE.eb \
  --toolchain foss/2024a \
  [--version VERSION | --latest [--overrides OVERRIDES.toml]...] \
  [--source-checksum DIGEST] \
  [--dep NAME=VERSION]... \
  --easyconfigs UPSTREAM \
  [--easyconfigs OVERLAY]... \
  [--hierarchy-fixture HIERARCHY.json] \
  [--stack-policy STACK.toml] \
  [--package-config PACKAGE.toml]... \
  [--foreign RECIPE]... \
  [--strict-patches] \
  [--emit-easyblock] \
  [--fetch-checksums [--latest-secondary] [--drop-patch NAME=REASON]...] \
  [--no-companions] \
  --out-dir BUNDLE

A bump writes the same manifest, planned SBOM, profile lock, and recipe layout as a new package. --dep becomes a locked Resolvo override. The solve fails when that name is not in the robot tree. A name the recipe does not depend on stays out of the emitted dependency list, and the lock records the pin outcome. Without --stack-policy, the solver uses an unconstrained policy for the requested toolchain. A changed package version without --source-checksum clears the old digest and produces a visible residual, because source bytes cannot be inferred safely. --package-config is repeatable and applies after the source recipe is parsed, as it does for package plan. --foreign names a Spack package.py or a conda-forge recipe whose dependencies the bump inspects for names the source .eb never declared; it is repeatable, and each hit is mapped to an EasyBuild package through the package-config aliases. --strict-patches fails the bump when a patch’s fate across a version change cannot be decided from the tree, or, with --fetch-checksums, from the new source; without it the patch is carried and a patch-decision residual records the review flag. --emit-easyblock is the same flag as on package plan. --fetch-checksums downloads each sources entry from the recipe’s source_urls when the version changed and --source-checksum was omitted, and writes that SHA-256 into the recipe and the SBOM. An archive URL that 404s under the recipe’s tag spelling is tried with the leading v added or removed, and the sources entry is rewritten to the name that served it. A later source whose bytes match the digest the recipe states is confirmed. A git checkout, a missing URL, or a failed download stays empty. The attempts are sources.fetch.json (one object per source: filename, url, sha256, errors, and respelled when the tag spelling changed) and a source:fetch residual. With the new source unpacked, each patch the tree cannot decide is tried the way EasyBuild’s patch step applies it (patch -p<level> at the level EasyBuild guesses, in recipe order): a patch that applies is carried, one that reverse-applies or whose modified files are all gone is dropped from patches and checksums, and anything else stays an undecided patch-decision residual naming the failed hunks. Three cases that neither apply nor reverse-apply are still decided from the source: a patch that only creates files the release already contains, a patch whose every change block’s new lines stand in the target file while its removed lines do not (the release made the change under different surrounding lines), and a patch that only pins bazel_dep modules MODULE.bazel already declares. Each is dropped with the file and line or the declared version as evidence. A patch still undecided is replaced by a rebased file named for the new release, <Name>-<version>_<description>.patch or the old name with its version replaced, when one sits beside the source recipe or under <letter>/<Name> of an --easyconfigs root and applies; the recipe entry, its checksum and the bundle take the new file. --drop-patch NAME=REASON (repeatable) drops a patch the operator decided against, the recipe’s or its own extension’s, and records the reason beside what the source alone said; a name the recipe does not list fails the bump. --latest-secondary moves each later source whose version a local_* variable holds (gc’s libatomic_ops) to its newest stable release, read with the package latest backends from the source URLs that carry that version and keyed by the component name in --overrides; the variable, checksum and plan digest move together and a secondary-source residual records the move. A local_* variable holding a 40-hex commit that is the commit of the old release’s GitHub tag (the first source’s archive name with the old version) moves to the new tag’s commit, as RStudio-Server’s local_git_rev must; a commit that is not the old tag’s is left as written, and each case is a release-commit residual. The patches of an extension that is the package itself are tried against its new archive. See output formats. For a PythonPackage or CargoPythonPackage recipe, the new source’s own pyproject.toml decides what the build needs: each [build-system] requires entry a recipe dependency does not already bundle (the Python module carries setuptools and wheel) becomes a build dependency on the module of that name in the target hierarchy, and one with no such module is a build-requirement-unmapped residual. A maturin or setuptools-rust build with a Cargo.lock also gets Rust, the recipe becomes a CargoPythonPackage, and crates is written from that lock. A dependency whose version the recipe writes in terms of its own, as ('Boost', version) or ('protobuf', version[2:]), is pinned at the version that expression gives after the bump. When the tree lacks that version and carries the dependency at another version in the target hierarchy, the dependency is bumped too, from that recipe and at its own toolchain level, into the same easyconfigs/ overlay; its plan, SBOM and lock go under packages/NAME-VERSION-TOOLCHAIN/. companions.json names each one, its source recipe, and the package that needed it, and build-order.json lists the companions before the root so campaign run builds them first. --no-companions leaves such a dependency for the version-bump-dropped-dep residual instead. --latest takes the version package latest picks for the recipe, so it conflicts with --version; --overrides adds override files to that decision. The bump fails when no stable release survives.

package latest

eb-stack package latest \
  --source RECIPE.eb [--source OTHER.eb]... \
  [--overrides OVERRIDES.toml]... \
  [--json]

The backend is read from the recipe’s source_urls: PyPI, GitHub, GitLab, CRAN, crates.io, or an HTTP directory listing. Every listed version is printed as kept, or dropped with its reason: a draft, a pre-release, a yanked release, or the per-package override (an odd-minor development series, a highest allowed major). eb-stack carries data/upstream-overrides.toml; --overrides files apply after it. The state is current, behind, pinned or unknown. --json prints one array with the same rows. GitHub requests send GITHUB_TOKEN or GH_TOKEN when set.

Bundle layout

BUNDLE/
  package.plan.json
  package.sbom.cdx.json
  closure.plan.json
  closure.sbom.cdx.json
  build-order.json
  locks/<profile>.lock.json
  easyconfigs/<letter>/<name>/<recipe>.eb

package plan writes the three closure files whenever --easyconfigs is passed. package inspect writes only the plan and planned SBOM.

package plan and package bump establish the resolves claim only when all requested locks and recipes are written successfully.

See package bundle reference for the complete manifest, planned SBOM, profile-lock, residual, and recipe contract.

Recipe commands

eb-stack recipe easyblock NAME
eb-stack recipe easyblock -- -NAME
eb-stack recipe easyblock --charmap

eb-stack recipe templates --export
eb-stack recipe templates --name NAME --version VERSION

eb-stack recipe hierarchy
eb-stack recipe hierarchy --export

eb-stack recipe check \
  --recipe RECIPE.eb \
  --easyconfigs UPSTREAM \
  [--easyconfigs OVERLAY]... \
  [--easyblocks DIR_OR_FILE]... \
  [--require-configopt FLAG]... \
  [--metadata-only] \
  [--verify-sources \
     [--seeded-from ECOSYSTEM \
        [--seeded-source-url URL] \
        [--seeded-git GIT_REMOTE]] \
     [--source-tree DIR]] \
  [--strict]

eb-stack recipe lint RECIPE.eb [RECIPE.eb ...] [--base BEFORE.eb]
eb-stack recipe format RECIPE.eb [RECIPE.eb ...] [--dry-run]
eb-stack recipe format RECIPE.eb --out FORMATTED.eb

recipe easyblock prints the class EasyBuild derives from a software name (EB_ plus the framework character map). --charmap prints that map as JSON instead. A hyphen-leading name is a name, not an option: pass it directly or after --.

recipe templates --export prints the constant table and the derived-key rules. With --name and --version it prints the values those rules produce for that pair, using the lowercased name for the forge-account defaults and system for the toolchain. recipe hierarchy prints the known fixture generations as JSON, parent and members, most minimal subtoolchain first. --export prints the whole table: those generations plus the synthetic GCC-family rule.

recipe lint prints JSON with clean, style (each finding carries path, line, column, code, severity, message, and, when set, info and in_review), check_contrib, and maintainer_acceptability. It does not rely on eb --check-contrib. Style codes: E999 (not valid Python; the syntax-tree rules are skipped); the easyconfigs CI lint F541 (f-string with no placeholder), W605 (invalid escape sequence), and W291 (trailing whitespace, including inside description); the check-contrib pycodestyle codes E501 (longer than 120) and E225/E226/E227/E228/E231; the easyconfigs test-suite checks EB_PY_NO_USER_SITE, EB_SANITY_PATHS_SHAPE, EB_LOCAL_VAR_NAMING, EB_REDEFINED_PARAM, and EB_CHECKSUM_COUNT; and the review conventions EB_FMT_MISSING_F, EB_FSTRING_QUOTE_REUSE, EB_TEMPLATE_UNKNOWN, and EB_PARAM_TUPLE_COMMA (errors) and EB_FMT_PERCENT, EB_FMT_CONCAT, EB_FMT_TEMPLATE_VAR, EB_SHLIB_HARDCODED, EB_DEP_DUPLICATE, EB_SANITY_CMD_BUILDDIR, and EB_SANITY_CMD_CD_INSTALLDIR (warnings). clean is false while any error or any in-scope EB_FMT_* finding remains; error_free counts errors only. An EB_FMT_* finding with info true is outside the review scope (an untouched %%(...)s-escaped entry or, with --base, a line unchanged from the base) and does not count. With --base the diff-mode rules also run: EB_SANITY_ENTRY_REMOVED and EB_EASYBLOCK_CHANGED (errors), EB_SANITY_PATHS_REMOVED (warning), and EB_FMT_TEMPLATE_REMOVED. EB_FSTRING_ESCAPED_TEMPLATE and EB_DEP_EMPTY_VERSION are errors. With --base, the file before the edit, it also reports EB_FMT_TEMPLATE_REMOVED where a template was replaced by a Python expression. f'lib/%(name)s.{SHLIB_EXT}' is correct and not flagged. When eb is on PATH, the command also runs eb --check-contrib and merges those findings (origin is eb --check-contrib). EB_STACK_EB names the eb to run instead, and EB_STACK_EB=none skips the check, which keeps a lint deterministic on a host whose eb would judge a recipe the local checks accept; the report says the check was skipped and why. A missing eb is reported in check_contrib and does not by itself fail the command. Error-severity findings do; warnings do not. recipe format only wraps mechanical E501 lines; the other codes stay in remaining.

recipe check refuses a recipe that is not valid Python. It prints the structured dependency result and fails when package metadata, checksum structure, required configuration flags, or robot dependencies are invalid. Missing dependencies remain explicit work; the command does not create placeholder recipes.

--require-configopt takes the flag as it appears in configopts, dashes included (--require-configopt --with-tests). --strict lists every statement that failed to parse and fails when that list is not empty. if, for, import, and the other control keywords stay unreported: that is the tolerant skip, not a parse failure. --verify-sources classifies each source URL and, with --seeded-from, rejects a checksum copied from a different artifact class. --seeded-source-url and --seeded-git require --seeded-from, which itself requires --verify-sources. --source-tree compares the version the unpacked tree declares with the recipe version.

It also prints easyblock_resolution: the class the recipe resolves to (named, or derived from the software name), the file that defines it, its bases and its extra_options keys. A bundle’s own easyblocks/ directory, beside the easyconfigs/ tree the recipe sits in, is searched first. Naming --easyblocks roots, such as an easyblocks checkout, asserts that the set is complete: a software-specific EB_ class found in none of them fails the check with the message EasyBuild would stop on.

recipe format only performs deterministic physical-line wrapping. Product configuration, source selection, checksums, patches, and sanity paths remain authored content.

Stack commands

stack capacity

Read one captured normalized scheduler observation with --snapshot. Scope it using the exact --partition, maintained physical-host --target and repeated literal --feature requirements. Resolve the single-node demand with --cpus, --memory-mib, --gpus and optional --gpu-type. --exclusive-node true|false records resolved whole-node policy; --max-slots-per-node caps its advertised slots. --generated-at and --max-age-seconds bound a deterministic timestamped replay. The output is advisory, contains no node names and grants no execution or storage admission.

stack shard --storage-policy selects an opt-in captured policy that allocates the smallest phases fitting its shared budget to shared storage and eligible overflow phases to node-local storage. It conflicts with --inode-quota. The policy preserves the configured --inode-margin and binds the actual ordinary and materialized execution-list hashes. The executor verifies storage and uses normal admission for every actual domain. See the capacity and phase-storage reference for the full schema.

eb-stack stack solve \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  (--stack-policy POLICY.json | --toolchain NAME/VERSION \
   [--root NAME]... [--roots-from-build-list LIST]...) \
  [--baseline-easyconfigs TREE] \
  [--baseline-toolchain-version VERSION] \
  [--baseline-easystack EASYSTACK.yml]... [--baseline-commits-from EB_REPO] \
  [--baseline-also-toolchain NAME/VERSION]... \
  [--filter-deps NAME,NAME...] \
  [--lock-out /tmp/stack.lock.json] \
  [--sbom-out /tmp/stack.cdx.json] \
  [--build-list-out /tmp/build.list] [--build-list-excludes-baseline] \
  [--jenkins-build-list-out /tmp/next.list [--jenkins-flags-from /tmp/previous.list]... \
   [--jenkins-gpu-systems gpu-a,gpu-b] [--jenkins-roots-only]] \
  [--stack-diff-out /tmp/stack.diff.md] \
  [--dot-out /tmp/stack.dot]

eb-stack stack sbom --lock /tmp/stack.lock.json [--out /tmp/stack.cdx.json]

eb-stack stack order \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  --root NAME [--root NAME==VERSION]... \
  [--filter-deps NAME,NAME...] \
  [--installed LIST]... \
  [--out /tmp/build-order.txt] \
  [--oldest] \
  [--easystack-out /tmp/order.yml] \
  [--hashes-out /tmp/hashes.txt]

eb-stack stack easystack \
  --lock /tmp/stack.lock.json \
  [--out stack.yml | --eessi-version 2026.06 --easybuild-version 5.4.1 \
   --generation 2026.1 --out-dir SOFTWARE_LAYER] \
  [--option FILE.eb:OPTION=VALUE]...

eb-stack stack gap \
  --site SITE-MODULES [--site OTHER]... \
  [--older EESSI-2025.06-MODULES]... \
  --installed CPU-MODULES [--installed ACCEL-CC-MODULES]... \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  --generation 2026.1 \
  [--eessi-version 2026.06] \
  [--exclude-file SITE-ONLY.tsv] \
  [--filter-deps NAME,NAME...] \
  [--open-layer OPEN.json]... [--open-easyconfigs OPEN.json]... \
  --out-dir GAP

eb-stack stack provided \
  --build-list SITE.list [--build-list OTHER.list]... \
  (--installed INSTALLED.txt)... [--baseline-lock LOCK.json] \
  [--family-lock LOCK.json]... \
  [--allow NAME]... \
  [--licensed NAME]... [--licensed-file NAMES.txt] \
  [--format text|github|json] [--json] [--strict]

eb-stack stack shadow \
  --easyconfigs tests/shadow_fixtures/easyconfigs \
  --base tests/shadow_fixtures/base \
  --base tests/shadow_fixtures/bare.txt \
  [--json]

eb-stack stack batch \
  (--root FILE.eb... | --roots-file ROOTS.txt) \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  [--commit-tree COMMIT=DIR]... \
  --installed INSTALLED.txt [--installed-modulerc MODULERC]... \
  [--open-prs OPEN.json]... [--open-repo EESSI/software-layer] \
  --eessi-version 2026.06 --easybuild-version 5.4.1 --generation 2026.1 \
  [--filter-deps NAME,NAME...] \
  [--allow-versions NAME@GENERATION]... \
  [--size 3] \
  [--easyconfigs-repo ~/src/easybuild-easyconfigs] \
  [--generated-at 2026-10-29T00:00:00Z] \
  [--arch riscv64]... \
  [--unsupported LIST.toml] \
  [--known-failures TABLE.toml] \
  [--oom-profile BUILD-MEMORY.toml] \
  [--oom-resources BOT-RESOURCES.toml] \
  --out-dir BATCHES

eb-stack stack shard \
  --build-list NODE-TYPE.list \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  [--commit-cache DIR] [--commit-tree COMMIT=DIR]... [--pr-tree N=DIR]... \
  [--easyconfigs-repo EASYBUILD-EASYCONFIGS] \
  [--materialize --build-robot-path PATH [--build-robot-path PATH]...] \
  [--installed-modules-dir MODULES/all]... [--installed INSTALLED.txt]... \
  --shards 4 \
  [--weights SECONDS.tsv] [--default-weight 1] \
  [--filter-deps NAME,NAME...] \
  [--lift-cpu-prerequisites] \
  [--generated-at 2026-11-03T00:00:00Z] \
  [--inode-quota 3000000] [--inode-footprints footprints.json] \
  [--inode-margin 100000] [--inode-line-overhead 1000] \
  --out-dir SHARDS

eb-stack stack footprints \
  [--ledger ledger/2025.jsonl]... [--table FOOTPRINTS.tsv]... \
  [--generated-at 2026-11-07T00:00:00Z] \
  --out footprints.json

eb-stack stack admit \
  --quota-json QUOTA.json|- | --quota-cmd 'CMD' \
  --footprint 351000 --job 4411 \
  --reservations RESERVATIONS/ [--margin 100000] \
  [--lock LOCKFILE [--lock-kind flock|posix]] \
  [--live-jobs 4401,4402 | --live-jobs-cmd 'CMD'] \
  [--unknown-reserve 150000] [--drop-stale] \
  [--ts 2026-11-07T00:00:00Z]

eb-stack stack arch \
  --easystack EASYSTACK.yml [--easystack OTHER.yml]... \
  [--arch riscv64]... \
  [--easyconfigs TREE]... \
  [--installed INSTALLED.txt | --installed riscv64=INSTALLED.txt]... \
  [--additional-arch FILE.yml | --additional-arch riscv64=DIR]... \
  [--unsupported LIST.toml] \
  [--probe-sources] \
  [--probe-retries N] \
  [--probe-timeout-s SECONDS]

eb-stack stack files \
  --easystack EASYSTACK.yml [--easystack OTHER.yml]... \
  [--easyconfigs TREE]... \
  [--commit-tree COMMIT=DIR]... \
  [--easyconfigs-repo ~/src/easybuild-easyconfigs] \
  [--sourcepath DIR]... \
  [--path-map FROM=TO]...

eb-stack stack generation \
  (--easystack EASYSTACK.yml | --pr PR.json)... \
  [--eessi-version 2025.06] \
  [--deployed DIR]... \
  [--allow-versions NAME@GENERATION]... \
  [--generations TABLE.toml]

eb-stack stack cuda-cc \
  --easystack EASYSTACK.yml [--easystack OTHER.yml]... \
  --easyconfigs TREE [--easyconfigs OVERLAY]... \
  [--target TARGET]... \
  [--cuda-table TABLE.toml] \
  [--accelerator-targets TARGETS.toml] \
  [--easyblock-map MAP.toml]

eb-stack stack known-failures \
  --easystack EASYSTACK.yml [--easystack OTHER.yml]... \
  [--arch aarch64/a64fx]... \
  [--easyconfigs TREE]... \
  [--installed INSTALLED.txt] \
  [--known-failures TABLE.toml] \
  [--eessi-version 2025.06] [--easybuild-version 5.4.0] \
  [--strict]

eb-stack stack oom \
  [--arch x86_64/amd/zen4]... \
  [--easystack EASYSTACK.yml]... \
  [--easyconfigs TREE]... \
  [--installed INSTALLED.txt] \
  [--profile BUILD-MEMORY.toml] \
  [--resources BOT-RESOURCES.toml] \
  [--node CORES:MEMORY_MIB[:TARGET]]... \
  [--job-memory JOB-MEMORY.toml | --mib-per-job JOB-MEMORY.toml] \
  [--hooks eb_hooks.py] \
  [--parallel N] \
  [--max-parallel N]

eb-stack stack sections \
  tests/jenkins_sections/next.list \
  --from tests/jenkins_sections/previous.list \
  --out /tmp/grouped.list

eb-stack stack from-commit \
  --easystack 20261011-eb-5.4.0-UCX.yml \
  [--easystack OTHER.yml]... \
  [--easybuild-version 5.4.0] \
  [--easyconfigs-repo ~/src/easybuild-easyconfigs] \
  [--easyblocks-repo ~/src/easybuild-easyblocks] \
  [--rewrite] \
  [--json]

stack sbom reads the easyconfigs the lock names. A runtime dependency that is also selected is a CycloneDX dependsOn edge. A builddependency is the component property eb_stack:buildDependsOn. stack solve --sbom-out writes that same document, checksums included. The MCP tool eb_stack_sbom writes it too. An unreadable path contributes no edge.

--backport-list writes a markdown list of locked modules whose reach class justifies a backport. A path ending in .md also writes the sibling .json. --cve is repeatable and resolves that advisory through Open Source Vulnerabilities (OSV). --osv-cache is a directory of recorded responses: queries.json maps each request URL to a body file. Without --cve, every id in that file is resolved, and the command does not query the network. --reachability is the same name class file as rebuild-set. The classes are cross-user, network-facing, local-only, and not-reachable. A name neither the built-in table nor that file mentions is unclassified. Only cross-user and network-facing are on the list. Blast radius is the module and every locked module that depends on it through a runtime or build edge. The command writes the CycloneDX file, then prints backport=, recorded=, stack=, and out=. --cve, --osv-cache, and --reachability need --backport-list. --backport-list needs --cve or --osv-cache. eb_stack_sbom takes the same arguments as backport_list, cve, osv_cache, and reachability.

stack solve is the multi-root EasyBuild stack solver. Package-profile stack preferences use the TOML schema documented with the package workflow; whole stack solving uses the JSON Policy schema. The policy file is --stack-policy, the same flag package plan and package bump take. 0.3.0 called this flag --policy on stack solve. That spelling is a hidden alias. It is left out of --help, it still names the policy file, and it prints warning: --policy is accepted for compatibility. Use --stack-policy.

--dot-out writes the solved dependency DAG. A node is name/version plus the toolchain, and the versionsuffix when the recipe has one. An edge points from a package to a dependency it selected and is labelled with the requirement; a builddependency is dashed. A .svg or .png path is rendered with the Graphviz dot program. Any other suffix, including .dot, is the DOT source. The MCP field dot_out writes the same file.

The baseline is either a previous generation (--baseline-easyconfigs, by default the first --easyconfigs tree) or what a set of easystacks installs (--baseline-easystack, repeatable). An easystack baseline is every entry plus the closure its recipes pin, read from the --easyconfigs trees and kept to the policy toolchain’s hierarchy. An entry pinned with from-commit is read from that commit of --baseline-commits-from when given, and matched by filename otherwise. Entries that name no recipe are listed on standard error. --baseline-also-toolchain keeps another family’s hierarchy in the baseline, such as lfoss/2026.1 beside a foss/2026.1 solve. A foss solve cannot see the LLVM family (lfoss, lompi, lfbf, llvm-compilers). A baseline easystack can name one of those toolchains. If the stack diff then marks that package added, the solve warns on standard error and still exits 0. The added row is the site layer because the foss universe dropped the LLVM recipe. Keep one lock per toolchain family, or pass --baseline-also-toolchain for that family so its hierarchy stays in the baseline. See one lock per toolchain family. --filter-deps drops each named dependency before the solve, together with the subtree under it, using EasyBuild’s matching: a bare name, name=version, or name=[low:high]. [ and ] include a limit. The opposite bracket excludes it, and an empty side is open. The last spec for a name wins. Pass EESSI-extend’s EASYBUILD_FILTER_DEPS list when solving against a European Environment for Scientific Software Installations (EESSI) baseline, or the build list carries modules the compat layer already provides. Against an EESSI version’s easystacks, unchanged in the stack diff means the software layer already provides that module, and --build-list-excludes-baseline drops those modules (same name, version, toolchain and versionsuffix) from the build list, leaving what the site layer builds on top in dependency order.

A Jenkins build list is one easyconfig filename per line with optional eb flags, # for comments. --roots-from-build-list takes the package name of every line as a root. --jenkins-build-list-out writes the solved build order in that format; each package keeps the --hook, --hooks, --include-easyblocks and --accept-eula-for flags of its last line in the --jenkins-flags-from lists, while flags tied to the old recipe (--from-commit, --from-pr, --ignore-test-failure, --rebuild) stay behind. A module whose name or versionsuffix says CUDA gets --include-systems with the --jenkins-gpu-systems list. --jenkins-roots-only writes one line per root application and leaves the dependencies to eb --robot. stack sbom --lock re-reads the easyconfigs named in the lock and restores their runtime dependsOn edges.

stack order sequences the recipes the roots pin, in an order that respects every dependency. Two builds of one name stay when EasyBuild installs them side by side, which is the same package key stack solve uses: another toolchain, a system bootstrap pair, or a build-only tool. A runtime dependency has to agree on one version per toolchain. When no version satisfies every such requirement the command exits 1, names each requester and the version it resolved, and writes no order file. It does not repin a recipe. See Order every pinned build. --root repeats, and name==version pins one build. --filter-deps drops each named dependency before that order is built, together with the subtree under it, using the same matching as stack solve; when the flag is absent the command reads EASYBUILD_FILTER_DEPS. --installed repeats. A file is module -t avail, module spider -t, or Name/version, and a directory is an Lmod tree, both read the same way as stack gap --installed. The version is the full module version, 1.3.1-GCCcore-15.2.0, the spelling under modules/all. A system toolchain contributes no toolchain segment. A listed module is a leaf: it stays out of the order, and so do the dependencies that were installed with it. A dependency the tree cannot resolve is still an error. The walk that remains uses the synthetic GCC-family rule recipe hierarchy --export prints, so an unpinned dependency of a GCC or GCCcore recipe stays in that family. --filter-deps is applied first, so a filtered dependency is not counted as a leaf. The line installed=<N> just before order= is how many distinct modules the walk stopped at. --oldest takes the oldest admissible version of an unpinned requirement, and the default is the newest. --out is one easyconfig path per line (default build-order.txt). --easystack-out writes that same sequence as an easystack. --hashes-out writes one line per module, <sha256> <module>, a digest of the easyconfig and of the hashes of everything it needs. A module whose recipe could not be read is still listed, and the stdout line incomplete= counts those. Stdout ends with order=<path> builds=<N>, and a name with two co-installable builds also prints multiple builds of <name>:.

stack easystack writes the lock, sorted by package name, as the easystack eb --easystack consumes: easyconfigs: is a YAML list of filenames in lock order. --option repeats and is spelled as on the eb command line without its dashes, so it is <file.eb>:<option>=<value>, for example --option CUDA-12.8.0.eb:accept-eula-for=CUDA. A file with options is a mapping; a file without them is a string. Stdout is easystack=<path> entries=<N> of=<packages>. --eessi-version writes the EESSI software-layer layout under --out-dir instead of one --out file: easystacks/software.eessi.io/<V>/eessi-<V>-eb-<X>-<generation>.yml for the --generation toolchains, eessi-<V>-eb-<X>-001-system.yml for recipes on the system toolchain, and the same names under accel/nvidia/ for CUDA builds. A recipe on another generation’s toolchain is an error.

stack sections regroups a Jenkins list under the #### Title #### headers of a previous list. Commented entries stay commented. A name no previous section files goes under --unfiled, whose default is New in this generation.

stack gap compares the modules users load with a target stack. --site is the site’s live trees. --older is an older EESSI version loaded beside them (repeatable). At least one of the two is required. --installed is the target and repeats for the CPU module tree and an accelerator compute-capability tree. A directory is walked (Name/version.lua under modules/all, Compiler, and MPI; .modulerc.lua and default are not modules). A file is module -t avail, module spider -t, or Name/version lines. Versions are ignored. A name the target has is in-stack (matched without regard to case). Each other name takes the first route that applies: excluded (a --exclude-file line, name<TAB>reason, for licensed, vendor, conda, and portal software), filtered (--filter-deps, or EASYBUILD_FILTER_DEPS when the flag is absent), open-layer (an --open-layer pull request adds an easystack line of that name), recipe (the newest easyconfig on the --generation toolchains, on a toolchain whose version is that generation, or on the system toolchain), open-easyconfigs (an --open-easyconfigs pull request adds a recipe at that generation; the match is the filename, and a toolchain version equal to --generation counts), or no-recipe (the newest easyconfig on any toolchain is named as the bump source). --eessi-version keeps --open-layer easystacks of that version; without it every easystack version counts. The command writes gap.tsv, one TSV per route (site-only.tsv for excluded, bump.tsv for no-recipe, and in-stack.tsv, filtered.tsv, open-layer.tsv, recipe.tsv, open-easyconfigs.tsv), roots.txt for stack batch --roots-file (the recipe rows only), bump-sources.txt for package bump --latest, and version-change.tsv. The last file has one row per old module version: moved names the target EESSI version, site-only is an excluded name, and every other missing route is gone until it lands.

stack provided reads each --build-list (a Jenkins list or one easyconfig path per line; # comments are skipped) and fails when a line’s package name is one --installed or --baseline-lock already provides. Version and toolchain are ignored, so a different build of the same software still fails. --allow NAME keeps a second version. It does not waive a wrong toolchain or a --hooks flag. A .eb line that names no package fails too. A line whose toolchain is not 2026.1 or GCCcore-15.2.0 below it (GCCcore-15.2.0, GCC-15.2.0, gfbf-2026.1, gompi-2026.1, foss-2026.1) fails, as does a line that sets --hooks or --hook (that replaces EESSI’s hooks). --family-lock is a lock the foss baseline cannot see, such as lfoss. A name only that lock has is reported with the lock path, not left as site layer. --licensed and --licensed-file warn for group-restricted names; data/site-licensed.txt is that list for VASP, MATLAB, ANSYS, ORCA, and Gurobi. With neither flag there is no warning. --strict makes a remaining warning exit 1. Stdout lists each hit as provided <file> name=<Name> list=<path> line=<N> and ends with hits= unparsed= allowed= provided= toolchain= hooks= family= licensed=. --format is text by default, github for workflow commands, or json. --json is the same as --format json. See fail a build list that names software the baseline provides and route the modules a stack does not have.

stack shadow reads each site-layer easyconfig and the EESSI base inventory. --base is loaded by the same reader as stack gap --installed: a directory is an Lmod tree (Name/version.lua; default and .modulerc.lua are not modules) and a file is module -t avail, module spider -t, or Name/version. Repeated --base paths are merged. A recipe whose software version equals a base version, or whose base version continues past that software version with - and the toolchain, is same. EasyBuild’s installed module version is that spelling: the software version, then - and the toolchain, then the versionsuffix. A recipe versionsuffix has to end the base version, so a CUDA variant is not the same version as the plain build. Another recorded version of the same name is different. A base name with an empty version set is unversioned. A name the base does not have is not reported. Stdout lists each shadow as <kind> <file> name=<Name> version=<version> base=<versions> and ends with same= different= unversioned= recipes= base=. --json prints the same fields. The command exits 1 when any shadow is found. A recipe that does not parse exits 1 and names the file. See report a site recipe that shadows an EESSI module.

stack batch plans software-layer pull requests whose builds do not overlap. A root’s set is its closure over the robot path minus the installed modules, as pr-overlap computes it. Roots whose sets meet ship together; a root whose set meets an open pull request’s waits for it. Open pull requests come from --open-prs (pr-overlap --pr JSON) and --open-repo, which reads the open pull requests of that repository over HTTPS. --commit-tree supplies the easyconfigs an open pull request pins with from-commit. --installed-modulerc adds module_version aliases to the installed list. The rest are packed --size roots per pull request in a fixed order, so the same inputs give the same plan. With --easyconfigs-repo, a root the --easybuild-version release lacks gets a from-commit pin. Each batch-NN directory holds the EESSI easystacks, a lock, a CycloneDX SBOM, reproduce.sh with the exact command and the eb --dry-run lines, batch.json and pr-body.md. plan.md and plan.json at the top record the waits, the unresolved roots and a SHA-256 of the inputs. --generated-at fixes the time written into the plan. --arch limits the unsupported-entry check to that family. With no --arch, every family in the bundled list is checked. --unsupported replaces that list. A hit exits 1 and writes nothing. The same --arch values select the known per-architecture failures each batch includes or depends on. pr-body.md gets a “Known per-architecture failures” block with each warning and its remedy, batch.json lists them under known_failures, and plan.md counts them per batch. A failure whose remedy is already pinned or in the easyconfig is left out. --known-failures replaces data/arch-known-failures.toml. The same plan is checked with stack oom and its warnings are written to pr-body.md, batch.json (oom.warnings), and plan.md. --oom-profile and --oom-resources replace the bundled build-memory and bot-resource tables for those warnings. Each batch also runs the stack generation second-version check: a name with two versions in one toolchain generation, next to one an earlier batch of the same run adds or the installed list already carries, is a warning. pr-body.md gets a “Second versions in one generation” block, batch.json records versions.warnings and versions.count, and plan.md counts them per batch, like the known-failure and out-of-memory warnings. --allow-versions NAME or --allow-versions NAME@GENERATION accepts one, as it does for stack generation, and repeats. These are warnings, so a batch still exits 0; a bad --allow-versions spec is a usage error. When the EESSI version is not in the generation table, the check is skipped for the run.

stack shard splits one Jenkins build list (one node type, after --include-systems is applied) into shards that can build at the same time. Each line is Name-version-toolchain.eb [eb options]; # lines are comments. A line’s set is its closure over the --easyconfigs robot path minus the installed modules, as pr-overlap computes a root’s set. Installed modules come from --installed-modules-dir, an Lmod modules/all tree read as Name/Version (EESSI’s tree for the CPU target and the site tree, for example), and from --installed lists, one Name/VersionFull per line; both repeat. A line with --rebuild or --force builds its own module even when it is installed. Two lines share a shard only when one line’s easyconfig is in the other’s set, or both name the same easyconfig; the line that is needed comes first. Every other module the sets of two or more shards need goes to the prelude, so the shards’ sets are disjoint. Groups are packed into at most --shards shards, heaviest first onto the lightest shard. A module weighs its --weights value (Name/VersionFull or the easyconfig filename, a tab, then seconds; a fraction rounds up), or --default-weight (1) when the file does not list it. --filter-deps, or EASYBUILD_FILTER_DEPS when the flag is absent, drops dependencies the way EasyBuild does.

A line with --from-commit=SHA or --from-pr=N is resolved the way EasyBuild 5.4 resolves it. The easyconfig the line names comes from that commit’s or pull request’s easyconfigs, because EasyBuild replaces a named file with the fetched copy. A dependency comes from the robot path when the robot path has a file of that name, and from the fetched easyconfigs otherwise, because EasyBuild appends them to the robot path after every configured path. A fetched easyconfig the robot path also has, with other content, therefore loses to the robot path’s for a dependency: each such module a closure builds is printed as a warning: and listed under shadowed in plan.json. --commit-tree COMMIT=DIR names the easyconfigs directory of one commit (an abbreviated commit matches) and --pr-tree N=DIR the easyconfigs one pull request changes; both repeat. --easyconfigs-repo DIR is an easybuild-easyconfigs git repository: a checkout, a bare repository, or a blobless clone. A commit it holds is read in place: one git ls-tree lists the tree, and only the easyconfigs that can decide the plan (those the robot path has no file of, and those a line names) are read, through one git cat-file --batch. Nothing is written to disk for it. A --commit-tree for the same commit comes first. --commit-cache DIR holds every other tree. A commit missing there is downloaded over https from codeload.github.com/easybuilders/easybuild-easyconfigs/tar.gz/ plus the commit. That address also serves a fork commit a pull request carries, and only its .eb files under easybuild/easyconfigs/ are kept, at DIR/SHA/easybuild/easyconfigs. A pull request’s head commit and changed easyconfigs come from api.github.com and are kept at DIR/pr-N-HEAD, so a pull request that moves is read again. GITHUB_TOKEN or GH_TOKEN is sent to api.github.com when set. A tree is written under a temporary name and renamed into place, so several planners can share one cache. A module the closures of two or more shards need goes to the prelude whichever tree it comes from; a prelude line for a module only a fetched tree provides carries that line’s --from-commit, --from-pr and --include-easyblocks-from-* options, so it builds the same easyconfig with the same easyblock.

--lift-cpu-prerequisites is opt-in. It writes CPU-only prerequisites (including CPU-only build-list roots and their missing CPU closure) to cpu-prerequisites.list, keeps shared accelerator packages in prelude.list, and leaves accelerator-only work on the CUDA shards. Off by default: only modules two or more shards share go to the prelude. EESSI refuses to install a CPU-only package into an /accel/ prefix; build the CPU list under the CPU install path first, then the accelerator prelude, then the shards.

--out-dir gets prelude.list (the prelude as easyconfig filenames in dependency order; empty when nothing is shared), shard-NN.list (the input lines verbatim, dependencies first), and plan.json (each line’s closure, group and shard, the weight of every shard and of the prelude, and a SHA-256 over the build list, the weights, the installed modules and every easyconfig a closure reached). With --lift-cpu-prerequisites, it also gets cpu-prerequisites.list when any CPU-only module or root was lifted. A shard-NN.list that an earlier run left and this run does not write is removed.

--materialize also writes, for each --from-commit source, the directory sources/v1/SHA/ and its manifest sources/v1/SHA.json (schema version 1: each file’s path, the commit path it was copied from, its SHA-256 and size, and a SHA-256 over the list), and prelude.local.list and shard-NN.local.list beside the verbatim lists. The directory is the part of the files_commit_SHA directory EasyBuild would build that the planned closures read: each named easyconfig at the top, and each easyconfig, patch, and local source at its path in the commit, selected wherever EasyBuild’s lookup finds it first in the commit. In the .local.list files such a line names that top copy (or keeps its file name when the robot path supplies the easyconfig), drops --from-commit, keeps every other option, and ends with --robot-paths= the --build-robot-path values and then the directory. Give --build-robot-path once per robot path EasyBuild uses on the build host, in EasyBuild’s order, as eb --show-config lists robot-paths; it is required with --materialize. An empty element in --robot-paths stands for EasyBuild’s built-in default, not for a value set in EASYBUILD_ROBOT_PATHS or a configuration file, so the line spells every path out. A line is copied unchanged, and listed with the reason under materialized.unchanged in plan.json, when it is opaque or planned over the robot path only, when its source is not a commit, when it carries an option that changes how EasyBuild finds easyconfigs (--robot with a path, --robot-paths, --try-*, --from-pr, a second --from-commit, a value written as its own word, and so on), or when a recipe in its closure names a patch through a template, or a patch that is neither on the robot path nor in the commit. A prelude line is rewritten only when a rewritten line’s closure selected its module. materialized in plan.json lists the build robot paths, the lists, each line written and whether its easyconfig came from the commit or the robot path, and each source directory with its manifest digest. Without --materialize, a prelude.local.list or sources/v1 an earlier run left is removed. --generated-at fixes the time in plan.json, so the same inputs give identical files. A --commit-cache download that fails before an HTTP status (the cache has no tree and the client never gets a status) exits 1 and writes nothing; the message names the cache, the commit, the URL, and the transport error. An HTTP error status is not that case, and neither is a commit --easyconfigs-repo lacks when no cache is given. A fetched line that still cannot be resolved (no tree for its commit, its easyconfig in neither its tree nor the robot path, or a requirement neither provides), and a line whose easyconfig is on the robot path but does not parse, is planned as an opaque line: its own group, no closure, the default weight, written to its shard unchanged. plan.json gives the kind under each line’s opaque, the cause under opaque_reason, and every opaque line under the top-level opaque; inputs.trees maps each fetched source to null when its tree was read or to the reason it was not. Each opaque line and each other caveat (a fetched line planned over the robot path only, a module two trees resolve differently) is printed to stderr as warning: ... and kept under warnings. Any other line whose easyconfig is not on the robot path, whose first word is not an .eb file, or whose closure has a requirement nothing satisfies exits 1, names every such line, and writes nothing. The command prints shards=<k> prelude=<n> lines=<m>, followed by opaque=<o> when a line is opaque.

Inode budget. A native build job keeps its build tree on the scratch file system while it runs, and that file system has an inode quota. Each shard in plan.json carries inode_footprint: the largest footprint of a module it builds (inode_peak_module names it) plus --inode-line-overhead inodes per build-list line (1000: the tarballs, the EasyBuild tmpdir, and the logs a line leaves on scratch until the job ends). Modules in a shard build one after another, so one build tree is on disk at a time. The prelude carries its own inode_footprint. A module’s footprint is its software name’s entry in --inode-footprints (a footprints.json from stack footprints, or a flat {"CP2K": 350000} object); an opaque line matches the longest name its filename starts with. Without the file, the built-in class table applies: CP2K 350000; LAMMPS, PyTorch, SeisSol, NVHPC, intel, intel-compilers 150000; any other package 60000. max_concurrent_shards is the shard count, or with --inode-quota the most shards, largest first, whose footprints sum to at most the quota minus --inode-margin (100000). A shard that does not fit on its own exits 1 and writes nothing; a margin at or above the quota is a usage error. inode_budget in plan.json records the quota, the margin, the budget, the footprints file and its SHA-256, the default footprint, and the line overhead. The quota, the margin, the overhead, and the footprints digest enter inputs.sha256 only when given, and the shard lists are the same with or without them. With a quota the summary line ends in max_concurrent=<c>.

stack footprints writes footprints.json: footprints, the peak build-tree inode count per software name, and default. A package’s value is the peak of build.inodes_peak over the records of every --ledger (a JSON Lines ledger, read and checked as ledger show reads it), and its line in every --table (Name, a tab or a space, then a whole number of inodes; # comments; a later file wins a name); when both name it, the larger. A package neither names takes the class table above. sources says which of ledger, table, ledger+table, or class set each value, with the module and the record count for a ledger peak. --generated-at fixes the time in the file. The command prints packages=<n> ledger=<a> table=<b> class=<c> records=<r>, where records counts the ledger records that carried build.inodes_peak.

stack admit is the controller’s gate before it starts one build job. --quota-json is a JSON object: inodes_available, or inodes_used with the limits. Which limit governs follows GPFS: inodes_soft below the soft limit (inodes_grace absent or none); inodes_hard while the grace period runs (inodes_grace a remaining time such as 6 days), since writes are allowed up to the hard limit then; the soft limit again once the grace has expired, when nothing is available. The decision reports grace and soft_exceeded. inodes_hard is carried through when known; - reads it from stdin, so the controller converts its mmlsquota output and pipes it. --footprint is the job’s inode_footprint from plan.json. --reservations is a directory of <job>.json files, one per admitted job. --live-jobs is the jobs still running, comma-separated, as the scheduler lists them (squeue --all, so hidden partitions count); eb-stack calls no scheduler itself. The job is admitted when the available inodes, minus the reservations of the live jobs, cover --footprint plus --margin (100000). A live job with no reservation is an unknown writer and counts --unknown-reserve inodes (0). A reservation whose job is not in --live-jobs is stale: not counted, and its file kept unless --drop-stale; without --live-jobs every other reservation counts and none is stale. The job’s own earlier reservation is never counted, so a job that asks again is judged on the others alone. --lock names a file held exclusively from the quota observation to the reservation write, so two controllers of one quota cannot both admit; --lock-kind is flock (default) or posix, an fcntl record lock over the whole file, the lock a controller written with Python’s fcntl.lockf holds, and it must match the other holders because the two are separate lock namespaces on Linux; --quota-cmd runs the quota command inside that window and reads its output as the quota, and --live-jobs-cmd runs the scheduler command there too (running jobs only, ids one per line or comma-separated, a +step suffix dropped); either command failing refuses the job (refusal says why) rather than admitting it. A --live-jobs list the shell expanded before the lock can miss a job that reserved in between, so it belongs to a controller that holds the lock itself. On admit the reservation <job>.json (job, footprint, margin, ts) is written; --ts fixes its time. --job is a file name: letters, digits, ., _, and -. The command prints one JSON line (job, admitted, available, used, soft, hard, reserved, footprint, margin, headroom, live_jobs, counted, stale, stale_dropped, unknown, unknown_reserved, refusal, reservation, ts) and exits 0 when admitted, 78 when refused. The footprint model is Sharding and inode admission. A reservation file that is not JSON, has no job or footprint, or names another job than its file name exits 1, because a reservation that cannot be read hides inodes.

stack arch

What it does: judge an easystack as a plan for one or more CPU architectures. An entry the architecture cannot build fails the command, whether the easystack names it or a dependency closes over it.

Why you’d use it: a shared EESSI easystack is built on every architecture. GDRCopy in that file fails the riscv64 bot even when the pull request added other lines.

eb-stack stack arch \
  --arch riscv64 \
  --easystack tests/arch_unsupported_fixtures/easystacks/eessi-2026.06-eb-5.4.0-2026.1.yml \
  --easyconfigs tests/arch_unsupported_fixtures/easyconfigs \
  --installed tests/arch_unsupported_fixtures/installed-riscv64.txt \
  --additional-arch tests/arch_unsupported_fixtures/additional_arch/riscv64/generic/eessi-2026.06-eb-5.4.0-2026.1.yml
GDRCopy-2.5.2-GCCcore-15.2.0.eb is unsupported on riscv64: needs CUDA; gdrconfig.h errors with architecture is not supported (software-layer #1775, #1822)
GDRCopy-2.5.2-GCCcore-15.2.0.eb is flagged on riscv64: it has never been installed there and is not in the riscv64 additional_arch easystack
parse_failed=0 unsupported=1 flagged=1

The unsupported line names the entry and the architecture, and the command exits 1. The flagged line is the cheap check: the entry is in the shared easystack, the installed list has never had that module, and the --additional-arch easystack does not name the file. required by on an unsupported line names the plan entry that pulled the dependency in. The plan itself did not name the unsupported entry. --easystack is the plan. --easyconfigs is the robot tree for that closure. --arch repeats; a target such as riscv64/generic uses the family before the slash. No --arch checks every family in the list. --installed and --additional-arch take a bare path for one architecture, or riscv64=path when several are checked. --unsupported replaces data/arch-unsupported.toml. The same refusal runs inside stack batch before a plan is written. With --easyconfigs, each file the plan names is parsed once per architecture, and a file that fails to parse on one is a parse_failed line that exits 1. --probe-sources adds a source URL probe, with HEAD and ranged GET fallback, on each family --arch names, or on x86_64, aarch64, and riscv64 with no --arch. A source no probed family can fetch is unreachable. A source another probed family can fetch is arch-missing, so with one --arch a missing source is unreachable. Two --arch values compare only that pair: a miss is arch-missing when the other named family fetched the same source slot, and unreachable when neither did. --probe-retries and --probe-timeout-s tune that network check.

Refuse a plan an architecture cannot build shows the dependency line and the bwa-mem2 line.

stack files

What it does: list the files each easyconfig in an easystack will look for on disk, and look for them the way EasyBuild does. Patches of the recipe, its extensions, and its components, patch files named only in checksums, and sources no URL downloads are checked. A missing patch or easyconfig fails the command.

Why you’d use it: software-layer pull request 1842 pinned a commit that lacked pip’s patch, and the bot failed at the patch step on every target.

eb-stack stack files \
  --easystack tests/stack_files_fixtures/pr1842/easystacks/20261022-eb-5.4.1-2022b-emSign-cert.yml \
  --easyconfigs tests/stack_files_fixtures/pr1842/robot-5.4.1 \
  --commit-tree 97a4c1a9c45b3bbbea67e085ed187055f473e757=tests/stack_files_fixtures/pr1842/commit-97a4c1a
Python-3.10.8-GCCcore-12.2.0.eb (from-commit 97a4c1a9c45b): patch pip-20.3.4_remove-emSign-root-cert.patch (extension pip 22.3.1) is not beside the easyconfig in p/Python/ at 97a4c1a9c45b or under p/Python/ on the robot path
missing=1 no_url=0 unchecked=0 easyconfigs=1 files=4

--easystack repeats, and every pin in those easystacks is on the robot path, as it is for one eb --easystack run. --easyconfigs is the robot tree of the EasyBuild release the bot runs. --commit-tree COMMIT=DIR names the easyconfigs directory of one pin; --easyconfigs-repo reads a pin no --commit-tree names from an easybuild-easyconfigs clone. --sourcepath adds a directory EasyBuild also searches. --path-map FROM=TO reads an absolute easystack path, such as a /cvmfs reprod easyconfig, from a local copy. A no_url line is a source no URL downloads; it does not fail the command. An unchecked line is an entry that could not be read, or a filename with a template eb-stack does not know. A component patch beside a pinned bundle is reported as missing under the component’s name (easybuild-framework#5133).

Find a missing patch before the bot does shows the component case and the reprod case.

stack generation

What it does: check each easystack entry’s toolchain against the generations of the EESSI version the easystack is for. It also refuses easystacks for two EESSI versions in one pull request, a second version of a software in one generation that nobody allowed, and an entry whose generation differs from the generation the easystack file name names.

Why you’d use it: EESSI 2025.06 carries 2024a, 2025a and 2025b. A 2023a recipe in a 2025.06 easystack fails on every architecture, and the bot builds one EESSI version per pull request.

eb-stack stack generation \
  --pr tests/generation_lint_fixtures/prs/1472-first-push.json
#1472: pv-1.7.24-GCCcore-12.3.0.eb in eessi-2025.06-eb-5.2.1-2025b.yml is outside EESSI 2025.06: GCCcore-12.3.0 is generation 2023a, which EESSI 2023.06 carries; 2025.06 carries 2024a, 2025a, 2025b
outside=1 mixed=0 versions=0 filename=0

The line names the entry, the easystack, the generation the toolchain is in, and the EESSI version that carries it. The command exits 1. --easystack takes easystack files, and every entry in them is the plan. --pr takes pull-request JSON in the pr-overlap --pr shape. Only the lines a pull request adds are checked there, and the rest of each changed file counts as already in the stack. The EESSI version is the directory under software.eessi.io/, or eessi-<V>-eb- in the file name. --eessi-version is used for a path that names neither, and a path that names another version is a mixed finding. --deployed takes easystacks already in the stack, or a directory of them, for the second-version check. --allow-versions takes NAME or NAME@GENERATION. --generations replaces data/eessi-generations.toml.

The generation the easystack is for is the token after eb-<X.Y.Z>-, up to the next - or the .yml or .yaml, as in -2025b.yml or -2023a-CUDA.yml. It counts only when it is a generation the table knows. An added entry whose own generation is carried by the EESSI version but differs from the file name’s generation is a filename finding: it belongs in the generation’s own easystack. A non-generation group such as 001-system, 010-CUDA or cc70-CUDA names no known generation and is not checked.

Refuse entries outside the EESSI version’s generations shows the two-version, second-version and file-name lines.

stack cuda-cc

What it does: checks CUDA compute-capability requests in the easystack closure. For each CUDA-dependent easyconfig with cuda_compute_capabilities, it compares that list against the selected CUDA version, the EESSI accelerator target table, and known easyblock mapping limits.

Why you’d use it: software-layer pull request 1461 failed on capability combinations that did not fit the CUDA release and target matrix.

The command exits 1 when any finding is present. Stdout lines name the easyconfig file and the failing combination, then end with checked=<N> findings=<M> targets=<K>. --target repeats and limits the accelerator targets checked. With no --target, every bundled target is checked. --cuda-table, --accelerator-targets, and --easyblock-map replace the bundled data files.

Check CUDA compute capabilities in an easystack plan shows a short real output and how to read it.

stack known-failures

What it does: read an easystack as a plan and warn about each entry, or dependency, that a bot run has already failed on one of the requested architectures. The warning names the easyconfig, the targets, the symptom, the established remedy as easyconfig text, and the EESSI hook that applies it.

Why you’d use it: an entry can build on fourteen targets and fail its tests on the fifteenth. Software-layer pull request 1829 added AITW-aiida-wood-permeability. Its closure builds orjson, and the orjson tests fail on a64fx because the build container gives UTC an hour’s offset. The table already knew that from pull request 1871.

eb-stack stack known-failures \
  --arch aarch64/a64fx \
  --easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
  --easyconfigs tests/arch_known_failures_fixtures/easyconfigs
orjson-3.11.8-GCCcore-15.2.0.eb on aarch64/a64fx: known test failure: test_datetime_zoneinfo_utc fails: the a64fx build container gives ZoneInfo('UTC') a +01:00 offset (required by AITW-aiida-wood-permeability-0.3.0-foss-2026.1.eb)
  remedy: append -k "not test_datetime_zoneinfo_utc" to the orjson pytest runtest on a64fx
  in EESSI: the pre_single_extension_orjson hook applies it
  seen in: software-layer #1871, #1829; software-layer-scripts #330
known=1 remedied=0

The first line is the warning: the easyconfig, the targets, the phase (fetch, build, test, test timeout, or sanity check), and the symptom. required by names the plan entry whose closure brought it in. remedy: is the text to put in the easyconfig. in EESSI: names the software-layer-scripts hook that already applies it, and caused by: names a hook whose rewrite is the cause. seen in: lists the pull requests. A pin in the easystack (from-commit or include-easyblocks-from-commit) that carries the fix, or an easyconfig that already contains it, turns the line into one remedied: line. known= counts warnings and remedied= counts those. The command exits 0 either way.

--strict exits 1 when a warning remains after the report, so a pre-bot script or CI job can fail on a known failure that has no remedy in place. The stdout is the same report; the error on stderr names each remaining warning by easyconfig and target. Remedied hits never count, so a plan whose every hit is remedied exits 0.

eb-stack stack known-failures --strict \
  --arch aarch64/a64fx \
  --easystack tests/arch_known_failures_fixtures/easystacks/1829/eessi-2026.06-eb-5.4.1-2026.1.yml \
  --easyconfigs tests/arch_known_failures_fixtures/easyconfigs

--arch takes a target (aarch64/a64fx) or a family (aarch64), and repeats. A family meets each of its targets. No --arch checks every architecture in the table. --easyconfigs is the robot tree for the closure and for the extension and marker checks; without it, only the file names in the easystack are matched. --installed lists modules that are not rebuilt, so they are not warned about. --eessi-version and --easybuild-version override eessi-YYYY.MM and eb-X.Y.Z in the easystack file name, which gate entries such as the 2025.06 haswell segfault and the EasyBuild 5.4 GROMACS sanity check. --known-failures replaces data/arch-known-failures.toml.

Warn about known per-architecture failures covers the remedied lines and the GROMACS hook entry.

stack sections

stack sections reads the build list and regroups its entries under the #### Title #### headers of --from. Each entry keeps its flags and whether it is commented out. Packages no section of --from names go under --unfiled (default New in this generation), last. The comment block before the first entry stays on top. Stdout is list=<path> sections=<written> of=<previous+1>. The of count is the previous list’s section count plus the unfiled header.

The recorded lists are tests/jenkins_sections/previous.list and tests/jenkins_sections/next.list. The command in the synopsis writes:

# next generation

#### MPI ####
# OpenMPI-5.0.10-GCC-15.2.0.eb --hooks=/sw/h.py

#### Chemistry ####
# GROMACS-2025.0-foss-2025b.eb

#### New in this generation ####
# CP2K-2026.1-foss-2025b.eb

stack from-commit

stack from-commit reads each --easystack and reports every from-commit and include-easyblocks-from-commit pin. The release is --easybuild-version X.Y.Z, or the eb-X.Y.Z in the file name when the flag is omitted. The flag overrides the file name. The tag is easybuild-easyconfigs-vX.Y.Z in --easyconfigs-repo, and easybuild-easyblocks-vX.Y.Z in --easyblocks-repo. A pin is released when that commit is an ancestor of the tag (a commit is an ancestor of itself), newer when the commit is in the checkout and is not an ancestor of the tag (later, or on another line of history), or unknown when the commit or the tag is missing. unknown does not guess. A released from-commit pin also says blob=same or blob=differs, comparing the easyconfig blob named by the tree at the pin with the blob at the tag. An include-easyblocks-from-commit pin compares the tree of the pin with the tree of the tag, because that pin does not name one easyconfig. released and same can be removed. released and differs needs a person: the release is not the pinned file. The command exits 1 when released is not zero, and also when the check itself fails. --rewrite drops each released pin with blob=same from the easystacks in place, keeps the others, and exits 0. --json prints the same report as JSON. See find from-commit pins an EasyBuild release already contains.

stack oom

eb-stack stack oom \
  [--arch x86_64/amd/zen4]... \
  [--easystack EASYSTACK.yml]... \
  [--easyconfigs TREE]... \
  [--installed INSTALLED.txt] \
  [--profile BUILD-MEMORY.toml] \
  [--resources BOT-RESOURCES.toml] \
  [--node CORES:MEMORY_MIB[:TARGET]]... \
  [--job-memory JOB-MEMORY.toml | --mib-per-job JOB-MEMORY.toml] \
  [--hooks eb_hooks.py] \
  [--parallel N] \
  [--max-parallel N]

What it does: estimate the peak memory of every build in a plan on each EESSI bot node, at the parallel level the bot job gives it, and warn where the estimate reaches the job’s memory. Each warning names the remedies the maintainers have used. Nothing is changed.

Why you’d use it: software-layer pull request 1772 lost three zen4 bot runs to a killed cc1 while building libxc, two at 8 processes and one at 5. The check names that build before the first run.

eb-stack stack oom \
  --arch x86_64/amd/zen4 \
  --easystack tests/oom_profile_fixtures/easystacks/pr1772-eessi-2026.06-eb-5.4.0-2026.1.yml
libxc-7.0.0-GCC-15.2.0.eb may run out of memory on x86_64/amd/zen4 (eessi-bot-mc-aws amd-zen4): 8 x 6.4 GiB = 51.2 GiB, the job has 32.0 GiB (software-layer #1772, software-layer-scripts #337)
  parallel 8: 16 cores, eb_hooks.py divides libxc by 2
  why: cc1 on the generated mgga_x_*.c sources; with GCC 15.2.0 on zen4, 8 and then 5 compilers ran out of a 32 GiB job
  remedy: maxparallel = 4 in libxc-7.0.0-GCC-15.2.0.eb, or a libxc limit of 4 for x86_64/amd/zen4 in PARALLELISM_LIMITS in software-layer-scripts eb_hooks.py; 6.4 GiB per process is a lower bound, so 4 may still be too many
  remedy: build x86_64/amd/zen4 on a node with more memory: eessi-bot-surf amd-zen4 (16 cores, 180.0 GiB, accel nvidia/cc90)
  remedy: a libxc divider of 3 left 5 cores and ran out too; a reduction of 8, as asked in the review of software-layer-scripts #337 (issuecomment-6599147327), is 2 cores on the AWS build cluster
oom=1 fits=1 builds=5 nodes=2

The first line is the warning: the build, the target, the bot instance and node, the estimate, and the job’s memory. parallel is how the level was reached. The parallel level starts at the node’s cores, capped at EasyBuild’s max-parallel of 16, then lowered by the EESSI PARALLELISM_LIMITS and by maxparallel in the easyconfig. The hook ops are divide_by_factor, max(1, parallel // factor), and set_maximum, min(parallel, n). A CPU-target key overrides '*'. Software with no entry takes the target’s own limit, a divide by 4 on A64FX. A divide that is not an integer is a warning of its own: Python’s // returns a float, and 16 // 2.5 is 6.0. The estimate uses the numeric floor. Write the factor as an integer. oom= counts warnings. fits= counts build and node pairs with a memory figure that fit. builds= and nodes= are what was checked.

--easystack is the plan. --easyconfigs adds the dependency closure, names the plan entry that pulled a build in with (required by ...), and reads maxparallel from each recipe. --installed drops modules already installed. --arch repeats and takes a target or a prefix such as aarch64. No --arch checks every node in the table; a target with no node is reported and skipped. --profile replaces data/build-memory.toml. --resources replaces data/eessi-bot-resources.toml. --job-memory, also spelled --mib-per-job, replaces data/job-memory.toml, the per-package mib_per_job table. A row with no number is unmeasured. libxc 7.0 is unmeasured; the peak RSS of its worst compile has not been measured, and the table does not guess. --node repeats. Each value is CORES:MEMORY_MIB or CORES:MEMORY_MIB:TARGET, and memory is MiB. It prints the parallelism the current hook leaves beside limit_by_memory(parallel, mib_per_job), which is max(1, min(parallel, memory_mib // mib_per_job)). For a package with a number, the memory cap min(current, memory_mib // mib_per_job) follows the hook’s level: the core cap with a memory ceiling on top. It is not raised to 1, so memory cap 0 with not one N MiB job fits says one compile needs more than the node’s memory. The memory a hook would read on the build machine is the smallest cgroup v2 memory.max on the process’s cgroup path, else MemTotal.

--parallel N and --max-parallel N are EasyBuild’s settings for the job, as EASYBUILD_PARALLEL and EASYBUILD_MAX_PARALLEL would set them; the bot sets neither. EasyBuild uses --parallel in place of the core count, capped at --max-parallel (in place of the table’s max_parallel) and at the easyconfig’s maxparallel. The hook applies its op to the node’s cores, not to either setting, and keeps the result only when it is lower. --node lines then end the hook formula with then --parallel N or then max-parallel N when that setting decided, and the node line names --parallel. A warning’s parallel line names --parallel and the cores the hook divided, and says when the hook’s result was not below EasyBuild’s level. 0 for either flag is status 2.

--hooks reads PARALLELISM_LIMITS from an EESSI eb_hooks.py in place of the hook rows of data/eessi-bot-resources.toml. The nodes and EasyBuild’s max-parallel still come from the table. The file is parsed, not run. A key is a string or a module-level string constant such as CPU_TARGET_A64FX, and a key the dict repeats keeps its last value, as in Python. An op other than divide_by_factor or set_maximum, a ** unpacking, or a key that is not a string is refused with its line: the parallel levels would be wrong without that row. The output starts with hooks: N PARALLELISM_LIMITS rows from FILE, and a divide that is not an integer is a warning that names the file and the line. A statement that adds to a name and drops the sum, such as msg+ "..." in post_ready_hook, is a warning that names the file, the line, and the function; msg += "..." is not. Pass --easystack, --node, --hooks, or a mix. The command exits 0 when it warns. See warn before a bot job runs out of memory.

stack bot-classify

What it does: read a saved EESSI bot comment, a _bot_job<id>.result file, or a slurm-<id>.out job output, and classify each bot job as real-failure, error-only, empty-tarball, infra, success, or unclassified. A real failure takes its sub-class from the campaign build-finding table; infra names the cause. Nothing is posted.

Why you’d use it: most FAILURE rows in software-layer pull requests 1873, 1869, 1834 and 1760 were an ERROR: line in the test step after the build had packaged its software, and 1732 and 1733 failed on an empty tarball. The class says which rows need a person.

eb-stack stack bot-classify \
  tests/bot_classify_fixtures/pr1818-riscv64-329698.md \
  tests/bot_classify_fixtures/slurm-329698.out
real-failure/resource riscv64/generic job 329698 (eessi-bot-riscv-prod): tests/bot_classify_fixtures/pr1818-riscv64-329698.md, tests/bot_classify_fixtures/slurm-329698.out
  why: the job output matched `FATAL:`
  evidence: found message matching FATAL:
  evidence: found message matching ERROR:
  evidence: no message matching No missing installations
  evidence: OSError: [Errno 28] No space left on device
real-failure=1 error-only=0 empty-tarball=0 infra=0 success=0 unclassified=0 jobs=1

Inputs that name the same job id are one job. A comment with several job tables gives one job per table, and a bot update that submitted no job is infra/bot-config. why is the rule the class came from, and each evidence line is a bot check or a job output line. The count line has one field per class. --json prints jobs and counts. A directory argument is status 2; otherwise the command exits 0. See read an EESSI bot FAILURE.

pr-overlap

eb-stack pr-overlap \
  --pr PR.json [PR.json...] [--pr PR.json]... \
  [--open OWNER/NAME] \
  [--base-ref REF] \
  --easyconfigs TREE [--easyconfigs TREE]... \
  [--commit-tree COMMIT=DIR]... \
  --installed installed.txt \
  [--eessi-version 2026.06] \
  [--installed-label LABEL] \
  [--exclude-path SUBSTR]... \
  [--include-path SUBSTR]... \
  [--filter-deps SPEC,SPEC...] \
  [--installed-modulerc PATH]... \
  [--easyblocks DIR]... \
  [--easyblocks-commit COMMIT=DIR]... \
  [--generated-at RFC3339] \
  [--merge-order] \
  [--merged N]... \
  [--impact N]... \
  [--repo-url URL] \
  [--bot-build-set] \
  [--arch-installed ARCH=FILE]... \
  [--out-dir /tmp/pr-overlap]

Reads each pull request’s easystack changes as data. It does not run the pull request and it does not build. Several JSON paths may follow one --pr, matching what a glob of the per-pull-request files expands to. For the given --eessi-version it keeps the easystack entries present at head and absent from base, finds those easyconfigs on the robot path, and closes them against --installed.

--bot-build-set closes every entry at the head of each changed easystack, not only the added lines. With --installed, that closure minus the installed list is to_build, and --merge-order and --impact read it. --arch-installed ARCH=FILE repeats, once per architecture, and implies --bot-build-set. It replaces --installed. The name before = is the label, so --installed-label is not used with it. One architecture writes the same single to_build as --bot-build-set. Two or more write a [[build_sets]] table per architecture, and merge order and impact follow those tables. The command is in what the bot builds on each architecture.

--easyconfigs is the release robot tree. Later trees win on the same name, version, toolchain, and versionsuffix. --commit-tree is the easyconfigs tree at one from-commit, and it wins over the release tree for entries pinned to that commit.

--exclude-path drops easystack paths that contain the substring. --include-path, when given, keeps only paths that contain one of the substrings. An empty include list keeps every path that survived the excludes.

An installed module is a leaf. Entries under a rebuilds/ easystack are built even when installed. A requirement no easyconfig satisfies is recorded and the rest of the closure is still written.

--filter-deps is comma-separated and uses the same matching as stack solve. A filtered dependency is dropped before the walk, so the modules reachable only through it are never opened. When the flag is omitted and EASYBUILD_FILTER_DEPS is set, that value is used. A non-empty flag wins over the environment.

--installed-modulerc is a .modulerc.lua file or a module tree. A module_version("Name/full", "alias") line makes Name/alias installed when Name/full is on the installed list.

--easyblocks is the installed easybuild/easyblocks package. Passing any root checks every easyblock class. A software-specific class none of the roots defines is recorded as easyblock <class> missing and that module is left out of to_build. Its dependencies stay. --easyblocks-commit is <commit>=<dir> for one include-easyblocks-from-commit: the directory is the easyblock files that commit touches, matching what EasyBuild includes ahead of the installed package. A class found there is counted, and the record notes the commit. A commit an entry names whose directory was not passed is noted as not provided.

--kind selects the pull request shape. software-layer is the default and reads easystack additions. easyconfigs reads changed .eb files. --installed is required for software-layer. For easyconfigs it is optional: with the flag, to_build starts as the closure minus that list; without it, to_build starts as the modules the base tree does not provide. A modified easyconfig, and a base-tree module classed must-rebuild, then join to_build, including when the module is already installed. review and no-rebuild reverse dependents are listed on the record and left out of to_build. A bundle whose exts_list names the patched package is must-rebuild. A runtime dependency is no-rebuild. A dependent reached only through builddependencies is review.

--eessi-version is ignored for easyconfigs and is omitted from the record. --repo-url defaults to https://github.com/EESSI/software-layer for software-layer and to https://github.com/easybuilders/easybuild-easyconfigs for easyconfigs.

--open OWNER/NAME reads the open pull requests of that repository over HTTPS and adds them to any --pr files. It requires --kind easyconfigs. The repository has to be owner/name. --base-ref is the git ref for the base text of each changed file and defaults to develop. A name such as release/2026.1 is accepted. A repository or ref that fails those checks is an error before a request is sent and before the output directory is written. A failure of the request itself is reported after the request is sent and still writes nothing. GitHub’s changed-file list stops at 3000 files, which is 30 pages when each page holds 100. That cap is not a failed request: the pull request is kept, its record notes truncated at GitHub's limit, and only the files GitHub returned are read.

This easyconfigs command was run on the recorded open pull requests. It wrote 4 records and exited 0. The summary line was prs=4 records=4 shared_modules=1 skipped_easyconfigs=0.

eb-stack pr-overlap \
  --kind easyconfigs \
  --pr tests/pr_overlap_easyconfigs_open/prs.json \
  --easyconfigs tests/pr_overlap_easyconfigs_open/tree \
  --generated-at 2026-10-24T00:00:00Z \
  --repo-url https://github.com/easybuilders/easybuild-easyconfigs \
  --out-dir /tmp/ec-overlap

The JSON is the four open pull requests recorded under tests/pr_overlap_easyconfigs_open/. 22823 and 24575 share Mathematica/14.2.1.

The output directory contains prs/<N>.toml, overlap.toml, and overlap.md. See output formats for the records. A closed pull request’s file is removed: the directory is the open set of this run.

--repo-url defaults to https://github.com/EESSI/software-layer. The report links each pull request there. Pass another URL to link somewhere else, or an empty URL to write #N text.

--merge-order also writes merge-order.toml and appends a Merge order table to overlap.md. Without the flag, prs/, overlap.toml, and overlap.md are unchanged. --merged N repeats and implies --merge-order. N has to be a number in the --pr input that produced a record. This run names a missing one:

eb-stack pr-overlap --pr tests/pr_overlap_layer/prs.json \
  --easyconfigs tests/pr_overlap_layer \
  --installed tests/pr_overlap_layer/installed.txt \
  --installed-label none \
  --generated-at 2026-10-24T00:00:00Z \
  --merge-order --merged 1585 --merged 999 \
  --out-dir /tmp/pr-overlap-out

The error is --merged 999: no pull request with that number. A number that is in the input but changes no easyconfig and no easystack is --merged N: pull request N has no record (no easyconfigs or easystack change). A rejected --merged or --impact does not create the output directory and does not change files already in it. The same command with only --merged 1585 treats pull request 1585 as already installed and orders the rest. Pull request 1585 adds SciPy-bundle-2026.05-gfbf-2026.1.eb and pull request 1728 adds PLUMED-2.10.0-foss-2026.1.eb. Both change easystacks/software.eessi.io/2026.06/eessi-2026.06-eb-5.4.0-2026.1.yml.

--impact N writes merge-impact.toml. Repeat the flag. N has to be a number in the --pr input that produced a record. A number that is not in the input is --impact 999: no pull request with that number. A pull request that is in the input but has no record is --impact N: pull request N has no record (no easyconfigs or easystack change). The file is the effect of merging that pull request alone: other pull requests whose to_build shrinks, and depends-on edges from it that resolve. It is not the residual after earlier steps of --merge-order, and it does not write merge-order.toml unless that flag (or --merged) is also set. A number that is also passed to --merged is an error: --impact N: already listed in --merged. Modules those merged pull requests build are already installed, so they do not appear in shrinks and those pull requests are not listed under affected. For --kind easyconfigs, resolves lists the modules on a depends-on row from that pull request, and after in merge-order.toml lists the pull request that adds them or that patches a module this pull request builds a dependent of. The recorded easyconfigs run is find open easybuild-easyconfigs pull requests that add the same software.

rebuild-set

eb-stack rebuild-set \
  (--changed FILE.eb [--changed FILE.eb]... | --findings findings.csv) \
  [--easystack STACK.yml]... \
  --easyconfigs TREE [--easyconfigs TREE]... \
  [--commit-tree COMMIT=DIR]... \
  --installed installed.txt \
  [--installed-label LABEL] \
  [--filter-deps SPEC,SPEC...] \
  [--installed-modulerc PATH]... \
  [--easyblocks DIR]... \
  [--easyblocks-commit COMMIT=DIR]... \
  [--generated-at RFC3339] \
  [--prefix "$EESSI_SOFTWARE_PATH"] \
  [--check-static] \
  [--reachability reach.txt] \
  [--out-dir /tmp/rebuild-set]

Resolves the deployed easystacks and the installed reverse dependents of each patched easyconfig. The patch keeps the filename and the version. Classification starts from the easyconfig graph. The closure stays inside the patched module’s toolchain generation: an unpinned dependency is resolved in the dependent’s own generation, and a module from another generation is not a row.

--changed is an easyconfig filename. Repeat it, or pass a file that does not end in .eb: one filename per line, blank lines and # comments skipped. The basename is what is matched. A filename that is not in the deployed graph is listed under missing and has no row.

--easystack is a deployed easystack, repeated. Every entry is a root. Without it, every installed module is a root, read as the easyconfig Name-VersionFull.eb. That is the stack of a site that installs from build lists instead of easystacks.

--findings is a vulnerability scanner export: comma-separated with a header, columns severity, vuln_id, package, and used_in required, version, fixed_in, and fix_state read when present. used_in lists modules separated by ;. Every installed module it names is added to --changed as Name-VersionFull.eb, and findings.json, findings.md, findings.csv, and backport.md are written beside the rebuild set. A module that is not installed is listed and has no closure. An export that names no installed module is status 2.

--reachability requires --findings. The file is one name class per line, # comments skipped. The class is cross-user, network-facing, local-only, or not-reachable. A name replaces the built-in class or adds one. The built-in names are Jupyter server (jupyter_server and jupyter-server), JupyterLab (jupyterlab and jupyter-lab), notebook, cURL, urllib3, aiohttp, tornado, cryptography, and git. Matching folds ASCII case and treats - as _. The whole name has to match, so curl does not match libcurl and jupyterlab does not match jupyterlab-server. A name in neither list is unclassified. backport.md lists a module only when its class is cross-user or network-facing. The count line includes backport=N. A class the file does not name is status 2. backport.md then writes a patch scaffold for each chosen advisory: <Name>-<version>_<ID>.patch, the patches assignment, and the checksum line to append. A fix commit URL is printed when the export names one (fix_commits in the CSV, separated by ;, or a JSON reference whose path contains /commit/). A module --filter-deps drops is listed under OS errata and is not given a scaffold. backport on the findings document stays the reach class. A cURL or libcurl finding adds the --filter-deps=cURL trade-off. The EasyBuild name in that flag is cURL.

--findings may also be Trivy (Results), Grype (matches), or OSV-scanner (results of packages) JSON. The shape is detected. Those exports do not name an EasyBuild module, so --installed is matched on the package name and version. A JSON file that is none of those three shapes is status 2.

The same run also writes fix-cost.json, fix-cost.tsv, and fix-cost.md, one row per advisory. The reach class is the one above. The bump column is the reverse-dependency closure and breaks reproducibility. The in-place column is the patched module, a bundle that includes it as an extension or a components entry, and a dependent known to link it statically. A builddependency-only dependent is known when configopts or buildopts request a static or a shared build, or when --prefix and --check-static already classified the row. Otherwise the link cannot be known, and the dependent is named and left out of the in-place count. The route is in-place for cross-user or network-facing, bump when a Python fix changes the minor or the major version, and defer otherwise. The roll-up is the fraction of the installed stack each strategy rebuilds. The count line adds fix_routes=N and those fractions beside backport=N.

Each module with a finding gets two costs. in_place is the module, the bundles that ship it, and its review dependents: what a backported patch that keeps the version rebuilds. bump is the module and every installed dependent: what a new version rebuilds, since EasyBuild writes each dependency version into the dependent’s module file and run path. The score weighs a unique id Critical 10, High 5, Medium 2, Low 1, and modules are ranked by score per in-place rebuild. Totals per severity take the union over the modules with a finding at that severity or above. --easyconfigs, --commit-tree, --installed, --filter-deps, --installed-modulerc, --easyblocks, and --easyblocks-commit mean the same as for pr-overlap. --filter-deps drops dependency tuples and does not strip exts_list. A missing easyblock class is recorded and the module stays, because it is already installed.

A runtime dependency is no-rebuild. A dependent reached only through builddependencies is review. The patched easyconfig, and a bundle whose exts_list names that package and version, are must-rebuild. A toolchain edge is not classified. A review dependent is one row, and changed lists every patched easyconfig that put it there. See what an in-place patch forces a rebuild of.

--prefix reads software/<name>/<version>/lib*/*.a. No static archive makes a builddependency-only dependent no-rebuild, with the reason no static archive installed. Archives that are installed keep the row in review and are named. A missing install directory is a note.

--check-static needs --prefix. It compares defined dynamic symbols of the remaining review dependents with the patched library’s exports. A hit is must-rebuild. No hit is no-rebuild. An empty export set stays in review. The scan sees dynamic symbols only, so a hidden or inlined static link is a false no-rebuild, and a short name shared with another library is a false must-rebuild.

The output directory contains rebuild-set.json, rebuild-set.md, and rebuilds/<YYYYMMDD>-eb-<version>-cve-patches.yml. The easystack contains class must-rebuild only, including a dependent that --check-static promoted. A second run replaces the rebuilds/*.yml files. rebuild-set.json and rebuild-set.md record the reach class of each patched easyconfig, including a filename listed under missing. --reachability replaces or adds names for that map as well as for backport.md. cross-user and network-facing justify a backport.

component-reach

eb-stack component-reach \
  (--component NAME --version VER | --purl PKG:TYPE/NAME@VER | --cve CVE-ID) \
  --generation 2026.1 \
  --easyconfigs TREE [--easyconfigs TREE]... \
  [--installed installed.txt] \
  [--easystack STACK.yml]... \
  [--easystack-url https://.../STACK.yml]... \
  [--compat-layer FILE] \
  [--filter-deps SPEC,SPEC...] \
  [--generated-at RFC3339] \
  [--out-dir /tmp/component-reach]

Lists every easyconfig and installed module in one toolchain generation that carries a component, then feeds the recipes that contain it into the rebuild closure. See which modules carry a component.

Pass --component and --version, or --purl, or --cve. --version is an exact version, *, or a requirement such as >=8,<9. With --purl it replaces the version in the URL. The purl type selects the carriers. generic and github match the easyconfig name. pypi, cargo, npm, golang, and cran match an extension or a bundled component of that language, plus a crate for cargo and a wheel for pypi. Another type exits 2: the error says that purl type has no carrier model. pkg:cargo/curl does not match the cURL recipe. --cve resolves the id through Open Source Vulnerabilities (OSV). When the Common Vulnerabilities and Exposures (CVE) record has only a git range, it follows GitHub Security Advisory (GHSA), RUSTSEC, PYSEC, and GO aliases. It keeps named version tags such as libXpm-3.5.17. A GIT range is mapped onto release versions: the affected entry’s versions list when it has one, and otherwise tags from the upstream repository (git ls-remote --tags, then git merge-base --is-ancestor on a blobless fetch). That lookup is cached and is skipped when it cannot reach the network, and the range then stays under Unresolved ranges. An event that does not parse is reported there too. A package whose ranges were all unparsed counts as review. An advisory that affects more than one package is written as one document with kind cve. The summary line includes reach, changed, must-rebuild, review, and no-rebuild. reach is the class of the component name: cross-user, network-facing, local-only, not-reachable, or unclassified. A --cve summary has no single class.

Names match ignoring ASCII letter case, on a whole token. A recipe is in the generation when its toolchain version equals --generation, or when the toolchain is a non-system member of the known foss hierarchy for that generation.

A hit is a dependency or builddependency tuple, a bundle components entry, an exts_list extension, a Cargo crate (a crates tuple, a filename such as libz-sys-1.1.29.tar.gz, or a checksum key such as rustls-webpki-0.102.8.tar.gz), a Python wheel (.whl in sources, checksums, an exts_list source, or source_tmpl), or a versioned name in sources or patches. A +name-version fragment inside a crate version counts as that vendored version. The recipe’s own tarball is not a crate it carries. A dependency or builddependency is a carrier and is not a patched root. The other roles are: the recipe contains the component, and its filename is a changed input of the closure.

--easystack-url repeats and must be https://. It is read the same way as --easystack.

--compat-layer is name version or name==version per line, # comments skipped. The compat layer is not an easyconfig, so a hit has an empty easyconfig and a note, and it is not a closure edge. --filter-deps matches rebuild-set and reads EASYBUILD_FILTER_DEPS when the flag is omitted. It does not strip exts_list.

With no --easystack, every easyconfig in the generation is the stack. With no --installed, every module in the generation is treated as installed. Pass both to visit only a mounted prefix. The closure classes are the same as rebuild-set, and they stay inside the generation.

The output directory contains component-reach.toml, component-reach.json, and component-reach.md. The TOML file starts with # Generated by `eb-stack component-reach`.

Target commands

eb-stack target list \
  --config examples/targets/base.toml \
  --config examples/targets/site.example.toml
eb-stack target doctor \
  --config examples/targets/base.toml \
  --config examples/targets/site.example.toml \
  --target NAME

Target layers are applied in argument order. A complete target contains:

  • transport: local or SSH;
  • executor: direct or Slurm;
  • runtime: host, Podman, or Docker;
  • EasyBuild workload: command, robot paths, work root, temporary root, and environment.

target doctor probes each layer separately and reports the exact routed command, exit code, stdout, and stderr.

See build target configuration for complete layer schemas, replacement semantics, ABI isolation, and the runnable local Podman target.

Campaign commands

eb-stack campaign run \
  --bundle BUNDLE \
  --config examples/targets/base.toml \
  --config examples/targets/site.example.toml \
  --target NAME \
  --state /tmp/campaign.json

eb-stack campaign status --state /tmp/campaign.json

eb-stack campaign finding claim \
  --state /tmp/campaign.json --id FINDING --owner WORKER

eb-stack campaign finding resolve \
  --state /tmp/campaign.json --id FINDING --owner WORKER \
  --action TEXT --evidence TEXT [--change PATH]...

campaign run resumes state with matching package identity, increments the attempt, stages the bundle, runs every emitted recipe, and executes each profile’s verification commands. Transport, executor, runtime, interrupted, checksum, patch, dependency, configure, compile, link, test, install, sanity, resource, timeout, and unknown failures are persisted as typed findings.

Finding claims are exclusive. A resolution records an action, evidence, and changed paths. A successful retry supersedes matching open or in-progress findings. Campaign state reports resolves, builds, and binary_verified independently.

See campaign state and typed findings for the serialized state, claim ladder, class/disposition table, and repair protocol.

Ledger commands

eb-stack ledger append RECORD.json \
  --log ledger/2026.06.jsonl \
  [--expect-head HEAD.json] \
  [--key KEY.pem --head-out HEAD.json] \
  [--tiles DIR] [--origin ORIGIN]

eb-stack ledger head \
  --log ledger/2026.06.jsonl --key KEY.pem --out HEAD.json \
  [--ts RFC3339] [--tiles DIR] [--origin ORIGIN]

eb-stack ledger tiles --log ledger/2026.06.jsonl [--tiles DIR]

eb-stack ledger find \
  --log ledger/2026.06.jsonl --public-key KEY.pub \
  --key pkg=PETSc [--key route=kvm ...] \
  [--tiles DIR] [--origin ORIGIN] [--json]

eb-stack ledger show --log ledger/2026.06.jsonl [--package NAME] [--json]

eb-stack ledger verify \
  --log ledger/2026.06.jsonl --public-key KEY.pub \
  --record N --head HEAD.json [--tiles DIR] [--origin ORIGIN]

eb-stack ledger verify \
  --log ledger/2026.06.jsonl --public-key KEY.pub \
  --from HEAD1.json --to HEAD2.json [--tiles DIR] [--origin ORIGIN]

A ledger is one append-only JSON Lines file per stack. Each line is one event on one installation, and every line of a file has the same stack. The file is an RFC 9162 Merkle log over the rfc9162 crate: the leaf of record N is SHA-256(0x00 || line N), over the bytes of the line without its newline, and an interior node is SHA-256(0x01 || left || right).

Canonical form. A record is a JSON object written as one line: keys sorted by their UTF-8 bytes at every depth, no whitespace outside strings, strings escaped as serde_json escapes them (", \ and control characters; other characters as UTF-8), numbers as integers only. A record with a fractional number is refused. Two files that differ only in key order and spacing append the same line.

Fields. append refuses a field outside this table.

FieldContent
tsUTC time, RFC 3339 (Z or +00:00). append fills the current time when absent
actorWho wrote it: a Jenkins run URL, a bot pull request and job, or a login
stack2026.06 (EESSI version) or 2025
targetCPU target and accelerator, e.g. x86_64/amd/zen4
moduleModule name, e.g. PETSc/3.25.0-foss-2026.1
locationThe installation path that RPATHs use
actioninstall, rebuild-inplace, rebuild-bump, retire, unretire, move, access, remove, finding, or correction
stageOptional: started, finished, or aborted, for an operation that runs in steps
routeOptional: native, kvm, or qemu, where the build ran
reason, easyconfig, sources, sbom, modulefile, buildOptional JSON: free text or an object of references and SHA-256 sums. build as an object may carry job (a ledger find key) and inodes_peak, the most inodes the build tree held on scratch, a whole number, which stack footprints reads
supersedes, superseded_by, moved_from, moved_to, correctsOptional id of a related record. A correction needs corrects
indexLeaf number. Written by append; a record that carries another value is refused
idSHA-256 of the canonical record without id and sig. Written by append; a record that carries another value is refused
sigOptional per-record signature, kept in the line and left out of id

The first fields are required non-empty strings: ts, actor, stack, target, module, location, and action. A wrong record is never edited: a correction record follows it. Licence numbers, keys and tokens do not belong in a record.

Reading a log. show, verify, head and append read every line and refuse the log when a line is not canonical, its index is not its line number, its id does not recompute, its stack differs from the first line, or the last line has no newline.

Heads. A head file is one JSON line: domain (always eb-stack-ledger-v1), size, root (hex), ts, public_key (hex), and signature (hex). The Ed25519 signature covers eb-stack-ledger-v1<TAB>size<TAB>root<TAB>ts. --key is an Ed25519 PKCS#8 PEM file, as openssl genpkey -algorithm ed25519 writes it. A key inside the log’s directory is refused, so the key is always an argument and never a file beside the log. --public-key is the matching PEM (openssl pkey -pubout), or the 64 hex characters of a head’s public_key.

append locks the log file, checks the log, refuses the record when the log is not at the size and root of --expect-head, writes the line, and, with --key and --head-out, writes the signed head after the append. It prints appended record N id ID; head size S root ROOT, then the tile log line below. head signs the log’s current head into --out, at --ts or now, and the tile log’s checkpoint. On an empty log with no --origin it signs the size-0 head alone and prints one line: there is no record to derive a checkpoint origin from, and verify of that head audits no tile log.

Locking and crashes. Writers take an exclusive POSIX record lock on LOG.posix-lock beside the log; head takes a shared lock on the same file, so a head is the log between two appends and an older head never replaces a newer one. head opens the log and the lock file read-only and needs no write access to the ledger directory, but refuses a log whose lock file no writer has created. append flushes the log and its directory before it reports a record, including a record it acknowledges as already recorded. A write that fails part way is cut back. Bytes after the last newline that are not a JSON value, left by an append that never finished, are copied to a new LOG.quarantine.* file and flushed before the next append cuts them from the log; append then adds ; moved N bytes of an interrupted append to PATH to its line. A complete JSON value without its newline is refused and left in place. A head file is replaced by renaming a flushed new file over it. A head path that names the log, its lock file, a quarantine file, or the signing key, directly or through a symbolic or hard link, is refused, and so is a head path that is a symbolic link. A signing key with more than one hard link is refused. The lock is a POSIX record lock: whether it excludes writers on several hosts depends on the shared file system’s lock support. show prints index, ts, action, target, module and actor per record, newest first, then K of N records. --package matches a whole module name, or the software name before its /. --json prints the records as a JSON array.

verify --record N --head HEAD checks HEAD’s signature against --public-key, computes the inclusion proof of record N from the first size lines, and checks it against HEAD’s root. A record removed from the log, or any line changed, makes the root differ. verify --from HEAD1 --to HEAD2 checks both signatures, computes the consistency proof from HEAD1’s size to HEAD2’s size, and checks that it yields both roots. A log rewritten under HEAD1 and signed again fails, because no proof yields the old root and the new one. After the proof, verify checks the tile log as described next, and prints tile log size S root ROOT extends the head and holds the same lines.

Tile log. Beside the file, append keeps the same lines as a C2SP tlog-tiles log through the rfc9162-tiles crate: --tiles DIR, or the log’s path with .tiles (ledger/2026.06.jsonl.tiles). The leaves are the same bytes, so the root over the tile log at size N is the root of the first N lines, and any tlog-tiles client reads the tree. Hash tiles live under tile/<L>/, entry bundles of 256 lines under tile/entries/, one key sidecar per sealed bundle under tile/index/, a manifest of the sidecars, the writer’s tree state under .state/, and the checkpoint. A line over 65535 bytes does not fit a bundle and the append fails.

The JSON Lines file is the ledger. The tile log follows it: append adds the new line, and tiles adds every line the tile log lacks, so an existing log gets a tile log with one ledger tiles --log LOG. A tile log whose entries disagree with the ledger is moved to DIR.stale-<time> and rebuilt from the ledger; the command prints a warning on stderr that names the old directory. The old directory keeps its checkpoint, which a consistency check against a later head or checkpoint still fails.

Keys. Each entry’s lookup keys are read from its line: module= (the whole module name), pkg= (the software name before the slash), target=, action=, route=, actor=, id= (the record id), and job= from a job string inside build, each for a field the record carries. find --key FIELD=VALUE reads the sidecars and the unsealed tail for each key and prints the records that carry every key, one index<TAB>line per record in index order, then K of N records match; checkpoint ORIGIN size N root ROOT. --json prints the records as a JSON array. A record past the checkpoint is counted in a last line, M more past the checkpoint, not shown; ledger head signs them, and is not printed. A --key without = is status 2.

Checkpoints. head, and append with --key, sign the tile log’s size and root as a C2SP checkpoint (an Ed25519 signed note) at DIR/checkpoint, with the same key as the head file. The origin line is --origin, or eb-stack-ledger/<stack>/<year> from the first record’s stack and the year of its ts, so it is the same for the life of the log; an origin with whitespace or a plus is status 2. The key name on the signature line is the origin, as the checkpoint specification asks. A checkpoint published for a larger size is never replaced. find reads the checkpoint, checks its signature against --public-key under the same origin, checks that the tile log’s tree extends the checkpoint, and checks the inclusion proof of every record it prints against the tree, so a record it prints is in the signed checkpoint. verify checks that the tile log’s tree extends the signed head (a consistency proof read from the tiles, each tile checked against the tree), that the tile log’s entries are the ledger’s lines for every record the head covers, and, when DIR/checkpoint exists, that it is signed by --public-key as the origin and is a prefix of the tree too. A changed tile, a changed bundle, a rewritten tree state, a tile log of another ledger, a missing tile log, or a tile log behind the head is status 1, and the message names the tile log and the file.

Index log. Inside the tile log, at DIR/index, a second tile log holds the sorted-run index of the records: each sealed run’s manifest is an entry there, so a checkpoint signs the index as well as the entries. The sync seals the unsealed tail into a run once it holds a full bundle of 256 records, and head then signs the index log’s own checkpoint under <origin>/index with the same key and prints it as a third line, index checkpoint ORIGIN/index size N root ROOT to PATH; an index log with no run is not signed and the line is absent. find proves the record in the tile checkpoint and the run manifest that located it in the index checkpoint, two roots under one key. verify checks the index checkpoint’s prefix as it checks the tile log’s, and a rebuild of a tile log that disagrees with the ledger moves both logs aside. The data tile log still holds exactly the ledger’s lines, so the signed heads, verify and any tlog-tiles client are unchanged; the index comes from the rfc9162-log crate.

repro summary

eb-stack repro summary \
  --scores DIR \
  [--ratchet ratchet.json] \
  [--json]

Reads every *.json case score the reproduction suite wrote under --scores (the directory EB_REPRO_SCORES points at when the suite runs) and prints the scoreboard table:

| Case | PR | Score | Allowance | Residual |
|---+---+---+---+---|
| reproduces_gromacs | - | EXACT | 1 | 0 |

2 cases: 1 EXACT, 1 SEMANTIC, 0 MATERIAL; allowance total 1

Score is EXACT, SEMANTIC, or MATERIAL. Allowance is how many hand-added lines the case still forgives. Residual is the raw diff lines left after that allowance. A pull request number prints as #N; a case with none prints -. --json prints the case records instead of the table: case, pull_request, source, target, score, allowance, stale_allowance, residual_lines. --ratchet reads {"note", "cases", "total"}. --ratchet exits nonzero when a case’s allowance disagrees with the committed count, when a case is missing from the file, or when total is not the sum of cases. A matching run also prints ratchet: <N> cases hold at <total>. An empty --scores directory is an error. See reproduction fidelity.

easyblock-reach

eb-stack easyblock-reach \
  --easyblocks EASYBLOCKS \
  [--framework FRAMEWORK]... \
  --easyconfigs EASYCONFIGS \
  [--file PATH]... \
  [--range A..B | --pr N] \
  [--remote origin] [--base develop] [--repo CLONE] \
  [--installed MODULES.txt] [--reverse] \
  [--generated-at RFC3339] \
  [--out-dir /tmp/easyblock-reach]

Pass at least one of --file, --range, and --pr, and at least one of --easyblocks and --framework. --range and --pr read Python files from the clone (--repo, or the first --easyblocks path). The report is reach.toml, reach.json, and reach.md. See which easyconfigs an easyblock change reaches.

MCP

eb-stack mcp [--workspace ROOT]

--workspace sets EB_STACK_WORKSPACE for the recipe file tools. When it is omitted, an existing EB_STACK_WORKSPACE is kept, and otherwise the server’s current directory is the root.

The stdio server exposes one MCP tool for each of these CLI workflows, plus sandboxed recipe and easyblock 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_capacity, 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_target_list, eb_target_doctor;
  • eb_campaign_run, eb_campaign_status;
  • eb_campaign_finding_claim, eb_campaign_finding_resolve;
  • eb_pr_overlap (the same writer as pr-overlap, including --kind easyconfigs, a merge order, and --impact), eb_rebuild_set, eb_component_reach, eb_easyblock_reach;
  • eb_repro_summary;
  • eb_ledger_append, eb_ledger_head, eb_ledger_tiles, eb_ledger_find, eb_ledger_show, eb_ledger_verify.

eb_recipe_read, eb_recipe_list, eb_recipe_edit, eb_recipe_write, and eb_recipe_grep resolve every path inside that root. A .. component, an absolute path outside the root, or a symlink whose target leaves the root is rejected and the file is not opened. eb_recipe_edit replaces an exact string. If old matches more than once, the file is left unchanged unless replace_all is true. eb_recipe_write does not create parent directories. eb_recipe_grep searches for a literal pattern (a regular expression when regex is true) under path (default the root), skips .git, does not follow symlinks, and returns at most max_matches (default and limit 200) matches. eb_recipe_check requires easyconfigs (minItems 1). An empty list returns argument easyconfigs requires at least one path. A .eb file there means the easyconfigs directory it sits in; any other file is an error that asks for a robot directory. A recipe that is not valid Python is refused. It also takes recipe, require_configopts, easyblocks, metadata_only, verify_sources, seeded_from, seeded_source_url, seeded_git, source_tree, and strict, and returns maintainer acceptability, easyblock resolution, strict skips, and source classes. eb_recipe_lint takes a recipes path array and returns results plus check_contrib, using the same findings and maintainer-acceptability as recipe lint. eb_recipe_format takes recipe, out, and dry_run, and a dry run does not write.

eb_recipe_easyblock takes name and charmap. charmap true prints the encoding table; a name prints the derived class. eb_recipe_templates takes name, version, and export. export true prints the constant table and the derived-key rules. eb_recipe_hierarchy takes export. The plain call prints the fixture generations. export true adds the synthetic GCC-family rule, the same split as recipe hierarchy --export. eb_stack_order requires easyconfigs and root, and takes out, oldest, easystack_out, hashes_out, filter_deps, and installed. filter_deps drops the same dependencies --filter-deps drops. installed is the module list or Lmod tree --installed reads. A module it names is left out of the order. The result field installed is how many modules the walk stopped at, and is null when the argument is omitted. eb_stack_easystack requires lock and takes out and option, each option a <file.eb>:<option>=<value> string, and eessi_version, easybuild_version, generation, and out_dir for the software-layer layout. eb_stack_sections requires list, from, and out, and takes unfiled. eb_stack_from_commit requires easystack and takes easyconfigs_repo, easyblocks_repo, easybuild_version, json, and rewrite. It returns the same report as stack from-commit. eb_stack_gap requires installed, easyconfigs, generation, and out_dir, and takes site, older, exclude_file, filter_deps, open_layer, open_easyconfigs, and eessi_version. eb_stack_provided requires build_list and takes installed, baseline_lock, allow, family_lock, licensed, licensed_file, json, format, and strict. A hit, an unparsed line, a wrong toolchain, a --hooks flag, or a family-lock name is a tool error. A licensed warning is not, unless strict is true. The text is the same report as stack provided. eb_stack_shadow requires easyconfigs and base, and takes json. It returns the same report as stack shadow. A shadow, or a recipe that does not parse, is a tool error. eb_stack_arch requires easystack and takes arch, easyconfigs, installed, additional_arch, unsupported, probe_sources, probe_retries, and probe_timeout_s. It returns the same report as stack arch, with parse_failed and source_findings, and a hit is a tool error. probe_sources probes the families arch names, as --arch does. Two names are compared only with each other. eb_stack_files requires easystack and takes easyconfigs, commit_tree, easyconfigs_repo, sourcepath, and path_map. It returns the same report as stack files, and a missing file is a tool error. eb_stack_cuda_cc requires easystack and easyconfigs, and takes target, cuda_table, accelerator_targets, and easyblock_map. It returns the same report as stack cuda-cc, and a finding is a tool error. eb_stack_generation takes easystack, pr, eessi_version, deployed, allow_versions, and generations, and needs one easystack or pr. It returns the same report as stack generation, and a finding is a tool error. eb_stack_known_failures requires easystack and takes arch, easyconfigs, installed, known_failures, eessi_version, easybuild_version, and strict. It returns text, the stack known-failures stdout, and hits, one object per line. Warnings are not a tool error unless strict is true, when a remaining warning is. eb_stack_oom takes easystack, arch, easyconfigs, installed, profile, resources, node, job_memory, hooks, parallel, and max_parallel. It needs an easystack, a node, or hooks. It returns the stack oom text, one object per warning, factor_warnings for a divide factor that is not an integer, and hook_warnings for a dropped sum in the hooks file. A warning is not a tool error. eb_stack_bot_classify requires files. It returns text, the stack bot-classify stdout, jobs, one object per job, and counts. eb_stack_batch takes arch, unsupported, known_failures, oom_profile, and oom_resources. It refuses an unsupported plan the same way and writes known-failures and oom warnings. eb_repro_summary requires scores and takes ratchet and json. eb_ledger_append requires log and record and takes expect_head, key, head_out, tiles and origin; it also returns index, id, size, root, the signed head, the tile log’s state under tiles and the checkpoint when a key signed one. eb_ledger_head requires log, key and out and takes ts, tiles and origin; it returns the head, tiles and checkpoint. eb_ledger_tiles requires log and takes tiles. eb_ledger_find requires log, public_key and keys (an array of field=value strings, at least one) and takes tiles, origin and json; it returns found (index and record per hit), unsigned, checkpoint_size and checkpoint_root. eb_ledger_show requires log and takes package and json, and returns the records. eb_ledger_verify requires log and public_key, and either record (an integer) with head, or from with to, and takes tiles and origin; it returns the proof nodes and the tile log’s tiles_size and tiles_root, and a proof or a tile log that does not check is a tool error. Each of these tools calls the same runner as the command of that name, and the text it returns is that command’s stdout.

The recipe tools run the same checks as the CLI. MCP results include the same artifacts, claims, findings, and next actions as the CLI. There is no separate compatibility command surface.

eb_package_plan accepts the CLI’s repeated source-checksum input as a source_checksums string array, and the catalog / source-root inputs as package_catalogs, package_sources, easybuild_sources, conda_sources, spack_sources, and cargo_sources arrays. emit_easyblock asks for an easyblock skeleton in the bundle and does not combine with package-closure planning. eb_package_plan also takes package_index, a CRAN PACKAGES file or a name==version list, the same input as --package-index. eb_package_bump optionals include version, source_checksum, dependencies (object map), hierarchy_fixture, stack_policy, package_configs, foreign (array of recipe paths, as --foreign), emit_easyblock, fetch_checksums, strict_patches, latest_secondary, drop_patch (object map from patch name to reason), and no_companions. strict_patches fails the bump when a patch’s fate cannot be decided, as --strict-patches does. fetch_checksums downloads the new source and writes its SHA-256, as --fetch-checksums does. no_companions is --no-companions; the result lists each companion bump under companions. eb_package_inspect, eb_package_plan, and eb_package_bump require toolchain as name/version or system. The schema says so with anyOf, because the retired split fields still satisfy the tools. As on the command line, eb_package_inspect and eb_package_plan accept a lone toolchain_version and use foss. eb_package_bump does not. eb_stack_solve reads the policy file as stack_policy. eb_recipe_lint takes a recipes path array, or a single recipe path, and returns reviewer-style findings plus check_contrib. eb_campaign_finding_resolve takes changes as an optional path array. eb_recipe_check’s easyconfigs array has minItems 1. eb_recipe_read, eb_recipe_list, eb_recipe_edit, and eb_recipe_write edit files only inside the workspace root.

Scheduler capacity and phase storage

See the captured capacity and storage-policy interface for schema, synthetic examples and the execution/admission contract. The optional stack shard --storage-policy path does not change default execution or grant admission.