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.
| Code | When |
|---|---|
| 0 | The command finished the work it was asked to do |
| 1 | The 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 |
| 2 | The command line was rejected before that work: a missing or unknown flag (clap), or a flag value or combination the command does not accept |
| 78 | stack 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 | Purpose |
|---|---|
package inspect | Parse conda-forge, Spack, PyPI, Comprehensive R Archive Network (CRAN), Cargo, LuaRocks, or Raku into a manifest and planned software bill of materials (SBOM) |
package plan | Materialize profiles, solve with Resolvo, and emit recipes |
package bump | Retarget an EasyBuild recipe and write the same canonical bundle |
package latest | Report the newest stable upstream release of EasyBuild recipes |
recipe easyblock | Print the easyblock class name EasyBuild derives for a software name |
recipe templates | Emit template constants and derived-key rules, or the values `–name` and `–version` resolve |
recipe hierarchy | Emit known toolchain hierarchies as JSON |
recipe check | Validate metadata and robot dependency availability |
recipe lint / recipe format | Report 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 sbom | Solve a multi-root stack or emit SBOM from a lock |
stack order | Sequence every pinned build a set of roots needs |
stack gap | Route each site module the stack lacks, name the recipe that closes it, and write one TSV per route |
stack batch | Plan software-layer pull requests whose builds do not overlap |
stack shard | Split a Jenkins build list into shards that build at once, with a prelude of shared dependencies, capped by a scratch inode quota |
stack capacity | Read timestamped advisory scheduler capacity with exact partition, host profile and resource constraints |
stack footprints | Write the peak build-tree inode count per software name from ledger records, a static table, and a class table |
stack admit | Admit one build job under the scratch inode quota, counting the reservations of the jobs still running |
stack easystack | Write a lock as the YAML eb --easystack reads |
stack sections | Regroup a Jenkins build list under a previous list’s headers |
stack from-commit | Report from-commit pins already contained in an EasyBuild release |
stack provided | Fail when a build list names software a baseline already provides |
stack shadow | Report a site-layer recipe that shadows an EESSI base-stack module |
stack arch | Refuse a plan an architecture cannot build, and flag entries never installed there |
stack files | Find the patch and source files an easystack’s easyconfigs need that no tree has |
stack generation | Refuse easystack entries outside their EESSI version’s toolchain generations |
stack cuda-cc | Refuse CUDA compute-capability combinations a plan cannot build on EESSI accelerator targets |
stack known-failures | Warn about known per-architecture test, build, fetch and sanity failures in a plan, with the easyconfig remedy |
stack oom | Warn about plan entries likely to run out of memory in a bot job, with remedies |
stack bot-classify | Classify EESSI bot job results as real failure, ERROR-only noise, empty tarball, or infra |
repro summary | Print a reproduction scoreboard from collected case scores |
pr-overlap | Close open pull-request easystack additions against an installed stack and report shared builds |
rebuild-set | Classify the installed reverse dependents of easyconfigs patched in place |
component-reach | List who carries a component in one generation and close their rebuild set |
target list / target doctor | Resolve layered target configuration and probe it |
campaign run / campaign status | Execute or inspect a persisted build campaign |
campaign finding claim | Claim an open finding for one worker |
campaign finding resolve | Resolve a claimed finding with an action, evidence, and the paths it changed |
ledger append | Append one event record to a per-stack ledger as a canonical JSON line and to its tile log, and sign the new head |
ledger head | Sign a ledger’s current head and the tile log’s checkpoint, for a published anchor |
ledger tiles | Write the tile log beside a ledger, or bring it up to date |
ledger find | Find records by field=value keys through the tile log’s sidecars, each proven in the checkpoint |
ledger show | List ledger records newest first, for one module or software name |
ledger verify | Check 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-reach | Report easyconfigs and installed modules reached by an easyblock or framework change |
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 BUNDLEThis 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.
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 BUNDLEPackage 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-sourceslayers ordered EasyBuild / conda-forge / Spack / Cargo roots;--easybuild-source,--conda-source,--spack-source, and--cargo-sourcepush roots without a TOML file (order is discovery order);--package-cataloglayers explicit foreign oreasybuild-bumpproviders as ordered overrides.--emit-easyblockrenders an easyblock skeleton for the package intoeasyblocks/<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 amissing-easyblockresidual. It does not combine with closure planning.package bumptakes 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.
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 BUNDLEA 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.
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/
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.
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.ebrecipe 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.
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.
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.ymlGDRCopy-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.
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-97a4c1aPython-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.
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.
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.
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/easyconfigsorjson-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 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 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.
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.ymllibxc-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.
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.outreal-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.
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-overlapThe 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-outThe 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.
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.
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`.
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 NAMETarget 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.
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.
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.
| Field | Content |
|---|---|
ts | UTC time, RFC 3339 (Z or +00:00). append fills the current time when absent |
actor | Who wrote it: a Jenkins run URL, a bot pull request and job, or a login |
stack | 2026.06 (EESSI version) or 2025 |
target | CPU target and accelerator, e.g. x86_64/amd/zen4 |
module | Module name, e.g. PETSc/3.25.0-foss-2026.1 |
location | The installation path that RPATHs use |
action | install, rebuild-inplace, rebuild-bump, retire, unretire, move, access, remove, finding, or correction |
stage | Optional: started, finished, or aborted, for an operation that runs in steps |
route | Optional: native, kvm, or qemu, where the build ran |
reason, easyconfig, sources, sbom, modulefile, build | Optional 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, corrects | Optional id of a related record. A correction needs corrects |
index | Leaf number. Written by append; a record that carries another value is refused |
id | SHA-256 of the canonical record without id and sig. Written by append; a record that carries another value is refused |
sig | Optional 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.
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.
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.
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_testonly withEB_STACK_ALLOW_BUILD=1);eb_stack_solve,eb_stack_sbom,eb_stack_order,eb_stack_easystack,eb_stack_gap,eb_stack_provided,eb_stack_shadow,eb_stack_batch,eb_stack_shard,eb_stack_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 aspr-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.
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.