Source code for eb_staged_rebuild.modulefile

"""Read and rewrite the install-directory assignment in an EasyBuild module file.

EasyBuild 5.4 writes Lua as ``local root = "<installdir>"`` and Tcl as
``set root <installdir>`` (no quotes). Both expressions are matched the way
``easybuild/tools/module_generator.py`` matches them.
"""

import os
import re

from eb_staged_rebuild.errors import Refused
from eb_staged_rebuild.paths import abspath

LUA_ROOT = re.compile(r'(?m)^local root\s*=\s*"((?:\\.|[^"\\])*)"\s*$')
TCL_ROOT = re.compile(r'(?m)^set root\s+(?:"([^"]*)"|(\S+))\s*$')
LOAD_STMT = re.compile(r'(?m)^\s*(?:load|depends_on)\("([^"]+)"\)')
TCL_LOAD = re.compile(r'(?m)^\s*module\s+load\s+\{?([^\s}]+)\}?\s*$')


[docs] def parse_root(text): """Return ``("lua"|"tcl", path)`` or ``(None, None)``.""" lua = LUA_ROOT.search(text) tcl = TCL_ROOT.search(text) if lua and tcl: if lua.start() < tcl.start(): return "lua", lua.group(1) return "tcl", tcl.group(1) or tcl.group(2) if lua: return "lua", lua.group(1) if tcl: return "tcl", tcl.group(1) or tcl.group(2) return None, None
[docs] def loads(text): """Dependency names from Lua ``load``/``depends_on`` and Tcl ``module load``.""" return LOAD_STMT.findall(text) + TCL_LOAD.findall(text)
[docs] def replace_path(text, old, new): """Replace ``old`` as a path token with ``new``. Existing occurrences of ``new`` are left in place, so a new path that contains the old path as a prefix is not rewritten twice. """ if old == new: return text if not old: raise Refused("refusing to rewrite an empty root") if "\n" in new or '"' in new or "\x00" in new: raise Refused("refusing to write a root that contains a quote, newline, or NUL") token = "\x00KEEP\x00" protected = text.replace(new, token) pattern = re.compile(re.escape(old) + r"(?![A-Za-z0-9._+\-])") replaced, count = pattern.subn(lambda _match: new, protected) if count < 1: raise Refused(f"module file does not contain the install path {old}") return replaced.replace(token, new)
[docs] def rewrite_root(text, new_root): """Point the module file at ``new_root`` and return the new text.""" kind, old = parse_root(text) if kind is None or not old: raise Refused("module file has no root assignment") new_root = abspath(new_root) updated = replace_path(text, old, new_root) kind2, got = parse_root(updated) if kind2 != kind or abspath(got) != new_root: raise Refused( f"rewritten module file root is {got!r}, expected {new_root}" ) return updated
[docs] def module_identity(modules_root, module_path): """``name/version`` from the last two path components of a module file.""" relative = os.path.relpath(module_path, modules_root) parts = relative.split(os.sep) if len(parts) < 2: return os.path.splitext(parts[-1])[0] stem = os.path.splitext(parts[-1])[0] return os.path.join(parts[-2], stem)
[docs] def generation_complete(path): """A generation is complete when it is a real directory with install metadata. Accepted markers, any one of them: * ``PAYLOAD`` (used by the fault-injection tests) * ``easybuild/*.eb`` (EasyBuild copies the easyconfig in at the end of a build) * ``.eb-staged-rebuild-complete`` (written by this tool before it publishes) """ from eb_staged_rebuild.paths import is_real_dir, lstat_kind if not is_real_dir(path): return False payload = os.path.join(path, "PAYLOAD") if lstat_kind(payload) == "file": return True stamp = os.path.join(path, ".eb-staged-rebuild-complete") if lstat_kind(stamp) == "file": return True meta = os.path.join(path, "easybuild") if is_real_dir(meta): for name in os.listdir(meta): if name.endswith(".eb") and lstat_kind(os.path.join(meta, name)) == "file": return True return False
[docs] def install_complete(path): """A staged install is complete when it is a real directory with build evidence. Accepted, any one of them: ``PAYLOAD`` (the fault-injection builders) or a readable ``easybuild/*.eb`` (EasyBuild copies the easyconfig in at the end of a build). The tool's own completion stamp is not evidence: a publication-preparation step that empties the tree leaves the stamp. """ from eb_staged_rebuild.paths import is_real_dir, lstat_kind if not is_real_dir(path): return False if lstat_kind(os.path.join(path, "PAYLOAD")) == "file": return True metadata = os.path.join(path, "easybuild") if not is_real_dir(metadata): return False try: names = os.listdir(metadata) except OSError: return False return any(name.endswith(".eb") and lstat_kind(os.path.join(metadata, name)) == "file" for name in names)