mod eb_parse#

module eb_parse#

Parse EasyBuild easyconfig (.eb) files into structured candidates.

Easyconfigs are a restricted Python DSL. This module evaluates that subset (assignments, lists/tuples/dicts, SYSTEM, local_* and other name refs) and resolves EasyBuild-style %(…)s templates derived from name / version / versionsuffix / toolchain — matching EasyBuild’s EasyConfigParser plus the core template set used for fixture goldens under fixtures/parser_hardcases/.

Functions

fn candidate_matches_dep(c: &Candidate, dep: &ResolvedDep) -> bool#

Whether a universe candidate satisfies a resolved dep (name + version + optional versionsuffix + optional per-dep toolchain). Cross-toolchain deps (e.g. eOn/gfbf depending on quill/GCCcore) are first-class here — unlike filter_toolchain which keeps only the policy toolchain.

When dep.toolchain is None, any toolchain with a matching name/version/suffix is accepted (legacy identity match). Prefer candidate_matches_dep_for_recipe so unpinned deps must live in the recipe generation hierarchy (closer to EasyBuild robot behaviour).

fn candidate_matches_dep_for_recipe(c: &Candidate, dep: &ResolvedDep, hierarchy: &crate::hierarchy::ToolchainHierarchy) -> bool#

Like candidate_matches_dep, but when the dep has no explicit toolchain pin, only candidates whose toolchain is a member of hierarchy count (e.g. CapnProto on GCCcore-14.x does not satisfy foss-2026.1 which needs GCCcore-15.2.0). Explicit fourth-tuple pins still match exactly, including cross-generation residuals (xtb/gfbf-2024a).

fn check_recipe_deps(recipe: &ResolvedEasyconfig, universe: &[Candidate]) -> RecipeDepCheck#

Check that every runtime/build dep of recipe appears as a candidate in universe (any tree layer already merged). Does not run the SAT solver — this is the packaging/robot completeness gate used before eb.

Unpinned deps must match a hierarchy member of the recipe toolchain (derived from the robot universe when possible), so an older-generation GCCcore candidate does not false-pass a newer foss recipe.

fn checksum_structure_findings(recipe: &ResolvedEasyconfig) -> Vec<String>#

Structural findings for the checksums list (EasyBuild convention: positional, all sources entries first, then patches). Catches the class of failure where a patch checksum is inserted in a source slot, which otherwise only surfaces as an eb “Missing checksum for X” abort after a build cycle has already been spent.

fn easyconfig_basename(name: &str, version: &str, tc: &Toolchain, versionsuffix: Option<&str>) -> String#

Conventional basename: Name-version-Toolchain-tcver.eb (+ versionsuffix).

fn easyconfig_letter_dir(name: &str) -> String#

EasyBuild-style letter directory for name (GROMACSg, xtbx).

fn filter_toolchain(cands: &[Candidate], tc: &Toolchain) -> Vec<Candidate>#
fn lock_from_candidates(cands: &[Candidate], generation_label: Option<String>, engine: &str) -> StackLock#
fn merge_candidates_with_precedence(layers: &[Vec<Candidate>]) -> Vec<Candidate>#

Merge candidate layers with later-layer precedence: when two candidates share the same name + version + toolchain + versionsuffix, the later layer wins (overlay). Distinct installable variants remain separate candidates.

Used for site overlays on top of an upstream easyconfigs tree.

fn packaging_gate(recipe: &ResolvedEasyconfig, required_configopts: &[&str]) -> Result<(), Vec<String>>#

Packaging gate: checksums present, moduleclass set, and optional required configopts substrings (e.g. -Dwith_tests=false).

A missing easyblock is not an error: EasyBuild derives the easyblock from the software name when the recipe omits it (OpenMPI -> EB_OpenMPI, GCC -> EB_GCC, …), which is how the majority of upstream recipes are written. Only recipes whose name does not map to a software-specific easyblock need to declare one, and the recipe author — not this gate — makes that call.

fn parse_easyconfig_file(path: &Path) -> Result<Candidate, ParseError>#

Parse one .eb file into a solver-facing Candidate.

fn parse_easyconfig_tree(root: &Path) -> Result<ParseTreeResult, ParseError>#

Walk a directory tree for *.eb and parse all easyconfigs.

Unparseable files are skipped (not fatal): they appear in ParseTreeResult::skipped so callers can report coverage without aborting a real multi-thousand-file tree on the first bad recipe.

