Repository: spack/spack

Issue:
Virtual dependencies not supported in Environments
A common use-case for Spack Environments is to ship a `spack.yaml` file with your software specifying a list of dependencies. If something like `mpi` is a dependency, you need a way to list it, even if you don't care which specific MPI implementation is used. This is currently not possible.

### Steps to reproduce the issue

1. Create a `spack.yaml` file like:
```yaml
spack:
  specs:
  - mpi
  concretization: together
```
2. Attempt to concretize/install the environment

### Error Message

```console
$ spack --debug install
...
Traceback (most recent call last):
  File "/home/t-astewart/spack/bin/spack", line 77, in <module>
    sys.exit(spack.main.main())
  File "/home/t-astewart/spack/lib/spack/spack/main.py", line 776, in main
    return _invoke_command(command, parser, args, unknown)
  File "/home/t-astewart/spack/lib/spack/spack/main.py", line 498, in _invoke_command
    return_val = command(parser, args)
  File "/home/t-astewart/spack/lib/spack/spack/cmd/install.py", line 338, in install
    concretized_specs = env.concretize(tests=tests)
  File "/home/t-astewart/spack/lib/spack/spack/environment.py", line 1209, in concretize
    return self._concretize_together(tests=tests)
  File "/home/t-astewart/spack/lib/spack/spack/environment.py", line 1248, in _concretize_together
    concrete_specs = spack.concretize.concretize_specs_together(
  File "/home/t-astewart/spack/lib/spack/spack/concretize.py", line 729, in concretize_specs_together
    return _concretize_specs_together_new(*abstract_specs, **kwargs)
  File "/home/t-astewart/spack/lib/spack/spack/concretize.py", line 741, in _concretize_specs_together_new
    return [answer[s.name].copy() for s in abstract_specs]
  File "/home/t-astewart/spack/lib/spack/spack/concretize.py", line 741, in <listcomp>
    return [answer[s.name].copy() for s in abstract_specs]
KeyError: 'mpi'
```

### Information on your system

* **Spack:** 0.8.17-20562-54b9fe219b
* **Python:** 3.8.1
* **Platform:** linux-ubuntu18.04-haswell
* **Concretizer:** clingo

### Additional information

- [x] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [x] I have searched the issues of this repo and believe this is not a duplicate
- [x] I have run the failing commands in debug mode and reported the output


Discussion:

Resolving patch:
diff --git a/lib/spack/spack/cmd/solve.py b/lib/spack/spack/cmd/solve.py
--- a/lib/spack/spack/cmd/solve.py
+++ b/lib/spack/spack/cmd/solve.py
@@ -116,9 +116,7 @@ def solve(parser, args):
 
     # dump the solutions as concretized specs
     if 'solutions' in dump:
-        best = min(result.answers)
-
-        opt, _, answer = best
+        opt, _, _ = min(result.answers)
         if ("opt" in dump) and (not args.format):
             tty.msg("Best of %d considered solutions." % result.nmodels)
             tty.msg("Optimization Criteria:")
@@ -132,16 +130,7 @@ def solve(parser, args):
                 color.cprint(fmt % (i + 1, name, val))
             print()
 
-        # iterate over roots from command line
-        for input_spec in specs:
-            key = input_spec.name
-            if input_spec.virtual:
-                providers = [spec.name for spec in answer.values()
-                             if spec.package.provides(key)]
-                key = providers[0]
-
-            spec = answer[key]
-
+        for spec in result.specs:
             # With -y, just print YAML to output.
             if args.format == 'yaml':
                 # use write because to_yaml already has a newline.
diff --git a/lib/spack/spack/concretize.py b/lib/spack/spack/concretize.py
--- a/lib/spack/spack/concretize.py
+++ b/lib/spack/spack/concretize.py
@@ -737,8 +737,7 @@ def _concretize_specs_together_new(*abstract_specs, **kwargs):
         result.print_cores()
         tty.die("Unsatisfiable spec.")
 
-    opt, i, answer = min(result.answers)
-    return [answer[s.name].copy() for s in abstract_specs]
+    return [s.copy() for s in result.specs]
 
 
 def _concretize_specs_together_original(*abstract_specs, **kwargs):
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -197,7 +197,7 @@ def check_packages_exist(specs):
 
 class Result(object):
     """Result of an ASP solve."""
-    def __init__(self, asp=None):
+    def __init__(self, specs, asp=None):
         self.asp = asp
         self.satisfiable = None
         self.optimal = None
@@ -211,12 +211,45 @@ def __init__(self, asp=None):
         # names of optimization criteria
         self.criteria = []
 
+        # Abstract user requests
+        self.abstract_specs = specs
+
+        # Concrete specs
+        self._concrete_specs = None
+
     def print_cores(self):
         for core in self.cores:
             tty.msg(
                 "The following constraints are unsatisfiable:",
                 *sorted(str(symbol) for symbol in core))
 
+    @property
+    def specs(self):
+        """List of concretized specs satisfying the initial
+        abstract request.
+        """
+        # The specs were already computed, return them
+        if self._concrete_specs:
+            return self._concrete_specs
+
+        # Assert prerequisite
+        msg = 'cannot compute specs ["satisfiable" is not True ]'
+        assert self.satisfiable, msg
+
+        self._concrete_specs = []
+        best = min(self.answers)
+        opt, _, answer = best
+        for input_spec in self.abstract_specs:
+            key = input_spec.name
+            if input_spec.virtual:
+                providers = [spec.name for spec in answer.values()
+                             if spec.package.provides(key)]
+                key = providers[0]
+
+            self._concrete_specs.append(answer[key])
+
+        return self._concrete_specs
+
 
 def _normalize_packages_yaml(packages_yaml):
     normalized_yaml = copy.copy(packages_yaml)
@@ -329,7 +362,7 @@ def solve(
         timer.phase("ground")
 
         # With a grounded program, we can run the solve.
-        result = Result()
+        result = Result(specs)
         models = []  # stable models if things go well
         cores = []   # unsatisfiable cores if they do not
 


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
@@ -2561,3 +2561,15 @@ def test_multiple_modules_post_env_hook(tmpdir, install_mockery, mock_fetch):
 
     assert view_prefix not in full_contents
     assert spec.prefix in full_contents
+
+
+@pytest.mark.regression('24148')
+def test_virtual_spec_concretize_together(tmpdir):
+    # An environment should permit to concretize "mpi"
+    e = ev.create('virtual_spec')
+    e.concretization = 'together'
+
+    e.add('mpi')
+    e.concretize()
+
+    assert any(s.package.provides('mpi') for _, s in e.concretized_specs())