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 dependencies crossing from a parent toolchain to a core toolchain 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 implicit deps follow EasyBuild’s minimal-toolchain search within the recipe generation.

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 non-SYSTEM candidates in hierarchy count for a non-SYSTEM recipe. EasyBuild may minimize an implicit dependency to a generation member such as GCCcore, but it only selects SYSTEM when the dependency tuple says so explicitly. Explicit fourth-tuple pins still match exactly, including explicitly selected cross-generation dependencies.

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.

Implicit deps must match a non-SYSTEM hierarchy member of the recipe toolchain (derived from the robot universe when possible), so neither an older-generation candidate nor a SYSTEM fallback can false-pass.

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 (ExamplePkge).

fn existing_versions(query: &ExistingVersionsQuery<'_>) -> Vec<String>

Versions of query.name already present at query.generation, sorted newest first by the same comparison the miner uses.

This is the producer for crate::miner::is_backfill’s existing_versions: without it a caller has to hand-build that list, which is why backfill detection had no production call site.

fn filter_toolchain(cands: &[Candidate], tc: &Toolchain) -> Vec<Candidate>

Keep only candidates built at exactly this toolchain.

The narrow form, for callers that mean one level and nothing under it. A solve wants filter_toolchain_hierarchy instead, since a recipe’s own build dependencies routinely sit one level down.

fn filter_toolchain_hierarchy(cands: &[Candidate], tc: &Toolchain, members: &[Toolchain]) -> Vec<Candidate>

Candidates built against exactly this toolchain. Keep the policy toolchain and everything below it in its hierarchy.

A stack is not built at one toolchain. A recipe at GCC-15.2.0 takes its CMake from GCCcore-15.2.0 and its licence file from SYSTEM, exactly as EasyBuild’s minimal-toolchain search does, so a universe filtered to the policy toolchain alone cannot satisfy the dependencies of its own members and reports them as missing packages.

members is the hierarchy for the policy toolchain, lowest first. Passing an empty slice reduces this to filter_toolchain.

fn lock_from_candidates(cands: &[Candidate], generation_label: Option<String>, engine: &str) -> StackLock

Build a lock from an already-selected candidate set.

The caller has done the choosing; this records it.

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_file_reporting(path: &Path) -> Result<(ResolvedEasyconfig, Vec<SkippedStatement>), ParseError>

As resolve_easyconfig_file, also returning the skipped statements.

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

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

Tolerant: a statement the parser cannot model is skipped so the rest of the recipe still resolves. Use resolve_easyconfig_str_reporting when you need to know what was skipped.

fn resolve_easyconfig_str_reporting(src: &str) -> Result<(ResolvedEasyconfig, Vec<SkippedStatement>), ParseError>

Resolve easyconfig text, also returning the statements the parser skipped.

resolve_easyconfig_str is this without the second value; the parse is identical, so asking for the report costs nothing and changes nothing.

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

Check that every dependency of every locked package is itself locked.

A lock that omits a transitive dependency installs a stack that cannot build, so this is worth checking before one is trusted.

fn version_field_to_req(version: &str) -> String

Map EasyBuild dependency version field to a solver requirement string.

Enums

enum ParseError

Why an easyconfig could not be read.

Io(String, std::io::Error)

The file could not be read.

Parse(String, String)

The file could not be understood. Carries the path and the reason, including a line number where the failure was positional.

Structs and Unions

struct ExistingVersionsQuery<'a>

Which versions of one package a universe already carries at one toolchain generation.

Struct-shaped like the other request types in this crate so the three fields cannot be transposed at a call site.

universe: &'a [Candidate]

Universe from parse_easyconfig_tree or parse_easyconfig_trees, live checkout or historical merge-base tree alike.

name: &'a str

Package name to match, exactly.

generation: &'a Toolchain

Toolchain generation to match, name and version.

struct MissingDep

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

name: String

Dependency that could not be matched.

version: String

Version the recipe asked for.

versionsuffix: Option<String>

Versionsuffix required, when one was.

toolchain: Option<Toolchain>

Toolchain required, when the entry pinned one.

role: String

Runtime vs build-time role in the recipe.

reason: String

Why nothing matched: absent entirely, or present at the wrong version or toolchain.

struct ParseTreeResult

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

candidates: Vec<Candidate>

Everything that parsed.

skipped: Vec<SkippedEasyconfig>

Everything that did not, kept so a tree is never silently partial.

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

How many parsed successfully.

fn skip_count(&self) -> usize

How many easyconfigs failed to parse.

struct RecipeDepCheck

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

recipe: String

Easyconfig that was checked.

name: String

Package it declares.

version: String

Version it declares.

toolchain: Toolchain

Toolchain it targets.

easyblock: Option<String>

Easyblock it uses, when it sets one.

configopts: Option<String>

Configure flags, after template expansion.

moduleclass: Option<String>

Moduleclass, when it sets one.

checksum_count: usize

How many checksum entries it carries.

missing: Vec<MissingDep>

Dependencies the robot tree does not provide.

found: Vec<String>

Dependencies that were matched.

unverified_toolchain: Option<String>

Set when the recipe’s toolchain generation could not be resolved, so matching ran without a hierarchy to filter by.

Every entry in found is then a name-and-version match that ignores toolchain, which can pair a recipe with a dependency from an unrelated generation (the EB_MAINT_CROSS_GEN shape). missing staying empty is not evidence the recipe resolves in EasyBuild; callers must say so rather than report a clean result.

Implementations

impl RecipeDepCheck

Functions

fn ok(&self) -> bool

Whether every dependency was found.

fn toolchain_verified(&self) -> bool

Whether the matches carry toolchain evidence, i.e. a hierarchy was known.

Distinct from Self::ok on purpose: ok answers “was anything left unmatched”, this answers “is a match worth anything”.

struct ResolvedDep

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

name: String

Dependency name.

version: String

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

versionsuffix: Option<String>

Versionsuffix required, when the entry names one.

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

Package name.

version: String

Package version after template expansion.

versionsuffix: Option<String>

Versionsuffix, when the easyconfig sets one.

toolchain: Toolchain

Toolchain this recipe builds against.

dependencies: Vec<ResolvedDep>

Runtime dependencies.

builddependencies: Vec<ResolvedDep>

Build-time-only dependencies.

exts_list: Vec<ResolvedExt>

Bundled extensions.

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).

source_urls: Vec<String>

Expanded source_urls entries, used to classify which artifact the recipe actually downloads.

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.

checksums_by_filename: BTreeMap<String, String>

Checksums the recipe states by filename, for the entries written as {'name': 'sha…'}. Position says nothing once one entry carries several hashes.

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

Extension name.

version: String

Extension version.

struct SkippedEasyconfig

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

path: String

Easyconfig that was skipped.

error: String

Why it could not be parsed.

struct SkippedStatement

Resolve easyconfig source text to fully expanded fields (no filesystem path). One statement the tolerant parser could not model, and why.

The parser skips an unrepresentable statement so a single oddity does not cost the whole easyconfig. That is the right default, but it means a recipe can parse “successfully” while a line the author cared about was dropped. These records are what --strict reports.

line: usize

1-based line the statement starts on.

message: String

Why it could not be parsed, already carrying its own line prefix.

text: String

The statement text as written, trimmed.

Implementations

impl SkippedStatement

Functions

fn reason(&self) -> &str

The reason with the parser’s own line N: position prefix stripped, so a caller that prints its own position does not repeat it.

Traits implemented

impl std::fmt::Display for SkippedStatement