fn parse_easyconfig_trees(roots: &[&Path]) -> Result<ParseTreeResult, ParseError>#

Parse multiple easyconfig trees and merge with later-path precedence. Skipped paths from every tree are retained.

fn resolve_easyconfig_file(path: &Path) -> Result<ResolvedEasyconfig, ParseError>#

Resolve one .eb file to fully expanded fields.

fn resolve_easyconfig_str(src: &str) -> Result<ResolvedEasyconfig, ParseError>#

Resolve easyconfig source text to fully expanded fields (no filesystem path).

fn validate_lock_deps(lock: &StackLock, cands: &[Candidate]) -> Result<(), String>#
fn version_field_to_req(version: &str) -> String#

Map EasyBuild dependency version field to a solver requirement string.

Enums

enum ParseError#
Io(String, std::io::Error)#
Parse(String, String)#

Structs and Unions

struct MissingDep#

One missing (or unmatched) dependency from a packaging/robot check.

name: String#
version: String#
versionsuffix: Option<String>#
toolchain: Option<Toolchain>#
role: String#

Runtime vs build-time role in the recipe.

reason: String#
struct ParseTreeResult#

Result of walking an easyconfig tree: successes + skipped unparseable files.

candidates: Vec<Candidate>#
skipped: Vec<SkippedEasyconfig>#

Implementations

impl ParseTreeResult#

Functions

fn coverage(&self) -> f64#

Coverage fraction parsed / (parsed + skipped); 1.0 when the tree is empty.

fn merge_with_precedence(mut self, other: ParseTreeResult) -> ParseTreeResult#

Merge another result (later candidates override on identity; skips append).

fn parsed_count(&self) -> usize#
fn skip_count(&self) -> usize#
struct RecipeDepCheck#

Result of checking that a recipe’s deps exist somewhere in a robot universe.

recipe: String#
name: String#
version: String#
toolchain: Toolchain#
easyblock: Option<String>#
configopts: Option<String>#
moduleclass: Option<String>#
checksum_count: usize#
missing: Vec<MissingDep>#
found: Vec<String>#

Implementations

impl RecipeDepCheck#

Functions

fn ok(&self) -> bool#
struct ResolvedDep#

One resolved dependency entry (2–4 element EasyBuild dependency tuple).

name: String#
version: String#

Raw version field after template/local resolution (may be 1.2.3 or >=1.2).

versionsuffix: Option<String>#
toolchain: Option<Toolchain>#

Per-dependency toolchain override (None = inherit the easyconfig toolchain).

struct ResolvedEasyconfig#

Fully resolved easyconfig fields (templates and locals applied).

Solver-facing co-selection uses Self::to_candidate. Packaging / contribution checks also use the optional metadata fields below (easyblock, configopts, moduleclass, …).

name: String#
version: String#
versionsuffix: Option<String>#
toolchain: Toolchain#
dependencies: Vec<ResolvedDep>#
builddependencies: Vec<ResolvedDep>#
exts_list: Vec<ResolvedExt>#
easyconfig_path: String#

Path of the source .eb when parsed from disk (empty for in-memory text).

easyblock: Option<String>#

EasyBuild easyblock class name (MesonNinja, CMakeMake, …).

configopts: Option<String>#

Meson/CMake/configure flags string after template expansion.

moduleclass: Option<String>#

EasyBuild moduleclass (chem, lib, tools, …).

homepage: Option<String>#

Homepage URL when set.

checksums: Vec<String>#

Source checksums list (strings), when present — used for packaging gates.

sources_count: usize#

Number of entries in sources (0 when the field is absent).

patch_names: Vec<String>#

Patch file names from patches (tuple/dict entries reduced to the name).

checksum_entry_keys: Vec<Vec<String>>#

Per-checksums-LIST-ENTRY dict keys (empty inner vec for plain-string entries). Parallel to the checksums list entries, not the flattened values, so a multi-arch dict entry stays one entry.

Implementations

impl ResolvedEasyconfig#

Functions

fn to_candidate(&self) -> Candidate#

Map into the solver-facing Candidate / DepReq shapes.

struct ResolvedExt#

One exts_list entry after resolution.

name: String#
version: String#
struct SkippedEasyconfig#

One easyconfig path that could not be parsed into a candidate.

path: String#
error: String#