Configuration: Allow requirements for virtual packages (#32369)
Extend the semantics of package requirements to allow using them also under a virtual package attribute in packages.yaml These requirements are enforced whenever that virtual spec is present in the DAG.
This commit is contained in:
committed by
GitHub
parent
eb1c9c1583
commit
51244abee9
@@ -396,6 +396,16 @@ choose between a set of options using ``any_of`` or ``one_of``:
|
|||||||
``mpich`` already includes a conflict, so this is redundant but
|
``mpich`` already includes a conflict, so this is redundant but
|
||||||
still demonstrates the concept).
|
still demonstrates the concept).
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
For ``any_of`` and ``one_of``, the order of specs indicates a
|
||||||
|
preference: items that appear earlier in the list are preferred
|
||||||
|
(note that these preferences can be ignored in favor of others).
|
||||||
|
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Setting default requirements
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
You can also set default requirements for all packages under ``all``
|
You can also set default requirements for all packages under ``all``
|
||||||
like this:
|
like this:
|
||||||
|
|
||||||
@@ -422,13 +432,33 @@ under ``all`` are disregarded. For example, with a configuration like this:
|
|||||||
Spack requires ``cmake`` to use ``gcc`` and all other nodes (including cmake dependencies)
|
Spack requires ``cmake`` to use ``gcc`` and all other nodes (including cmake dependencies)
|
||||||
to use ``clang``.
|
to use ``clang``.
|
||||||
|
|
||||||
Other notes about ``requires``:
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Setting requirements on virtual specs
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
* You cannot specify requirements for virtual packages (e.g. you can
|
A requirement on a virtual spec applies whenever that virtual is present in the DAG. This
|
||||||
specify requirements for ``openmpi`` but not ``mpi``).
|
can be useful for fixing which virtual provider you want to use:
|
||||||
* For ``any_of`` and ``one_of``, the order of specs indicates a
|
|
||||||
preference: items that appear earlier in the list are preferred
|
.. code-block:: yaml
|
||||||
(note that these preferences can be ignored in favor of others).
|
|
||||||
|
packages:
|
||||||
|
mpi:
|
||||||
|
require: 'mvapich2 %gcc'
|
||||||
|
|
||||||
|
With the configuration above the only allowed ``mpi`` provider is ``mvapich2 %gcc``.
|
||||||
|
|
||||||
|
Requirements on the virtual spec and on the specific provider are both applied, if present. For
|
||||||
|
instance with a configuration like:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
packages:
|
||||||
|
mpi:
|
||||||
|
require: 'mvapich2 %gcc'
|
||||||
|
mvapich2:
|
||||||
|
require: '~cuda'
|
||||||
|
|
||||||
|
you will use ``mvapich2~cuda %gcc`` as an ``mpi`` provider.
|
||||||
|
|
||||||
.. _package_permissions:
|
.. _package_permissions:
|
||||||
|
|
||||||
|
|||||||
@@ -945,6 +945,13 @@ def package_requirement_rules(self, pkg):
|
|||||||
requirements = config.get(pkg_name, {}).get("require", []) or config.get("all", {}).get(
|
requirements = config.get(pkg_name, {}).get("require", []) or config.get("all", {}).get(
|
||||||
"require", []
|
"require", []
|
||||||
)
|
)
|
||||||
|
rules = self._rules_from_requirements(pkg_name, requirements)
|
||||||
|
self.emit_facts_from_requirement_rules(rules, virtual=False)
|
||||||
|
|
||||||
|
def _rules_from_requirements(self, pkg_name, requirements):
|
||||||
|
"""Manipulate requirements from packages.yaml, and return a list of tuples
|
||||||
|
with a uniform structure (name, policy, requirements).
|
||||||
|
"""
|
||||||
if isinstance(requirements, string_types):
|
if isinstance(requirements, string_types):
|
||||||
rules = [(pkg_name, "one_of", [requirements])]
|
rules = [(pkg_name, "one_of", [requirements])]
|
||||||
else:
|
else:
|
||||||
@@ -953,17 +960,7 @@ def package_requirement_rules(self, pkg):
|
|||||||
for policy in ("one_of", "any_of"):
|
for policy in ("one_of", "any_of"):
|
||||||
if policy in requirement:
|
if policy in requirement:
|
||||||
rules.append((pkg_name, policy, requirement[policy]))
|
rules.append((pkg_name, policy, requirement[policy]))
|
||||||
|
return rules
|
||||||
for requirement_grp_id, (pkg_name, policy, requirement_grp) in enumerate(rules):
|
|
||||||
self.gen.fact(fn.requirement_group(pkg_name, requirement_grp_id))
|
|
||||||
self.gen.fact(fn.requirement_policy(pkg_name, requirement_grp_id, policy))
|
|
||||||
for requirement_weight, spec_str in enumerate(requirement_grp):
|
|
||||||
spec = spack.spec.Spec(spec_str)
|
|
||||||
if not spec.name:
|
|
||||||
spec.name = pkg_name
|
|
||||||
member_id = self.condition(spec, imposed_spec=spec, name=pkg_name)
|
|
||||||
self.gen.fact(fn.requirement_group_member(member_id, pkg_name, requirement_grp_id))
|
|
||||||
self.gen.fact(fn.requirement_has_weight(member_id, requirement_weight))
|
|
||||||
|
|
||||||
def pkg_rules(self, pkg, tests):
|
def pkg_rules(self, pkg, tests):
|
||||||
pkg = packagize(pkg)
|
pkg = packagize(pkg)
|
||||||
@@ -1057,7 +1054,7 @@ def pkg_rules(self, pkg, tests):
|
|||||||
|
|
||||||
self.package_requirement_rules(pkg)
|
self.package_requirement_rules(pkg)
|
||||||
|
|
||||||
def condition(self, required_spec, imposed_spec=None, name=None, msg=None):
|
def condition(self, required_spec, imposed_spec=None, name=None, msg=None, node=False):
|
||||||
"""Generate facts for a dependency or virtual provider condition.
|
"""Generate facts for a dependency or virtual provider condition.
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
@@ -1067,6 +1064,8 @@ def condition(self, required_spec, imposed_spec=None, name=None, msg=None):
|
|||||||
name (str or None): name for `required_spec` (required if
|
name (str or None): name for `required_spec` (required if
|
||||||
required_spec is anonymous, ignored if not)
|
required_spec is anonymous, ignored if not)
|
||||||
msg (str or None): description of the condition
|
msg (str or None): description of the condition
|
||||||
|
node (bool): if False does not emit "node" or "virtual_node" requirements
|
||||||
|
from the imposed spec
|
||||||
Returns:
|
Returns:
|
||||||
int: id of the condition created by this function
|
int: id of the condition created by this function
|
||||||
"""
|
"""
|
||||||
@@ -1083,7 +1082,7 @@ def condition(self, required_spec, imposed_spec=None, name=None, msg=None):
|
|||||||
self.gen.fact(fn.condition_requirement(condition_id, pred.name, *pred.args))
|
self.gen.fact(fn.condition_requirement(condition_id, pred.name, *pred.args))
|
||||||
|
|
||||||
if imposed_spec:
|
if imposed_spec:
|
||||||
self.impose(condition_id, imposed_spec, node=False, name=name)
|
self.impose(condition_id, imposed_spec, node=node, name=name)
|
||||||
|
|
||||||
return condition_id
|
return condition_id
|
||||||
|
|
||||||
@@ -1161,6 +1160,37 @@ def provider_defaults(self):
|
|||||||
lambda v, p, i: self.gen.fact(fn.default_provider_preference(v, p, i)),
|
lambda v, p, i: self.gen.fact(fn.default_provider_preference(v, p, i)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def provider_requirements(self):
|
||||||
|
self.gen.h2("Requirements on virtual providers")
|
||||||
|
msg = (
|
||||||
|
"Internal Error: possible_virtuals is not populated. Please report to the spack"
|
||||||
|
" maintainers"
|
||||||
|
)
|
||||||
|
packages_yaml = spack.config.config.get("packages")
|
||||||
|
assert self.possible_virtuals is not None, msg
|
||||||
|
for virtual_str in sorted(self.possible_virtuals):
|
||||||
|
requirements = packages_yaml.get(virtual_str, {}).get("require", [])
|
||||||
|
rules = self._rules_from_requirements(virtual_str, requirements)
|
||||||
|
self.emit_facts_from_requirement_rules(rules, virtual=True)
|
||||||
|
|
||||||
|
def emit_facts_from_requirement_rules(self, rules, virtual=False):
|
||||||
|
"""Generate facts to enforce requirements from packages.yaml."""
|
||||||
|
for requirement_grp_id, (pkg_name, policy, requirement_grp) in enumerate(rules):
|
||||||
|
self.gen.fact(fn.requirement_group(pkg_name, requirement_grp_id))
|
||||||
|
self.gen.fact(fn.requirement_policy(pkg_name, requirement_grp_id, policy))
|
||||||
|
for requirement_weight, spec_str in enumerate(requirement_grp):
|
||||||
|
spec = spack.spec.Spec(spec_str)
|
||||||
|
if not spec.name:
|
||||||
|
spec.name = pkg_name
|
||||||
|
when_spec = spec
|
||||||
|
if virtual:
|
||||||
|
when_spec = spack.spec.Spec(pkg_name)
|
||||||
|
member_id = self.condition(
|
||||||
|
required_spec=when_spec, imposed_spec=spec, name=pkg_name, node=virtual
|
||||||
|
)
|
||||||
|
self.gen.fact(fn.requirement_group_member(member_id, pkg_name, requirement_grp_id))
|
||||||
|
self.gen.fact(fn.requirement_has_weight(member_id, requirement_weight))
|
||||||
|
|
||||||
def external_packages(self):
|
def external_packages(self):
|
||||||
"""Facts on external packages, as read from packages.yaml"""
|
"""Facts on external packages, as read from packages.yaml"""
|
||||||
# Read packages.yaml and normalize it, so that it
|
# Read packages.yaml and normalize it, so that it
|
||||||
@@ -1930,6 +1960,7 @@ def setup(self, driver, specs, reuse=None):
|
|||||||
|
|
||||||
self.virtual_providers()
|
self.virtual_providers()
|
||||||
self.provider_defaults()
|
self.provider_defaults()
|
||||||
|
self.provider_requirements()
|
||||||
self.external_packages()
|
self.external_packages()
|
||||||
self.flag_defaults()
|
self.flag_defaults()
|
||||||
|
|
||||||
|
|||||||
@@ -504,9 +504,12 @@ error(2, "Attempted to use external for '{0}' which does not satisfy any configu
|
|||||||
% Config required semantics
|
% Config required semantics
|
||||||
%-----------------------------------------------------------------------------
|
%-----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
activate_requirement_rules(Package) :- node(Package).
|
||||||
|
activate_requirement_rules(Package) :- virtual_node(Package).
|
||||||
|
|
||||||
requirement_group_satisfied(Package, X) :-
|
requirement_group_satisfied(Package, X) :-
|
||||||
1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } 1,
|
1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } 1,
|
||||||
node(Package),
|
activate_requirement_rules(Package),
|
||||||
requirement_policy(Package, X, "one_of"),
|
requirement_policy(Package, X, "one_of"),
|
||||||
requirement_group(Package, X).
|
requirement_group(Package, X).
|
||||||
|
|
||||||
@@ -519,7 +522,7 @@ requirement_weight(Package, W) :-
|
|||||||
|
|
||||||
requirement_group_satisfied(Package, X) :-
|
requirement_group_satisfied(Package, X) :-
|
||||||
1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } ,
|
1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } ,
|
||||||
node(Package),
|
activate_requirement_rules(Package),
|
||||||
requirement_policy(Package, X, "any_of"),
|
requirement_policy(Package, X, "any_of"),
|
||||||
requirement_group(Package, X).
|
requirement_group(Package, X).
|
||||||
|
|
||||||
@@ -535,7 +538,7 @@ requirement_weight(Package, W) :-
|
|||||||
requirement_group_satisfied(Package, X).
|
requirement_group_satisfied(Package, X).
|
||||||
|
|
||||||
error(2, "Cannot satisfy requirement group for package '{0}'", Package) :-
|
error(2, "Cannot satisfy requirement group for package '{0}'", Package) :-
|
||||||
node(Package),
|
activate_requirement_rules(Package),
|
||||||
requirement_group(Package, X),
|
requirement_group(Package, X),
|
||||||
not requirement_group_satisfied(Package, X).
|
not requirement_group_satisfied(Package, X).
|
||||||
|
|
||||||
|
|||||||
@@ -349,3 +349,66 @@ def test_default_and_package_specific_requirements(
|
|||||||
assert spec.satisfies(specific_exp)
|
assert spec.satisfies(specific_exp)
|
||||||
for s in spec.traverse(root=False):
|
for s in spec.traverse(root=False):
|
||||||
assert s.satisfies(generic_exp)
|
assert s.satisfies(generic_exp)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mpi_requirement", ["mpich", "mpich2", "zmpi"])
|
||||||
|
def test_requirements_on_virtual(mpi_requirement, concretize_scope, mock_packages):
|
||||||
|
if spack.config.get("config:concretizer") == "original":
|
||||||
|
pytest.skip("Original concretizer does not support configuration" " requirements")
|
||||||
|
conf_str = """\
|
||||||
|
packages:
|
||||||
|
mpi:
|
||||||
|
require: "{}"
|
||||||
|
""".format(
|
||||||
|
mpi_requirement
|
||||||
|
)
|
||||||
|
update_packages_config(conf_str)
|
||||||
|
|
||||||
|
spec = Spec("callpath").concretized()
|
||||||
|
assert "mpi" in spec
|
||||||
|
assert mpi_requirement in spec
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"mpi_requirement,specific_requirement",
|
||||||
|
[
|
||||||
|
("mpich", "@3.0.3"),
|
||||||
|
("mpich2", "%clang"),
|
||||||
|
("zmpi", "%gcc"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_requirements_on_virtual_and_on_package(
|
||||||
|
mpi_requirement, specific_requirement, concretize_scope, mock_packages
|
||||||
|
):
|
||||||
|
if spack.config.get("config:concretizer") == "original":
|
||||||
|
pytest.skip("Original concretizer does not support configuration" " requirements")
|
||||||
|
conf_str = """\
|
||||||
|
packages:
|
||||||
|
mpi:
|
||||||
|
require: "{0}"
|
||||||
|
{0}:
|
||||||
|
require: "{1}"
|
||||||
|
""".format(
|
||||||
|
mpi_requirement, specific_requirement
|
||||||
|
)
|
||||||
|
update_packages_config(conf_str)
|
||||||
|
|
||||||
|
spec = Spec("callpath").concretized()
|
||||||
|
assert "mpi" in spec
|
||||||
|
assert mpi_requirement in spec
|
||||||
|
assert spec["mpi"].satisfies(specific_requirement)
|
||||||
|
|
||||||
|
|
||||||
|
def test_incompatible_virtual_requirements_raise(concretize_scope, mock_packages):
|
||||||
|
if spack.config.get("config:concretizer") == "original":
|
||||||
|
pytest.skip("Original concretizer does not support configuration" " requirements")
|
||||||
|
conf_str = """\
|
||||||
|
packages:
|
||||||
|
mpi:
|
||||||
|
require: "mpich"
|
||||||
|
"""
|
||||||
|
update_packages_config(conf_str)
|
||||||
|
|
||||||
|
spec = Spec("callpath ^zmpi")
|
||||||
|
with pytest.raises(UnsatisfiableSpecError):
|
||||||
|
spec.concretize()
|
||||||
|
|||||||
Reference in New Issue
Block a user