From 9e4b0eb34a66927ca92df79dedc68d35c9fbd4ae Mon Sep 17 00:00:00 2001
From: Massimiliano Culpo <massimiliano.culpo@googlemail.com>
Date: Mon, 1 May 2017 22:08:47 +0200
Subject: [PATCH] Multi-valued variants (#2386)

Modifications:
- added support for multi-valued variants
- refactored code related to variants into variant.py
- added new generic features to AutotoolsPackage that leverage multi-valued variants
- modified openmpi to use new features
- added unit tests for the new semantics
---
 lib/spack/llnl/util/lang.py                   |  20 +-
 lib/spack/llnl/util/tty/__init__.py           |  16 +-
 lib/spack/spack/build_systems/autotools.py    |  45 ++
 lib/spack/spack/cmd/info.py                   |  98 ++-
 lib/spack/spack/cmd/spec.py                   |   3 +-
 lib/spack/spack/concretize.py                 |   7 +-
 lib/spack/spack/directives.py                 |  36 +-
 lib/spack/spack/error.py                      |  15 +
 lib/spack/spack/spec.py                       | 233 +++----
 lib/spack/spack/test/build_systems.py         |  30 +
 lib/spack/spack/test/concretize.py            |   2 +-
 lib/spack/spack/test/conftest.py              |   2 +-
 lib/spack/spack/test/spec_semantics.py        | 141 +++-
 lib/spack/spack/test/spec_yaml.py             |   6 +
 lib/spack/spack/test/variant.py               | 656 ++++++++++++++++++
 lib/spack/spack/variant.py                    | 580 +++++++++++++++-
 .../repos/builtin.mock/packages/a/package.py  |  32 +-
 .../packages/multivalue_variant/package.py    |  57 ++
 .../repos/builtin/packages/cdo/package.py     |   3 +-
 .../builtin/packages/mvapich2/package.py      | 278 +++-----
 .../repos/builtin/packages/netcdf/package.py  |  26 +-
 .../repos/builtin/packages/openmpi/package.py | 113 ++-
 22 files changed, 1961 insertions(+), 438 deletions(-)
 create mode 100644 lib/spack/spack/test/variant.py
 create mode 100644 var/spack/repos/builtin.mock/packages/multivalue_variant/package.py

diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py
index 4943c9df67..9821ec7416 100644
--- a/lib/spack/llnl/util/lang.py
+++ b/lib/spack/llnl/util/lang.py
@@ -266,10 +266,28 @@ def setter(name, value):
 
 
 @key_ordering
-class HashableMap(dict):
+class HashableMap(collections.MutableMapping):
     """This is a hashable, comparable dictionary.  Hash is performed on
        a tuple of the values in the dictionary."""
 
+    def __init__(self):
+        self.dict = {}
+
+    def __getitem__(self, key):
+        return self.dict[key]
+
+    def __setitem__(self, key, value):
+        self.dict[key] = value
+
+    def __iter__(self):
+        return iter(self.dict)
+
+    def __len__(self):
+        return len(self.dict)
+
+    def __delitem__(self, key):
+        del self.dict[key]
+
     def _cmp_key(self):
         return tuple(sorted(self.values()))
 
diff --git a/lib/spack/llnl/util/tty/__init__.py b/lib/spack/llnl/util/tty/__init__.py
index e5c3ba8110..b28ac22c1f 100644
--- a/lib/spack/llnl/util/tty/__init__.py
+++ b/lib/spack/llnl/util/tty/__init__.py
@@ -22,22 +22,22 @@
 # License along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
-import sys
-import os
-import textwrap
 import fcntl
-import termios
+import os
 import struct
+import sys
+import termios
+import textwrap
 import traceback
 from six import StringIO
 from six.moves import input
 
 from llnl.util.tty.color import *
 
-_debug   = False
+_debug = False
 _verbose = False
 _stacktrace = False
-indent  = "  "
+indent = "  "
 
 
 def is_verbose():
@@ -100,7 +100,7 @@ def msg(message, *args, **kwargs):
 def info(message, *args, **kwargs):
     format = kwargs.get('format', '*b')
     stream = kwargs.get('stream', sys.stdout)
-    wrap   = kwargs.get('wrap', False)
+    wrap = kwargs.get('wrap', False)
     break_long_words = kwargs.get('break_long_words', False)
     st_countback = kwargs.get('countback', 3)
 
@@ -218,7 +218,7 @@ def hline(label=None, **kwargs):
         char (str): Char to draw the line with.  Default '-'
         max_width (int): Maximum width of the line.  Default is 64 chars.
     """
-    char      = kwargs.pop('char', '-')
+    char = kwargs.pop('char', '-')
     max_width = kwargs.pop('max_width', 64)
     if kwargs:
         raise TypeError(
diff --git a/lib/spack/spack/build_systems/autotools.py b/lib/spack/spack/build_systems/autotools.py
index ffd00e7f69..2e4c86ea3e 100644
--- a/lib/spack/spack/build_systems/autotools.py
+++ b/lib/spack/spack/build_systems/autotools.py
@@ -289,6 +289,51 @@ def check(self):
             self._if_make_target_execute('test')
             self._if_make_target_execute('check')
 
+    def _activate_or_not(self, active, inactive, name, active_parameters=None):
+        spec = self.spec
+        args = []
+        # For each allowed value in the list of values
+        for value in self.variants[name].values:
+            # Check if the value is active in the current spec
+            condition = '{name}={value}'.format(name=name, value=value)
+            activated = condition in spec
+            # Search for an override in the package for this value
+            override_name = '{0}_or_{1}_{2}'.format(active, inactive, value)
+            line_generator = getattr(self, override_name, None)
+            # If not available use a sensible default
+            if line_generator is None:
+                def _default_generator(is_activated):
+                    if is_activated:
+                        line = '--{0}-{1}'.format(active, value)
+                        if active_parameters is not None and active_parameters(value):  # NOQA=ignore=E501
+                            line += '={0}'.format(active_parameters(value))
+                        return line
+                    return '--{0}-{1}'.format(inactive, value)
+                line_generator = _default_generator
+            args.append(line_generator(activated))
+        return args
+
+    def with_or_without(self, name, active_parameters=None):
+        """Inspects the multi-valued variant 'name' and returns the configure
+        arguments that activate / deactivate the selected feature.
+
+        :param str name: name of a valid multi-valued variant
+        :param callable active_parameters: if present accepts a single value
+            and returns the parameter to be used leading to an entry of the
+            type '--with-{name}={parameter}
+        """
+        return self._activate_or_not(
+            'with', 'without', name, active_parameters
+        )
+
+    def enable_or_disable(self, name, active_parameters=None):
+        """Inspects the multi-valued variant 'name' and returns the configure
+        arguments that activate / deactivate the selected feature.
+        """
+        return self._activate_or_not(
+            'enable', 'disable', name, active_parameters
+        )
+
     run_after('install')(PackageBase._run_default_install_time_test_callbacks)
 
     def installcheck(self):
diff --git a/lib/spack/spack/cmd/info.py b/lib/spack/spack/cmd/info.py
index 799471ffcc..86ec839b90 100644
--- a/lib/spack/spack/cmd/info.py
+++ b/lib/spack/spack/cmd/info.py
@@ -25,7 +25,7 @@
 from __future__ import print_function
 
 import textwrap
-
+import itertools
 from llnl.util.tty.colify import *
 import spack
 import spack.fetch_strategy as fs
@@ -49,6 +49,81 @@ def setup_parser(subparser):
         'name', metavar="PACKAGE", help="name of package to get info for")
 
 
+class VariantFormatter(object):
+    def __init__(self, variants, max_widths=(25, 20, 35)):
+        self.variants = variants
+        self.headers = ('Name [Default]', 'Allowed values', 'Description')
+        # Set max headers lengths
+        self.max_column_widths = max_widths
+
+        # Formats
+        fmt_name = '{0} [{1}]'
+
+        # Initialize column widths with the length of the
+        # corresponding headers, as they cannot be shorter
+        # than that
+        self.column_widths = [len(x) for x in self.headers]
+
+        # Update according to line lengths
+        for k, v in variants.items():
+            candidate_max_widths = (
+                len(fmt_name.format(k, self.default(v))),  # Name [Default]
+                len(v.allowed_values),  # Allowed values
+                len(v.description)  # Description
+            )
+
+            self.column_widths = (
+                max(self.column_widths[0], candidate_max_widths[0]),
+                max(self.column_widths[1], candidate_max_widths[1]),
+                max(self.column_widths[2], candidate_max_widths[2])
+            )
+
+        # Reduce to at most the maximum allowed
+        self.column_widths = (
+            min(self.column_widths[0], self.max_column_widths[0]),
+            min(self.column_widths[1], self.max_column_widths[1]),
+            min(self.column_widths[2], self.max_column_widths[2])
+        )
+
+        # Compute the format
+        self.fmt = "%%-%ss%%-%ss%%s" % (
+            self.column_widths[0] + 4,
+            self.column_widths[1] + 4
+        )
+
+    def default(self, v):
+        s = 'on' if v.default is True else 'off'
+        if not isinstance(v.default, bool):
+            s = v.default
+        return s
+
+    @property
+    def lines(self):
+        if not self.variants:
+            yield "    None"
+        else:
+            yield "    " + self.fmt % self.headers
+            yield '\n'
+            for k, v in sorted(self.variants.items()):
+                name = textwrap.wrap(
+                    '{0} [{1}]'.format(k, self.default(v)),
+                    width=self.column_widths[0]
+                )
+                allowed = textwrap.wrap(
+                    v.allowed_values,
+                    width=self.column_widths[1]
+                )
+                description = textwrap.wrap(
+                    v.description,
+                    width=self.column_widths[2]
+                )
+                for t in itertools.izip_longest(
+                        name, allowed, description, fillvalue=''
+                ):
+                    yield "    " + self.fmt % t
+                yield ''  # Trigger a new line
+
+
 def print_text_info(pkg):
     """Print out a plain text description of a package."""
     header = "{0}:   ".format(pkg.build_system_class)
@@ -70,25 +145,10 @@ def print_text_info(pkg):
 
     print()
     print("Variants:")
-    if not pkg.variants:
-        print("    None")
-    else:
-        pad = padder(pkg.variants, 4)
-
-        maxv = max(len(v) for v in sorted(pkg.variants))
-        fmt = "%%-%ss%%-10s%%s" % (maxv + 4)
-
-        print("    " + fmt % ('Name',   'Default',   'Description'))
-        print()
-        for name in sorted(pkg.variants):
-            v = pkg.variants[name]
-            default = 'on' if v.default else 'off'
-
-            lines = textwrap.wrap(v.description)
-            lines[1:] = ["      " + (" " * maxv) + l for l in lines[1:]]
-            desc = "\n".join(lines)
 
-            print("    " + fmt % (name, default, desc))
+    formatter = VariantFormatter(pkg.variants)
+    for line in formatter.lines:
+        print(line)
 
     print()
     print("Installation Phases:")
diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py
index d89707f230..2e917d2ee3 100644
--- a/lib/spack/spack/cmd/spec.py
+++ b/lib/spack/spack/cmd/spec.py
@@ -69,7 +69,8 @@ def spec(parser, args):
     for spec in spack.cmd.parse_specs(args.specs):
         # With -y, just print YAML to output.
         if args.yaml:
-            spec.concretize()
+            if spec.name in spack.repo:
+                spec.concretize()
             print(spec.to_yaml())
             continue
 
diff --git a/lib/spack/spack/concretize.py b/lib/spack/spack/concretize.py
index 5507b599ff..d7f21e8c81 100644
--- a/lib/spack/spack/concretize.py
+++ b/lib/spack/spack/concretize.py
@@ -245,14 +245,15 @@ def concretize_variants(self, spec):
         """
         changed = False
         preferred_variants = PackagePrefs.preferred_variants(spec.name)
-        for name, variant in spec.package_class.variants.items():
+        pkg_cls = spec.package_class
+        for name, variant in pkg_cls.variants.items():
             if name not in spec.variants:
                 changed = True
                 if name in preferred_variants:
                     spec.variants[name] = preferred_variants.get(name)
                 else:
-                    spec.variants[name] = spack.spec.VariantSpec(
-                        name, variant.default)
+                    spec.variants[name] = variant.make_default()
+
         return changed
 
     def concretize_compiler(self, spec):
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index 7a245f606c..43ac71c679 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -368,9 +368,37 @@ def _execute(pkg):
 
 
 @directive('variants')
-def variant(name, default=False, description=""):
+def variant(
+        name,
+        default=None,
+        description='',
+        values=(True, False),
+        multi=False,
+        validator=None
+):
     """Define a variant for the package. Packager can specify a default
-    value (on or off) as well as a text description."""
+    value as well as a text description.
+
+    Args:
+        name (str): name of the variant
+        default (str or bool): default value for the variant, if not
+            specified otherwise the default will be False for a boolean
+            variant and 'nothing' for a multi-valued variant
+        description (str): description of the purpose of the variant
+        values (tuple or callable): either a tuple of strings containing the
+            allowed values, or a callable accepting one value and returning
+            True if it is valid
+        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
+    """
+
+    if default is None:
+        default = False if values == (True, False) else ''
+
+    default = default
     description = str(description).strip()
 
     def _execute(pkg):
@@ -379,7 +407,9 @@ def _execute(pkg):
             msg = "Invalid variant name in {0}: '{1}'"
             raise DirectiveError(directive, msg.format(pkg.name, name))
 
-        pkg.variants[name] = Variant(default, description)
+        pkg.variants[name] = Variant(
+            name, default, description, values, multi, validator
+        )
     return _execute
 
 
diff --git a/lib/spack/spack/error.py b/lib/spack/spack/error.py
index cd1ae5b25c..09969b2b41 100644
--- a/lib/spack/spack/error.py
+++ b/lib/spack/spack/error.py
@@ -100,3 +100,18 @@ def __init__(self, message, url):
             "No network connection: " + str(message),
             "URL was: " + str(url))
         self.url = url
+
+
+class SpecError(SpackError):
+    """Superclass for all errors that occur while constructing specs."""
+
+
+class UnsatisfiableSpecError(SpecError):
+    """Raised when a spec conflicts with package constraints.
+       Provide the requirement that was violated when raising."""
+    def __init__(self, provided, required, constraint_type):
+        super(UnsatisfiableSpecError, self).__init__(
+            "%s does not satisfy %s" % (provided, required))
+        self.provided = provided
+        self.required = required
+        self.constraint_type = constraint_type
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 0d8fb2893b..0cf392a7ce 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -121,12 +121,14 @@
 from llnl.util.lang import *
 from llnl.util.tty.color import *
 from spack.build_environment import get_path_from_module, load_module
+from spack.error import SpecError, UnsatisfiableSpecError
 from spack.provider_index import ProviderIndex
 from spack.util.crypto import prefix_bits
 from spack.util.executable import Executable
 from spack.util.prefix import Prefix
 from spack.util.spack_yaml import syaml_dict
 from spack.util.string import *
+from spack.variant import *
 from spack.version import *
 from yaml.error import MarkedYAMLError
 
@@ -606,81 +608,6 @@ def __str__(self):
                                 self.spec.name if self.spec else None)
 
 
-@key_ordering
-class VariantSpec(object):
-    """Variants are named, build-time options for a package.  Names depend
-       on the particular package being built, and each named variant can
-       be enabled or disabled.
-    """
-
-    def __init__(self, name, value):
-        self.name = name
-        self.value = value
-
-    def _cmp_key(self):
-        return (self.name, self.value)
-
-    def copy(self):
-        return VariantSpec(self.name, self.value)
-
-    def __str__(self):
-        if type(self.value) == bool:
-            return '{0}{1}'.format('+' if self.value else '~', self.name)
-        else:
-            return ' {0}={1} '.format(self.name, self.value)
-
-
-class VariantMap(HashableMap):
-
-    def __init__(self, spec):
-        super(VariantMap, self).__init__()
-        self.spec = spec
-
-    def satisfies(self, other, strict=False):
-        if strict or self.spec._concrete:
-            return all(k in self and self[k].value == other[k].value
-                       for k in other)
-        else:
-            return all(self[k].value == other[k].value
-                       for k in other if k in self)
-
-    def constrain(self, other):
-        """Add all variants in other that aren't in self to self.
-
-        Raises an error if any common variants don't match.
-        Return whether the spec changed.
-        """
-        if other.spec._concrete:
-            for k in self:
-                if k not in other:
-                    raise UnsatisfiableVariantSpecError(self[k], '<absent>')
-
-        changed = False
-        for k in other:
-            if k in self:
-                if self[k].value != other[k].value:
-                    raise UnsatisfiableVariantSpecError(self[k], other[k])
-            else:
-                self[k] = other[k].copy()
-                changed = True
-        return changed
-
-    @property
-    def concrete(self):
-        return self.spec._concrete or all(
-            v in self for v in self.spec.package_class.variants)
-
-    def copy(self):
-        clone = VariantMap(None)
-        for name, variant in self.items():
-            clone[name] = variant.copy()
-        return clone
-
-    def __str__(self):
-        sorted_keys = sorted(self.keys())
-        return ''.join(str(self[key]) for key in sorted_keys)
-
-
 _valid_compiler_flags = [
     'cflags', 'cxxflags', 'fflags', 'ldflags', 'ldlibs', 'cppflags']
 
@@ -1094,17 +1021,6 @@ def _add_version(self, version):
         """Called by the parser to add an allowable version."""
         self.versions.add(version)
 
-    def _add_variant(self, name, value):
-        """Called by the parser to add a variant."""
-        if name in self.variants:
-            raise DuplicateVariantError(
-                "Cannot specify variant '%s' twice" % name)
-        if isinstance(value, string_types) and value.upper() == 'TRUE':
-            value = True
-        elif isinstance(value, string_types) and value.upper() == 'FALSE':
-            value = False
-        self.variants[name] = VariantSpec(name, value)
-
     def _add_flag(self, name, value):
         """Called by the parser to add a known flag.
         Known flags currently include "arch"
@@ -1124,7 +1040,13 @@ def _add_flag(self, name, value):
             assert(self.compiler_flags is not None)
             self.compiler_flags[name] = value.split()
         else:
-            self._add_variant(name, value)
+            # All other flags represent variants. 'foo=true' and 'foo=false'
+            # map to '+foo' and '~foo' respectively. As such they need a
+            # BoolValuedVariant instance.
+            if str(value).upper() == 'TRUE' or str(value).upper() == 'FALSE':
+                self.variants[name] = BoolValuedVariant(name, value)
+            else:
+                self.variants[name] = MultiValuedVariant(name, value)
 
     def _set_architecture(self, **kwargs):
         """Called by the parser to set the architecture."""
@@ -1424,8 +1346,11 @@ def to_node_dict(self):
         if self.namespace:
             d['namespace'] = self.namespace
 
-        params = syaml_dict(sorted(
-            (name, v.value) for name, v in self.variants.items()))
+        params = syaml_dict(
+            sorted(
+                v.yaml_entry() for _, v in self.variants.items()
+            )
+        )
         params.update(sorted(self.compiler_flags.items()))
         if params:
             d['parameters'] = params
@@ -1491,11 +1416,14 @@ def from_node_dict(node):
                 if name in _valid_compiler_flags:
                     spec.compiler_flags[name] = value
                 else:
-                    spec.variants[name] = VariantSpec(name, value)
-
+                    spec.variants[name] = MultiValuedVariant.from_node_dict(
+                        name, value
+                    )
         elif 'variants' in node:
             for name, value in node['variants'].items():
-                spec.variants[name] = VariantSpec(name, value)
+                spec.variants[name] = MultiValuedVariant.from_node_dict(
+                    name, value
+                )
             for name in FlagMap.valid_compiler_flags():
                 spec.compiler_flags[name] = []
 
@@ -2076,7 +2004,7 @@ def normalize(self, force=False):
             self._mark_concrete(False)
 
         # Ensure first that all packages & compilers in the DAG exist.
-        self.validate_names()
+        self.validate_or_raise()
         # Get all the dependencies into one DependencyMap
         spec_deps = self.flat_dependencies(copy=False, deptype_query=alldeps)
 
@@ -2110,11 +2038,13 @@ def normalized(self):
         clone.normalize()
         return clone
 
-    def validate_names(self):
-        """This checks that names of packages and compilers in this spec are real.
-           If they're not, it will raise either UnknownPackageError or
-           UnsupportedCompilerError.
+    def validate_or_raise(self):
+        """Checks that names and values in this spec are real. If they're not,
+        it will raise an appropriate exception.
         """
+        # FIXME: this function should be lazy, and collect all the errors
+        # FIXME: before raising the exceptions, instead of being greedy and
+        # FIXME: raise just the first one encountered
         for spec in self.traverse():
             # raise an UnknownPackageError if the spec's package isn't real.
             if (not spec.virtual) and spec.name:
@@ -2125,16 +2055,44 @@ def validate_names(self):
                 if not compilers.supported(spec.compiler):
                     raise UnsupportedCompilerError(spec.compiler.name)
 
-            # Ensure that variants all exist.
-            for vname, variant in spec.variants.items():
-                if vname not in spec.package_class.variants:
-                    raise UnknownVariantError(spec.name, vname)
+            # FIXME: Move the logic below into the variant.py module
+            # Ensure correctness of variants (if the spec is not virtual)
+            if not spec.virtual:
+                pkg_cls = spec.package_class
+                pkg_variants = pkg_cls.variants
+                not_existing = set(spec.variants) - set(pkg_variants)
+                if not_existing:
+                    raise UnknownVariantError(spec.name, not_existing)
+
+                for name, v in [(x, y) for (x, y) in spec.variants.items()]:
+                    # When parsing a spec every variant of the form
+                    # 'foo=value' will be interpreted by default as a
+                    # multi-valued variant. During validation of the
+                    # variants we use the information in the package
+                    # to turn any variant that needs it to a single-valued
+                    # variant.
+                    pkg_variant = pkg_variants[name]
+                    pkg_variant.validate_or_raise(v, pkg_cls)
+                    spec.variants.substitute(
+                        pkg_variant.make_variant(v._original_value)
+                    )
 
     def constrain(self, other, deps=True):
         """Merge the constraints of other with self.
 
         Returns True if the spec changed as a result, False if not.
         """
+        # If we are trying to constrain a concrete spec, either the spec
+        # already satisfies the constraint (and the method returns False)
+        # or it raises an exception
+        if self.concrete:
+            if self.satisfies(other):
+                return False
+            else:
+                raise UnsatisfiableSpecError(
+                    self, other, 'constrain a concrete spec'
+                )
+
         other = self._autospec(other)
 
         if not (self.name == other.name or
@@ -2150,11 +2108,11 @@ def constrain(self, other, deps=True):
         if not self.versions.overlaps(other.versions):
             raise UnsatisfiableVersionSpecError(self.versions, other.versions)
 
-        for v in other.variants:
-            if (v in self.variants and
-                    self.variants[v].value != other.variants[v].value):
-                raise UnsatisfiableVariantSpecError(self.variants[v],
-                                                    other.variants[v])
+        for v in [x for x in other.variants if x in self.variants]:
+            if not self.variants[v].compatible(other.variants[v]):
+                raise UnsatisfiableVariantSpecError(
+                    self.variants[v], other.variants[v]
+                )
 
         # TODO: Check out the logic here
         sarch, oarch = self.architecture, other.architecture
@@ -2328,6 +2286,30 @@ def satisfies(self, other, deps=True, strict=False, strict_deps=False):
         elif strict and (other.compiler and not self.compiler):
             return False
 
+        # If self is a concrete spec, and other is not virtual, then we need
+        # to substitute every multi-valued variant that needs it with a
+        # single-valued variant.
+        if self.concrete:
+            for name, v in [(x, y) for (x, y) in other.variants.items()]:
+                # When parsing a spec every variant of the form
+                # 'foo=value' will be interpreted by default as a
+                # multi-valued variant. During validation of the
+                # variants we use the information in the package
+                # to turn any variant that needs it to a single-valued
+                # variant.
+                pkg_cls = type(other.package)
+                try:
+                    pkg_variant = other.package.variants[name]
+                    pkg_variant.validate_or_raise(v, pkg_cls)
+                except (SpecError, KeyError):
+                    # Catch the two things that could go wrong above:
+                    # 1. name is not a valid variant (KeyError)
+                    # 2. the variant is not validated (SpecError)
+                    return False
+                other.variants.substitute(
+                    pkg_variant.make_variant(v._original_value)
+                )
+
         var_strict = strict
         if (not self.name) or (not other.name):
             var_strict = True
@@ -3118,10 +3100,12 @@ def spec(self, name):
                 added_version = True
 
             elif self.accept(ON):
-                spec._add_variant(self.variant(), True)
+                name = self.variant()
+                spec.variants[name] = BoolValuedVariant(name, True)
 
             elif self.accept(OFF):
-                spec._add_variant(self.variant(), False)
+                name = self.variant()
+                spec.variants[name] = BoolValuedVariant(name, False)
 
             elif self.accept(PCT):
                 spec._set_compiler(self.compiler())
@@ -3275,10 +3259,6 @@ def base32_prefix_bits(hash_string, bits):
     return prefix_bits(hash_bytes, bits)
 
 
-class SpecError(spack.error.SpackError):
-    """Superclass for all errors that occur while constructing specs."""
-
-
 class SpecParseError(SpecError):
     """Wrapper for ParseError for when we're parsing specs."""
     def __init__(self, parse_error):
@@ -3291,10 +3271,6 @@ class DuplicateDependencyError(SpecError):
     """Raised when the same dependency occurs in a spec twice."""
 
 
-class DuplicateVariantError(SpecError):
-    """Raised when the same variant occurs in a spec twice."""
-
-
 class DuplicateCompilerSpecError(SpecError):
     """Raised when the same compiler occurs in a spec twice."""
 
@@ -3306,13 +3282,6 @@ def __init__(self, compiler_name):
             "The '%s' compiler is not yet supported." % compiler_name)
 
 
