summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPeter Scheibel <scheibel1@llnl.gov>2022-08-16 11:44:30 -0700
committerGitHub <noreply@github.com>2022-08-16 11:44:30 -0700
commit8281a0c5feabfc4fe180846d6fe95cfe53420bc5 (patch)
treeecf8d87450b065195cdc3a1eccabaeb92c1ca51a /lib
parent0d981a012d67e9532f42b6f5b0d0bb1e25ec3897 (diff)
downloadspack-8281a0c5feabfc4fe180846d6fe95cfe53420bc5.tar.gz
spack-8281a0c5feabfc4fe180846d6fe95cfe53420bc5.tar.bz2
spack-8281a0c5feabfc4fe180846d6fe95cfe53420bc5.tar.xz
spack-8281a0c5feabfc4fe180846d6fe95cfe53420bc5.zip
Configuration: allow users to enforce hard spec constraints (#27987)
Spack doesn't have an easy way to say something like "If I build package X, then I *need* version Y": * If you specify something on the command line, then you ensure that the constraints are applied, but the package is always built * Likewise if you `spack add X...`` to your environment, the constraints are guaranteed to hold, but the environment always builds the package * You can add preferences to packages.yaml, but these are not guaranteed to hold (Spack can choose other settings) This commit adds a 'require' subsection to packages.yaml: the specs added there are guaranteed to hold. The commit includes documentation for the feature. Co-authored-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/build_settings.rst66
-rw-r--r--lib/spack/spack/schema/packages.py23
-rw-r--r--lib/spack/spack/solver/asp.py26
-rw-r--r--lib/spack/spack/solver/concretize.lp178
-rw-r--r--lib/spack/spack/test/concretize_preferences.py14
-rw-r--r--lib/spack/spack/test/concretize_requirements.py299
-rw-r--r--lib/spack/spack/test/conftest.py14
7 files changed, 545 insertions, 75 deletions
diff --git a/lib/spack/docs/build_settings.rst b/lib/spack/docs/build_settings.rst
index 568b92aac3..c306536c51 100644
--- a/lib/spack/docs/build_settings.rst
+++ b/lib/spack/docs/build_settings.rst
@@ -339,6 +339,72 @@ concretization rules. A provider lists a value that packages may
``depend_on`` (e.g, MPI) and a list of rules for fulfilling that
dependency.
+.. _package-requirements:
+
+--------------------
+Package Requirements
+--------------------
+
+You can use the configuration to force the concretizer to choose
+specific properties for packages when building them. Like preferences,
+these are only applied when the package is required by some other
+request (e.g. if the package is needed as a dependency of a
+request to ``spack install``).
+
+An example of where this is useful is if you have a package that
+is normally built as a dependency but only under certain circumstances
+(e.g. only when a variant on a dependent is active): you can make
+sure that it always builds the way you want it to; this distinguishes
+package configuration requirements from constraints that you add to
+``spack install`` or to environments (in those cases, the associated
+packages are always built).
+
+The following is an example of how to enforce package properties in
+``packages.yaml``:
+
+.. code-block:: yaml
+
+ packages:
+ libfabric:
+ require: "@1.13.2"
+ openmpi:
+ require:
+ - any_of: ["~cuda", "gcc"]
+ mpich:
+ require:
+ - one_of: ["+cuda", "+rocm"]
+
+Requirements are expressed using Spec syntax (the same as what is provided
+to ``spack install``). In the simplest case, you can specify attributes
+that you always want the package to have by providing a single spec to
+``require``; in the above example, ``libfabric`` will always build
+with version 1.13.2.
+
+You can provide a more-relaxed constraint and allow the concretizer to
+choose between a set of options using ``any_of`` or ``one_of``:
+
+* ``any_of`` is a list of specs. One of those specs must be satisfied
+ and it is also allowed for the concretized spec to match more than one.
+ In the above example, that means you could build ``openmpi+cuda%gcc``,
+ ``openmpi~cuda%clang`` or ``openmpi~cuda%gcc`` (in the last case,
+ note that both specs in the ``any_of`` for ``openmpi`` are
+ satisfied).
+* ``one_of`` is also a list of specs, and the final concretized spec
+ must match exactly one of them. In the above example, that means
+ you could build ``mpich+cuda`` or ``mpich+rocm`` but not
+ ``mpich+cuda+rocm`` (note the current package definition for
+ ``mpich`` already includes a conflict, so this is redundant but
+ still demonstrates the concept).
+
+Other notes about ``requires``:
+
+* You can only specify requirements for specific packages: you cannot
+ add ``requires`` under ``all``.
+* You cannot specify requirements for virtual packages (e.g. you can
+ specify requirements for ``openmpi`` but not ``mpi``).
+* 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).
.. _package_permissions:
diff --git a/lib/spack/spack/schema/packages.py b/lib/spack/spack/schema/packages.py
index 87d489028e..92fe9e0ba8 100644
--- a/lib/spack/spack/schema/packages.py
+++ b/lib/spack/spack/schema/packages.py
@@ -21,6 +21,29 @@ properties = {
"default": {},
"additionalProperties": False,
"properties": {
+ "require": {
+ "oneOf": [
+ # 'require' can be a list of requirement_groups.
+ # each requirement group is a list of one or more
+ # specs. Either at least one or exactly one spec
+ # in the group must be satisfied (depending on
+ # whether you use "any_of" or "one_of",
+ # repectively)
+ {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "one_of": {"type": "array"},
+ "any_of": {"type": "array"},
+ },
+ },
+ },
+ # Shorthand for a single requirement group with
+ # one member
+ {"type": "string"},
+ ]
+ },
"version": {
"type": "array",
"default": [],
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index c28e6f459a..080e21ca5f 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -927,6 +927,30 @@ class SpackSolverSetup(object):
fn.node_compiler_preference(pkg.name, cspec.name, cspec.version, -i * 100)
)
+ def package_requirement_rules(self, pkg):
+ pkg_name = pkg.name
+ config = spack.config.get("packages")
+ requirements = config.get(pkg_name, {}).get("require", [])
+ if isinstance(requirements, string_types):
+ rules = [(pkg_name, "one_of", [requirements])]
+ else:
+ rules = []
+ for requirement in requirements:
+ for policy in ("one_of", "any_of"):
+ if policy in requirement:
+ rules.append((pkg_name, policy, requirement[policy]))
+
+ 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):
pkg = packagize(pkg)
@@ -1017,6 +1041,8 @@ class SpackSolverSetup(object):
lambda v, p, i: self.gen.fact(fn.pkg_provider_preference(pkg.name, v, p, i)),
)
+ self.package_requirement_rules(pkg)
+
def condition(self, required_spec, imposed_spec=None, name=None, msg=None):
"""Generate facts for a dependency or virtual provider condition.
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index 8ed6b1b6bb..3a497c1d3b 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -501,6 +501,49 @@ error(2, "Attempted to use external for '{0}' which does not satisfy any configu
#defined external_spec_condition/5.
%-----------------------------------------------------------------------------
+% Config required semantics
+%-----------------------------------------------------------------------------
+
+requirement_group_satisfied(Package, X) :-
+ 1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } 1,
+ node(Package),
+ requirement_policy(Package, X, "one_of"),
+ requirement_group(Package, X).
+
+requirement_weight(Package, W) :-
+ condition_holds(Y),
+ requirement_has_weight(Y, W),
+ requirement_group_member(Y, Package, X),
+ requirement_policy(Package, X, "one_of"),
+ requirement_group_satisfied(Package, X).
+
+requirement_group_satisfied(Package, X) :-
+ 1 { condition_holds(Y) : requirement_group_member(Y, Package, X) } ,
+ node(Package),
+ requirement_policy(Package, X, "any_of"),
+ requirement_group(Package, X).
+
+requirement_weight(Package, W) :-
+ W = #min {
+ Z : requirement_has_weight(Y, Z), condition_holds(Y), requirement_group_member(Y, Package, X);
+ % We need this to avoid an annoying warning during the solve
+ % concretize.lp:1151:5-11: info: tuple ignored:
+ % #sup@73
+ 10000
+ },
+ requirement_policy(Package, X, "any_of"),
+ requirement_group_satisfied(Package, X).
+
+error(2, "Cannot satisfy requirement group for package '{0}'", Package) :-
+ node(Package),
+ requirement_group(Package, X),
+ not requirement_group_satisfied(Package, X).
+
+#defined requirement_group/2.
+#defined requirement_group_member/3.
+#defined requirement_has_weight/2.
+
+%-----------------------------------------------------------------------------
% Variant semantics
%-----------------------------------------------------------------------------
% a variant is a variant of a package if it is a variant under some condition
@@ -898,8 +941,7 @@ error(2, "No valid version for '{0}' compiler '{1}' satisfies '@{2}'", Package,
% the compiler associated with the node satisfy the same constraint
node_compiler_version_satisfies(Package, Compiler, Constraint)
:- node_compiler_version(Package, Compiler, Version),
- compiler_version_satisfies(Compiler, Constraint, Version),
- build(Package).
+ compiler_version_satisfies(Compiler, Constraint, Version).
#defined compiler_version_satisfies/3.
@@ -1092,12 +1134,24 @@ opt_criterion(100, "number of packages to build (vs. reuse)").
#minimize { 1@100,Package : build(Package), optimize_for_reuse() }.
#defined optimize_for_reuse/0.
+% A condition group specifies one or more specs that must be satisfied.
+% Specs declared first are preferred, so we assign increasing weights and
+% minimize the weights.
+opt_criterion(75, "requirement weight").
+#minimize{ 0@275: #true }.
+#minimize{ 0@75: #true }.
+#minimize {
+ Weight@75+Priority
+ : requirement_weight(Package, Weight),
+ build_priority(Package, Priority)
+}.
+
% Minimize the number of deprecated versions being used
-opt_criterion(15, "deprecated versions used").
-#minimize{ 0@215: #true }.
-#minimize{ 0@15: #true }.
+opt_criterion(73, "deprecated versions used").
+#minimize{ 0@273: #true }.
+#minimize{ 0@73: #true }.
#minimize{
- 1@15+Priority,Package
+ 1@73+Priority,Package
: deprecated(Package, _),
build_priority(Package, Priority)
}.
@@ -1106,51 +1160,51 @@ opt_criterion(15, "deprecated versions used").
% 1. Version weight
% 2. Number of variants with a non default value, if not set
% for the root(Package)
-opt_criterion(14, "version weight").
-#minimize{ 0@214: #true }.
-#minimize{ 0@14: #true }.
+opt_criterion(70, "version weight").
+#minimize{ 0@270: #true }.
+#minimize{ 0@70: #true }.
#minimize {
- Weight@14+Priority
+ Weight@70+Priority
: root(Package),version_weight(Package, Weight),
build_priority(Package, Priority)
}.
-opt_criterion(13, "number of non-default variants (roots)").
-#minimize{ 0@213: #true }.
-#minimize{ 0@13: #true }.
+opt_criterion(65, "number of non-default variants (roots)").
+#minimize{ 0@265: #true }.
+#minimize{ 0@65: #true }.
#minimize {
- 1@13+Priority,Package,Variant,Value
+ 1@65+Priority,Package,Variant,Value
: variant_not_default(Package, Variant, Value),
root(Package),
build_priority(Package, Priority)
}.
-opt_criterion(12, "preferred providers for roots").
-#minimize{ 0@212 : #true }.
-#minimize{ 0@12: #true }.
+opt_criterion(60, "preferred providers for roots").
+#minimize{ 0@260: #true }.
+#minimize{ 0@60: #true }.
#minimize{
- Weight@12+Priority,Provider,Virtual
+ Weight@60+Priority,Provider,Virtual
: provider_weight(Provider, Virtual, Weight),
root(Provider),
build_priority(Provider, Priority)
}.
-opt_criterion(11, "default values of variants not being used (roots)").
-#minimize{ 0@211: #true }.
-#minimize{ 0@11: #true }.
+opt_criterion(55, "default values of variants not being used (roots)").
+#minimize{ 0@255: #true }.
+#minimize{ 0@55: #true }.
#minimize{
- 1@11+Priority,Package,Variant,Value
+ 1@55+Priority,Package,Variant,Value
: variant_default_not_used(Package, Variant, Value),
root(Package),
build_priority(Package, Priority)
}.
% Try to use default variants or variants that have been set
-opt_criterion(10, "number of non-default variants (non-roots)").
-#minimize{ 0@210: #true }.
-#minimize{ 0@10: #true }.
+opt_criterion(50, "number of non-default variants (non-roots)").
+#minimize{ 0@250: #true }.
+#minimize{ 0@50: #true }.
#minimize {
- 1@10+Priority,Package,Variant,Value
+ 1@50+Priority,Package,Variant,Value
: variant_not_default(Package, Variant, Value),
not root(Package),
build_priority(Package, Priority)
@@ -1158,91 +1212,91 @@ opt_criterion(10, "number of non-default variants (non-roots)").
% Minimize the weights of the providers, i.e. use as much as
% possible the most preferred providers
-opt_criterion(9, "preferred providers (non-roots)").
-#minimize{ 0@209: #true }.
-#minimize{ 0@9: #true }.
+opt_criterion(45, "preferred providers (non-roots)").
+#minimize{ 0@245: #true }.
+#minimize{ 0@45: #true }.
#minimize{
- Weight@9+Priority,Provider,Virtual
+ Weight@45+Priority,Provider,Virtual
: provider_weight(Provider, Virtual, Weight), not root(Provider),
build_priority(Provider, Priority)
}.
% Try to minimize the number of compiler mismatches in the DAG.
-opt_criterion(8, "compiler mismatches").
-#minimize{ 0@208: #true }.
-#minimize{ 0@8: #true }.
+opt_criterion(40, "compiler mismatches").
+#minimize{ 0@240: #true }.
+#minimize{ 0@40: #true }.
#minimize{
- 1@8+Priority,Package,Dependency
+ 1@40+Priority,Package,Dependency
: compiler_mismatch(Package, Dependency),
build_priority(Package, Priority)
}.
% Try to minimize the number of compiler mismatches in the DAG.
-opt_criterion(7, "OS mismatches").
-#minimize{ 0@207: #true }.
-#minimize{ 0@7: #true }.
+opt_criterion(35, "OS mismatches").
+#minimize{ 0@235: #true }.
+#minimize{ 0@35: #true }.
#minimize{
- 1@7+Priority,Package,Dependency
+ 1@35+Priority,Package,Dependency
: node_os_mismatch(Package, Dependency),
build_priority(Package, Priority)
}.
-opt_criterion(6, "non-preferred OS's").
-#minimize{ 0@206: #true }.
-#minimize{ 0@6: #true }.
+opt_criterion(30, "non-preferred OS's").
+#minimize{ 0@230: #true }.
+#minimize{ 0@30: #true }.
#minimize{
- Weight@6+Priority,Package
+ Weight@30+Priority,Package
: node_os_weight(Package, Weight),
build_priority(Package, Priority)
}.
% Choose more recent versions for nodes
-opt_criterion(5, "version badness").
-#minimize{ 0@205: #true }.
-#minimize{ 0@5: #true }.
+opt_criterion(25, "version badness").
+#minimize{ 0@225: #true }.
+#minimize{ 0@25: #true }.
#minimize{
- Weight@5+Priority,Package
+ Weight@25+Priority,Package
: version_weight(Package, Weight),
build_priority(Package, Priority)
}.
% Try to use all the default values of variants
-opt_criterion(4, "default values of variants not being used (non-roots)").
-#minimize{ 0@204: #true }.
-#minimize{ 0@4: #true }.
+opt_criterion(20, "default values of variants not being used (non-roots)").
+#minimize{ 0@220: #true }.
+#minimize{ 0@20: #true }.
#minimize{
- 1@4+Priority,Package,Variant,Value
+ 1@20+Priority,Package,Variant,Value
: variant_default_not_used(Package, Variant, Value),
not root(Package),
build_priority(Package, Priority)
}.
% Try to use preferred compilers
-opt_criterion(3, "non-preferred compilers").
-#minimize{ 0@203: #true }.
-#minimize{ 0@3: #true }.
+opt_criterion(15, "non-preferred compilers").
+#minimize{ 0@215: #true }.
+#minimize{ 0@15: #true }.
#minimize{
- Weight@3+Priority,Package
+ Weight@15+Priority,Package
: compiler_weight(Package, Weight),
build_priority(Package, Priority)
}.
% Minimize the number of mismatches for targets in the DAG, try
% to select the preferred target.
-opt_criterion(2, "target mismatches").
-#minimize{ 0@202: #true }.
-#minimize{ 0@2: #true }.
+opt_criterion(10, "target mismatches").
+#minimize{ 0@210: #true }.
+#minimize{ 0@10: #true }.
#minimize{
- 1@2+Priority,Package,Dependency
+ 1@10+Priority,Package,Dependency
: node_target_mismatch(Package, Dependency),
build_priority(Package, Priority)
}.
-opt_criterion(1, "non-preferred targets").
-#minimize{ 0@201: #true }.
-#minimize{ 0@1: #true }.
+opt_criterion(5, "non-preferred targets").
+#minimize{ 0@205: #true }.
+#minimize{ 0@5: #true }.
#minimize{
- Weight@1+Priority,Package
+ Weight@5+Priority,Package
: node_target_weight(Package, Weight),
build_priority(Package, Priority)
}.
diff --git a/lib/spack/spack/test/concretize_preferences.py b/lib/spack/spack/test/concretize_preferences.py
index 379e5447d0..9ad5a498ee 100644
--- a/lib/spack/spack/test/concretize_preferences.py
+++ b/lib/spack/spack/test/concretize_preferences.py
@@ -12,24 +12,12 @@ import spack.config
import spack.package_prefs
import spack.repo
import spack.util.spack_yaml as syaml
-from spack.config import ConfigError, ConfigScope
+from spack.config import ConfigError
from spack.spec import Spec
from spack.version import Version
@pytest.fixture()
-def concretize_scope(mutable_config, tmpdir):
- """Adds a scope for concretization preferences"""
- tmpdir.ensure_dir("concretize")
- mutable_config.push_scope(ConfigScope("concretize", str(tmpdir.join("concretize"))))
-
- yield
-
- mutable_config.pop_scope()
- spack.repo.path._provider_index = None
-
-
-@pytest.fixture()
def configure_permissions():
conf = syaml.load_config(
"""\
diff --git a/lib/spack/spack/test/concretize_requirements.py b/lib/spack/spack/test/concretize_requirements.py
new file mode 100644
index 0000000000..42d680dd72
--- /dev/null
+++ b/lib/spack/spack/test/concretize_requirements.py
@@ -0,0 +1,299 @@
+# Copyright 2013-2022 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 sys
+
+import pytest
+
+import spack.config
+import spack.repo
+import spack.util.spack_yaml as syaml
+from spack.solver.asp import UnsatisfiableSpecError
+from spack.spec import Spec
+
+pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Windows uses old concretizer")
+
+
+def update_packages_config(conf_str):
+ conf = syaml.load_config(conf_str)
+ spack.config.set("packages", conf["packages"], scope="concretize")
+
+
+_pkgx = (
+ "x",
+ """\
+class X(Package):
+ version('1.1')
+ version('1.0')
+ version('0.9')
+
+ variant('shared', default=True,
+ description='Build shared libraries')
+
+ depends_on('y')
+""",
+)
+
+
+_pkgy = (
+ "y",
+ """\
+class Y(Package):
+ version('2.5')
+ version('2.4')
+ version('2.3', deprecated=True)
+
+ variant('shared', default=True,
+ description='Build shared libraries')
+""",
+)
+
+
+_pkgv = (
+ "v",
+ """\
+class V(Package):
+ version('2.1')
+ version('2.0')
+""",
+)
+
+
+@pytest.fixture
+def create_test_repo(tmpdir, mutable_config):
+ repo_path = str(tmpdir)
+ repo_yaml = tmpdir.join("repo.yaml")
+ with open(str(repo_yaml), "w") as f:
+ f.write(
+ """\
+repo:
+ namespace: testcfgrequirements
+"""
+ )
+
+ packages_dir = tmpdir.join("packages")
+ for (pkg_name, pkg_str) in [_pkgx, _pkgy, _pkgv]:
+ pkg_dir = packages_dir.ensure(pkg_name, dir=True)
+ pkg_file = pkg_dir.join("package.py")
+ with open(str(pkg_file), "w") as f:
+ f.write(pkg_str)
+
+ yield spack.repo.Repo(repo_path)
+
+
+@pytest.fixture
+def test_repo(create_test_repo, monkeypatch, mock_stage):
+ with spack.repo.use_repositories(create_test_repo) as mock_repo_path:
+ yield mock_repo_path
+
+
+class MakeStage(object):
+ def __init__(self, stage):
+ self.stage = stage
+
+ def __call__(self, *args, **kwargs):
+ return self.stage
+
+
+@pytest.fixture
+def fake_installs(monkeypatch, tmpdir):
+ stage_path = str(tmpdir.ensure("fake-stage", dir=True))
+ universal_unused_stage = spack.stage.DIYStage(stage_path)
+ monkeypatch.setattr(
+ spack.package_base.Package, "_make_stage", MakeStage(universal_unused_stage)
+ )
+
+
+def test_requirement_isnt_optional(concretize_scope, test_repo):
+ """If a user spec requests something that directly conflicts
+ with a requirement, make sure we get an error.
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ x:
+ require: "@1.0"
+"""
+ update_packages_config(conf_str)
+ with pytest.raises(UnsatisfiableSpecError):
+ Spec("x@1.1").concretize()
+
+
+def test_requirement_is_successfully_applied(concretize_scope, test_repo):
+ """If a simple requirement can be satisfied, make sure the
+ concretization succeeds and the requirement spec is applied.
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ s1 = Spec("x").concretized()
+ # Without any requirements/preferences, the later version is preferred
+ assert s1.satisfies("@1.1")
+
+ conf_str = """\
+packages:
+ x:
+ require: "@1.0"
+"""
+ update_packages_config(conf_str)
+ s2 = Spec("x").concretized()
+ # The requirement forces choosing the eariler version
+ assert s2.satisfies("@1.0")
+
+
+def test_multiple_packages_requirements_are_respected(concretize_scope, test_repo):
+ """Apply requirements to two packages; make sure the concretization
+ succeeds and both requirements are respected.
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ x:
+ require: "@1.0"
+ y:
+ require: "@2.4"
+"""
+ update_packages_config(conf_str)
+ spec = Spec("x").concretized()
+ assert spec["x"].satisfies("@1.0")
+ assert spec["y"].satisfies("@2.4")
+
+
+def test_oneof(concretize_scope, test_repo):
+ """'one_of' allows forcing the concretizer to satisfy one of
+ the specs in the group (but not all have to be satisfied).
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ y:
+ require:
+ - one_of: ["@2.4", "~shared"]
+"""
+ update_packages_config(conf_str)
+ spec = Spec("x").concretized()
+ # The concretizer only has to satisfy one of @2.4/~shared, and @2.4
+ # comes first so it is prioritized
+ assert spec["y"].satisfies("@2.4+shared")
+
+
+def test_one_package_multiple_oneof_groups(concretize_scope, test_repo):
+ """One package has two 'one_of' groups; check that both are
+ applied.
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ y:
+ require:
+ - one_of: ["@2.4%gcc", "@2.5%clang"]
+ - one_of: ["@2.5~shared", "@2.4+shared"]
+"""
+ update_packages_config(conf_str)
+
+ s1 = Spec("y@2.5").concretized()
+ assert s1.satisfies("%clang~shared")
+
+ s2 = Spec("y@2.4").concretized()
+ assert s2.satisfies("%gcc+shared")
+
+
+def test_requirements_for_package_that_is_not_needed(concretize_scope, test_repo):
+ """Specify requirements for specs that are not concretized or
+ a dependency of a concretized spec (in other words, none of
+ the requirements are used for the requested spec).
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ # Note that the exact contents aren't important since this isn't
+ # intended to be used, but the important thing is that a number of
+ # packages have requirements applied
+ conf_str = """\
+packages:
+ x:
+ require: "@1.0"
+ y:
+ require:
+ - one_of: ["@2.4%gcc", "@2.5%clang"]
+ - one_of: ["@2.5~shared", "@2.4+shared"]
+"""
+ update_packages_config(conf_str)
+
+ s1 = Spec("v").concretized()
+ assert s1.satisfies("@2.1")
+
+
+def test_oneof_ordering(concretize_scope, test_repo):
+ """Ensure that earlier elements of 'one_of' have higher priority.
+ This priority should override default priority (e.g. choosing
+ later versions).
+ """
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ y:
+ require:
+ - one_of: ["@2.4", "@2.5"]
+"""
+ update_packages_config(conf_str)
+
+ s1 = Spec("y").concretized()
+ assert s1.satisfies("@2.4")
+
+ s2 = Spec("y@2.5").concretized()
+ assert s2.satisfies("@2.5")
+
+
+def test_reuse_oneof(concretize_scope, create_test_repo, mutable_database, fake_installs):
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ conf_str = """\
+packages:
+ y:
+ require:
+ - one_of: ["@2.5", "%gcc"]
+"""
+
+ with spack.repo.use_repositories(create_test_repo):
+ s1 = Spec("y@2.5%gcc").concretized()
+ s1.package.do_install(fake=True, explicit=True)
+
+ update_packages_config(conf_str)
+
+ with spack.config.override("concretizer:reuse", True):
+ s2 = Spec("y").concretized()
+ assert not s2.satisfies("@2.5 %gcc")
+
+
+def test_requirements_are_higher_priority_than_deprecation(concretize_scope, test_repo):
+ """Test that users can override a deprecated version with a requirement."""
+ if spack.config.get("config:concretizer") == "original":
+ pytest.skip("Original concretizer does not support configuration" " requirements")
+
+ # @2.3 is a deprecated versions. Ensure that any_of picks both constraints,
+ # since they are possible
+ conf_str = """\
+packages:
+ y:
+ require:
+ - any_of: ["@2.3", "%gcc"]
+"""
+ update_packages_config(conf_str)
+
+ s1 = Spec("y").concretized()
+ assert s1.satisfies("@2.3")
+ assert s1.satisfies("%gcc")
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index 3440351e93..1bf7af5f3b 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -706,6 +706,20 @@ def mutable_empty_config(tmpdir_factory, configuration_dir):
yield cfg
+@pytest.fixture(scope="function")
+def concretize_scope(mutable_config, tmpdir):
+ """Adds a scope for concretization preferences"""
+ tmpdir.ensure_dir("concretize")
+ mutable_config.push_scope(
+ spack.config.ConfigScope("concretize", str(tmpdir.join("concretize")))
+ )
+
+ yield
+
+ mutable_config.pop_scope()
+ spack.repo.path._provider_index = None
+
+
@pytest.fixture
def no_compilers_yaml(mutable_config):
"""Creates a temporary configuration without compilers.yaml"""