From 0cd97d584ec437ca11f4b88bbf0e549135fb0ea7 Mon Sep 17 00:00:00 2001 From: Wouter Deconinck Date: Thu, 22 Aug 2024 15:43:08 -0500 Subject: [PATCH] RPackage: parse description for incomplete dependencies --- lib/spack/spack/build_systems/r.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lib/spack/spack/build_systems/r.py b/lib/spack/spack/build_systems/r.py index 87b1e104aeb..b7e7e41df9e 100644 --- a/lib/spack/spack/build_systems/r.py +++ b/lib/spack/spack/build_systems/r.py @@ -1,12 +1,19 @@ # Copyright Spack Project Developers. See COPYRIGHT file for details. # # SPDX-License-Identifier: (Apache-2.0 OR MIT) +import re from typing import Optional, Tuple +import llnl.util.filesystem as fs +import llnl.util.lang as lang +import llnl.util.tty as tty +import spack.deptypes as dt from llnl.util.filesystem import mkdirp from llnl.util.lang import ClassProperty, classproperty +from spack.dependency import Dependency from spack.directives import extends +from spack.spec import Spec from .generic import GenericBuilder, Package @@ -38,6 +45,59 @@ def install(self, pkg, spec, prefix): """Installs an R package.""" mkdirp(pkg.module.r_lib_dir) + try: + # TODO: use a more sustainable dcf parser + import pycran + + # Read DESCRIPTION file with dependency information + # https://r-pkgs.org/description.html + r_deps = [] + with open(fs.join_path(self.stage.source_path, 'DESCRIPTION')) as file: + for desc in pycran.parse(file.read()): + r_deps.extend([d.strip() for d in desc.get("Imports", None).split(",")]) + r_deps.extend([d.strip() for d in desc.get("Depends", None).split(",")]) + + # Convert to spack dependencies format for comparison + deps = {} + for r_dep in r_deps: + p = re.search(r"^[\w_-]+", r_dep) # first word, incl. underscore or dash + v = re.search("(?<=[(]).*(?=[)])", r_dep) # everything between parentheses + # require valid package + assert(p, f"Unable to find package name in {r_dep}") + r_spec = f"r-{p[0].strip().lower()}" if p[0].lower() != "r" else "r" + # allow minimum or pinned versions + if v: + v = re.sub(r">=\s([\d.-]+)", r"@\1:", v[0]) # >= + v = re.sub(r"==\s([\d.-]+)", r"@\1", v) # == + deps[r_spec] = Dependency(pkg, Spec(r_spec + v), dt.BUILD | dt.RUN) + else: + deps[r_spec] = Dependency(pkg, Spec(r_spec), dt.BUILD | dt.RUN) + + # Retrieve dependencies for current spack package and version + spack_dependencies = [] + for when, dep in pkg.dependencies.items(): + if spec.satisfies(when): + spack_dependencies.append(dep) + merged_dependencies = {} + for dep in spack_dependencies: + for n, d in dep.items(): + if d in merged_dependencies: + merged_dependencies[n].merge(d) + else: + merged_dependencies[n] = d + + # For each R dependency, ensure Spack dependency is at least as strong + for dep in sorted(deps.keys()): + if dep in merged_dependencies: + if not merged_dependencies[dep].spec.satisfies(deps[dep].spec): + tty.debug(f' depends_on("{deps[dep].spec}", when="@{pkg.version}:", type=("build", "run"))') + else: + tty.debug(f' depends_on("{deps[dep].spec}", when="@{pkg.version}:", type=("build", "run"))') + + except ImportError: + tty.debug("R package dependency verification requires pycran") + pass + config_args = self.configure_args() config_vars = self.configure_vars()