-class UnknownVariantError(SpecError):
-    """Raised when the same variant occurs in a spec twice."""
-    def __init__(self, pkg, variant):
-        super(UnknownVariantError, self).__init__(
-            "Package %s has no variant %s!" % (pkg, variant))
-
-
 class DuplicateArchitectureError(SpecError):
     """Raised when the same architecture occurs in a spec twice."""
 
@@ -3354,17 +3323,6 @@ def __init__(self, vpkg, providers):
         self.providers = providers
 
 
-class UnsatisfiableSpecError(SpecError):
-    """Raised when a spec conflicts with package constraints.
-       Provide the requirement that was violated when raising."""
-    def __init__(self, provided, required, constraint_type):
-        super(UnsatisfiableSpecError, self).__init__(
-            "%s does not satisfy %s" % (provided, required))
-        self.provided = provided
-        self.required = required
-        self.constraint_type = constraint_type
-
-
 class UnsatisfiableSpecNameError(UnsatisfiableSpecError):
     """Raised when two specs aren't even for the same package."""
     def __init__(self, provided, required):
@@ -3386,13 +3344,6 @@ def __init__(self, provided, required):
             provided, required, "compiler")
 
 
-class UnsatisfiableVariantSpecError(UnsatisfiableSpecError):
-    """Raised when a spec variant conflicts with package constraints."""
-    def __init__(self, provided, required):
-        super(UnsatisfiableVariantSpecError, self).__init__(
-            provided, required, "variant")
-
-
 class UnsatisfiableCompilerFlagSpecError(UnsatisfiableSpecError):
     """Raised when a spec variant conflicts with package constraints."""
     def __init__(self, provided, required):
