eb-stack ports a recipe and bumps one

a port starts from a foreign recipe; a bump starts from a working easyconfig; eb still builds

Port writes a file from someone else’s recipe. Bump moves pins on a file that already works. eb still has to run.

Questions

  • What does a conda-forge recipe.yaml become, as an easyconfig?

  • What does a bump change in a GROMACS file, and what does it leave?

  • What still has to run eb?

What you will be able to do

  1. Given a conda-forge recipe or a Spack package.py, name the command that writes the .eb, and say what still has to be built.

  2. Read a bump: name every pin that moved with the toolchain, and the lines that did not.

  3. Tell a port of software EasyBuild does not have from an annual retarget of software it does.

eb-stack performs two tasks. One task is a port that takes input from conda-forge or Spack and produces an EasyBuild recipe. The other task is a bump that rewrites an easyconfig written for one toolchain generation for the next. Neither task invokes eb. Both generate files that a human then builds. eb-stack is the tool. A planned CycloneDX SBOM is one of the files it writes. The next figure is that SBOM for a GROMACS bump, drawn by eb-stack stack graph (and by scripts/sbom_to_dot.py).

FFTW, OpenBLAS, OpenMPI, GROMACS as a CycloneDX dependency graph

Planned SBOM for a GROMACS generation. Edges are runtime dependsOn.

port and bump both write a file; neither runs eb

eb-stack writes an easyconfig. A port starts from someone else’s recipe. A bump retargets a generation. eb still has to run.

This page describes those two tasks and includes the files on the page.

A port starts as someone else’s recipe

The conda-forge feedstock for eOn 2.16.0 is frozen in eb-stack’s fixtures. It includes name, version, sources, host dependencies, and tests. That file is a conda-forge feedstock, which EasyBuild cannot read.

context:
 version: "2.16.0"

package:
 name: eon
 version: ${{ version }}

source:
 - url: https://github.com/TheochemUI/eOn/releases/download/v${{ version }}/eon-v${{ version }}.tar.xz
 sha256: 3d4da89a393c8821bf370cb97c9d2403718d83f9cbb5e8b918cd90af14ed52dc
 - url: https://github.com/OmniPotentRPC/rgpot/archive/refs/tags/v2.2.1.tar.gz
 sha256: d4687bc719e19174e89288dd16dd45d7a8645d7205c7f8d8fc4d677266055918
 target_directory: subprojects/rgpot
 - url: https://github.com/lode-org/readcon-core/archive/refs/tags/v0.13.1.tar.gz
 sha256: a261e69b87228dcc8f161eef800590b0395b4c93605b838c6216d40a1780f0bf
 target_directory: readcon-core-src

requirements:
 host:
 - python
 - numpy
 - pyyaml
 - eigen >=3.4,<3.5
 - libblas
 - quill
 - xtb
 - libtorch
 - libmetatomic-torch >=0.1.15,<0.2

The command package plan converts the file to an easyconfig. --format conda-forge selects the parser. --stack-policy is required.

eb-stack package plan \
 --source fixtures/foreign_ingest/conda_eon/recipe.yaml \
 --format conda-forge \
 --toolchain-name foss \
 --toolchain-version 2024a \
 --package-config examples/packages/common.toml \
 --package-config examples/packages/eon.toml \
 --easyconfigs /path/to/easybuild-easyconfigs \
 --stack-policy site/foss-2024a.toml \
 --out-dir /tmp/eon-from-conda

The command writes this tree:

package.plan.json
package.sbom.cdx.json
locks/default.lock.json
easyconfigs/e/eOn/eOn-2.16.0-foss-2024a.eb

The .eb is an emitted recipe; EasyBuild has compiled nothing yet. Lock denotes solver assignment. Plan includes parsed foreign recipe and residuals; parser‑invented items appear as typed leftovers, not guessed dependencies. Inspect writes a work queue, not a recipe. The feedstock and the Spack recipe are side by side. Name and version remain retained. eon transformed into eOn. eigen >=3.4,<3.5 converted into pin for this generation. libtorch renamed to PyTorch. Meson flags originally set by feedstock’s build.sh now represented as configopts.

easyblock = 'MesonNinja'

name = 'eOn'
version = '2.16.0'
toolchain = {'name': 'foss', 'version': '2024a'}

sources = [
 {'download_filename': 'v%(version)s.tar.gz', 'filename': SOURCE_TAR_GZ},
 {'filename': 'rgpot-2.2.1.tar.gz'},
 {'filename': 'readcon-core-0.13.1.tar.gz'},
]

dependencies = [
 ('Python', '3.12.3'),
 ('SciPy-bundle', '2024.05'),
 ('PyYAML', '6.0.2'),
 ('Eigen', '3.4.0'),
 ('xtb', '6.7.1', '', ('gfbf', '2024a')),
 ('PyTorch', '2.6.0'),
 ('metatomic-torch', '0.1.15'),
]

configopts = "-Dbuildtype=release -Dwith_fortran=true -Dwith_cuh2=true "
configopts += "-Dwith_metatomic=true -Dwith_xtb=true -Dwith_serve=true "
configopts += "-Dwith_rgpot=true -Dpip_metatomic=false"

Three sources are used. The target_directory: subprojects/rgpot entry became a staged extract in the full file; the widget above keeps the names. In the full recipe, sanity_check_commands runs eonclient --version and python -c "import eon"; this mirrors the feedstock’s tests: block. A Spack package.py file is the other input. The paper Gamblin et al. [2015] describes that format. A spec is a DAG of constraints (name, versions, variants, compiler, arch, deps). A concretiser selects one graph. The same software is parsed differently. --format spack specifies the parser. The file is Python and is not executed; literal depends_on and version calls are interpreted as syntax.

