diff options
author | Massimiliano Culpo <massimiliano.culpo@gmail.com> | 2024-12-05 18:10:06 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-05 18:10:06 +0100 |
commit | c1b2ac549d8ba6832e40146e866ab3298c74d579 (patch) | |
tree | 0dfbd937cd31c76e56009749570f3781e02be59c | |
parent | 4693b323ac591714dc0651711019fb94ce4aaf44 (diff) | |
download | spack-c1b2ac549d8ba6832e40146e866ab3298c74d579.tar.gz spack-c1b2ac549d8ba6832e40146e866ab3298c74d579.tar.bz2 spack-c1b2ac549d8ba6832e40146e866ab3298c74d579.tar.xz spack-c1b2ac549d8ba6832e40146e866ab3298c74d579.zip |
solver: partition classes related to requirement parsing into their own file (#47915)
-rw-r--r-- | lib/spack/spack/solver/asp.py | 228 | ||||
-rw-r--r-- | lib/spack/spack/solver/concretize.lp | 2 | ||||
-rw-r--r-- | lib/spack/spack/solver/requirements.py | 232 |
3 files changed, 238 insertions, 224 deletions
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index a57e1183b4..436776e272 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -48,8 +48,6 @@ import spack.variant as vt import spack.version as vn import spack.version.git_ref_lookup from spack import traverse -from spack.config import get_mark_from_yaml_data -from spack.error import SpecSyntaxError from .core import ( AspFunction, @@ -65,6 +63,7 @@ from .core import ( parse_term, ) from .counter import FullDuplicatesCounter, MinimalDuplicatesCounter, NoDuplicatesCounter +from .requirements import RequirementKind, RequirementParser, RequirementRule from .version_order import concretization_version_order GitOrStandardVersion = Union[spack.version.GitVersion, spack.version.StandardVersion] @@ -144,17 +143,6 @@ def named_spec( 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): """Data class to contain information on declared versions used in the solve""" @@ -757,17 +745,6 @@ class ErrorHandler: 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): """Data class to collect information on compilers""" @@ -1146,6 +1123,7 @@ class SpackSolverSetup: def __init__(self, tests: bool = False): # these are all initialized in setup() self.gen: "ProblemInstanceBuilder" = ProblemInstanceBuilder() + self.requirement_parser = RequirementParser(spack.config.CONFIG) self.possible_virtuals: Set[str] = set() self.assumptions: List[Tuple["clingo.Symbol", bool]] = [] # type: ignore[name-defined] @@ -1332,8 +1310,7 @@ class SpackSolverSetup: self.gen.newline() def package_requirement_rules(self, pkg): - parser = RequirementParser(spack.config.CONFIG) - self.emit_facts_from_requirement_rules(parser.rules(pkg)) + self.emit_facts_from_requirement_rules(self.requirement_parser.rules(pkg)) def pkg_rules(self, pkg, tests): pkg = self.pkg_class(pkg) @@ -1811,9 +1788,8 @@ class SpackSolverSetup: def provider_requirements(self): self.gen.h2("Requirements on virtual providers") - parser = RequirementParser(spack.config.CONFIG) 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: self.emit_facts_from_requirement_rules(rules) self.trigger_rules() @@ -3088,202 +3064,6 @@ class ProblemInstanceBuilder: 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: """Parses configuration files, and builds a list of possible compilers for the solve.""" diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp index 9bb237754e..ebcffa784d 100644 --- a/lib/spack/spack/solver/concretize.lp +++ b/lib/spack/spack/solver/concretize.lp @@ -1003,6 +1003,8 @@ variant_default_not_used(node(ID, Package), Variant, Value) node_has_variant(node(ID, Package), Variant, _), not attr("variant_value", node(ID, Package), Variant, Value), 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)). % The variant is set in an external spec diff --git a/lib/spack/spack/solver/requirements.py b/lib/spack/spack/solver/requirements.py new file mode 100644 index 0000000000..412f4c22cd --- /dev/null +++ b/lib/spack/spack/solver/requirements.py @@ -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 |