diff --git a/lib/spack/spack/test/build_systems.py b/lib/spack/spack/test/build_systems.py
index 2cafba0333..8e771c8a68 100644
--- a/lib/spack/spack/test/build_systems.py
+++ b/lib/spack/spack/test/build_systems.py
@@ -24,6 +24,8 @@
 ##############################################################################
 
 import spack
+import pytest
+
 from spack.build_environment import get_std_cmake_args
 from spack.spec import Spec
 
@@ -40,3 +42,31 @@ def test_cmake_std_args(config, builtin_mock):
     s.concretize()
     pkg = spack.repo.get(s)
     assert get_std_cmake_args(pkg)
+
+
+@pytest.mark.usefixtures('config', 'builtin_mock')
+class TestAutotoolsPackage(object):
+
+    def test_with_or_without(self):
+        s = Spec('a')
+        s.concretize()
+        pkg = spack.repo.get(s)
+
+        # Called without parameters
+        l = pkg.with_or_without('foo')
+        assert '--with-bar' in l
+        assert '--without-baz' in l
+        assert '--no-fee' in l
+
+        def activate(value):
+            return 'something'
+
+        l = pkg.with_or_without('foo', active_parameters=activate)
+        assert '--with-bar=something' in l
+        assert '--without-baz' in l
+        assert '--no-fee' in l
+
+        l = pkg.enable_or_disable('foo')
+        assert '--enable-bar' in l
+        assert '--disable-baz' in l
+        assert '--disable-fee' in l
diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py
index 2063088184..779d4f8816 100644
--- a/lib/spack/spack/test/concretize.py
+++ b/lib/spack/spack/test/concretize.py
@@ -75,7 +75,7 @@ def check_concretize(abstract_spec):
         # dag
         'callpath', 'mpileaks', 'libelf',
         # variant
-        'mpich+debug', 'mpich~debug', 'mpich debug=2', 'mpich',
+        'mpich+debug', 'mpich~debug', 'mpich debug=True', 'mpich',
         # compiler flags
         'mpich cppflags="-O3"',
         # with virtual
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index 120425794f..3c725e229b 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -55,7 +55,7 @@
 @pytest.fixture(autouse=True)
 def no_stdin_duplication(monkeypatch):
     """Duplicating stdin (or any other stream) returns an empty
-    cStringIO object.
+    StringIO object.
     """
     monkeypatch.setattr(llnl.util.lang, 'duplicate_stream',
                         lambda x: StringIO())
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index f071bcc833..306a6ad98f 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -24,7 +24,9 @@
 ##############################################################################
 import spack.architecture
 import pytest
+
 from spack.spec import *
+from spack.variant import *
 
 
 def check_satisfies(spec, anon_spec, concrete=False):
@@ -243,6 +245,128 @@ def test_satisfies_matching_variant(self):
         check_satisfies('mpich~foo', 'mpich foo=FALSE')
         check_satisfies('mpich foo=False', 'mpich~foo')
 
+    def test_satisfies_multi_value_variant(self):
+        # Check quoting
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'multivalue_variant foo="bar,baz"')
+        check_satisfies('multivalue_variant foo=bar,baz',
+                        'multivalue_variant foo=bar,baz')
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'multivalue_variant foo=bar,baz')
+
+        # A more constrained spec satisfies a less constrained one
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'multivalue_variant foo="bar"')
+
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'multivalue_variant foo="baz"')
+
+        check_satisfies('multivalue_variant foo="bar,baz,barbaz"',
+                        'multivalue_variant foo="bar,baz"')
+
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'foo="bar,baz"')
+
+        check_satisfies('multivalue_variant foo="bar,baz"',
+                        'foo="bar"')
+
+    def test_satisfies_single_valued_variant(self):
+        """Tests that the case reported in
+        https://github.com/LLNL/spack/pull/2386#issuecomment-282147639
+        is handled correctly.
+        """
+        a = Spec('a foobar=bar')
+        a.concretize()
+
+        assert a.satisfies('foobar=bar')
+
+    def test_unsatisfiable_multi_value_variant(self):
+
+        # Semantics for a multi-valued variant is different
+        # Depending on whether the spec is concrete or not
+
+        a = Spec('multivalue_variant foo="bar"', concrete=True)
+        spec_str = 'multivalue_variant foo="bar,baz"'
+        b = Spec(spec_str)
+        assert not a.satisfies(b)
+        assert not a.satisfies(spec_str)
+        # A concrete spec cannot be constrained further
+        with pytest.raises(UnsatisfiableSpecError):
+            a.constrain(b)
+
+        a = Spec('multivalue_variant foo="bar"')
+        spec_str = 'multivalue_variant foo="bar,baz"'
+        b = Spec(spec_str)
+        assert not a.satisfies(b)
+        assert not a.satisfies(spec_str)
+        # An abstract spec can instead be constrained
+        assert a.constrain(b)
+
+        a = Spec('multivalue_variant foo="bar,baz"', concrete=True)
+        spec_str = 'multivalue_variant foo="bar,baz,quux"'
+        b = Spec(spec_str)
+        assert not a.satisfies(b)
+        assert not a.satisfies(spec_str)
+        # A concrete spec cannot be constrained further
+        with pytest.raises(UnsatisfiableSpecError):
+            a.constrain(b)
+
+        a = Spec('multivalue_variant foo="bar,baz"')
+        spec_str = 'multivalue_variant foo="bar,baz,quux"'
+        b = Spec(spec_str)
+        assert not a.satisfies(b)
+        assert not a.satisfies(spec_str)
+        # An abstract spec can instead be constrained
+        assert a.constrain(b)
+        # ...but will fail during concretization if there are
+        # values in the variant that are not allowed
+        with pytest.raises(InvalidVariantValueError):
+            a.concretize()
+
+        # This time we'll try to set a single-valued variant
+        a = Spec('multivalue_variant fee="bar"')
+        spec_str = 'multivalue_variant fee="baz"'
+        b = Spec(spec_str)
+        assert not a.satisfies(b)
+        assert not a.satisfies(spec_str)
+        # A variant cannot be parsed as single-valued until we try to
+        # concretize. This means that we can constrain the variant above
+        assert a.constrain(b)
+        # ...but will fail during concretization if there are
+        # multiple values set
+        with pytest.raises(MultipleValuesInExclusiveVariantError):
+            a.concretize()
+
+        # FIXME: remove after having checked the correctness of the semantics
+        # check_unsatisfiable('multivalue_variant foo="bar,baz"',
+        #                     'multivalue_variant foo="bar,baz,quux"',
+        #                     concrete=True)
+        # check_unsatisfiable('multivalue_variant foo="bar,baz"',
+        #                     'multivalue_variant foo="bar,baz,quux"',
+        #                     concrete=True)
+
+        # but succeed for abstract ones (b/c they COULD satisfy the
+        # constraint if constrained)
+        # check_satisfies('multivalue_variant foo="bar"',
+        #                 'multivalue_variant foo="bar,baz"')
+
+        # check_satisfies('multivalue_variant foo="bar,baz"',
+        #                 'multivalue_variant foo="bar,baz,quux"')
+
+    def test_unsatisfiable_variant_types(self):
+        # These should fail due to incompatible types
+        check_unsatisfiable('multivalue_variant +foo',
+                            'multivalue_variant foo="bar"')
+
+        check_unsatisfiable('multivalue_variant ~foo',
+                            'multivalue_variant foo="bar"')
+
+        check_unsatisfiable('multivalue_variant foo="bar"',
+                            'multivalue_variant +foo')
+
+        check_unsatisfiable('multivalue_variant foo="bar"',
+                            'multivalue_variant ~foo')
+
     def test_satisfies_unconstrained_variant(self):
         # only asked for mpich, no constraints.  Either will do.
         check_satisfies('mpich+foo', 'mpich')
@@ -266,7 +390,7 @@ def test_unsatisfiable_variant_mismatch(self):
         # No matchi in specs
         check_unsatisfiable('mpich~foo', 'mpich+foo')
         check_unsatisfiable('mpich+foo', 'mpich~foo')
-        check_unsatisfiable('mpich foo=1', 'mpich foo=2')
+        check_unsatisfiable('mpich foo=True', 'mpich foo=False')
 
     def test_satisfies_matching_compiler_flag(self):
         check_satisfies('mpich cppflags="-O3"', 'mpich cppflags="-O3"')
@@ -416,6 +540,19 @@ def test_constrain_variants(self):
             'libelf+debug~foo', 'libelf+debug', 'libelf+debug~foo'
         )
 