class Eon(MesonPackage):
    homepage = "https://eondocs.org/"
    url = "https://github.com/TheochemUI/eOn/releases/download/v2.16.0/eon-v2.16.0.tar.xz"

    version("2.16.0", sha256="3d4da89a393c8821bf370cb97c9d2403718d83f9cbb5e8b918cd90af14ed52dc")

    depends_on("meson@1.8.0:", type="build")  # meson 1.8.0 or newer
    depends_on("python@3.11:", type=("build", "run"))
    depends_on("py-numpy", type=("build", "run"))
    depends_on("eigen@3.4:")
    depends_on("quill@11:")
    depends_on("readcon-core@0.13.1:")
    depends_on("highway")
    depends_on("libinih")

depends_on("eigen@3.4:") (eigen 3.4 or newer) defines a range. EasyBuild does not support ranges (A pin is for one generation). The solver selects Eigen-3.4.0 from the robot tree for that generation. A def patch(self): that executes sed is not a file. It becomes a judgment residual. The emitted .eb does not claim that the sed occurred. Inspect writes the work queue. The campaign then runs it. The queue is visible first. Plan then consumes the robot tree only for items already decided. package inspect uses the same parsers without performing the solve. It writes package.plan.json. It writes a planned SBOM. It produces no .eb. Use it to read residuals. Use package plan to select non‑blocking items. Use package plan to emit the file.

A bump starts as an easyconfig that already works

GROMACS 2024.4 builds with foss/2023b. The file exists. The yearly job uses the same software on foss/2024a. Manual copying of the file and alteration of four version strings creates pins that become undocumented floors.

A bump writes one file. EasyBuild has not started.

$ eb-stack package bump \
> --source tests/repro_fixtures/gromacs/GROMACS-2024.4-foss-2023b.eb \
> --toolchain-name foss \
> --toolchain-version 2024a \
> --easyconfigs tests/repro_fixtures/universe_foss_2024a \
> --out-dir /tmp/gromacs-2024a
wrote /tmp/gromacs-2024a/GROMACS-2024.4-foss-2024a.eb

Recorded: workstation, eb-stack 0.2.0, 2026-09-11, tests/repro_fixtures/gromacs plus universe_foss_2024a

Parameter

2023b

2024a

toolchain

foss/2023b

foss/2024a

CMake

3.27.6

3.29.3

scikit-build-core

0.9.3

0.11.1

Python

3.11.5

3.12.3

SciPy-bundle

2023.11

2024.05

networkx

3.2.1

3.4.2

mpi4py

3.1.5

4.0.1

Unchanged lines include name, version, sources, patches, checksums, configopts, exts_list. The application version is 2024.4. Tarball bytes are identical. Each pin on the toolchain moved together with the toolchain. The candidate set is the 2024a robot tree. Resolvo chose a single consistent closure.

name = 'GROMACS'
version = '2024.4'
toolchain = {'name': 'foss', 'version': '2024a'}
toolchainopts = {'openmp': True, 'usempi': True}

builddependencies = [
 ('CMake', '3.29.3'),
 ('scikit-build-core', '0.11.1'),
]
dependencies = [
 ('Python', '3.12.3'),
 ('SciPy-bundle', '2024.05'),
 ('networkx', '3.4.2'),
 ('mpi4py', '4.0.1'),
]

Use the same command with --version and --source-checksum to increase the application version. If the checksum is omitted, the tarball name changes. The hash becomes stale. That missing hash is a blocking residual, and nothing guesses a checksum for it.

Two commands, then eb

Job

Command

Input

Output

read a foreign recipe

package inspect

recipe.yaml / package.py

plan + SBOM, no .eb

port it

package plan

the same, plus a robot tree and a stack policy

plan, lock, .eb

retarget a generation

package bump

an existing .eb plus a robot tree

plan, lock, new .eb

build what was written

campaign run

the bundle

eb, on a named target

recipe lint examines the file. recipe check examines the file. See Format, lint and check are three different questions. campaign run is the initial command that performs compilation. See A campaign finding is a typed failure, not a log. A green plan represents the claim resolves. It does not represent the claim builds.

EB-Port-1 — Port, bump, or neither

Four starting points. For each, name the eb-stack command, or say the work is not eb-stack’s.

  1. fixtures/foreign_ingest/spack_qmcpack/package.py, no QMCPACK easyconfig in the robot.

  2. GROMACS-2024.4-foss-2023b.eb, site moving to foss/2024a.

  3. The .eb from (1), in a bundle, nobody has run eb yet.

  4. A tarball and a README, no conda-forge recipe, no Spack package, no easyconfig.

Solution
  1. package plan --format spack. Inspect first if the residuals are unknown. The output is an easyconfig plus a lock.

  2. package bump. The source is already an easyconfig. The robot tree for 2024a is the candidate set.

  3. campaign run, or eb on that file. eb-stack has finished the generator’s job.

  4. Writing one from nothing. There is nothing to parse. The five mandatory parameters are written by hand.

The full sequence of inspect, bump, site generation, and upstream PR appears in SeisSol, from a Spack recipe to a PR.

What to remember

  • A port reads conda-forge or Spack and writes an easyconfig. package plan --format conda-forge or --format spack.

  • A bump reads an easyconfig and writes the same software on a new toolchain generation. Application version, sources and patches stay; every pin that lives on the toolchain moves.

  • A Spack eigen@3.4: is a lower bound. The robot either finds a pin this generation ships or the range stays a residual.

  • Nothing in inspect, plan or bump compiles the package. campaign run calls eb.