solver: partition classes related to requirement parsing into their own file (#47915)
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							4693b323ac
						
					
				
				
					commit
					c1b2ac549d
				
			@@ -48,8 +48,6 @@
 | 
				
			|||||||
import spack.version as vn
 | 
					import spack.version as vn
 | 
				
			||||||
import spack.version.git_ref_lookup
 | 
					import spack.version.git_ref_lookup
 | 
				
			||||||
from spack import traverse
 | 
					from spack import traverse
 | 
				
			||||||
from spack.config import get_mark_from_yaml_data
 | 
					 | 
				
			||||||
from spack.error import SpecSyntaxError
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .core import (
 | 
					from .core import (
 | 
				
			||||||
    AspFunction,
 | 
					    AspFunction,
 | 
				
			||||||
@@ -65,6 +63,7 @@
 | 
				
			|||||||
    parse_term,
 | 
					    parse_term,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from .counter import FullDuplicatesCounter, MinimalDuplicatesCounter, NoDuplicatesCounter
 | 
					from .counter import FullDuplicatesCounter, MinimalDuplicatesCounter, NoDuplicatesCounter
 | 
				
			||||||
 | 
					from .requirements import RequirementKind, RequirementParser, RequirementRule
 | 
				
			||||||
from .version_order import concretization_version_order
 | 
					from .version_order import concretization_version_order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GitOrStandardVersion = Union[spack.version.GitVersion, spack.version.StandardVersion]
 | 
					GitOrStandardVersion = Union[spack.version.GitVersion, spack.version.StandardVersion]
 | 
				
			||||||
@@ -144,17 +143,6 @@ def named_spec(
 | 
				
			|||||||
        spec.name = old_name
 | 
					        spec.name = old_name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RequirementKind(enum.Enum):
 | 
					 | 
				
			||||||
    """Purpose / provenance of a requirement"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #: Default requirement expressed under the 'all' attribute of packages.yaml
 | 
					 | 
				
			||||||
    DEFAULT = enum.auto()
 | 
					 | 
				
			||||||
    #: Requirement expressed on a virtual package
 | 
					 | 
				
			||||||
    VIRTUAL = enum.auto()
 | 
					 | 
				
			||||||
    #: Requirement expressed on a specific package
 | 
					 | 
				
			||||||
    PACKAGE = enum.auto()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DeclaredVersion(NamedTuple):
 | 
					class DeclaredVersion(NamedTuple):
 | 
				
			||||||
    """Data class to contain information on declared versions used in the solve"""
 | 
					    """Data class to contain information on declared versions used in the solve"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -757,17 +745,6 @@ def on_model(model):
 | 
				
			|||||||
        raise UnsatisfiableSpecError(msg)
 | 
					        raise UnsatisfiableSpecError(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RequirementRule(NamedTuple):
 | 
					 | 
				
			||||||
    """Data class to collect information on a requirement"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pkg_name: str
 | 
					 | 
				
			||||||
    policy: str
 | 
					 | 
				
			||||||
    requirements: List["spack.spec.Spec"]
 | 
					 | 
				
			||||||
    condition: "spack.spec.Spec"
 | 
					 | 
				
			||||||
    kind: RequirementKind
 | 
					 | 
				
			||||||
    message: Optional[str]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class KnownCompiler(NamedTuple):
 | 
					class KnownCompiler(NamedTuple):
 | 
				
			||||||
    """Data class to collect information on compilers"""
 | 
					    """Data class to collect information on compilers"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1146,6 +1123,7 @@ class SpackSolverSetup:
 | 
				
			|||||||
    def __init__(self, tests: bool = False):
 | 
					    def __init__(self, tests: bool = False):
 | 
				
			||||||
        # these are all initialized in setup()
 | 
					        # these are all initialized in setup()
 | 
				
			||||||
        self.gen: "ProblemInstanceBuilder" = ProblemInstanceBuilder()
 | 
					        self.gen: "ProblemInstanceBuilder" = ProblemInstanceBuilder()
 | 
				
			||||||
 | 
					        self.requirement_parser = RequirementParser(spack.config.CONFIG)
 | 
				
			||||||
        self.possible_virtuals: Set[str] = set()
 | 
					        self.possible_virtuals: Set[str] = set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assumptions: List[Tuple["clingo.Symbol", bool]] = []  # type: ignore[name-defined]
 | 
					        self.assumptions: List[Tuple["clingo.Symbol", bool]] = []  # type: ignore[name-defined]
 | 
				
			||||||
@@ -1332,8 +1310,7 @@ def compiler_facts(self):
 | 
				
			|||||||
            self.gen.newline()
 | 
					            self.gen.newline()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def package_requirement_rules(self, pkg):
 | 
					    def package_requirement_rules(self, pkg):
 | 
				
			||||||
        parser = RequirementParser(spack.config.CONFIG)
 | 
					        self.emit_facts_from_requirement_rules(self.requirement_parser.rules(pkg))
 | 
				
			||||||
        self.emit_facts_from_requirement_rules(parser.rules(pkg))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pkg_rules(self, pkg, tests):
 | 
					    def pkg_rules(self, pkg, tests):
 | 
				
			||||||
        pkg = self.pkg_class(pkg)
 | 
					        pkg = self.pkg_class(pkg)
 | 
				
			||||||
@@ -1811,9 +1788,8 @@ def provider_defaults(self):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def provider_requirements(self):
 | 
					    def provider_requirements(self):
 | 
				
			||||||
        self.gen.h2("Requirements on virtual providers")
 | 
					        self.gen.h2("Requirements on virtual providers")
 | 
				
			||||||
        parser = RequirementParser(spack.config.CONFIG)
 | 
					 | 
				
			||||||
        for virtual_str in sorted(self.possible_virtuals):
 | 
					        for virtual_str in sorted(self.possible_virtuals):
 | 
				
			||||||
            rules = parser.rules_from_virtual(virtual_str)
 | 
					            rules = self.requirement_parser.rules_from_virtual(virtual_str)
 | 
				
			||||||
            if rules:
 | 
					            if rules:
 | 
				
			||||||
                self.emit_facts_from_requirement_rules(rules)
 | 
					                self.emit_facts_from_requirement_rules(rules)
 | 
				
			||||||
                self.trigger_rules()
 | 
					                self.trigger_rules()
 | 
				
			||||||
@@ -3088,202 +3064,6 @@ def value(self) -> str:
 | 
				
			|||||||
        return "".join(self.asp_problem)
 | 
					        return "".join(self.asp_problem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_spec_from_yaml_string(string: str) -> "spack.spec.Spec":
 | 
					 | 
				
			||||||
    """Parse a spec from YAML and add file/line info to errors, if it's available.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Parse a ``Spec`` from the supplied string, but also intercept any syntax errors and
 | 
					 | 
				
			||||||
    add file/line information for debugging using file/line annotations from the string.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Arguments:
 | 
					 | 
				
			||||||
        string: a string representing a ``Spec`` from config YAML.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    """
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        return spack.spec.Spec(string)
 | 
					 | 
				
			||||||
    except SpecSyntaxError as e:
 | 
					 | 
				
			||||||
        mark = get_mark_from_yaml_data(string)
 | 
					 | 
				
			||||||
        if mark:
 | 
					 | 
				
			||||||
            msg = f"{mark.name}:{mark.line + 1}: {str(e)}"
 | 
					 | 
				
			||||||
            raise SpecSyntaxError(msg) from e
 | 
					 | 
				
			||||||
        raise e
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class RequirementParser:
 | 
					 | 
				
			||||||
    """Parses requirements from package.py files and configuration, and returns rules."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, configuration):
 | 
					 | 
				
			||||||
        self.config = configuration
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules(self, pkg: "spack.package_base.PackageBase") -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        result = []
 | 
					 | 
				
			||||||
        result.extend(self.rules_from_package_py(pkg))
 | 
					 | 
				
			||||||
        result.extend(self.rules_from_require(pkg))
 | 
					 | 
				
			||||||
        result.extend(self.rules_from_prefer(pkg))
 | 
					 | 
				
			||||||
        result.extend(self.rules_from_conflict(pkg))
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules_from_package_py(self, pkg) -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        rules = []
 | 
					 | 
				
			||||||
        for when_spec, requirement_list in pkg.requirements.items():
 | 
					 | 
				
			||||||
            for requirements, policy, message in requirement_list:
 | 
					 | 
				
			||||||
                rules.append(
 | 
					 | 
				
			||||||
                    RequirementRule(
 | 
					 | 
				
			||||||
                        pkg_name=pkg.name,
 | 
					 | 
				
			||||||
                        policy=policy,
 | 
					 | 
				
			||||||
                        requirements=requirements,
 | 
					 | 
				
			||||||
                        kind=RequirementKind.PACKAGE,
 | 
					 | 
				
			||||||
                        condition=when_spec,
 | 
					 | 
				
			||||||
                        message=message,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        return rules
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules_from_virtual(self, virtual_str: str) -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        requirements = self.config.get("packages", {}).get(virtual_str, {}).get("require", [])
 | 
					 | 
				
			||||||
        return self._rules_from_requirements(
 | 
					 | 
				
			||||||
            virtual_str, requirements, kind=RequirementKind.VIRTUAL
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules_from_require(self, pkg: "spack.package_base.PackageBase") -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        kind, requirements = self._raw_yaml_data(pkg, section="require")
 | 
					 | 
				
			||||||
        return self._rules_from_requirements(pkg.name, requirements, kind=kind)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules_from_prefer(self, pkg: "spack.package_base.PackageBase") -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        result = []
 | 
					 | 
				
			||||||
        kind, preferences = self._raw_yaml_data(pkg, section="prefer")
 | 
					 | 
				
			||||||
        for item in preferences:
 | 
					 | 
				
			||||||
            spec, condition, message = self._parse_prefer_conflict_item(item)
 | 
					 | 
				
			||||||
            result.append(
 | 
					 | 
				
			||||||
                # A strong preference is defined as:
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # require:
 | 
					 | 
				
			||||||
                # - any_of: [spec_str, "@:"]
 | 
					 | 
				
			||||||
                RequirementRule(
 | 
					 | 
				
			||||||
                    pkg_name=pkg.name,
 | 
					 | 
				
			||||||
                    policy="any_of",
 | 
					 | 
				
			||||||
                    requirements=[spec, spack.spec.Spec("@:")],
 | 
					 | 
				
			||||||
                    kind=kind,
 | 
					 | 
				
			||||||
                    message=message,
 | 
					 | 
				
			||||||
                    condition=condition,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def rules_from_conflict(self, pkg: "spack.package_base.PackageBase") -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        result = []
 | 
					 | 
				
			||||||
        kind, conflicts = self._raw_yaml_data(pkg, section="conflict")
 | 
					 | 
				
			||||||
        for item in conflicts:
 | 
					 | 
				
			||||||
            spec, condition, message = self._parse_prefer_conflict_item(item)
 | 
					 | 
				
			||||||
            result.append(
 | 
					 | 
				
			||||||
                # A conflict is defined as:
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # require:
 | 
					 | 
				
			||||||
                # - one_of: [spec_str, "@:"]
 | 
					 | 
				
			||||||
                RequirementRule(
 | 
					 | 
				
			||||||
                    pkg_name=pkg.name,
 | 
					 | 
				
			||||||
                    policy="one_of",
 | 
					 | 
				
			||||||
                    requirements=[spec, spack.spec.Spec("@:")],
 | 
					 | 
				
			||||||
                    kind=kind,
 | 
					 | 
				
			||||||
                    message=message,
 | 
					 | 
				
			||||||
                    condition=condition,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        return result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _parse_prefer_conflict_item(self, item):
 | 
					 | 
				
			||||||
        # The item is either a string or an object with at least a "spec" attribute
 | 
					 | 
				
			||||||
        if isinstance(item, str):
 | 
					 | 
				
			||||||
            spec = parse_spec_from_yaml_string(item)
 | 
					 | 
				
			||||||
            condition = spack.spec.Spec()
 | 
					 | 
				
			||||||
            message = None
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            spec = parse_spec_from_yaml_string(item["spec"])
 | 
					 | 
				
			||||||
            condition = spack.spec.Spec(item.get("when"))
 | 
					 | 
				
			||||||
            message = item.get("message")
 | 
					 | 
				
			||||||
        return spec, condition, message
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _raw_yaml_data(self, pkg: "spack.package_base.PackageBase", *, section: str):
 | 
					 | 
				
			||||||
        config = self.config.get("packages")
 | 
					 | 
				
			||||||
        data = config.get(pkg.name, {}).get(section, [])
 | 
					 | 
				
			||||||
        kind = RequirementKind.PACKAGE
 | 
					 | 
				
			||||||
        if not data:
 | 
					 | 
				
			||||||
            data = config.get("all", {}).get(section, [])
 | 
					 | 
				
			||||||
            kind = RequirementKind.DEFAULT
 | 
					 | 
				
			||||||
        return kind, data
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _rules_from_requirements(
 | 
					 | 
				
			||||||
        self, pkg_name: str, requirements, *, kind: RequirementKind
 | 
					 | 
				
			||||||
    ) -> List[RequirementRule]:
 | 
					 | 
				
			||||||
        """Manipulate requirements from packages.yaml, and return a list of tuples
 | 
					 | 
				
			||||||
        with a uniform structure (name, policy, requirements).
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        if isinstance(requirements, str):
 | 
					 | 
				
			||||||
            requirements = [requirements]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        rules = []
 | 
					 | 
				
			||||||
        for requirement in requirements:
 | 
					 | 
				
			||||||
            # A string is equivalent to a one_of group with a single element
 | 
					 | 
				
			||||||
            if isinstance(requirement, str):
 | 
					 | 
				
			||||||
                requirement = {"one_of": [requirement]}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for policy in ("spec", "one_of", "any_of"):
 | 
					 | 
				
			||||||
                if policy not in requirement:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                constraints = requirement[policy]
 | 
					 | 
				
			||||||
                # "spec" is for specifying a single spec
 | 
					 | 
				
			||||||
                if policy == "spec":
 | 
					 | 
				
			||||||
                    constraints = [constraints]
 | 
					 | 
				
			||||||
                    policy = "one_of"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # validate specs from YAML first, and fail with line numbers if parsing fails.
 | 
					 | 
				
			||||||
                constraints = [
 | 
					 | 
				
			||||||
                    parse_spec_from_yaml_string(constraint) for constraint in constraints
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                when_str = requirement.get("when")
 | 
					 | 
				
			||||||
                when = parse_spec_from_yaml_string(when_str) if when_str else spack.spec.Spec()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                constraints = [
 | 
					 | 
				
			||||||
                    x
 | 
					 | 
				
			||||||
                    for x in constraints
 | 
					 | 
				
			||||||
                    if not self.reject_requirement_constraint(pkg_name, constraint=x, kind=kind)
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
                if not constraints:
 | 
					 | 
				
			||||||
                    continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                rules.append(
 | 
					 | 
				
			||||||
                    RequirementRule(
 | 
					 | 
				
			||||||
                        pkg_name=pkg_name,
 | 
					 | 
				
			||||||
                        policy=policy,
 | 
					 | 
				
			||||||
                        requirements=constraints,
 | 
					 | 
				
			||||||
                        kind=kind,
 | 
					 | 
				
			||||||
                        message=requirement.get("message"),
 | 
					 | 
				
			||||||
                        condition=when,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
        return rules
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def reject_requirement_constraint(
 | 
					 | 
				
			||||||
        self, pkg_name: str, *, constraint: spack.spec.Spec, kind: RequirementKind
 | 
					 | 
				
			||||||
    ) -> bool:
 | 
					 | 
				
			||||||
        """Returns True if a requirement constraint should be rejected"""
 | 
					 | 
				
			||||||
        if kind == RequirementKind.DEFAULT:
 | 
					 | 
				
			||||||
            # Requirements under all: are applied only if they are satisfiable considering only
 | 
					 | 
				
			||||||
            # package rules, so e.g. variants must exist etc. Otherwise, they are rejected.
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                s = spack.spec.Spec(pkg_name)
 | 
					 | 
				
			||||||
                s.constrain(constraint)
 | 
					 | 
				
			||||||
                s.validate_or_raise()
 | 
					 | 
				
			||||||
            except spack.error.SpackError as e:
 | 
					 | 
				
			||||||
                tty.debug(
 | 
					 | 
				
			||||||
                    f"[SETUP] Rejecting the default '{constraint}' requirement "
 | 
					 | 
				
			||||||
                    f"on '{pkg_name}': {str(e)}",
 | 
					 | 
				
			||||||
                    level=2,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                return True
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class CompilerParser:
 | 
					class CompilerParser:
 | 
				
			||||||
    """Parses configuration files, and builds a list of possible compilers for the solve."""
 | 
					    """Parses configuration files, and builds a list of possible compilers for the solve."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1003,6 +1003,8 @@ variant_default_not_used(node(ID, Package), Variant, Value)
 | 
				
			|||||||
     node_has_variant(node(ID, Package), Variant, _),
 | 
					     node_has_variant(node(ID, Package), Variant, _),
 | 
				
			||||||
     not attr("variant_value", node(ID, Package), Variant, Value),
 | 
					     not attr("variant_value", node(ID, Package), Variant, Value),
 | 
				
			||||||
     not propagate(node(ID, Package), variant_value(Variant, _, _)),
 | 
					     not propagate(node(ID, Package), variant_value(Variant, _, _)),
 | 
				
			||||||
 | 
					     % variant set explicitly don't count for this metric
 | 
				
			||||||
 | 
					     not attr("variant_set", node(ID, Package), Variant, _),
 | 
				
			||||||
     attr("node", node(ID, Package)).
 | 
					     attr("node", node(ID, Package)).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
% The variant is set in an external spec
 | 
					% The variant is set in an external spec
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										232
									
								
								lib/spack/spack/solver/requirements.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								lib/spack/spack/solver/requirements.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,232 @@
 | 
				
			|||||||
 | 
					# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
 | 
				
			||||||
 | 
					# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
				
			||||||
 | 
					import enum
 | 
				
			||||||
 | 
					from typing import List, NamedTuple, Optional, Sequence
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from llnl.util import tty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import spack.config
 | 
				
			||||||
 | 
					import spack.error
 | 
				
			||||||
 | 
					import spack.package_base
 | 
				
			||||||
 | 
					import spack.spec
 | 
				
			||||||
 | 
					from spack.config import get_mark_from_yaml_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequirementKind(enum.Enum):
 | 
				
			||||||
 | 
					    """Purpose / provenance of a requirement"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #: Default requirement expressed under the 'all' attribute of packages.yaml
 | 
				
			||||||
 | 
					    DEFAULT = enum.auto()
 | 
				
			||||||
 | 
					    #: Requirement expressed on a virtual package
 | 
				
			||||||
 | 
					    VIRTUAL = enum.auto()
 | 
				
			||||||
 | 
					    #: Requirement expressed on a specific package
 | 
				
			||||||
 | 
					    PACKAGE = enum.auto()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequirementRule(NamedTuple):
 | 
				
			||||||
 | 
					    """Data class to collect information on a requirement"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pkg_name: str
 | 
				
			||||||
 | 
					    policy: str
 | 
				
			||||||
 | 
					    requirements: Sequence[spack.spec.Spec]
 | 
				
			||||||
 | 
					    condition: spack.spec.Spec
 | 
				
			||||||
 | 
					    kind: RequirementKind
 | 
				
			||||||
 | 
					    message: Optional[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RequirementParser:
 | 
				
			||||||
 | 
					    """Parses requirements from package.py files and configuration, and returns rules."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, configuration: spack.config.Configuration):
 | 
				
			||||||
 | 
					        self.config = configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        result = []
 | 
				
			||||||
 | 
					        result.extend(self.rules_from_package_py(pkg))
 | 
				
			||||||
 | 
					        result.extend(self.rules_from_require(pkg))
 | 
				
			||||||
 | 
					        result.extend(self.rules_from_prefer(pkg))
 | 
				
			||||||
 | 
					        result.extend(self.rules_from_conflict(pkg))
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules_from_package_py(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        rules = []
 | 
				
			||||||
 | 
					        for when_spec, requirement_list in pkg.requirements.items():
 | 
				
			||||||
 | 
					            for requirements, policy, message in requirement_list:
 | 
				
			||||||
 | 
					                rules.append(
 | 
				
			||||||
 | 
					                    RequirementRule(
 | 
				
			||||||
 | 
					                        pkg_name=pkg.name,
 | 
				
			||||||
 | 
					                        policy=policy,
 | 
				
			||||||
 | 
					                        requirements=requirements,
 | 
				
			||||||
 | 
					                        kind=RequirementKind.PACKAGE,
 | 
				
			||||||
 | 
					                        condition=when_spec,
 | 
				
			||||||
 | 
					                        message=message,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					        return rules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules_from_virtual(self, virtual_str: str) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        requirements = self.config.get("packages", {}).get(virtual_str, {}).get("require", [])
 | 
				
			||||||
 | 
					        return self._rules_from_requirements(
 | 
				
			||||||
 | 
					            virtual_str, requirements, kind=RequirementKind.VIRTUAL
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules_from_require(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        kind, requirements = self._raw_yaml_data(pkg, section="require")
 | 
				
			||||||
 | 
					        return self._rules_from_requirements(pkg.name, requirements, kind=kind)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules_from_prefer(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        result = []
 | 
				
			||||||
 | 
					        kind, preferences = self._raw_yaml_data(pkg, section="prefer")
 | 
				
			||||||
 | 
					        for item in preferences:
 | 
				
			||||||
 | 
					            spec, condition, message = self._parse_prefer_conflict_item(item)
 | 
				
			||||||
 | 
					            result.append(
 | 
				
			||||||
 | 
					                # A strong preference is defined as:
 | 
				
			||||||
 | 
					                #
 | 
				
			||||||
 | 
					                # require:
 | 
				
			||||||
 | 
					                # - any_of: [spec_str, "@:"]
 | 
				
			||||||
 | 
					                RequirementRule(
 | 
				
			||||||
 | 
					                    pkg_name=pkg.name,
 | 
				
			||||||
 | 
					                    policy="any_of",
 | 
				
			||||||
 | 
					                    requirements=[spec, spack.spec.Spec("@:")],
 | 
				
			||||||
 | 
					                    kind=kind,
 | 
				
			||||||
 | 
					                    message=message,
 | 
				
			||||||
 | 
					                    condition=condition,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def rules_from_conflict(self, pkg: spack.package_base.PackageBase) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        result = []
 | 
				
			||||||
 | 
					        kind, conflicts = self._raw_yaml_data(pkg, section="conflict")
 | 
				
			||||||
 | 
					        for item in conflicts:
 | 
				
			||||||
 | 
					            spec, condition, message = self._parse_prefer_conflict_item(item)
 | 
				
			||||||
 | 
					            result.append(
 | 
				
			||||||
 | 
					                # A conflict is defined as:
 | 
				
			||||||
 | 
					                #
 | 
				
			||||||
 | 
					                # require:
 | 
				
			||||||
 | 
					                # - one_of: [spec_str, "@:"]
 | 
				
			||||||
 | 
					                RequirementRule(
 | 
				
			||||||
 | 
					                    pkg_name=pkg.name,
 | 
				
			||||||
 | 
					                    policy="one_of",
 | 
				
			||||||
 | 
					                    requirements=[spec, spack.spec.Spec("@:")],
 | 
				
			||||||
 | 
					                    kind=kind,
 | 
				
			||||||
 | 
					                    message=message,
 | 
				
			||||||
 | 
					                    condition=condition,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _parse_prefer_conflict_item(self, item):
 | 
				
			||||||
 | 
					        # The item is either a string or an object with at least a "spec" attribute
 | 
				
			||||||
 | 
					        if isinstance(item, str):
 | 
				
			||||||
 | 
					            spec = parse_spec_from_yaml_string(item)
 | 
				
			||||||
 | 
					            condition = spack.spec.Spec()
 | 
				
			||||||
 | 
					            message = None
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            spec = parse_spec_from_yaml_string(item["spec"])
 | 
				
			||||||
 | 
					            condition = spack.spec.Spec(item.get("when"))
 | 
				
			||||||
 | 
					            message = item.get("message")
 | 
				
			||||||
 | 
					        return spec, condition, message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _raw_yaml_data(self, pkg: spack.package_base.PackageBase, *, section: str):
 | 
				
			||||||
 | 
					        config = self.config.get("packages")
 | 
				
			||||||
 | 
					        data = config.get(pkg.name, {}).get(section, [])
 | 
				
			||||||
 | 
					        kind = RequirementKind.PACKAGE
 | 
				
			||||||
 | 
					        if not data:
 | 
				
			||||||
 | 
					            data = config.get("all", {}).get(section, [])
 | 
				
			||||||
 | 
					            kind = RequirementKind.DEFAULT
 | 
				
			||||||
 | 
					        return kind, data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _rules_from_requirements(
 | 
				
			||||||
 | 
					        self, pkg_name: str, requirements, *, kind: RequirementKind
 | 
				
			||||||
 | 
					    ) -> List[RequirementRule]:
 | 
				
			||||||
 | 
					        """Manipulate requirements from packages.yaml, and return a list of tuples
 | 
				
			||||||
 | 
					        with a uniform structure (name, policy, requirements).
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        if isinstance(requirements, str):
 | 
				
			||||||
 | 
					            requirements = [requirements]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rules = []
 | 
				
			||||||
 | 
					        for requirement in requirements:
 | 
				
			||||||
 | 
					            # A string is equivalent to a one_of group with a single element
 | 
				
			||||||
 | 
					            if isinstance(requirement, str):
 | 
				
			||||||
 | 
					                requirement = {"one_of": [requirement]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for policy in ("spec", "one_of", "any_of"):
 | 
				
			||||||
 | 
					                if policy not in requirement:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                constraints = requirement[policy]
 | 
				
			||||||
 | 
					                # "spec" is for specifying a single spec
 | 
				
			||||||
 | 
					                if policy == "spec":
 | 
				
			||||||
 | 
					                    constraints = [constraints]
 | 
				
			||||||
 | 
					                    policy = "one_of"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # validate specs from YAML first, and fail with line numbers if parsing fails.
 | 
				
			||||||
 | 
					                constraints = [
 | 
				
			||||||
 | 
					                    parse_spec_from_yaml_string(constraint) for constraint in constraints
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					                when_str = requirement.get("when")
 | 
				
			||||||
 | 
					                when = parse_spec_from_yaml_string(when_str) if when_str else spack.spec.Spec()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                constraints = [
 | 
				
			||||||
 | 
					                    x
 | 
				
			||||||
 | 
					                    for x in constraints
 | 
				
			||||||
 | 
					                    if not self.reject_requirement_constraint(pkg_name, constraint=x, kind=kind)
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					                if not constraints:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                rules.append(
 | 
				
			||||||
 | 
					                    RequirementRule(
 | 
				
			||||||
 | 
					                        pkg_name=pkg_name,
 | 
				
			||||||
 | 
					                        policy=policy,
 | 
				
			||||||
 | 
					                        requirements=constraints,
 | 
				
			||||||
 | 
					                        kind=kind,
 | 
				
			||||||
 | 
					                        message=requirement.get("message"),
 | 
				
			||||||
 | 
					                        condition=when,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					        return rules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reject_requirement_constraint(
 | 
				
			||||||
 | 
					        self, pkg_name: str, *, constraint: spack.spec.Spec, kind: RequirementKind
 | 
				
			||||||
 | 
					    ) -> bool:
 | 
				
			||||||
 | 
					        """Returns True if a requirement constraint should be rejected"""
 | 
				
			||||||
 | 
					        if kind == RequirementKind.DEFAULT:
 | 
				
			||||||
 | 
					            # Requirements under all: are applied only if they are satisfiable considering only
 | 
				
			||||||
 | 
					            # package rules, so e.g. variants must exist etc. Otherwise, they are rejected.
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                s = spack.spec.Spec(pkg_name)
 | 
				
			||||||
 | 
					                s.constrain(constraint)
 | 
				
			||||||
 | 
					                s.validate_or_raise()
 | 
				
			||||||
 | 
					            except spack.error.SpackError as e:
 | 
				
			||||||
 | 
					                tty.debug(
 | 
				
			||||||
 | 
					                    f"[SETUP] Rejecting the default '{constraint}' requirement "
 | 
				
			||||||
 | 
					                    f"on '{pkg_name}': {str(e)}",
 | 
				
			||||||
 | 
					                    level=2,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def parse_spec_from_yaml_string(string: str) -> spack.spec.Spec:
 | 
				
			||||||
 | 
					    """Parse a spec from YAML and add file/line info to errors, if it's available.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Parse a ``Spec`` from the supplied string, but also intercept any syntax errors and
 | 
				
			||||||
 | 
					    add file/line information for debugging using file/line annotations from the string.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Arguments:
 | 
				
			||||||
 | 
					        string: a string representing a ``Spec`` from config YAML.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return spack.spec.Spec(string)
 | 
				
			||||||
 | 
					    except spack.error.SpecSyntaxError as e:
 | 
				
			||||||
 | 
					        mark = get_mark_from_yaml_data(string)
 | 
				
			||||||
 | 
					        if mark:
 | 
				
			||||||
 | 
					            msg = f"{mark.name}:{mark.line + 1}: {str(e)}"
 | 
				
			||||||
 | 
					            raise spack.error.SpecSyntaxError(msg) from e
 | 
				
			||||||
 | 
					        raise e
 | 
				
			||||||
		Reference in New Issue
	
	Block a user