summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/packaging_guide.rst168
-rw-r--r--lib/spack/spack/build_systems/autotools.py10
-rw-r--r--lib/spack/spack/build_systems/cuda.py12
-rw-r--r--lib/spack/spack/directives.py61
-rw-r--r--lib/spack/spack/pkgkit.py3
-rw-r--r--lib/spack/spack/test/build_systems.py21
-rw-r--r--lib/spack/spack/test/spec_semantics.py52
-rw-r--r--lib/spack/spack/test/variant.py59
-rw-r--r--lib/spack/spack/variant.py179
9 files changed, 546 insertions, 19 deletions
diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst
index 1c78a396e3..641bb74fea 100644
--- a/lib/spack/docs/packaging_guide.rst
+++ b/lib/spack/docs/packaging_guide.rst
@@ -1077,6 +1077,174 @@ Go cannot be used to fetch a particular commit or branch, it always
downloads the head of the repository. This download method is untrusted,
and is not recommended. Use another fetch strategy whenever possible.
+--------
+Variants
+--------
+
+Many software packages can be configured to enable optional
+features, which often come at the expense of additional dependencies or
+longer build-times. To be flexible enough and support a wide variety of
+use cases, Spack permits to expose to the end-user the ability to choose
+which features should be activated in a package at the time it is installed.
+The mechanism to be employed is the :py:func:`spack.directives.variant` directive.
+
+^^^^^^^^^^^^^^^^
+Boolean variants
+^^^^^^^^^^^^^^^^
+
+In their simplest form variants are boolean options specified at the package
+level:
+
+ .. code-block:: python
+
+ class Hdf5(AutotoolsPackage):
+ ...
+ variant(
+ 'shared', default=True, description='Builds a shared version of the library'
+ )
+
+with a default value and a description of their meaning / use in the package.
+*Variants can be tested in any context where a spec constraint is expected.*
+In the example above the ``shared`` variant is tied to the build of shared dynamic
+libraries. To pass the right option at configure time we can branch depending on
+its value:
+
+ .. code-block:: python
+
+ def configure_args(self):
+ ...
+ if '+shared' in self.spec:
+ extra_args.append('--enable-shared')
+ else:
+ extra_args.append('--disable-shared')
+ extra_args.append('--enable-static-exec')
+
+As explained in :ref:`basic-variants` the constraint ``+shared`` means
+that the boolean variant is set to ``True``, while ``~shared`` means it is set
+to ``False``.
+Another common example is the optional activation of an extra dependency
+which requires to use the variant in the ``when`` argument of
+:py:func:`spack.directives.depends_on`:
+
+ .. code-block:: python
+
+ class Hdf5(AutotoolsPackage):
+ ...
+ variant('szip', default=False, description='Enable szip support')
+ depends_on('szip', when='+szip')
+
+as shown in the snippet above where ``szip`` is modeled to be an optional
+dependency of ``hdf5``.
+
+^^^^^^^^^^^^^^^^^^^^^
+Multi-valued variants
+^^^^^^^^^^^^^^^^^^^^^
+
+If need be, Spack can go beyond Boolean variants and permit an arbitrary
+number of allowed values. This might be useful when modeling
+options that are tightly related to each other.
+The values in this case are passed to the :py:func:`spack.directives.variant`
+directive as a tuple:
+
+ .. code-block:: python
+
+ class Blis(Package):
+ ...
+ variant(
+ 'threads', default='none', description='Multithreading support',
+ values=('pthreads', 'openmp', 'none'), multi=False
+ )
+
+In the example above the argument ``multi`` is set to ``False`` to indicate
+that only one among all the variant values can be active at any time. This
+constraint is enforced by the parser and an error is emitted if a user
+specifies two or more values at the same time:
+
+ .. code-block:: console
+
+ $ spack spec blis threads=openmp,pthreads
+ Input spec
+ --------------------------------
+ blis threads=openmp,pthreads
+
+ Concretized
+ --------------------------------
+ ==> Error: multiple values are not allowed for variant "threads"
+
+Another useful note is that *Python's* ``None`` *is not allowed as a default value*
+and therefore it should not be used to denote that no feature was selected.
+Users should instead select another value, like ``'none'``, and handle it explicitly
+within the package recipe if need be:
+
+ .. code-block:: python
+
+ if self.spec.variants['threads'].value == 'none':
+ options.append('--no-threads')
+
+In cases where multiple values can be selected at the same time ``multi`` should
+be set to ``True``:
+
+ .. code-block:: python
+
+ class Gcc(AutotoolsPackage):
+ ...
+ variant(
+ 'languages', default='c,c++,fortran',
+ values=('ada', 'brig', 'c', 'c++', 'fortran',
+ 'go', 'java', 'jit', 'lto', 'objc', 'obj-c++'),
+ multi=True,
+ description='Compilers and runtime libraries to build'
+ )
+
+Within a package recipe a multi-valued variant is tested using a ``key=value`` syntax:
+
+ .. code-block:: python
+
+ if 'languages=jit' in spec:
+ options.append('--enable-host-shared')
+
+"""""""""""""""""""""""""""""""""""""""""""
+Complex validation logic for variant values
+"""""""""""""""""""""""""""""""""""""""""""
+To cover complex use cases, the :py:func:`spack.directives.variant` directive
+could accept as the ``values`` argument a full-fledged object which has
+``default`` and other arguments of the directive embedded as attributes.
+
+An example, already implemented in Spack's core, is :py:class:`spack.variant.DisjointSetsOfValues`.
+This class is used to implement a few convenience functions, like
+:py:func:`spack.variant.any_combination_of`:
+
+ .. code-block:: python
+
+ class Adios(AutotoolsPackage):
+ ...
+ variant(
+ 'staging',
+ values=any_combination_of('flexpath', 'dataspaces'),
+ description='Enable dataspaces and/or flexpath staging transports'
+ )
+
+that allows any combination of the specified values, and also allows the
+user to specify ``'none'`` (as a string) to choose none of them.
+The objects returned by these functions can be modified at will by chaining
+method calls to change the default value, customize the error message or
+other similar operations:
+
+ .. code-block:: python
+
+ class Mvapich2(AutotoolsPackage):
+ ...
+ variant(
+ 'process_managers',
+ description='List of the process managers to activate',
+ values=disjoint_sets(
+ ('auto',), ('slurm',), ('hydra', 'gforker', 'remshell')
+ ).prohibit_empty_set().with_error(
+ "'slurm' or 'auto' cannot be activated along with "
+ "other process managers"
+ ).with_default('auto').with_non_feature_values('auto'),
+ )
+
------------------------------------
Resources (expanding extra tarballs)
------------------------------------
diff --git a/lib/spack/spack/build_systems/autotools.py b/lib/spack/spack/build_systems/autotools.py
index 12fe7ae210..3673fb7cce 100644
--- a/lib/spack/spack/build_systems/autotools.py
+++ b/lib/spack/spack/build_systems/autotools.py
@@ -359,9 +359,17 @@ class AutotoolsPackage(PackageBase):
options = [(name, condition in spec)]
else:
condition = '{name}={value}'
+ # "feature_values" is used to track values which correspond to
+ # features which can be enabled or disabled as understood by the
+ # package's build system. It excludes values which have special
+ # meanings and do not correspond to features (e.g. "none")
+ feature_values = getattr(
+ self.variants[name].values, 'feature_values', None
+ ) or self.variants[name].values
+
options = [
(value, condition.format(name=name, value=value) in spec)
- for value in self.variants[name].values
+ for value in feature_values
]
# For each allowed value in the list of values
diff --git a/lib/spack/spack/build_systems/cuda.py b/lib/spack/spack/build_systems/cuda.py
index 1cecdf2ccd..97dba30a16 100644
--- a/lib/spack/spack/build_systems/cuda.py
+++ b/lib/spack/spack/build_systems/cuda.py
@@ -5,8 +5,11 @@
from spack.package import PackageBase
from spack.directives import depends_on, variant, conflicts
+
import platform
+import spack.variant
+
class CudaPackage(PackageBase):
"""Auxiliary class which contains CUDA variant, dependencies and conflicts
@@ -19,11 +22,12 @@ class CudaPackage(PackageBase):
description='Build with CUDA')
# see http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#gpu-feature-list
# https://developer.nvidia.com/cuda-gpus
- variant('cuda_arch', default=None,
+ variant('cuda_arch',
description='CUDA architecture',
- values=('20', '30', '32', '35', '50', '52', '53', '60', '61',
- '62', '70'),
- multi=True)
+ values=spack.variant.any_combination_of(
+ '20', '30', '32', '35', '50', '52', '53', '60', '61',
+ '62', '70'
+ ))
# see http://docs.nvidia.com/cuda/cuda-compiler-driver-nvcc/index.html#nvcc-examples
# and http://llvm.org/docs/CompileCudaWithLLVM.html#compiling-cuda-code
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index d167f856e2..08df20ae88 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -34,6 +34,7 @@ import re
from six import string_types
import llnl.util.lang
+import llnl.util.tty.color
import spack.error
import spack.patch
@@ -439,7 +440,7 @@ def variant(
default=None,
description='',
values=None,
- multi=False,
+ multi=None,
validator=None):
"""Define a variant for the package. Packager can specify a default
value as well as a text description.
@@ -456,23 +457,65 @@ def variant(
multi (bool): if False only one value per spec is allowed for
this variant
validator (callable): optional group validator to enforce additional
- logic. It receives a tuple of values and should raise an instance
- of SpackError if the group doesn't meet the additional constraints
+ logic. It receives the package name, the variant name and a tuple
+ of values and should raise an instance of SpackError if the group
+ doesn't meet the additional constraints
+
+ Raises:
+ DirectiveError: if arguments passed to the directive are invalid
"""
+ def format_error(msg, pkg):
+ msg += " @*r{{[{0}, variant '{1}']}}"
+ return llnl.util.tty.color.colorize(msg.format(pkg.name, name))
+
if name in reserved_names:
- raise ValueError("The variant name '%s' is reserved by Spack" % name)
+ def _raise_reserved_name(pkg):
+ msg = "The name '%s' is reserved by Spack" % name
+ raise DirectiveError(format_error(msg, pkg))
+ return _raise_reserved_name
+ # Ensure we have a sequence of allowed variant values, or a
+ # predicate for it.
if values is None:
- if default in (True, False) or (type(default) is str and
- default.upper() in ('TRUE', 'FALSE')):
+ if str(default).upper() in ('TRUE', 'FALSE'):
values = (True, False)
else:
values = lambda x: True
- if default is None:
- default = False if values == (True, False) else ''
+ # The object defining variant values might supply its own defaults for
+ # all the other arguments. Ensure we have no conflicting definitions
+ # in place.
+ for argument in ('default', 'multi', 'validator'):
+ # TODO: we can consider treating 'default' differently from other
+ # TODO: attributes and let a packager decide whether to use the fluent
+ # TODO: interface or the directive argument
+ if hasattr(values, argument) and locals()[argument] is not None:
+ def _raise_argument_error(pkg):
+ msg = "Remove specification of {0} argument: it is handled " \
+ "by an attribute of the 'values' argument"
+ raise DirectiveError(format_error(msg.format(argument), pkg))
+ return _raise_argument_error
+
+ # Allow for the object defining the allowed values to supply its own
+ # default value and group validator, say if it supports multiple values.
+ default = getattr(values, 'default', default)
+ validator = getattr(values, 'validator', validator)
+ multi = getattr(values, 'multi', bool(multi))
+
+ # Here we sanitize against a default value being either None
+ # or the empty string, as the former indicates that a default
+ # was not set while the latter will make the variant unparsable
+ # from the command line
+ if default is None or default == '':
+ def _raise_default_not_set(pkg):
+ if default is None:
+ msg = "either a default was not explicitly set, " \
+ "or 'None' was used"
+ elif default == '':
+ msg = "the default cannot be an empty string"
+ raise DirectiveError(format_error(msg, pkg))
+ return _raise_default_not_set
- default = default
description = str(description).strip()
def _execute_variant(pkg):
diff --git a/lib/spack/spack/pkgkit.py b/lib/spack/spack/pkgkit.py
index d1a39c1a31..85c1ee264e 100644
--- a/lib/spack/spack/pkgkit.py
+++ b/lib/spack/spack/pkgkit.py
@@ -47,3 +47,6 @@ from spack.util.executable import *
from spack.package import \
install_dependency_symlinks, flatten_dependencies, \
DependencyConflictError, InstallError, ExternalPackageError
+
+from spack.variant import any_combination_of, auto_or_any_combination_of
+from spack.variant import disjoint_sets
diff --git a/lib/spack/spack/test/build_systems.py b/lib/spack/spack/test/build_systems.py
index a2e684aecf..1932d1bbc2 100644
--- a/lib/spack/spack/test/build_systems.py
+++ b/lib/spack/spack/test/build_systems.py
@@ -123,8 +123,11 @@ class TestAutotoolsPackage(object):
s.concretize()
pkg = spack.repo.get(s)
- # Called without parameters
options = pkg.with_or_without('foo')
+
+ # Ensure that values that are not representing a feature
+ # are not used by with_or_without
+ assert '--without-none' not in options
assert '--with-bar' in options
assert '--without-baz' in options
assert '--no-fee' in options
@@ -133,14 +136,30 @@ class TestAutotoolsPackage(object):
return 'something'
options = pkg.with_or_without('foo', activation_value=activate)
+ assert '--without-none' not in options
assert '--with-bar=something' in options
assert '--without-baz' in options
assert '--no-fee' in options
options = pkg.enable_or_disable('foo')
+ assert '--disable-none' not in options
assert '--enable-bar' in options
assert '--disable-baz' in options
assert '--disable-fee' in options
options = pkg.with_or_without('bvv')
assert '--with-bvv' in options
+
+ def test_none_is_allowed(self):
+ s = Spec('a foo=none')
+ s.concretize()
+ pkg = spack.repo.get(s)
+
+ options = pkg.with_or_without('foo')
+
+ # Ensure that values that are not representing a feature
+ # are not used by with_or_without
+ assert '--with-none' not in options
+ assert '--without-bar' in options
+ assert '--without-baz' in options
+ assert '--no-fee' in options
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index 9ff5795e30..4ad9c5f9c7 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -3,7 +3,7 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
-import spack.architecture
+import sys
import pytest
from spack.spec import Spec, UnsatisfiableSpecError
@@ -11,6 +11,10 @@ from spack.spec import substitute_abstract_variants, parse_anonymous_spec
from spack.variant import InvalidVariantValueError
from spack.variant import MultipleValuesInExclusiveVariantError
+import spack.architecture
+import spack.directives
+import spack.error
+
def target_factory(spec_string, target_concrete):
spec = Spec(spec_string) if spec_string else Spec()
@@ -785,3 +789,49 @@ class TestSpecSematics(object):
s.compiler_flags[x] == ['-O0', '-g']
for x in ('cflags', 'cxxflags', 'fflags')
)
+
+ def test_any_combination_of(self):
+ # Test that using 'none' and another value raise during concretization
+ spec = Spec('multivalue_variant foo=none,bar')
+ with pytest.raises(spack.error.SpecError) as exc_info:
+ spec.concretize()
+
+ assert "is mutually exclusive with any of the" in str(exc_info.value)
+
+ @pytest.mark.skipif(
+ sys.version_info[0] == 2, reason='__wrapped__ requires python 3'
+ )
+ def test_errors_in_variant_directive(self):
+ variant = spack.directives.variant.__wrapped__
+
+ class Pkg(object):
+ name = 'PKG'
+
+ # We can't use names that are reserved by Spack
+ fn = variant('patches')
+ with pytest.raises(spack.directives.DirectiveError) as exc_info:
+ fn(Pkg())
+ assert "The name 'patches' is reserved" in str(exc_info.value)
+
+ # We can't have conflicting definitions for arguments
+ fn = variant(
+ 'foo', values=spack.variant.any_combination_of('fee', 'foom'),
+ default='bar'
+ )
+ with pytest.raises(spack.directives.DirectiveError) as exc_info:
+ fn(Pkg())
+ assert " it is handled by an attribute of the 'values' " \
+ "argument" in str(exc_info.value)
+
+ # We can't leave None as a default value
+ fn = variant('foo', default=None)
+ with pytest.raises(spack.directives.DirectiveError) as exc_info:
+ fn(Pkg())
+ assert "either a default was not explicitly set, or 'None' was used"\
+ in str(exc_info.value)
+
+ # We can't use an empty string as a default value
+ fn = variant('foo', default='')
+ with pytest.raises(spack.directives.DirectiveError) as exc_info:
+ fn(Pkg())
+ assert "the default cannot be an empty string" in str(exc_info.value)
diff --git a/lib/spack/spack/test/variant.py b/lib/spack/spack/test/variant.py
index c28bdf7da8..2a937422d3 100644
--- a/lib/spack/spack/test/variant.py
+++ b/lib/spack/spack/test/variant.py
@@ -13,6 +13,9 @@ from spack.variant import UnsatisfiableVariantSpecError
from spack.variant import InconsistentValidationError
from spack.variant import MultipleValuesInExclusiveVariantError
from spack.variant import InvalidVariantValueError, DuplicateVariantError
+from spack.variant import disjoint_sets
+
+import spack.error
class TestMultiValuedVariant(object):
@@ -692,3 +695,59 @@ class TestVariantMapTest(object):
c['feebar'] = SingleValuedVariant('feebar', 'foo')
c['shared'] = BoolValuedVariant('shared', True)
assert str(c) == ' feebar=foo foo=bar,baz foobar=fee +shared'
+
+
+def test_disjoint_set_initialization_errors():
+ # Constructing from non-disjoint sets should raise an exception
+ with pytest.raises(spack.error.SpecError) as exc_info:
+ disjoint_sets(('a', 'b'), ('b', 'c'))
+ assert 'sets in input must be disjoint' in str(exc_info.value)
+
+ # A set containing the reserved item 'none' along with other items
+ # should raise an exception
+ with pytest.raises(spack.error.SpecError) as exc_info:
+ disjoint_sets(('a', 'b'), ('none', 'c'))
+ assert "The value 'none' represents the empty set," in str(exc_info.value)
+
+
+def test_disjoint_set_initialization():
+ # Test that no error is thrown when the sets are disjoint
+ d = disjoint_sets(('a',), ('b', 'c'), ('e', 'f'))
+
+ assert d.default is 'none'
+ assert d.multi is True
+ assert set(x for x in d) == set(['none', 'a', 'b', 'c', 'e', 'f'])
+
+
+def test_disjoint_set_fluent_methods():
+ # Construct an object without the empty set
+ d = disjoint_sets(('a',), ('b', 'c'), ('e', 'f')).prohibit_empty_set()
+ assert set(('none',)) not in d.sets
+
+ # Call this 2 times to check that no matter whether
+ # the empty set was allowed or not before, the state
+ # returned is consistent.
+ for _ in range(2):
+ d = d.allow_empty_set()
+ assert set(('none',)) in d.sets
+ assert 'none' in d
+ assert 'none' in [x for x in d]
+ assert 'none' in d.feature_values
+
+ # Marking a value as 'non-feature' removes it from the
+ # list of feature values, but not for the items returned
+ # when iterating over the object.
+ d = d.with_non_feature_values('none')
+ assert 'none' in d
+ assert 'none' in [x for x in d]
+ assert 'none' not in d.feature_values
+
+ # Call this 2 times to check that no matter whether
+ # the empty set was allowed or not before, the state
+ # returned is consistent.
+ for _ in range(2):
+ d = d.prohibit_empty_set()
+ assert set(('none',)) not in d.sets
+ assert 'none' not in d
+ assert 'none' not in [x for x in d]
+ assert 'none' not in d.feature_values
diff --git a/lib/spack/spack/variant.py b/lib/spack/spack/variant.py
index 05e9ac810d..2874eeb60a 100644
--- a/lib/spack/spack/variant.py
+++ b/lib/spack/spack/variant.py
@@ -9,14 +9,21 @@ variants both in packages and in specs.
import functools
import inspect
+import itertools
import re
from six import StringIO
+import llnl.util.tty.color
import llnl.util.lang as lang
import spack.directives
import spack.error as error
+try:
+ from collections.abc import Sequence
+except ImportError:
+ from collections import Sequence
+
class Variant(object):
"""Represents a variant in a package, as declared in the
@@ -71,8 +78,8 @@ class Variant(object):
else:
# Otherwise assume values is the set of allowed explicit values
- self.values = tuple(values)
- allowed = self.values + (self.default,)
+ self.values = values
+ allowed = tuple(self.values) + (self.default,)
self.single_value_validator = lambda x: x in allowed
self.multi = multi
@@ -118,7 +125,7 @@ class Variant(object):
# Validate the group of values if needed
if self.group_validator is not None:
- self.group_validator(value)
+ self.group_validator(pkg.name, self.name, value)
@property
def allowed_values(self):
@@ -598,6 +605,172 @@ def substitute_abstract_variants(spec):
spec.variants.substitute(new_variant)
+# The class below inherit from Sequence to disguise as a tuple and comply
+# with the semantic expected by the 'values' argument of the variant directive
+class DisjointSetsOfValues(Sequence):
+ """Allows combinations from one of many mutually exclusive sets.
+
+ The value ``('none',)`` is reserved to denote the empty set
+ and therefore no other set can contain the item ``'none'``.
+
+ Args:
+ *sets (list of tuples): mutually exclusive sets of values
+ """
+
+ _empty_set = set(('none',))
+
+ def __init__(self, *sets):
+ self.sets = [set(x) for x in sets]
+
+ # 'none' is a special value and can appear only in a set of
+ # a single element
+ if any('none' in s and s != set(('none',)) for s in self.sets):
+ raise error.SpecError("The value 'none' represents the empty set,"
+ " and must appear alone in a set. Use the "
+ "method 'allow_empty_set' to add it.")
+
+ # Sets should not intersect with each other
+ if any(s1 & s2 for s1, s2 in itertools.combinations(self.sets, 2)):
+ raise error.SpecError('sets in input must be disjoint')
+
+ #: Attribute used to track values which correspond to
+ #: features which can be enabled or disabled as understood by the
+ #: package's build system.
+ self.feature_values = tuple(itertools.chain.from_iterable(self.sets))
+ self.default = None
+ self.multi = True
+ self.error_fmt = "this variant accepts combinations of values from " \
+ "exactly one of the following sets '{values}' " \
+ "@*r{{[{package}, variant '{variant}']}}"
+
+ def with_default(self, default):
+ """Sets the default value and returns self."""
+ self.default = default
+ return self
+
+ def with_error(self, error_fmt):
+ """Sets the error message format and returns self."""
+ self.error_fmt = error_fmt
+ return self
+
+ def with_non_feature_values(self, *values):
+ """Marks a few values as not being tied to a feature."""
+ self.feature_values = tuple(
+ x for x in self.feature_values if x not in values
+ )
+ return self
+
+ def allow_empty_set(self):
+ """Adds the empty set to the current list of disjoint sets."""
+ if self._empty_set in self.sets:
+ return self
+
+ # Create a new object to be returned
+ object_with_empty_set = type(self)(('none',), *self.sets)
+ object_with_empty_set.error_fmt = self.error_fmt
+ object_with_empty_set.feature_values = self.feature_values + ('none', )
+ return object_with_empty_set
+
+ def prohibit_empty_set(self):
+ """Removes the empty set from the current list of disjoint sets."""
+ if self._empty_set not in self.sets:
+ return self
+
+ # Create a new object to be returned
+ sets = [s for s in self.sets if s != self._empty_set]
+ object_without_empty_set = type(self)(*sets)
+ object_without_empty_set.error_fmt = self.error_fmt
+ object_without_empty_set.feature_values = tuple(
+ x for x in self.feature_values if x != 'none'
+ )
+ return object_without_empty_set
+
+ def __getitem__(self, idx):
+ return tuple(itertools.chain.from_iterable(self.sets))[idx]
+
+ def __len__(self):
+ return len(itertools.chain.from_iterable(self.sets))
+
+ @property
+ def validator(self):
+ def _disjoint_set_validator(pkg_name, variant_name, values):
+ # If for any of the sets, all the values are in it return True
+ if any(all(x in s for x in values) for s in self.sets):
+ return
+
+ format_args = {
+ 'variant': variant_name, 'package': pkg_name, 'values': values
+ }
+ msg = self.error_fmt + \
+ " @*r{{[{package}, variant '{variant}']}}"
+ msg = llnl.util.tty.color.colorize(msg.format(**format_args))
+ raise error.SpecError(msg)
+ return _disjoint_set_validator
+
+
+def _a_single_value_or_a_combination(single_value, *values):
+ error = "the value '" + single_value + \
+ "' is mutually exclusive with any of the other values"
+ return DisjointSetsOfValues(
+ (single_value,), values
+ ).with_default(single_value).with_error(error).\
+ with_non_feature_values(single_value)
+
+
+# TODO: The factories below are used by package writers to set values of
+# TODO: multi-valued variants. It could be worthwhile to gather them in
+# TODO: a common namespace (like 'multi') in the future.
+
+
+def any_combination_of(*values):
+ """Multi-valued variant that allows any combination of the specified
+ values, and also allows the user to specify 'none' (as a string) to choose
+ none of them.
+
+ It is up to the package implementation to handle the value 'none'
+ specially, if at all.
+
+ Args:
+ *values: allowed variant values
+
+ Returns:
+ a properly initialized instance of DisjointSetsOfValues
+ """
+ return _a_single_value_or_a_combination('none', *values)
+
+
+def auto_or_any_combination_of(*values):
+ """Multi-valued variant that allows any combination of a set of values
+ (but not the empty set) or 'auto'.
+
+ Args:
+ *values: allowed variant values
+
+ Returns:
+ a properly initialized instance of DisjointSetsOfValues
+ """
+ return _a_single_value_or_a_combination('auto', *values)
+
+
+#: Multi-valued variant that allows any combination picking
+#: from one of multiple disjoint sets
+def disjoint_sets(*sets):
+ """Multi-valued variant that allows any combination picking from one
+ of multiple disjoint sets of values, and also allows the user to specify
+ 'none' (as a string) to choose none of them.
+
+ It is up to the package implementation to handle the value 'none'
+ specially, if at all.
+
+ Args:
+ *sets:
+
+ Returns:
+ a properly initialized instance of DisjointSetsOfValues
+ """
+ return DisjointSetsOfValues(*sets).allow_empty_set().with_default('none')
+
+
class DuplicateVariantError(error.SpecError):
"""Raised when the same variant occurs in a spec twice."""