Repository: spack/spack

Issue:
Definitions as requirements in environments
It would be very useful to be able to use definitions as requirements in specs so as to make environments very easy to share and reuse:

For example we declare the compiler, core compiler and mpi:

```
  definitions:
    - mygcc: [ gcc@8.3.0 ]
    - mybasegcc: [ gcc@4.8.5 ]
    - mympi: [ mpich@3.3.2+slurm pmi=pmi2 ]

  specs:
  - matrix:
    - [$mygcc]
    - [% $mybasegcc]
  
  - matrix:
    - [openblas@0.3.8, $mympi , fftw~mpi]
    - [% mygcc ]
  - matrix:
    - [openfoam,  fftw]
    - [^ $mympi]
    - [% $mygcc]
```

At present this isn't possible as `%$mygcc` is invalid syntax (needs expansion?) so even if we can define things to be installed they have to be added by hand when used as requirements:

```
  - matrix:
    - [openblas, $mympi , fftw~mpi]
    - [ "%gcc@8.3.0" ]
  - matrix:
    - [openfoam, fftw]
    - ["^mpich@3.3.2+slurm pmi=pmi2"]
    - ["%gcc@8.3.0"]
```





Discussion:
I think this should probably only apply to the `%` and `^` sigils (as those can reasonably be expected to be both a package name and a constraint on another package).

In terms of implementation, I think it is much easier to support `[$^mpis]` than `[^$mpis]` as a syntax for this. Does anyone else have strong feelings about the exact syntax for this?

Resolving patch:
diff --git a/lib/spack/spack/spec_list.py b/lib/spack/spack/spec_list.py
--- a/lib/spack/spack/spec_list.py
+++ b/lib/spack/spack/spec_list.py
@@ -124,12 +124,27 @@ def _expand_references(self, yaml):
         if isinstance(yaml, list):
             for idx, item in enumerate(yaml):
                 if isinstance(item, string_types) and item.startswith('$'):
-                    name = item[1:]
+                    # Reference type can add a constraint to items
+                    if item[1] in ('^', '%'):
+                        name = item[2:]
+                        sigil = item[1]
+                    else:
+                        name = item[1:]
+                        sigil = ''
                     if name in self._reference:
                         ret = [self._expand_references(i) for i in yaml[:idx]]
                         ret += self._reference[name].specs_as_yaml_list
                         ret += self._expand_references(yaml[idx + 1:])
-                        return ret
+
+                        # Add the sigil if we're mapping a sigil to a ref
+                        def sigilify(arg):
+                            if isinstance(arg, dict):
+                                if sigil:
+                                    arg['sigil'] = sigil
+                                return arg
+                            else:
+                                return sigil + arg
+                        return list(map(sigilify, ret))
                     else:
                         msg = 'SpecList %s refers to ' % self.name
                         msg += 'named list %s ' % name
@@ -159,13 +174,17 @@ def _expand_matrix_constraints(object, specify=True):
         new_row = []
         for r in row:
             if isinstance(r, dict):
-                new_row.extend(_expand_matrix_constraints(r, specify=False))
+                new_row.extend(
+                    [[' '.join(c)]
+                     for c in _expand_matrix_constraints(r, specify=False)])
             else:
                 new_row.append([r])
         expanded_rows.append(new_row)
 
-    results = []
     excludes = object.get('exclude', [])  # only compute once
+    sigil = object.get('sigil', '')
+
+    results = []
     for combo in itertools.product(*expanded_rows):
         # Construct a combined spec to test against excludes
         flat_combo = [constraint for list in combo for constraint in list]
@@ -174,6 +193,9 @@ def _expand_matrix_constraints(object, specify=True):
         if any(test_spec.satisfies(x) for x in excludes):
             continue
 
+        if sigil:  # add sigil if necessary
+            ordered_combo[0] = sigil + ordered_combo[0]
+
         # Add to list of constraints
         if specify:
             results.append([Spec(x) for x in ordered_combo])


Test patch:
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -994,6 +994,55 @@ def test_stack_yaml_definitions(tmpdir):
         assert Spec('callpath') in test.user_specs
 
 
+def test_stack_yaml_definitions_as_constraints(tmpdir):
+    filename = str(tmpdir.join('spack.yaml'))
+    with open(filename, 'w') as f:
+        f.write("""\
+env:
+  definitions:
+    - packages: [mpileaks, callpath]
+    - mpis: [mpich, openmpi]
+  specs:
+    - matrix:
+      - [$packages]
+      - [$^mpis]
+""")
+    with tmpdir.as_cwd():
+        env('create', 'test', './spack.yaml')
+        test = ev.read('test')
+
+        assert Spec('mpileaks^mpich') in test.user_specs
+        assert Spec('callpath^mpich') in test.user_specs
+        assert Spec('mpileaks^openmpi') in test.user_specs
+        assert Spec('callpath^openmpi') in test.user_specs
+
+
+def test_stack_yaml_definitions_as_constraints_on_matrix(tmpdir):
+    filename = str(tmpdir.join('spack.yaml'))
+    with open(filename, 'w') as f:
+        f.write("""\
+env:
+  definitions:
+    - packages: [mpileaks, callpath]
+    - mpis:
+      - matrix:
+        - [mpich]
+        - ['@3.0.4', '@3.0.3']
+  specs:
+    - matrix:
+      - [$packages]
+      - [$^mpis]
+""")
+    with tmpdir.as_cwd():
+        env('create', 'test', './spack.yaml')
+        test = ev.read('test')
+
+        assert Spec('mpileaks^mpich@3.0.4') in test.user_specs
+        assert Spec('callpath^mpich@3.0.4') in test.user_specs
+        assert Spec('mpileaks^mpich@3.0.3') in test.user_specs
+        assert Spec('callpath^mpich@3.0.3') in test.user_specs
+
+
 @pytest.mark.regression('12095')
 def test_stack_yaml_definitions_write_reference(tmpdir):
     filename = str(tmpdir.join('spack.yaml'))