Repository: spack/spack

Issue:
Cryptic Errors on Invalid Dependency Options
The update #12889 changed `fftw+float+double` to `fftw precision=float,double`, which makes downstream packages request the wrong variant.  Assuming all downstream packages need to be changed, there should be some explanation of what happened.

Instead, spack crashes with the cryptic error "KeyError: 'double'" or "KeyError: 'float'".

### Steps to reproduce the issue

```console
$ spack spec accfft
```

### Error Message
```console
Input spec
--------------------------------
accfft

Concretized
--------------------------------
==> Error: 'float'
```

The key error itself comes from **spack/variants.py: substitute_abstract_variants()**
```python
    for name, v in spec.variants.items():  # [('double', BoolValuedVariant('double', True)), ('openmp', BoolValuedVariant('openmp', True)), ('float', BoolValuedVariant('float', True)), ('mpi', BoolValuedVariant('mpi', False))]
        if name in spack.directives.reserved_names:
            continue
        pkg_variant = spec.package_class.variants[name]  # *** KeyError: 'double'
        new_variant = pkg_variant.make_variant(v._original_value)
        pkg_variant.validate_or_raise(new_variant, spec.package_class)
        spec.variants.substitute(new_variant)
```
here,
```python

spec.package_class.variants = {
  'openmp': <spack.variant.Variant object at 0x7f526dfdd190>,
  'pfft_patches': <spack.variant.Variant object at 0x7f526dfdd150>,
  'precision': <spack.variant.Variant object at 0x7f526dfdd210>,
  'mpi': <spack.variant.Variant object at 0x7f526dfdd1d0>
  }
```

I think the right solution is to make the error message more readable so that future package problems can be tracked down. The line `pkg_variant = spec.package_class.variants[name]` should have a try-except clause or the magic inside `repo.py: get_pkg_class(self, pkg_name)` should make variants a proper VariantMap, which can then override __getitem__. 


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -2645,7 +2645,7 @@ def validate_or_raise(self):
                 not_existing = set(spec.variants) - (
                     set(pkg_variants) | set(spack.directives.reserved_names))
                 if not_existing:
-                    raise UnknownVariantError(spec.name, not_existing)
+                    raise UnknownVariantError(spec, not_existing)
 
                 substitute_abstract_variants(spec)
 
diff --git a/lib/spack/spack/variant.py b/lib/spack/spack/variant.py
--- a/lib/spack/spack/variant.py
+++ b/lib/spack/spack/variant.py
@@ -600,7 +600,9 @@ def substitute_abstract_variants(spec):
     for name, v in spec.variants.items():
         if name in spack.directives.reserved_names:
             continue
-        pkg_variant = spec.package_class.variants[name]
+        pkg_variant = spec.package_class.variants.get(name, None)
+        if not pkg_variant:
+            raise UnknownVariantError(spec, [name])
         new_variant = pkg_variant.make_variant(v._original_value)
         pkg_variant.validate_or_raise(new_variant, spec.package_class)
         spec.variants.substitute(new_variant)
@@ -778,12 +780,13 @@ class DuplicateVariantError(error.SpecError):
 
 class UnknownVariantError(error.SpecError):
     """Raised when an unknown variant occurs in a spec."""
-
-    def __init__(self, pkg, variants):
+    def __init__(self, spec, variants):
         self.unknown_variants = variants
-        super(UnknownVariantError, self).__init__(
-            'Package {0} has no variant {1}!'.format(pkg, comma_or(variants))
-        )
+        variant_str = 'variant' if len(variants) == 1 else 'variants'
+        msg = ('trying to set {0} "{1}" in package "{2}", but the package'
+               ' has no such {0} [happened during concretization of {3}]')
+        msg = msg.format(variant_str, comma_or(variants), spec.name, spec.root)
+        super(UnknownVariantError, self).__init__(msg)
 
 
 class InconsistentValidationError(error.SpecError):


Test patch:
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -9,7 +9,7 @@
 from spack.spec import Spec, UnsatisfiableSpecError, SpecError
 from spack.spec import substitute_abstract_variants
 from spack.spec import SpecFormatSigilError, SpecFormatStringError
-from spack.variant import InvalidVariantValueError
+from spack.variant import InvalidVariantValueError, UnknownVariantError
 from spack.variant import MultipleValuesInExclusiveVariantError
 
 import spack.architecture
@@ -976,3 +976,9 @@ def test_forwarding_of_architecture_attributes(self):
     def test_target_constraints(self, spec, constraint, expected_result):
         s = Spec(spec)
         assert s.satisfies(constraint) is expected_result
+
+    @pytest.mark.regression('13124')
+    def test_error_message_unknown_variant(self):
+        s = Spec('mpileaks +unknown')
+        with pytest.raises(UnknownVariantError, match=r'package has no such'):
+            s.concretize()