+    def test_constrain_multi_value_variant(self):
+        check_constrain(
+            'multivalue_variant foo="bar,baz"',
+            'multivalue_variant foo="bar"',
+            'multivalue_variant foo="baz"'
+        )
+
+        check_constrain(
+            'multivalue_variant foo="bar,baz,barbaz"',
+            'multivalue_variant foo="bar,barbaz"',
+            'multivalue_variant foo="baz"'
+        )
+
     def test_constrain_compiler_flags(self):
         check_constrain(
             'libelf cflags="-O3" cppflags="-Wall"',
@@ -455,7 +592,7 @@ def test_invalid_constraint(self):
 
         check_invalid_constraint('libelf+debug', 'libelf~debug')
         check_invalid_constraint('libelf+debug~foo', 'libelf+debug+foo')
-        check_invalid_constraint('libelf debug=2', 'libelf debug=1')
+        check_invalid_constraint('libelf debug=True', 'libelf debug=False')
 
         check_invalid_constraint(
             'libelf cppflags="-O3"', 'libelf cppflags="-O2"')
diff --git a/lib/spack/spack/test/spec_yaml.py b/lib/spack/spack/test/spec_yaml.py
index adf262a60e..866cdebd26 100644
--- a/lib/spack/spack/test/spec_yaml.py
+++ b/lib/spack/spack/test/spec_yaml.py
@@ -74,6 +74,12 @@ def test_concrete_spec(config, builtin_mock):
     check_yaml_round_trip(spec)
 
 
+def test_yaml_multivalue():
+    spec = Spec('multivalue_variant foo="bar,baz"')
+    spec.concretize()
+    check_yaml_round_trip(spec)
+
+
 def test_yaml_subdag(config, builtin_mock):
     spec = Spec('mpileaks^mpich+debug')
     spec.concretize()
diff --git a/lib/spack/spack/test/variant.py b/lib/spack/spack/test/variant.py
new file mode 100644
index 0000000000..0c546a5dac
--- /dev/null
+++ b/lib/spack/spack/test/variant.py
@@ -0,0 +1,656 @@
+##############################################################################
+# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the LICENSE file for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+
+import pytest
+import numbers
+
+from spack.variant import *
+
+
+class TestMultiValuedVariant(object):
+
+    def test_initialization(self):
+
+        # Basic properties
+        a = MultiValuedVariant('foo', 'bar,baz')
+        assert repr(a) == "MultiValuedVariant('foo', 'bar,baz')"
+        assert str(a) == 'foo=bar,baz'
+        assert a.value == ('bar', 'baz')
+        assert 'bar' in a
+        assert 'baz' in a
+        assert eval(repr(a)) == a
+
+        # Spaces are trimmed
+        b = MultiValuedVariant('foo', 'bar, baz')
+        assert repr(b) == "MultiValuedVariant('foo', 'bar, baz')"
+        assert str(b) == 'foo=bar,baz'
+        assert b.value == ('bar', 'baz')
+        assert 'bar' in b
+        assert 'baz' in b
+        assert a == b
+        assert hash(a) == hash(b)
+        assert eval(repr(b)) == a
+
+        # Order is not important
+        c = MultiValuedVariant('foo', 'baz, bar')
+        assert repr(c) == "MultiValuedVariant('foo', 'baz, bar')"
+        assert str(c) == 'foo=bar,baz'
+        assert c.value == ('bar', 'baz')
+        assert 'bar' in c
+        assert 'baz' in c
+        assert a == c
+        assert hash(a) == hash(c)
+        assert eval(repr(c)) == a
+
+        # Check the copy
+        d = a.copy()
+        assert repr(a) == repr(d)
+        assert str(a) == str(d)
+        assert d.value == ('bar', 'baz')
+        assert 'bar' in d
+        assert 'baz' in d
+        assert a == d
+        assert a is not d
+        assert hash(a) == hash(d)
+        assert eval(repr(d)) == a
+
+    def test_satisfies(self):
+
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = MultiValuedVariant('foo', 'bar')
+        c = MultiValuedVariant('fee', 'bar,baz')
+        d = MultiValuedVariant('foo', 'True')
+
+        # 'foo=bar,baz' satisfies 'foo=bar'
+        assert a.satisfies(b)
+
+        # 'foo=bar' does not satisfy 'foo=bar,baz'
+        assert not b.satisfies(a)
+
+        # 'foo=bar,baz' does not satisfy 'foo=bar,baz' and vice-versa
+        assert not a.satisfies(c)
+        assert not c.satisfies(a)
+
+        # Cannot satisfy the constraint with an object of
+        # another type
+        b_sv = SingleValuedVariant('foo', 'bar')
+        assert not b.satisfies(b_sv)
+
+        d_bv = BoolValuedVariant('foo', 'True')
+        assert not d.satisfies(d_bv)
+
+    def test_compatible(self):
+
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = MultiValuedVariant('foo', 'True')
+        c = MultiValuedVariant('fee', 'bar,baz')
+        d = MultiValuedVariant('foo', 'bar,barbaz')
+
+        # If the name of two multi-valued variants is the same,
+        # they are compatible
+        assert a.compatible(b)
+        assert not a.compatible(c)
+        assert a.compatible(d)
+
+        assert b.compatible(a)
+        assert not b.compatible(c)
+        assert b.compatible(d)
+
+        assert not c.compatible(a)
+        assert not c.compatible(b)
+        assert not c.compatible(d)
+
+        assert d.compatible(a)
+        assert d.compatible(b)
+        assert not d.compatible(c)
+
+        # Can't be compatible with other types
+        b_bv = BoolValuedVariant('foo', 'True')
+        assert not b.compatible(b_bv)
+
+        b_sv = SingleValuedVariant('foo', 'True')
+        assert not b.compatible(b_sv)
+
+    def test_constrain(self):
+
+        # Try to constrain on a value with less constraints than self
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = MultiValuedVariant('foo', 'bar')
+
+        changed = a.constrain(b)
+        assert not changed
+        t = MultiValuedVariant('foo', 'bar,baz')
+        assert a == t
+
+        # Try to constrain on a value with more constraints than self
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = MultiValuedVariant('foo', 'bar')
+
+        changed = b.constrain(a)
+        assert changed
+        t = MultiValuedVariant('foo', 'bar,baz')
+        assert a == t
+
+        # Try to constrain on the same value
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = a.copy()
+
+        changed = a.constrain(b)
+        assert not changed
+        t = MultiValuedVariant('foo', 'bar,baz')
+        assert a == t
+
+        # Try to constrain on a different name
+        a = MultiValuedVariant('foo', 'bar,baz')
+        b = MultiValuedVariant('fee', 'bar')
+
+        with pytest.raises(ValueError):
+            a.constrain(b)
+
+        # Try to constrain on other types
+        a = MultiValuedVariant('foo', 'bar,baz')
+        sv = SingleValuedVariant('foo', 'bar')
+        bv = BoolValuedVariant('foo', 'True')
+        for v in (sv, bv):
+            with pytest.raises(TypeError):
+                a.constrain(v)
+
+    def test_yaml_entry(self):
+
+        a = MultiValuedVariant('foo', 'bar,baz,barbaz')
+        b = MultiValuedVariant('foo', 'bar, baz,  barbaz')
+        expected = ('foo', sorted(['bar', 'baz', 'barbaz']))
+
+        assert a.yaml_entry() == expected
+        assert b.yaml_entry() == expected
+
+        a = MultiValuedVariant('foo', 'bar')
+        expected = ('foo', sorted(['bar']))
+
+        assert a.yaml_entry() == expected
+
+
+class TestSingleValuedVariant(object):
+
+    def test_initialization(self):
+
+        # Basic properties
+        a = SingleValuedVariant('foo', 'bar')
+        assert repr(a) == "SingleValuedVariant('foo', 'bar')"
+        assert str(a) == 'foo=bar'
+        assert a.value == 'bar'
+        assert 'bar' in a
+        assert eval(repr(a)) == a
+
+        # Raise if multiple values are passed
+        with pytest.raises(ValueError):
+            SingleValuedVariant('foo', 'bar, baz')
+
+        # Check the copy
+        b = a.copy()
+        assert repr(a) == repr(b)
+        assert str(a) == str(b)
+        assert b.value == 'bar'
+        assert 'bar' in b
+        assert a == b
+        assert a is not b
+        assert hash(a) == hash(b)
+        assert eval(repr(b)) == a
+
+    def test_satisfies(self):
+        a = SingleValuedVariant('foo', 'bar')
+        b = SingleValuedVariant('foo', 'bar')
+        c = SingleValuedVariant('foo', 'baz')
+        d = SingleValuedVariant('fee', 'bar')
+        e = SingleValuedVariant('foo', 'True')
+
+        # 'foo=bar' can only satisfy 'foo=bar'
+        assert a.satisfies(b)
+        assert not a.satisfies(c)
+        assert not a.satisfies(d)
+
+        assert b.satisfies(a)
+        assert not b.satisfies(c)
+        assert not b.satisfies(d)
+
+        assert not c.satisfies(a)
+        assert not c.satisfies(b)
+        assert not c.satisfies(d)
+
+        # Cannot satisfy the constraint with an object of
+        # another type
+        a_mv = MultiValuedVariant('foo', 'bar')
+        assert not a.satisfies(a_mv)
+
+        e_bv = BoolValuedVariant('foo', 'True')
+        assert not e.satisfies(e_bv)
+
+    def test_compatible(self):
+
+        a = SingleValuedVariant('foo', 'bar')
+        b = SingleValuedVariant('fee', 'bar')
+        c = SingleValuedVariant('foo', 'baz')
+        d = SingleValuedVariant('foo', 'bar')
+
+        # If the name of two multi-valued variants is the same,
+        # they are compatible
+        assert not a.compatible(b)
+        assert not a.compatible(c)
+        assert a.compatible(d)
+
+        assert not b.compatible(a)
+        assert not b.compatible(c)
+        assert not b.compatible(d)
+
+        assert not c.compatible(a)
+        assert not c.compatible(b)
+        assert not c.compatible(d)
+
+        assert d.compatible(a)
+        assert not d.compatible(b)
+        assert not d.compatible(c)
+
+        # Can't be compatible with other types
+        a_mv = MultiValuedVariant('foo', 'bar')
+        assert not a.compatible(a_mv)
+
+        e = SingleValuedVariant('foo', 'True')
+        e_bv = BoolValuedVariant('foo', 'True')
+        assert not e.compatible(e_bv)
+
+    def test_constrain(self):
+
+        # Try to constrain on a value equal to self
+        a = SingleValuedVariant('foo', 'bar')
+        b = SingleValuedVariant('foo', 'bar')
+
+        changed = a.constrain(b)
+        assert not changed
+        t = SingleValuedVariant('foo', 'bar')
+        assert a == t
+
+        # Try to constrain on a value with a different value
+        a = SingleValuedVariant('foo', 'bar')
+        b = SingleValuedVariant('foo', 'baz')
+
+        with pytest.raises(UnsatisfiableVariantSpecError):
+            b.constrain(a)
+
+        # Try to constrain on a value with a different value
+        a = SingleValuedVariant('foo', 'bar')
+        b = SingleValuedVariant('fee', 'bar')
+
+        with pytest.raises(ValueError):
+            b.constrain(a)
+
+        # Try to constrain on the same value
+        a = SingleValuedVariant('foo', 'bar')
+        b = a.copy()
+
+        changed = a.constrain(b)
+        assert not changed
+        t = SingleValuedVariant('foo', 'bar')
+        assert a == t
+
+        # Try to constrain on other values
+        a = SingleValuedVariant('foo', 'True')
+        mv = MultiValuedVariant('foo', 'True')
+        bv = BoolValuedVariant('foo', 'True')
+        for v in (mv, bv):
+            with pytest.raises(TypeError):
+                a.constrain(v)
+
+    def test_yaml_entry(self):
+        a = SingleValuedVariant('foo', 'bar')
+        expected = ('foo', 'bar')
+
+        assert a.yaml_entry() == expected
+
+
+class TestBoolValuedVariant(object):
+
+    def test_initialization(self):
+        # Basic properties - True value
+        for v in (True, 'True', 'TRUE', 'TrUe'):
+            a = BoolValuedVariant('foo', v)
+            assert repr(a) == "BoolValuedVariant('foo', {0})".format(repr(v))
+            assert str(a) == '+foo'
+            assert a.value is True
+            assert True in a
+            assert eval(repr(a)) == a
+
+        # Copy - True value
+        b = a.copy()
+        assert repr(a) == repr(b)
+        assert str(a) == str(b)
+        assert b.value is True
+        assert True in b
+        assert a == b
+        assert a is not b
+        assert hash(a) == hash(b)
+        assert eval(repr(b)) == a
+
+        # Basic properties - False value
+        for v in (False, 'False', 'FALSE', 'FaLsE'):
+            a = BoolValuedVariant('foo', v)
+            assert repr(a) == "BoolValuedVariant('foo', {0})".format(repr(v))
+            assert str(a) == '~foo'
+            assert a.value is False
+            assert False in a
+            assert eval(repr(a)) == a
+
+        # Copy - True value
+        b = a.copy()
+        assert repr(a) == repr(b)
+        assert str(a) == str(b)
+        assert b.value is False
+        assert False in b
+        assert a == b
+        assert a is not b
+        assert eval(repr(b)) == a
+
+        # Invalid values
+        for v in ('bar', 'bar,baz'):
+            with pytest.raises(ValueError):
+                BoolValuedVariant('foo', v)
+
+    def test_satisfies(self):
+        a = BoolValuedVariant('foo', True)
+        b = BoolValuedVariant('foo', False)
+        c = BoolValuedVariant('fee', False)
+        d = BoolValuedVariant('foo', 'True')
+
+        assert not a.satisfies(b)
+        assert not a.satisfies(c)
+        assert a.satisfies(d)
+
+        assert not b.satisfies(a)
+        assert not b.satisfies(c)
+        assert not b.satisfies(d)
+
+        assert not c.satisfies(a)
+        assert not c.satisfies(b)
+        assert not c.satisfies(d)
+
+        assert d.satisfies(a)
+        assert not d.satisfies(b)
+        assert not d.satisfies(c)
+
+        # Cannot satisfy the constraint with an object of
+        # another type
+        d_mv = MultiValuedVariant('foo', 'True')
+        assert not d.satisfies(d_mv)
+
+        d_sv = SingleValuedVariant('foo', 'True')
+        assert not d.satisfies(d_sv)
+
+    def test_compatible(self):
+
+        a = BoolValuedVariant('foo', True)
+        b = BoolValuedVariant('fee', True)
+        c = BoolValuedVariant('foo', False)
+        d = BoolValuedVariant('foo', 'True')
+
+        # If the name of two multi-valued variants is the same,
+        # they are compatible
+        assert not a.compatible(b)
+        assert not a.compatible(c)
+        assert a.compatible(d)
+
+        assert not b.compatible(a)
+        assert not b.compatible(c)
+        assert not b.compatible(d)
+
+        assert not c.compatible(a)
+        assert not c.compatible(b)
+        assert not c.compatible(d)
+
+        assert d.compatible(a)
+        assert not d.compatible(b)
+        assert not d.compatible(c)
+
+        # Can't be compatible with other types
+        d_mv = MultiValuedVariant('foo', 'True')
+        assert not d.compatible(d_mv)
+
+        d_sv = SingleValuedVariant('foo', 'True')
+        assert not d.compatible(d_sv)
+
+    def test_constrain(self):
+        # Try to constrain on a value equal to self
+        a = BoolValuedVariant('foo', 'True')
+        b = BoolValuedVariant('foo', True)
+
+        changed = a.constrain(b)
+        assert not changed
+        t = BoolValuedVariant('foo', True)
+        assert a == t
+
+        # Try to constrain on a value with a different value
+        a = BoolValuedVariant('foo', True)
+        b = BoolValuedVariant('foo', False)
+
+        with pytest.raises(UnsatisfiableVariantSpecError):
+            b.constrain(a)
+
+        # Try to constrain on a value with a different value
+        a = BoolValuedVariant('foo', True)
+        b = BoolValuedVariant('fee', True)
+
+        with pytest.raises(ValueError):
+            b.constrain(a)
+
+        # Try to constrain on the same value
+        a = BoolValuedVariant('foo', True)
+        b = a.copy()
+
+        changed = a.constrain(b)
+        assert not changed
+        t = BoolValuedVariant('foo', True)
+        assert a == t
+
+        # Try to constrain on other values
+        a = BoolValuedVariant('foo', 'True')
+        sv = SingleValuedVariant('foo', 'True')
+        mv = MultiValuedVariant('foo', 'True')
+        for v in (sv, mv):
+            with pytest.raises(TypeError):
+                a.constrain(v)
+
+    def test_yaml_entry(self):
+
+        a = BoolValuedVariant('foo', 'True')
+        expected = ('foo', True)
+        assert a.yaml_entry() == expected
+
+        a = BoolValuedVariant('foo', 'False')
+        expected = ('foo', False)
+        assert a.yaml_entry() == expected
+
+
+def test_from_node_dict():
+    a = MultiValuedVariant.from_node_dict('foo', ['bar'])
+    assert type(a) == MultiValuedVariant
+
+    a = MultiValuedVariant.from_node_dict('foo', 'bar')
+    assert type(a) == SingleValuedVariant
+
+    a = MultiValuedVariant.from_node_dict('foo', 'true')
+    assert type(a) == BoolValuedVariant
+
+
+class TestVariant(object):
+
+    def test_validation(self):
+        a = Variant(
+            'foo',
+            default='',
+            description='',
+            values=('bar', 'baz', 'foobar'),
+            multi=False
+        )
+        # Valid vspec, shouldn't raise
+        vspec = a.make_variant('bar')
+        a.validate_or_raise(vspec)
+
+        # Multiple values are not allowed
+        with pytest.raises(MultipleValuesInExclusiveVariantError):
+            vspec.value = 'bar,baz'
+
+        # Inconsistent vspec
+        vspec.name = 'FOO'
+        with pytest.raises(InconsistentValidationError):
+            a.validate_or_raise(vspec)
+
+        # Valid multi-value vspec
+        a.multi = True
+        vspec = a.make_variant('bar,baz')
+        a.validate_or_raise(vspec)
+        # Add an invalid value
+        vspec.value = 'bar,baz,barbaz'
+        with pytest.raises(InvalidVariantValueError):
+            a.validate_or_raise(vspec)
+
+    def test_callable_validator(self):
+
+        def validator(x):
+            try:
+                return isinstance(int(x), numbers.Integral)
+            except ValueError:
+                return False
+
+        a = Variant(
+            'foo',
+            default=1024,
+            description='',
+            values=validator,
+            multi=False
+        )
+        vspec = a.make_default()
+        a.validate_or_raise(vspec)
+        vspec.value = 2056
+        a.validate_or_raise(vspec)
+        vspec.value = 'foo'
+        with pytest.raises(InvalidVariantValueError):
+            a.validate_or_raise(vspec)
+
+    def test_representation(self):
+        a = Variant(
+            'foo',
+            default='',
+            description='',
+            values=('bar', 'baz', 'foobar'),
+            multi=False
+        )
+        assert a.allowed_values == 'bar, baz, foobar'
+
+
+class TestVariantMapTest(object):
+
+    def test_invalid_values(self):
+        # Value with invalid type
+        a = VariantMap(None)
+        with pytest.raises(TypeError):
+            a['foo'] = 2
+
+        # Duplicate variant
+        a['foo'] = MultiValuedVariant('foo', 'bar,baz')
+        with pytest.raises(DuplicateVariantError):
+            a['foo'] = MultiValuedVariant('foo', 'bar')
+
+        with pytest.raises(DuplicateVariantError):
+            a['foo'] = SingleValuedVariant('foo', 'bar')
+
+        with pytest.raises(DuplicateVariantError):
+            a['foo'] = BoolValuedVariant('foo', True)
+
+        # Non matching names between key and vspec.name
+        with pytest.raises(KeyError):
+            a['bar'] = MultiValuedVariant('foo', 'bar')
+
+    def test_set_item(self):
+        # Check that all the three types of variants are accepted
+        a = VariantMap(None)
+
+        a['foo'] = BoolValuedVariant('foo', True)
+        a['bar'] = SingleValuedVariant('bar', 'baz')
+        a['foobar'] = MultiValuedVariant('foobar', 'a, b, c, d, e')
+
+    def test_substitute(self):
+        # Check substitution of a key that exists
+        a = VariantMap(None)
+        a['foo'] = BoolValuedVariant('foo', True)
+        a.substitute(SingleValuedVariant('foo', 'bar'))
+
+        # Trying to substitute something that is not
+        # in the map will raise a KeyError
+        with pytest.raises(KeyError):
+            a.substitute(BoolValuedVariant('bar', True))
+
+    def test_satisfies_and_constrain(self):
+        # foo=bar foobar=fee feebar=foo
+        a = VariantMap(None)
+        a['foo'] = MultiValuedVariant('foo', 'bar')
+        a['foobar'] = SingleValuedVariant('foobar', 'fee')
+        a['feebar'] = SingleValuedVariant('feebar', 'foo')
+
+        # foo=bar,baz foobar=fee shared=True
+        b = VariantMap(None)
+        b['foo'] = MultiValuedVariant('foo', 'bar, baz')
+        b['foobar'] = SingleValuedVariant('foobar', 'fee')
+        b['shared'] = BoolValuedVariant('shared', True)
+
+        assert not a.satisfies(b)
+        assert b.satisfies(a)
+
+        assert not a.satisfies(b, strict=True)
+        assert not b.satisfies(a, strict=True)
+
+        # foo=bar,baz foobar=fee feebar=foo shared=True
+        c = VariantMap(None)
+        c['foo'] = MultiValuedVariant('foo', 'bar, baz')
+        c['foobar'] = SingleValuedVariant('foobar', 'fee')
+        c['feebar'] = SingleValuedVariant('feebar', 'foo')
+        c['shared'] = BoolValuedVariant('shared', True)
+
+        assert a.constrain(b)
+        assert a == c
+
+    def test_copy(self):
+        a = VariantMap(None)
+        a['foo'] = BoolValuedVariant('foo', True)
+        a['bar'] = SingleValuedVariant('bar', 'baz')
+        a['foobar'] = MultiValuedVariant('foobar', 'a, b, c, d, e')
+
+        c = a.copy()
+        assert a == c
+
+    def test_str(self):
+        c = VariantMap(None)
+        c['foo'] = MultiValuedVariant('foo', 'bar, baz')
+        c['foobar'] = SingleValuedVariant('foobar', 'fee')
+        c['feebar'] = SingleValuedVariant('feebar', 'foo')
+        c['shared'] = BoolValuedVariant('shared', True)
+        assert str(c) == ' feebar=foo foo=bar,baz foobar=fee +shared'
diff --git a/lib/spack/spack/variant.py b/lib/spack/spack/variant.py
index b2c1a73489..7102676b69 100644
--- a/lib/spack/spack/variant.py
+++ b/lib/spack/spack/variant.py
@@ -22,17 +22,583 @@
 # License along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
-"""Variant is a class describing flags on builds, or "variants".
+"""The variant module contains data structures that are needed to manage
+variants both in packages and in specs.
+"""
 
-Could be generalized later to describe aribitrary parameters, but
-currently variants are just flags.
+import inspect
+import re
 
-"""
+import llnl.util.lang as lang
+import spack.error as error
+from six import StringIO
 
 
 class Variant(object):
-    """Represents a variant on a build.  Can be either on or off."""
+    """Represents a variant in a package, as declared in the
+    variant directive.
+    """
 
-    def __init__(self, default, description):
-        self.default     = default
+    def __init__(
+            self,
+            name,
+            default,
+            description,
+            values=(True, False),
+            multi=False,
+            validator=None
+    ):
+        """Initialize a package variant.
+
+        Args:
+            name (str): name of the variant
+            default (str): default value for the variant in case
+                nothing has been specified
+            description (str): purpose of the variant
+            values (sequence): sequence of allowed values or a callable
+                accepting a single value as argument and returning True if the
+                value is good, False otherwise
+            multi (bool): whether multiple CSV are allowed
+            validator (callable): optional callable used to enforce
+                additional logic on the set of values being validated
+        """
+        self.name = name
+        self.default = default
         self.description = str(description)
+
+        if callable(values):
+            # If 'values' is a callable, assume it is a single value
+            # validator and reset the values to be explicit during debug
+            self.single_value_validator = values
+            self.values = None
+        else:
+            # Otherwise assume values is the set of allowed explicit values
+            self.values = tuple(values)
+            allowed = self.values + (self.default,)
+            self.single_value_validator = lambda x: x in allowed
+
+        self.multi = multi
+        self.group_validator = validator
+
+    def validate_or_raise(self, vspec, pkg=None):
+        """Validate a variant spec against this package variant. Raises an
+        exception if any error is found.
+
+        Args:
+            vspec (VariantSpec): instance to be validated
+            pkg (Package): the package that required the validation,
+                if available
+
+        Raises:
+            InconsistentValidationError: if ``vspec.name != self.name``
+
+            MultipleValuesInExclusiveVariantError: if ``vspec`` has
+                multiple values but ``self.multi == False``
+
+            InvalidVariantValueError: if ``vspec.value`` contains
+                invalid values
+        """
+        # Check the name of the variant
+        if self.name != vspec.name:
+            raise InconsistentValidationError(vspec, self)
+
+        # Check the values of the variant spec
+        value = vspec.value
+        if isinstance(vspec.value, (bool, str)):
+            value = (vspec.value,)
+
+        # If the value is exclusive there must be at most one
+        if not self.multi and len(value) != 1:
+            raise MultipleValuesInExclusiveVariantError(vspec, pkg)
+
+        # Check and record the values that are not allowed
+        not_allowed_values = [
+            x for x in value if not self.single_value_validator(x)
+        ]
+        if not_allowed_values:
+            raise InvalidVariantValueError(self, not_allowed_values, pkg)
+
+        # Validate the group of values if needed
+        if self.group_validator is not None:
+            self.group_validator(value)
+
+    @property
+    def allowed_values(self):
+        """Returns a string representation of the allowed values for
+        printing purposes
+
+        Returns:
+            str: representation of the allowed values
+        """
+        # Join an explicit set of allowed values
+        if self.values is not None:
+            v = tuple(str(x) for x in self.values)
+            return ', '.join(v)
+        # In case we were given a single-value validator
+        # print the docstring
+        docstring = inspect.getdoc(self.single_value_validator)
+        v = docstring if docstring else ''
+        return v
+
+    def make_default(self):
+        """Factory that creates a variant holding the default value.
+
+        Returns:
+            MultiValuedVariant or SingleValuedVariant or BoolValuedVariant:
+                instance of the proper variant
+        """
+        return self.make_variant(self.default)
+
+    def make_variant(self, value):
+        """Factory that creates a variant holding the value passed as
+        a parameter.
+
+        Args:
+            value: value that will be hold by the variant
+
+        Returns:
+            MultiValuedVariant or SingleValuedVariant or BoolValuedVariant:
+                instance of the proper variant
+        """
+        return self.variant_cls(self.name, value)
+
+    @property
+    def variant_cls(self):
+        """Proper variant class to be used for this configuration."""
+        if self.multi:
+            return MultiValuedVariant
+        elif self.values == (True, False):
+            return BoolValuedVariant
+        return SingleValuedVariant
+
+
+@lang.key_ordering
+class MultiValuedVariant(object):
+    """A variant that can hold multiple values at once."""
+
+    @staticmethod
+    def from_node_dict(name, value):
+        """Reconstruct a variant from a node dict."""
+        if isinstance(value, list):
+            value = ','.join(value)
+            return MultiValuedVariant(name, value)
+        elif str(value).upper() == 'TRUE' or str(value).upper() == 'FALSE':
+            return BoolValuedVariant(name, value)
+        return SingleValuedVariant(name, value)
+
+    def __init__(self, name, value):
+        self.name = name
+
+        # Stores 'value' after a bit of massaging
+        # done by the property setter
+        self._value = None
+        self._original_value = None
+
+        # Invokes property setter
+        self.value = value
+
+    @property
+    def value(self):
+        """Returns a tuple of strings containing the values stored in
+        the variant.
+
+        Returns:
+            tuple of str: values stored in the variant
+        """
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        self._value_setter(value)
+
+    def _value_setter(self, value):
+        # Store the original value
+        self._original_value = value
+
+        # Store a tuple of CSV string representations
+        # Tuple is necessary here instead of list because the
+        # values need to be hashed
+        t = re.split(r'\s*,\s*', str(value))
+
+        # With multi-value variants it is necessary
+        # to remove duplicates and give an order
+        # to a set
+        self._value = tuple(sorted(set(t)))
+
+    def _cmp_key(self):
+        return self.name, self.value
+
+    def copy(self):
+        """Returns an instance of a variant equivalent to self
+
+        Returns:
+            any variant type: a copy of self
+
+        >>> a = MultiValuedVariant('foo', True)
+        >>> b = a.copy()
+        >>> assert a == b
+        >>> assert a is not b
+        """
+        return type(self)(self.name, self._original_value)
+
+    def satisfies(self, other):
+        """Returns true if ``other.name == self.name`` and ``other.value`` is
+        a strict subset of self. Does not try to validate.
+
+        Args:
+            other: constraint to be met for the method to return True
+
+        Returns:
+            bool: True or False
+        """
+        # If types are different the constraint is not satisfied
+        if type(other) != type(self):
+            return False
+
+        # If names are different then `self` does not satisfy `other`
+        # (`foo=bar` does not satisfy `baz=bar`)
+        if other.name != self.name:
+            return False
+
+        # Otherwise we want all the values in `other` to be also in `self`
+        return all(v in self.value for v in other.value)
+
+    def compatible(self, other):
+        """Returns True if self and other are compatible, False otherwise.
+
+        As there is no semantic check, two VariantSpec are compatible if
+        either they contain the same value or they are both multi-valued.
+
+        Args:
+            other: instance against which we test compatibility
+
+        Returns:
+            bool: True or False
+        """
+        # If types are different they are not compatible
+        if type(other) != type(self):
+            return False
+
+        # If names are different then they are not compatible
+        return other.name == self.name
+
+    def constrain(self, other):
+        """Modify self to match all the constraints for other if both
+        instances are multi-valued. Returns True if self changed,
+        False otherwise.
+
+        Args:
+            other: instance against which we constrain self
+
+        Returns:
+            bool: True or False
+        """
+        # If types are different they are not compatible
+        if type(other) != type(self):
+            msg = 'other must be of type \'{0.__name__}\''
+            raise TypeError(msg.format(type(self)))
+
+        if self.name != other.name:
+            raise ValueError('variants must have the same name')
+        old_value = self.value
+        self.value = ','.join(sorted(set(self.value + other.value)))
+        return old_value != self.value
+
+    def yaml_entry(self):
+        """Returns a key, value tuple suitable to be an entry in a yaml dict.
+
+        Returns:
+            tuple: (name, value_representation)
+        """
+        return self.name, list(self.value)
+
+    def __contains__(self, item):
+        return item in self._value
+
+    def __repr__(self):
+        cls = type(self)
+        return '{0.__name__}({1}, {2})'.format(
+            cls, repr(self.name), repr(self._original_value)
+        )
+
+    def __str__(self):
+        return '{0}={1}'.format(
+            self.name, ','.join(str(x) for x in self.value)
+        )
+
+
+class SingleValuedVariant(MultiValuedVariant):
+    """A variant that can hold multiple values, but one at a time."""
+
+    def _value_setter(self, value):
+        # Treat the value as a multi-valued variant
+        super(SingleValuedVariant, self)._value_setter(value)
+
+        # Then check if there's only a single value
+        if len(self._value) != 1:
+            raise MultipleValuesInExclusiveVariantError(self, None)
+        self._value = str(self._value[0])
+
+    def __str__(self):
+        return '{0}={1}'.format(self.name, self.value)
+
+    def satisfies(self, other):
+        # If types are different the constraint is not satisfied
+        if type(other) != type(self):
+            return False
+
+        # If names are different then `self` does not satisfy `other`
+        # (`foo=bar` does not satisfy `baz=bar`)
+        if other.name != self.name:
+            return False
+
+        return self.value == other.value
+
+    def compatible(self, other):
+        return self.satisfies(other)
+
+    def constrain(self, other):
+        if type(other) != type(self):
+            msg = 'other must be of type \'{0.__name__}\''
+            raise TypeError(msg.format(type(self)))
+
+        if self.name != other.name:
+            raise ValueError('variants must have the same name')
+
+        if self.value != other.value:
+            raise UnsatisfiableVariantSpecError(other.value, self.value)
+        return False
+
+    def __contains__(self, item):
+        return item == self.value
+
+    def yaml_entry(self):
+        return self.name, self.value
+
+
+class BoolValuedVariant(SingleValuedVariant):
+    """A variant that can hold either True or False."""
+
+    def _value_setter(self, value):
+        # Check the string representation of the value and turn
+        # it to a boolean
+        if str(value).upper() == 'TRUE':
+            self._original_value = value
+            self._value = True
+        elif str(value).upper() == 'FALSE':
+            self._original_value = value
+            self._value = False
+        else:
+            msg = 'cannot construct a BoolValuedVariant from '
+            msg += 'a value that does not represent a bool'
+            raise ValueError(msg)
+
+    def __contains__(self, item):
+        return item is self.value
+
+    def __str__(self):
+        return '{0}{1}'.format('+' if self.value else '~', self.name)
+
+
+class VariantMap(lang.HashableMap):
+    """Map containing variant instances. New values can be added only
+    if the key is not already present.
+    """
+
+    def __init__(self, spec):
+        super(VariantMap, self).__init__()
+        self.spec = spec
+
+    def __setitem__(self, name, vspec):
+        # Raise a TypeError if vspec is not of the right type
+        if not isinstance(vspec, MultiValuedVariant):
+            msg = 'VariantMap accepts only values of type VariantSpec'
+            raise TypeError(msg)
+
+        # Raise an error if the variant was already in this map
+        if name in self.dict:
+            msg = 'Cannot specify variant "{0}" twice'.format(name)
+            raise DuplicateVariantError(msg)
+
+        # Raise an error if name and vspec.name don't match
+        if name != vspec.name:
+            msg = 'Inconsistent key "{0}", must be "{1}" to match VariantSpec'
+            raise KeyError(msg.format(name, vspec.name))
+
+        # Set the item
+        super(VariantMap, self).__setitem__(name, vspec)
+
+    def substitute(self, vspec):
+        """Substitutes the entry under ``vspec.name`` with ``vspec``.
+
+        Args:
+            vspec: variant spec to be substituted
+        """
+        if vspec.name not in self:
+            msg = 'cannot substitute a key that does not exist [{0}]'
+            raise KeyError(msg.format(vspec.name))
+
+        # Set the item
+        super(VariantMap, self).__setitem__(vspec.name, vspec)
+
+    def satisfies(self, other, strict=False):
+        """Returns True if this VariantMap is more constrained than other,
+        False otherwise.
+
+        Args:
+            other (VariantMap): VariantMap instance to satisfy
+            strict (bool): if True return False if a key is in other and
+                not in self, otherwise discard that key and proceed with
+                evaluation
+
+        Returns:
+            bool: True or False
+        """
+        to_be_checked = [k for k in other]
+
+        strict_or_concrete = strict
+        if self.spec is not None:
+            strict_or_concrete |= self.spec._concrete
+
+        if not strict_or_concrete:
+            to_be_checked = filter(lambda x: x in self, to_be_checked)
+
+        return all(k in self and self[k].satisfies(other[k])
+                   for k in to_be_checked)
+
+    def constrain(self, other):
+        """Add all variants in other that aren't in self to self. Also
+        constrain all multi-valued variants that are already present.
+        Return True if self changed, False otherwise
+
+        Args:
+            other (VariantMap): instance against which we constrain self
+
+        Returns:
+            bool: True or False
+        """
+        if other.spec is not None and other.spec._concrete:
+            for k in self:
+                if k not in other:
+                    raise UnsatisfiableVariantSpecError(self[k], '<absent>')
+
+        changed = False
+        for k in other:
+            if k in self:
+                # If they are not compatible raise an error
+                if not self[k].compatible(other[k]):
+                    raise UnsatisfiableVariantSpecError(self[k], other[k])
+                # If they are compatible merge them
+                changed |= self[k].constrain(other[k])
+            else:
+                # If it is not present copy it straight away
+                self[k] = other[k].copy()
+                changed = True
+
+        return changed
+
+    @property
+    def concrete(self):
+        """Returns True if the spec is concrete in terms of variants.
+
+        Returns:
+            bool: True or False
+        """
+        return self.spec._concrete or all(
+            v in self for v in self.spec.package_class.variants
+        )
+
+    def copy(self):
+        """Return an instance of VariantMap equivalent to self.
+
+        Returns:
+            VariantMap: a copy of self
+        """
+        clone = VariantMap(self.spec)
+        for name, variant in self.items():
+            clone[name] = variant.copy()
+        return clone
+
+    def __str__(self):
+        # print keys in order
+        sorted_keys = sorted(self.keys())
+
+        # add spaces before and after key/value variants.
+        string = StringIO()
+
+        kv = False
+        for key in sorted_keys:
+            vspec = self[key]
+
+            if not isinstance(vspec.value, bool):
+                # add space before all kv pairs.
+                string.write(' ')
+                kv = True
+            else:
+                # not a kv pair this time
+                if kv:
+                    # if it was LAST time, then pad after.
+                    string.write(' ')
+                kv = False
+
+            string.write(str(vspec))
+
+        return string.getvalue()
+
+
+class DuplicateVariantError(error.SpecError):
+    """Raised when the same variant occurs in a spec twice."""
+
+
+class UnknownVariantError(error.SpecError):
+    """Raised when an unknown variant occurs in a spec."""
+
+    def __init__(self, pkg, variant):
+        super(UnknownVariantError, self).__init__(
+            'Package {0} has no variant {1}!'.format(pkg, variant)
+        )
+
+
+class InconsistentValidationError(error.SpecError):
+    """Raised if the wrong validator is used to validate a variant."""
+    def __init__(self, vspec, variant):
+        msg = ('trying to validate variant "{0.name}" '
+               'with the validator of "{1.name}"')
+        super(InconsistentValidationError, self).__init__(
+            msg.format(vspec, variant)
+        )
+
+
+class MultipleValuesInExclusiveVariantError(error.SpecError, ValueError):
+    """Raised when multiple values are present in a variant that wants
+    only one.
+    """
+    def __init__(self, variant, pkg):
+        msg = 'multiple values are not allowed for variant "{0.name}"{1}'
+        pkg_info = ''
+        if pkg is not None:
+            pkg_info = ' in package "{0}"'.format(pkg.name)
+        super(MultipleValuesInExclusiveVariantError, self).__init__(
+            msg.format(variant, pkg_info)
+        )
+
+
+class InvalidVariantValueError(error.SpecError):
+    """Raised when a valid variant has at least an invalid value."""
+
+    def __init__(self, variant, invalid_values, pkg):
+        msg = 'invalid values for variant "{0.name}"{2}: {1}\n'
+        pkg_info = ''
+        if pkg is not None:
+            pkg_info = ' in package "{0}"'.format(pkg.name)
+        super(InvalidVariantValueError, self).__init__(
+            msg.format(variant, invalid_values, pkg_info)
+        )
+
+
+class UnsatisfiableVariantSpecError(error.UnsatisfiableSpecError):
+    """Raised when a spec variant conflicts with package constraints."""
+
+    def __init__(self, provided, required):
+        super(UnsatisfiableVariantSpecError, self).__init__(
+            provided, required, "variant")
diff --git a/var/spack/repos/builtin.mock/packages/a/package.py b/var/spack/repos/builtin.mock/packages/a/package.py
index 0d75ee1256..b697f4d2a9 100644
--- a/var/spack/repos/builtin.mock/packages/a/package.py
+++ b/var/spack/repos/builtin.mock/packages/a/package.py
@@ -25,7 +25,7 @@
 from spack import *
 
 
-class A(Package):
+class A(AutotoolsPackage):
     """Simple package with no dependencies"""
 
     homepage = "http://www.example.com"
@@ -33,5 +33,35 @@ class A(Package):
 
     version('1.0', '0123456789abcdef0123456789abcdef')
 
+    variant(
+        'foo',
+        values=('bar', 'baz', 'fee'),
+        default='bar',
+        description='',
+        multi=True
+    )
+
+    variant(
+        'foobar',
+        values=('bar', 'baz', 'fee'),
+        default='bar',
+        description='',
+        multi=False
+    )
+
+    def with_or_without_fee(self, activated):
+        if not activated:
+            return '--no-fee'
+        return '--fee-all-the-time'
+
+    def autoreconf(self, spec, prefix):
+        pass
+
+    def configure(self, spec, prefix):
+        pass
+
+    def build(self, spec, prefix):
+        pass
+
     def install(self, spec, prefix):
         pass
diff --git a/var/spack/repos/builtin.mock/packages/multivalue_variant/package.py b/var/spack/repos/builtin.mock/packages/multivalue_variant/package.py
new file mode 100644
index 0000000000..13c3b02e77
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/multivalue_variant/package.py
@@ -0,0 +1,57 @@
+##############################################################################
+# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
+# Produced at the Lawrence Livermore National Laboratory.
+#
+# This file is part of Spack.
+# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
+# LLNL-CODE-647188
+#
+# For details, see https://github.com/llnl/spack
+# Please also see the LICENSE file for our notice and the LGPL.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License (as
+# published by the Free Software Foundation) version 2.1, February 1999.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
+# conditions of the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##############################################################################
+from spack import *
+
+
+class MultivalueVariant(Package):
+    homepage = "http://www.llnl.gov"
+    url = "http://www.llnl.gov/mpileaks-1.0.tar.gz"
+
+    version(1.0, 'foobarbaz')
+    version(2.1, 'foobarbaz')
+    version(2.2, 'foobarbaz')
+    version(2.3, 'foobarbaz')
+
+    variant('debug', default=False, description='Debug variant')
+    variant(
+        'foo',
+        description='Multi-valued variant',
+        values=('bar', 'baz', 'barbaz'),
+        multi=True
+    )
+
+    variant(
+        'fee',
+        description='Single-valued variant',
+        default='bar',
+        values=('bar', 'baz', 'barbaz'),
+        multi=False
+    )
+
+    depends_on('mpi')
+    depends_on('callpath')
+
+    def install(self, spec, prefix):
+        pass
diff --git a/var/spack/repos/builtin/packages/cdo/package.py b/var/spack/repos/builtin/packages/cdo/package.py
index 90039d4479..3cee41b299 100644
--- a/var/spack/repos/builtin/packages/cdo/package.py
+++ b/var/spack/repos/builtin/packages/cdo/package.py
@@ -27,7 +27,8 @@
 
 class Cdo(Package):
     """CDO is a collection of command line Operators to manipulate and analyse
-    Climate and NWP model Data. """
+    Climate and NWP model Data.
+    """
 
     homepage = "https://code.zmaw.de/projects/cdo"
     url      = "https://code.zmaw.de/attachments/download/12760/cdo-1.7.2.tar.gz"
diff --git a/var/spack/repos/builtin/packages/mvapich2/package.py b/var/spack/repos/builtin/packages/mvapich2/package.py
index d952165ac1..3844f804be 100644
--- a/var/spack/repos/builtin/packages/mvapich2/package.py
+++ b/var/spack/repos/builtin/packages/mvapich2/package.py
@@ -22,11 +22,20 @@
 # License along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
-from spack import *
 import sys
 
+from spack import *
+from spack.error import SpackError
+
+
+def _process_manager_validator(values):
+    if len(values) > 1 and 'slurm' in values:
+        raise SpackError(
+            'slurm cannot be activated along with other process managers'
+        )
 
-class Mvapich2(Package):
+
+class Mvapich2(AutotoolsPackage):
     """MVAPICH2 is an MPI implementation for Infiniband networks."""
     homepage = "http://mvapich.cse.ohio-state.edu/"
     url = "http://mvapich.cse.ohio-state.edu/download/mvapich/mv2/mvapich2-2.2.tar.gz"
@@ -55,62 +64,39 @@ class Mvapich2(Package):
     #   serialized  - User serializes calls to MPI (MPI_THREAD_SERIALIZED)
     #   multiple    - Fully multi-threaded (MPI_THREAD_MULTIPLE)
     #   runtime     - Alias to "multiple"
-    variant('threads', default='multiple',
-            description='Control the level of thread support')
+    variant(
+        'threads',
+        default='multiple',
+        values=('single', 'funneled', 'serialized', 'multiple'),
+        multi=False,
+        description='Control the level of thread support'
+    )
 
     # 32 is needed when job size exceeds 32768 cores
-    variant('ch3_rank_bits', default=32,
-            description='Number of bits allocated to the rank field (16 or 32)'
-            )
-
-    ##########
-    # TODO : Process managers should be grouped into the same variant,
-    # as soon as variant capabilities will be extended See
-    # https://groups.google.com/forum/#!topic/spack/F8-f8B4_0so
-    SLURM = 'slurm'
-    HYDRA = 'hydra'
-    GFORKER = 'gforker'
-    REMSHELL = 'remshell'
-    SLURM_INCOMPATIBLE_PMS = (HYDRA, GFORKER, REMSHELL)
-    variant(SLURM, default=False,
-            description='Set slurm as the only process manager')
-    variant(HYDRA, default=False,
-            description='Set hydra as one of the process managers')
-    variant(GFORKER, default=False,
-            description='Set gforker as one of the process managers')
-    variant(REMSHELL, default=False,
-            description='Set remshell as one of the process managers')
-    ##########
-
-    ##########
-    # TODO : Network types should be grouped into the same variant, as
-    # soon as variant capabilities will be extended
-    PSM = 'psm'
-    SOCK = 'sock'
-    NEMESISIBTCP = 'nemesisibtcp'
-    NEMESISIB = 'nemesisib'
-    NEMESIS = 'nemesis'
-    MRAIL = 'mrail'
-    SUPPORTED_NETWORKS = (PSM, SOCK, NEMESIS, NEMESISIB, NEMESISIBTCP)
-    variant(
-        PSM, default=False,
-        description='Configure for QLogic PSM-CH3')
-    variant(
-        SOCK, default=False,
-        description='Configure for TCP/IP-CH3')
-    variant(
-        NEMESISIBTCP, default=False,
-        description='Configure for both OFA-IB-Nemesis and TCP/IP-Nemesis')
     variant(
-        NEMESISIB, default=False,
-        description='Configure for OFA-IB-Nemesis')
+        'ch3_rank_bits',
+        default='32',
+        values=('16', '32'),
+        multi=False,
+        description='Number of bits allocated to the rank field (16 or 32)'
+    )
+
     variant(
-        NEMESIS, default=False,
-        description='Configure for TCP/IP-Nemesis')
+        'process_managers',
+        description='List of the process managers to activate',
+        values=('slurm', 'hydra', 'gforker', 'remshell'),
+        multi=True,
+        validator=_process_manager_validator
+    )
+
     variant(
-        MRAIL, default=False,
-        description='Configure for OFA-IB-CH3')
-    ##########
+        'fabrics',
+        description='The fabric enabled for this build',
+        default='psm',
+        values=(
+            'psm', 'sock', 'nemesisib', 'nemesis', 'mrail', 'nemesisibtcp'
+        )
+    )
 
     # FIXME : CUDA support is missing
     depends_on('bison')
@@ -123,110 +109,52 @@ def url_for_version(self, version):
         else:
             return "%s/mvapich/mv2/mvapich2-%s.tar.gz"  % (base_url, version)
 
-    @staticmethod
-    def enabled(x):
-        """Given a variant name returns the string that means the variant is
-        enabled
-
-        :param x: variant name
-        :return:
-        """
-        return '+' + x
-
-    def set_build_type(self, spec, configure_args):
-        """Appends to configure_args the flags that depends only on the build
-        type (i.e. release or debug)
-
-        :param spec: spec
-        :param configure_args: list of current configure arguments
-        """
-        if '+debug' in spec:
-            build_type_options = [
-                "--disable-fast",
-                "--enable-error-checking=runtime",
-                "--enable-error-messages=all",
-                # Permits debugging with TotalView
-                "--enable-g=dbg", "--enable-debuginfo"
-            ]
-        else:
-            build_type_options = ["--enable-fast=all"]
-
-        configure_args.extend(build_type_options)
+    @property
+    def process_manager_options(self):
+        spec = self.spec
 
-    def set_process_manager(self, spec, configure_args):
-        """Appends to configure_args the flags that will enable the
-        appropriate process managers
+        other_pms = []
+        for x in ('hydra', 'gforker', 'remshell'):
+            if 'process_managers={0}'.format(x) in spec:
+                other_pms.append(x)
+        opts = ['--with-pm=%s' % ':'.join(other_pms)]
 
-        :param spec: spec
-        :param configure_args: list of current configure arguments
-        """
-        # Check that slurm variant is not activated together with
-        # other pm variants
-        has_slurm_incompatible_variants = \
-            any(self.enabled(x) in spec
-                for x in Mvapich2.SLURM_INCOMPATIBLE_PMS)
-
-        if self.enabled(Mvapich2.SLURM) in spec and \
-           has_slurm_incompatible_variants:
-            raise RuntimeError(" %s : 'slurm' cannot be activated \
-            together with other process managers" % self.name)
-
-        process_manager_options = []
         # See: http://slurm.schedmd.com/mpi_guide.html#mvapich2
-        if self.enabled(Mvapich2.SLURM) in spec:
+        if 'process_managers=slurm' in spec:
             if self.version > Version('2.0'):
-                process_manager_options = [
-                    "--with-pmi=pmi2",
-                    "--with-pm=slurm"
+                opts = [
+                    '--with-pmi=pmi2',
+                    '--with-pm=slurm'
                 ]
             else:
-                process_manager_options = [
-                    "--with-pmi=slurm",
-                    "--with-pm=no"
+                opts = [
+                    '--with-pmi=slurm',
+                    '--with-pm=no'
                 ]
 
-        elif has_slurm_incompatible_variants:
-            pms = []
-            # The variant name is equal to the process manager name in
-            # the configuration options
-            for x in Mvapich2.SLURM_INCOMPATIBLE_PMS:
-                if self.enabled(x) in spec:
-                    pms.append(x)
-            process_manager_options = [
-                "--with-pm=%s" % ':'.join(pms)
-            ]
-        configure_args.extend(process_manager_options)
-
-    def set_network_type(self, spec, configure_args):
-        # Check that at most one variant has been activated
-        count = 0
-        for x in Mvapich2.SUPPORTED_NETWORKS:
-            if self.enabled(x) in spec:
-                count += 1
-        if count > 1:
-            raise RuntimeError('network variants are mutually exclusive \
-            (only one can be selected at a time)')
-
-        network_options = []
+        return opts
+
+    @property
+    def network_options(self):
+        opts = []
         # From here on I can suppose that only one variant has been selected
-        if self.enabled(Mvapich2.PSM) in spec:
-            network_options = ["--with-device=ch3:psm"]
-        elif self.enabled(Mvapich2.SOCK) in spec:
-            network_options = ["--with-device=ch3:sock"]
-        elif self.enabled(Mvapich2.NEMESISIBTCP) in spec:
-            network_options = ["--with-device=ch3:nemesis:ib,tcp"]
-        elif self.enabled(Mvapich2.NEMESISIB) in spec:
-            network_options = ["--with-device=ch3:nemesis:ib"]
-        elif self.enabled(Mvapich2.NEMESIS) in spec:
-            network_options = ["--with-device=ch3:nemesis"]
-        elif self.enabled(Mvapich2.MRAIL) in spec:
-            network_options = ["--with-device=ch3:mrail", "--with-rdma=gen2"]
-
-        configure_args.extend(network_options)
+        if 'fabrics=psm' in self.spec:
+            opts = ["--with-device=ch3:psm"]
+        elif 'fabrics=sock' in self.spec:
+            opts = ["--with-device=ch3:sock"]
+        elif 'fabrics=nemesisibtcp' in self.spec:
+            opts = ["--with-device=ch3:nemesis:ib,tcp"]
+        elif 'fabrics=nemesisib' in self.spec:
+            opts = ["--with-device=ch3:nemesis:ib"]
+        elif 'fabrics=nemesis' in self.spec:
+            opts = ["--with-device=ch3:nemesis"]
+        elif 'fabrics=mrail' in self.spec:
+            opts = ["--with-device=ch3:mrail", "--with-rdma=gen2"]
+        return opts
 
     def setup_environment(self, spack_env, run_env):
-        if self.enabled(Mvapich2.SLURM) in self.spec and \
-           self.version > Version('2.0'):
+        spec = self.spec
+        if 'process_managers=slurm' in spec and spec.satisfies('@2.0:'):
             run_env.set('SLURM_MPI_TYPE', 'pmi2')
 
     def setup_dependent_environment(self, spack_env, run_env, dependent_spec):
@@ -251,48 +179,44 @@ def setup_dependent_package(self, module, dependent_spec):
             join_path(self.prefix.lib, 'libmpi.{0}'.format(dso_suffix))
         ]
 
-    def install(self, spec, prefix):
+    @run_before('configure')
+    def die_without_fortran(self):
         # Until we can pass variants such as +fortran through virtual
         # dependencies depends_on('mpi'), require Fortran compiler to
         # avoid delayed build errors in dependents.
         if (self.compiler.f77 is None) or (self.compiler.fc is None):
-            raise InstallError('Mvapich2 requires both C and Fortran ',
-                               'compilers!')
-
-        # we'll set different configure flags depending on our
-        # environment
-        configure_args = [
-            "--prefix=%s" % prefix,
-            "--enable-shared",
-            "--enable-romio",
-            "--disable-silent-rules",
+            raise InstallError(
+                'Mvapich2 requires both C and Fortran compilers!'
+            )
+
+    def configure_args(self):
+        args = [
+            '--enable-shared',
+            '--enable-romio',
+            '-disable-silent-rules',
+            '--enable-fortran=all',
             "--enable-threads={0}".format(spec.variants['threads'].value),
             "--with-ch3-rank-bits={0}".format(
                 spec.variants['ch3_rank_bits'].value),
         ]
 
-        if self.compiler.f77 and self.compiler.fc:
-            configure_args.append("--enable-fortran=all")
-        elif self.compiler.f77:
-            configure_args.append("--enable-fortran=f77")
-        elif self.compiler.fc:
-            configure_args.append("--enable-fortran=fc")
+        if '+debug' in self.spec:
+            args.extend([
+                '--disable-fast',
+                '--enable-error-checking=runtime',
+                '--enable-error-messages=all',
+                # Permits debugging with TotalView
+                '--enable-g=dbg',
+                '--enable-debuginfo'
+            ])
         else:
-            configure_args.append("--enable-fortran=none")
-
-        # Set the type of the build (debug, release)
-        self.set_build_type(spec, configure_args)
-        # Set the process manager
-        self.set_process_manager(spec, configure_args)
-        # Determine network type by variant
-        self.set_network_type(spec, configure_args)
-
-        configure(*configure_args)
-        make()
-        make("install")
+            args.append('--enable-fast=all')
 
-        self.filter_compilers()
+        args.extend(self.process_manager_options)
+        args.extend(self.network_options)
+        return args
 
+    @run_after('install')
     def filter_compilers(self):
         """Run after install to make the MPI compilers use the
            compilers that Spack built the package with.
@@ -302,7 +226,7 @@ def filter_compilers(self):
            be bound to whatever compiler they were built with.
         """
         bin = self.prefix.bin
-        mpicc  = join_path(bin, 'mpicc')
+        mpicc = join_path(bin, 'mpicc')
         mpicxx = join_path(bin, 'mpicxx')
         mpif77 = join_path(bin, 'mpif77')
         mpif90 = join_path(bin, 'mpif90')
diff --git a/var/spack/repos/builtin/packages/netcdf/package.py b/var/spack/repos/builtin/packages/netcdf/package.py
index ca6a30e9a4..f3f0ed59a6 100644
--- a/var/spack/repos/builtin/packages/netcdf/package.py
+++ b/var/spack/repos/builtin/packages/netcdf/package.py
@@ -24,6 +24,16 @@
 ##############################################################################
 from spack import *
 
+import numbers
+
+
+def is_integral(x):
+    """Any integer value"""
+    try:
+        return isinstance(int(x), numbers.Integral) and not isinstance(x, bool)
+    except ValueError:
+        return False
+
 
 class Netcdf(AutotoolsPackage):
     """NetCDF is a set of software libraries and self-describing,
@@ -52,10 +62,18 @@ class Netcdf(AutotoolsPackage):
     # These variants control the number of dimensions (i.e. coordinates and
     # attributes) and variables (e.g. time, entity ID, number of coordinates)
     # that can be used in any particular NetCDF file.
-    variant('maxdims', default=1024,
-            description='Defines the maximum dimensions of NetCDF files.')
-    variant('maxvars', default=8192,
-            description='Defines the maximum variables of NetCDF files.')
+    variant(
+        'maxdims',
+        default=1024,
+        description='Defines the maximum dimensions of NetCDF files.',
+        values=is_integral
+    )
+    variant(
+        'maxvars',
+        default=8192,
+        description='Defines the maximum variables of NetCDF files.',
+        values=is_integral
+    )
 
     depends_on("m4", type='build')
     depends_on("hdf", when='+hdf4')
diff --git a/var/spack/repos/builtin/packages/openmpi/package.py b/var/spack/repos/builtin/packages/openmpi/package.py
index 63579efe0e..2761df543d 100644
--- a/var/spack/repos/builtin/packages/openmpi/package.py
+++ b/var/spack/repos/builtin/packages/openmpi/package.py
@@ -22,13 +22,16 @@
 # License along with this program; if not, write to the Free Software
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ##############################################################################
-from spack import *
+
 import os
 
+from spack import *
+
 
 def _verbs_dir():
     """Try to find the directory where the OpenFabrics verbs package is
-    installed. Return None if not found."""
+    installed. Return None if not found.
+    """
     try:
         # Try to locate Verbs by looking for a utility in the path
         ibv_devices = which("ibv_devices")
@@ -43,7 +46,7 @@ def _verbs_dir():
         if path == "/":
             path = "/usr"
         return path
-    except:
+    except TypeError:
         return None
 
 
@@ -82,22 +85,20 @@ class Openmpi(AutotoolsPackage):
     patch('configure.patch', when="@1.10.0:1.10.1")
     patch('fix_multidef_pmi_class.patch', when="@2.0.0:2.0.1")
 
-    # Fabrics
-    variant('psm', default=False, description='Build support for the PSM library')
-    variant('psm2', default=False,
-            description='Build support for the Intel PSM2 library')
-    variant('pmi', default=False,
-            description='Build support for PMI-based launchers')
-    variant('verbs', default=_verbs_dir() is not None,
-            description='Build support for OpenFabrics verbs')
-    variant('mxm', default=False, description='Build Mellanox Messaging support')
-
-    # Schedulers
-    # TODO: support for alps and loadleveler is missing
-    variant('tm', default=False,
-            description='Build TM (Torque, PBSPro, and compatible) support')
-    variant('slurm', default=False,
-            description='Build SLURM scheduler component')
+    variant(
+        'fabrics',
+        default=None if _verbs_dir() is None else 'verbs',
+        description='List of fabrics that are enabled',
+        values=('psm', 'psm2', 'pmi', 'verbs', 'mxm'),
+        multi=True
+    )
+
+    variant(
+        'schedulers',
+        description='List of schedulers for which support is enabled',
+        values=('alps', 'lsf', 'tm', 'slurm', 'sge', 'loadleveler'),
+        multi=True
+    )
 
     # Additional support options
     variant('java', default=False, description='Build Java support')
@@ -114,7 +115,7 @@ class Openmpi(AutotoolsPackage):
     depends_on('hwloc')
     depends_on('hwloc +cuda', when='+cuda')
     depends_on('jdk', when='+java')
-    depends_on('sqlite', when='+sqlite3')
+    depends_on('sqlite', when='+sqlite3@:1.11')
 
     def url_for_version(self, version):
         return "http://www.open-mpi.org/software/ompi/v%s/downloads/openmpi-%s.tar.bz2" % (
@@ -153,15 +154,22 @@ def setup_dependent_package(self, module, dependent_spec):
             join_path(self.prefix.lib, 'libmpi.{0}'.format(dso_suffix))
         ]
 
-    @property
-    def verbs(self):
+    def with_or_without_verbs(self, activated):
         # Up through version 1.6, this option was previously named
         # --with-openib
-        if self.spec.satisfies('@:1.6'):
-            return 'openib'
+        opt = 'openib'
         # In version 1.7, it was renamed to be --with-verbs
-        elif self.spec.satisfies('@1.7:'):
-            return 'verbs'
+        if self.spec.satisfies('@1.7:'):
+            opt = 'verbs'
+        # If the option has not been activated return
+        # --without-openib or --without-verbs
+        if not activated:
+            return '--without-{0}'.format(opt)
+        line = '--with-{0}'.format(opt)
+        path = _verbs_dir()
+        if (path is not None) and (path not in ('/usr', '/usr/local')):
+            line += '={0}'.format(path)
+        return line
 
     @run_before('autoreconf')
     def die_without_fortran(self):
@@ -175,48 +183,17 @@ def die_without_fortran(self):
 
     def configure_args(self):
         spec = self.spec
-
         config_args = [
             '--enable-shared',
-            '--enable-static',
-            '--enable-mpi-cxx',
-            # Schedulers
-            '--with-tm' if '+tm' in spec else '--without-tm',
-            '--with-slurm' if '+slurm' in spec else '--without-slurm',
-            # Fabrics
-            '--with-psm' if '+psm' in spec else '--without-psm',
+            '--enable-static'
         ]
+        if self.spec.satisfies('@2.0:'):
+            # for Open-MPI 2.0:, C++ bindings are disabled by default.
+            config_args.extend(['--enable-mpi-cxx'])
 
-        # Intel PSM2 support
-        if spec.satisfies('@1.10:'):
-            if '+psm2' in spec:
-                config_args.append('--with-psm2')
-            else:
-                config_args.append('--without-psm2')
-
-        # PMI support
-        if spec.satisfies('@1.5.5:'):
-            if '+pmi' in spec:
-                config_args.append('--with-pmi')
-            else:
-                config_args.append('--without-pmi')
-
-        # Mellanox Messaging support
-        if spec.satisfies('@1.5.4:'):
-            if '+mxm' in spec:
-                config_args.append('--with-mxm')
-            else:
-                config_args.append('--without-mxm')
-
-        # OpenFabrics verbs support
-        if '+verbs' in spec:
-            path = _verbs_dir()
-            if path is not None and path not in ('/usr', '/usr/local'):
-                config_args.append('--with-{0}={1}'.format(self.verbs, path))
-            else:
-                config_args.append('--with-{0}'.format(self.verbs))
-        else:
-            config_args.append('--without-{0}'.format(self.verbs))
+        # Fabrics and schedulers
+        config_args.extend(self.with_or_without('fabrics'))
+        config_args.extend(self.with_or_without('schedulers'))
 
         # Hwloc support
         if spec.satisfies('@1.5.2:'):
@@ -270,11 +247,11 @@ def configure_args(self):
     @run_after('install')
     def filter_compilers(self):
         """Run after install to make the MPI compilers use the
-           compilers that Spack built the package with.
+        compilers that Spack built the package with.
 
-           If this isn't done, they'll have CC, CXX and FC set
-           to Spack's generic cc, c++ and f90.  We want them to
-           be bound to whatever compiler they were built with.
+        If this isn't done, they'll have CC, CXX and FC set
+        to Spack's generic cc, c++ and f90.  We want them to
+        be bound to whatever compiler they were built with.
         """
         kwargs = {'ignore_absent': True, 'backup': False, 'string': False}
         wrapper_basepath = join_path(self.prefix, 'share', 'openmpi')
-- 
GitLab